Browse Source

Adding a specialized query compiler for SQL Server

Jose Lorenzo Rodriguez 12 years ago
parent
commit
3519c06115

+ 11 - 24
src/Database/Dialect/SqlserverDialectTrait.php

@@ -17,6 +17,7 @@ namespace Cake\Database\Dialect;
 use Cake\Database\Dialect\TupleComparisonTranslatorTrait;
 use Cake\Database\Expression\FunctionExpression;
 use Cake\Database\SqlDialectTrait;
+use Cake\Database\SqlserverCompiler;
 
 /**
  * Contains functions that encapsulates the SQL dialect used by MySQL,
@@ -53,38 +54,16 @@ trait SqlserverDialectTrait {
 
 		if ($limit && $offset === null) {
 			$query->modifier(['_auto_top_' => sprintf('TOP %d', $limit)]);
-			$query->limit(null);
 		}
 
-		if ($offset !== null) {
-			$offsetSql = sprintf('%d ROWS', $offset);
-			if ($limit) {
-				$offsetSql .= sprintf(' FETCH FIRST %d ROWS ONLY', $limit);
-			}
-			$query->offset($query->newExpr()->add($offsetSql));
-			$query->limit(null);
-
-			if (!$query->clause('order')) {
-				$query->order([$query->connection()->newQuery()->select(['NULL'])]);
-			}
+		if ($offset !== null && !$query->clause('order')) {
+			$query->order([$query->connection()->newQuery()->select(['NULL'])]);
 		}
 
 		return $query;
 	}
 
 /**
- * Check identify before insert
- *
- * @param Cake\Database\Query $query
- * @return Cake\Database\Query
- */
-	protected function _insertQueryTranslator($query) {
-		// @todo check primary key and than execute: SET IDENTITY_INSERT [table] ON (before) and OFF after the insert
-		// @see https://github.com/cakephp/cakephp/blob/master/lib/Cake/Model/Datasource/Database/Sqlserver.php#L345
-		return $query;
-	}
-
-/**
  * Returns an dictionary of expressions to be transformed when compiling a Query
  * to SQL. Array keys are method names to be called in this class
  *
@@ -170,4 +149,12 @@ trait SqlserverDialectTrait {
 		return 'ROLLBACK TRANSACTION t' . $name;
 	}
 
+/**
+ * {@inheritdoc}
+ *
+ */
+	public function newCompiler() {
+		return new SqlserverCompiler();
+	}
+
 }

+ 8 - 64
src/Database/Query.php

@@ -193,10 +193,9 @@ class Query implements ExpressionInterface, IteratorAggregate {
 	}
 
 /**
- * Will iterate over every part that should be included for an specific query
- * type and execute the passed visitor function for each of them. Traversing
- * functions can aggregate results using variables in the closure or instance
- * variables. This function is commonly used as a way for traversing all query parts that
+ * Will iterate over every specified part. Traversing functions can aggregate
+ * results using variables in the closure or instance variables. This function
+ * is commonly used as a way for traversing all query parts that
  * are going to be used for constructing a query.
  *
  * The callback will receive 2 parameters, the first one is the value of the query
@@ -208,74 +207,19 @@ class Query implements ExpressionInterface, IteratorAggregate {
  *		if ($clause === 'select') {
  *			var_dump($value);
  *		}
- *	});
+ *	}, ['select', 'from']);
  * }}}
  *
  * @param callable $visitor a function or callable to be executed for each part
+ * @param array $parts the query clasuses to traverse
  * @return Query
  */
-	public function traverse(callable $visitor) {
-		$this->{'_traverse' . ucfirst($this->_type)}($visitor);
-		return $this;
-	}
-
-/**
- * Helper function that will iterate over all query parts needed for a SELECT statement
- * and execute the $visitor callback for each of them.
- *
- * The callback will receive 2 parameters, the first one is the value of the query
- * part that is being iterated and the second the name of such part.
- *
- * @param callable $visitor a function or callable to be executed for each part
- * @return void
- */
-	protected function _traverseSelect(callable $visitor) {
-		$parts = [
-			'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit',
-			'offset', 'union', 'epilog'
-		];
-		foreach ($parts as $name) {
-			$visitor($this->_parts[$name], $name);
-		}
-	}
-
-/**
- * Helper function that iterates the query parts needed for DELETE statements.
- *
- * @param callable $visitor A callable to execute for each part of the query.
- * @return void
- */
-	protected function _traverseDelete(callable $visitor) {
-		$parts = ['delete', 'from', 'where', 'epilog'];
-		foreach ($parts as $name) {
-			$visitor($this->_parts[$name], $name);
-		}
-	}
-
-/**
- * Helper function that iterates the query parts needed for UPDATE statements.
- *
- * @param callable $visitor A callable to execute for each part of the query.
- * @return void
- */
-	protected function _traverseUpdate(callable $visitor) {
-		$parts = ['update', 'set', 'where', 'epilog'];
-		foreach ($parts as $name) {
-			$visitor($this->_parts[$name], $name);
-		}
-	}
-
-/**
- * Helper function that iterates the query parts needed for INSERT statements.
- *
- * @param callable $visitor A callable to execute for each part of the query.
- * @return void
- */
-	protected function _traverseInsert(callable $visitor) {
-		$parts = ['insert', 'values', 'epilog'];
+	public function traverse(callable $visitor, array $parts = []) {
+		$parts = $parts ?: array_keys($this->_parts);
 		foreach ($parts as $name) {
 			$visitor($this->_parts[$name], $name);
 		}
+		return $this;
 	}
 
 /**

+ 42 - 1
src/Database/QueryCompiler.php

@@ -42,6 +42,36 @@ class QueryCompiler {
 		'epilog' => ' %s'
 	];
 
+/**
+ * The list of query clauses to traverse for generating a SELECT statment
+ *
+ * @var array
+ */
+	protected $_selectParts = [
+		'select', 'from', 'join', 'where', 'group', 'having', 'order', 'limit',
+		'offset', 'union', 'epilog'
+	];
+
+/**
+ * The list of query clauses to traverse for generating an UPDATE statment
+ *
+ * @var array
+ */
+	protected $_updateParts = ['update', 'set', 'where', 'epilog'];
+
+/**
+ * The list of query clauses to traverse for generating a DELETE statment
+ *
+ * @var array
+ */
+	protected $_deleteParts = ['delete', 'from', 'where', 'epilog'];
+
+/**
+ * The list of query clauses to traverse for generating an INSERT statment
+ *
+ * @var array
+ */
+	protected $_insertParts = ['insert', 'values', 'epilog'];
 
 /**
  * Returns the SQL representation of the provided query after generating
@@ -51,7 +81,11 @@ class QueryCompiler {
  */
 	public function compile($query, $generator) {
 		$sql = '';
-		$query->traverse($this->_sqlCompiler($sql, $query, $generator));
+		$type = $query->type();
+		$query->traverse(
+			$this->_sqlCompiler($sql, $query, $generator),
+			$this->{'_' . $type . 'Parts'}
+		);
 		return $sql;
 	}
 
@@ -86,6 +120,7 @@ class QueryCompiler {
  * DISTINCT clause for the query.
  *
  * @param array $parts list of fields to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string
  */
@@ -126,6 +161,7 @@ class QueryCompiler {
  * converting expression objects to string.
  *
  * @param array $parts list of tables to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string
  */
@@ -149,6 +185,7 @@ class QueryCompiler {
  * to be used.
  *
  * @param array $parts list of joins to be transformed to string
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string
  */
@@ -172,6 +209,7 @@ class QueryCompiler {
  * Helper function to generate SQL for SET expressions.
  *
  * @param array $parts List of keys & values to set.
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string
  */
@@ -195,6 +233,7 @@ class QueryCompiler {
  * dialect.
  *
  * @param array $parts list of queries to be operated with UNION
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string
  */
@@ -211,6 +250,7 @@ class QueryCompiler {
  * Builds the SQL fragment for INSERT INTO.
  *
  * @param array $parts
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string SQL fragment.
  */
@@ -224,6 +264,7 @@ class QueryCompiler {
  * Builds the SQL fragment for INSERT INTO.
  *
  * @param array $parts
+ * @param \Cake\Database\Query $query The query that is being compiled
  * @param \Cake\Database\ValueBinder $generator the placeholder generator to be used in expressions
  * @return string SQL fragment.
  */

+ 69 - 0
src/Database/SqlserverCompiler.php

@@ -0,0 +1,69 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+namespace Cake\Database;
+
+use Cake\Database\QueryCompiler;
+
+/**
+ * Responsible for compiling a Query object into its SQL representation
+ * for SQL Server
+ *
+ */
+class SqlserverCompiler extends QueryCompiler {
+
+/**
+ * {@inheritdoc}
+ *
+ */
+	protected $_templates = [
+		'delete' => 'DELETE',
+		'update' => 'UPDATE %s',
+		'where' => ' WHERE %s',
+		'group' => ' GROUP BY %s ',
+		'having' => ' HAVING %s ',
+		'order' => ' %s',
+		'offset' => ' OFFSET %s ROWS',
+		'epilog' => ' %s'
+	];
+
+/**
+ * {@inheritdoc}
+ *
+ */
+	protected $_selectParts = [
+		'select', 'from', 'join', 'where', 'group', 'having', 'order', 'offset',
+		'limit', 'union', 'epilog'
+	];
+
+/**
+ * Generates the LIMIT part of a SQL query
+ *
+ * @param int $limit the limit clause
+ * @param \Cake\Database\Query $query The query that is being compiled
+ * @return string
+ */
+	protected function _buildLimitPart($limit, $query) {
+		if ($limit === null) {
+			return '';
+		}
+
+		if ($query->clause('offset') === null) {
+			return;
+		}
+
+		return sprintf(' FETCH FIRST %d ROWS ONLY', $limit);
+	}
+
+}