Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * TMsssqlCommandBuilder class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link http://www.pradosoft.com/
7
 * @copyright Copyright &copy; 2005-2008 PradoSoft
8
 * @license http://www.pradosoft.com/license/
9
 * @version $Id: TDbCommandBuilder.php 1863 2007-04-12 12:43:49Z wei $
10
 * @package System.Data.Common
11
 */
12
 
13
Prado::using('System.Data.Common.TDbCommandBuilder');
14
 
15
/**
16
 * TMssqlCommandBuilder provides specifics methods to create limit/offset query commands
17
 * for MSSQL servers.
18
 *
19
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
20
 * @version $Id: TDbCommandBuilder.php 1863 2007-04-12 12:43:49Z wei $
21
 * @package System.Data.Common
22
 * @since 3.1
23
 */
24
class TMssqlCommandBuilder extends TDbCommandBuilder
25
{
26
	/**
27
	 * Overrides parent implementation. Uses "SELECT @@Identity".
28
	 * @return integer last insert id, null if none is found.
29
	 */
30
	public function getLastInsertID()
31
	{
32
		foreach($this->getTableInfo()->getColumns() as $column)
33
		{
34
			if($column->hasSequence())
35
			{
36
				$command = $this->getDbConnection()->createCommand('SELECT @@Identity');
37
				return intval($command->queryScalar());
38
			}
39
		}
40
	}
41
 
42
	/**
43
	 * Overrides parent implementation. Alters the sql to apply $limit and $offset.
44
	 * The idea for limit with offset is done by modifying the sql on the fly
45
	 * with numerous assumptions on the structure of the sql string.
46
	 * The modification is done with reference to the notes from
47
	 * http://troels.arvin.dk/db/rdbms/#select-limit-offset
48
	 *
49
	 * <code>
50
	 * SELECT * FROM (
51
	 *  SELECT TOP n * FROM (
52
	 *    SELECT TOP z columns      -- (z=n+skip)
53
	 *    FROM tablename
54
	 *    ORDER BY key ASC
55
	 *  ) AS FOO ORDER BY key DESC -- ('FOO' may be anything)
56
	 * ) AS BAR ORDER BY key ASC    -- ('BAR' may be anything)
57
	 * </code>
58
	 *
59
	 * <b>Regular expressions are used to alter the SQL query. The resulting SQL query
60
	 * may be malformed for complex queries.</b> The following restrictions apply
61
	 *
62
	 * <ul>
63
	 *   <li>
64
	 * In particular, <b>commas</b> should <b>NOT</b>
65
	 * be used as part of the ordering expression or identifier. Commas must only be
66
	 * used for separating the ordering clauses.
67
	 *  </li>
68
	 *  <li>
69
	 * In the ORDER BY clause, the column name should NOT be be qualified
70
	 * with a table name or view name. Alias the column names or use column index.
71
	 * </li>
72
	 * <li>
73
	 * No clauses should follow the ORDER BY clause, e.g. no COMPUTE or FOR clauses.
74
	 * </li>
75
	 *
76
	 * @param string SQL query string.
77
	 * @param integer maximum number of rows, -1 to ignore limit.
78
	 * @param integer row offset, -1 to ignore offset.
79
	 * @return string SQL with limit and offset.
80
	 */
81
	public function applyLimitOffset($sql, $limit=-1, $offset=-1)
82
	{
83
		$limit = $limit!==null ? intval($limit) : -1;
84
		$offset = $offset!==null ? intval($offset) : -1;
85
		if ($limit > 0 && $offset <= 0) //just limit
86
			$sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $limit", $sql);
87
		else if($limit > 0 && $offset > 0)
88
			$sql = $this->rewriteLimitOffsetSql($sql, $limit,$offset);
89
		return $sql;
90
	}
91
 
92
	/**
93
	 * Rewrite sql to apply $limit > and $offset > 0 for MSSQL database.
94
	 * See http://troels.arvin.dk/db/rdbms/#select-limit-offset
95
	 * @param string sql query
96
	 * @param integer $limit > 0
97
	 * @param integer $offset > 0
98
	 * @return sql modified sql query applied with limit and offset.
99
	 */
100
	protected function rewriteLimitOffsetSql($sql, $limit, $offset)
101
	{
102
		$fetch = $limit+$offset;
103
		$sql = preg_replace('/^([\s(])*SELECT( DISTINCT)?(?!\s*TOP\s*\()/i',"\\1SELECT\\2 TOP $fetch", $sql);
104
		$ordering = $this->findOrdering($sql);
105
 
106
		$orginalOrdering = $this->joinOrdering($ordering);
107
		$reverseOrdering = $this->joinOrdering($this->reverseDirection($ordering));
108
		$sql = "SELECT * FROM (SELECT TOP {$limit} * FROM ($sql) as [__inner top table__] {$reverseOrdering}) as [__outer top table__] {$orginalOrdering}";
109
		return $sql;
110
	}
111
 
112
	/**
113
	 * Base on simplified syntax http://msdn2.microsoft.com/en-us/library/aa259187(SQL.80).aspx
114
	 *
115
	 * @param string $sql
116
	 * @return array ordering expression as key and ordering direction as value
117
	 */
118
	protected function findOrdering($sql)
119
	{
120
		if(!preg_match('/ORDER BY/i', $sql))
121
			return array();
122
		$matches=array();
123
		$ordering=array();
124
		preg_match_all('/(ORDER BY)[\s"\[](.*)(ASC|DESC)?(?:[\s"\[]|$|COMPUTE|FOR)/i', $sql, $matches);
125
		if(count($matches)>1 && count($matches[2]) > 0)
126
		{
127
			$parts = explode(',', $matches[2][0]);
128
			foreach($parts as $part)
129
			{
130
				$subs=array();
131
				if(preg_match_all('/(.*)[\s"\]](ASC|DESC)$/i', trim($part), $subs))
132
				{
133
					if(count($subs) > 1 && count($subs[2]) > 0)
134
					{
135
						$ordering[$subs[1][0]] = $subs[2][0];
136
					}
137
					//else what?
138
				}
139
				else
140
					$ordering[trim($part)] = 'ASC';
141
			}
142
		}
143
		return $ordering;
144
	}
145
 
146
	/**
147
	 * @param array ordering obtained from findOrdering()
148
	 * @return string concat the orderings
149
	 */
150
	protected function joinOrdering($orders)
151
	{
152
		if(count($orders)>0)
153
		{
154
			$str=array();
155
			foreach($orders as $column => $direction)
156
				$str[] = $column.' '.$direction;
157
			return 'ORDER BY '.implode(', ', $str);
158
		}
159
	}
160
 
161
	/**
162
	 * @param array original ordering
163
	 * @return array ordering with reversed direction.
164
	 */
165
	protected function reverseDirection($orders)
166
	{
167
		foreach($orders as $column => $direction)
168
			$orders[$column] = strtolower(trim($direction))==='desc' ? 'ASC' : 'DESC';
169
		return $orders;
170
	}
171
}
172