Browse Source

Merge branch '2.0-mssql' into 2.0

Juan Basso 15 years ago
parent
commit
2ea5be5de2

+ 2 - 2
app/Config/database.php.default

@@ -2,7 +2,7 @@
 /**
  * This is core configuration file.
  *
- * Use it to configure core behaviour ofCake.
+ * Use it to configure core behaviour of Cake.
  *
  * PHP 5
  *
@@ -31,7 +31,7 @@
  *		Database/Mysql 		- MySQL 4 & 5,
  *		Database/Sqlite		- SQLite (PHP5 only),
  *		Database/Postgres	- PostgreSQL 7 and higher,
- *		Database/Mssql		- Microsoft SQL Server 2000 and higher,
+ *		Database/Sqlserver	- Microsoft SQL Server 2005 and higher,
  *		Database/Oracle		- Oracle 8 and higher
  *
  * You can add custom database drivers (or override existing drivers) by adding the

+ 1 - 1
lib/Cake/Console/Command/Task/DbConfigTask.php

@@ -104,7 +104,7 @@ class DbConfigTask extends Shell {
 				}
 			}
 
-			$driver = $this->in(__d('cake_console', 'Driver:'), array('Mssql', 'Mysql', 'Oracle', 'Postgres', 'Sqlite'), 'Mysql');
+			$driver = $this->in(__d('cake_console', 'Driver:'), array('Mysql', 'Oracle', 'Postgres', 'Sqlite', 'Sqlserver'), 'Mysql');
 
 			$persistent = $this->in(__d('cake_console', 'Persistent Connection?'), array('y', 'n'), 'n');
 			if (strtolower($persistent) == 'n') {

+ 2 - 2
lib/Cake/Console/templates/skel/Config/database.php.default

@@ -2,7 +2,7 @@
 /**
  * This is core configuration file.
  *
- * Use it to configure core behaviour ofCake.
+ * Use it to configure core behaviour of Cake.
  *
  * PHP 5
  *
@@ -31,7 +31,7 @@
  *		Database/Mysql 		- MySQL 4 & 5,
  *		Database/Sqlite		- SQLite (PHP5 only),
  *		Database/Postgres	- PostgreSQL 7 and higher,
- *		Database/Mssql		- Microsoft SQL Server 2000 and higher,
+ *		Database/Sqlserver	- Microsoft SQL Server 2005 and higher,
  *		Database/Oracle		- Oracle 8 and higher
  *
  * You can add custom database drivers (or override existing drivers) by adding the

+ 176 - 207
lib/Cake/Model/Datasource/Database/Mssql.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * MS SQL layer for DBO
+ * MS SQL Server layer for DBO
  *
  * PHP 5
  *
@@ -17,6 +17,8 @@
  * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
  */
 
+App::uses('DboSource', 'Model/Datasource');
+
 /**
  * MS SQL layer for DBO
  *
@@ -24,14 +26,14 @@
  *
  * @package       cake.libs.model.datasources.dbo
  */
-class DboMssql extends DboSource {
+class Sqlserver extends DboSource {
 
 /**
  * Driver description
  *
  * @var string
  */
-	public $description = "MS SQL DBO Driver";
+	public $description = "SQL Server DBO Driver";
 
 /**
  * Starting quote character for quoted identifiers
@@ -53,7 +55,14 @@ class DboMssql extends DboSource {
  *
  * @var array
  */
-	private $__fieldMappings = array();
+	protected $_fieldMappings = array();
+
+/**
+ * Storing the last affected value
+ *
+ * @var mixed
+ */
+	protected $_lastAffected = false;
 
 /**
  * Base configuration settings for MS SQL driver
@@ -62,11 +71,10 @@ class DboMssql extends DboSource {
  */
 	protected $_baseConfig = array(
 		'persistent' => true,
-		'host' => 'localhost',
-		'login' => 'root',
+		'host' => '(local)\sqlexpress',
+		'login' => '',
 		'password' => '',
-		'database' => 'cake',
-		'port' => '1433',
+		'database' => 'cake'
 	);
 
 /**
@@ -92,7 +100,6 @@ class DboMssql extends DboSource {
  * Index of basic SQL commands
  *
  * @var array
- * @access protected
  */
 	protected $_commands = array(
 		'begin'    => 'BEGIN TRANSACTION',
@@ -104,92 +111,44 @@ class DboMssql extends DboSource {
  * Define if the last query had error
  *
  * @var string
- * @access private
  */
 	private $__lastQueryHadError = false;
-/**
- * MS SQL DBO driver constructor; sets SQL Server error reporting defaults
- *
- * @param array $config Configuration data from app/config/databases.php
- * @return boolean True if connected successfully, false on error
- */
-	function __construct($config, $autoConnect = true) {
-		if ($autoConnect) {
-			if (!function_exists('mssql_min_message_severity')) {
-				trigger_error(__d('cake_dev', "PHP SQL Server interface is not installed, cannot continue. For troubleshooting information, see http://php.net/mssql/"), E_USER_WARNING);
-			}
-			mssql_min_message_severity(15);
-			mssql_min_error_severity(2);
-		}
-		return parent::__construct($config, $autoConnect);
-	}
 
 /**
  * Connects to the database using options in the given configuration array.
  *
  * @return boolean True if the database could be connected, else false
  */
-	function connect() {
+	public function connect() {
 		$config = $this->config;
-
-		$os = env('OS');
-		if (!empty($os) && strpos($os, 'Windows') !== false) {
-			$sep = ',';
-		} else {
-			$sep = ':';
-		}
 		$this->connected = false;
-
-		if (is_numeric($config['port'])) {
-			$port = $sep . $config['port'];	// Port number
-		} elseif ($config['port'] === null) {
-			$port = '';						// No port - SQL Server 2005
-		} else {
-			$port = '\\' . $config['port'];	// Named pipe
-		}
-
-		if (!$config['persistent']) {
-			$this->connection = mssql_connect($config['host'] . $port, $config['login'], $config['password'], true);
-		} else {
-			$this->connection = mssql_pconnect($config['host'] . $port, $config['login'], $config['password']);
-		}
-
-		if (mssql_select_db($config['database'], $this->connection)) {
-			$this->_execute("SET DATEFORMAT ymd");
+		try {
+			$flags = array(PDO::ATTR_PERSISTENT => $config['persistent']);
+			if (!empty($config['encoding'])) {
+				$flags[PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
+			}
+			$this->_connection = new PDO(
+				"sqlsrv:server={$config['host']};Database={$config['database']}",
+				$config['login'],
+				$config['password'],
+				$flags
+			);
 			$this->connected = true;
+		} catch (PDOException $e) {
+			throw new MissingConnectionException(array('class' => $e->getMessage()));
 		}
+
+//		$this->_execute("SET DATEFORMAT ymd");
 		return $this->connected;
 	}
 
 /**
- * Check that MsSQL is installed/loaded
+ * Check that PDO SQL Server is installed/loaded
  *
  * @return boolean
  */
-	function enabled() {
-		return extension_loaded('mssql');
-	}
-/**
- * Disconnects from database.
- *
- * @return boolean True if the database could be disconnected, else false
- */
-	function disconnect() {
-		@mssql_free_result($this->results);
-		$this->connected = !@mssql_close($this->connection);
-		return !$this->connected;
-	}
-
-/**
- * Executes given SQL statement.
- *
- * @param string $sql SQL statement
- * @return resource Result resource identifier
- */
-	protected function _execute($sql) {
-		$result = @mssql_query($sql, $this->connection);
-		$this->__lastQueryHadError = ($result === false);
-		return $result;
+	public function enabled() {
+		return in_array('sqlsrv', PDO::getAvailableDrivers());
 	}
 
 /**
@@ -197,23 +156,24 @@ class DboMssql extends DboSource {
  *
  * @return array Array of tablenames in the database
  */
-	function listSources() {
+	public function listSources() {
 		$cache = parent::listSources();
-
-		if ($cache != null) {
+		if ($cache !== null) {
 			return $cache;
 		}
-		$result = $this->fetchAll('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES', false);
+		$result = $this->_execute("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'");
 
-		if (!$result || empty($result)) {
+		if (!$result) {
+			$result->closeCursor();
 			return array();
 		} else {
 			$tables = array();
 
-			foreach ($result as $table) {
-				$tables[] = $table[0]['TABLE_NAME'];
+			while ($line = $result->fetch()) {
+				$tables[] = $line[0];
 			}
 
+			$result->closeCursor();
 			parent::listSources($tables);
 			return $tables;
 		}
@@ -225,42 +185,45 @@ class DboMssql extends DboSource {
  * @param Model $model Model object to describe
  * @return array Fields in table. Keys are name and type
  */
-	function describe($model) {
+	public function describe($model) {
 		$cache = parent::describe($model);
-
 		if ($cache != null) {
 			return $cache;
 		}
-
+		$fields = false;
 		$table = $this->fullTableName($model, false);
-		$cols = $this->fetchAll("SELECT COLUMN_NAME as Field, DATA_TYPE as Type, COL_LENGTH('" . $table . "', COLUMN_NAME) as Length, IS_NULLABLE As [Null], COLUMN_DEFAULT as [Default], COLUMNPROPERTY(OBJECT_ID('" . $table . "'), COLUMN_NAME, 'IsIdentity') as [Key], NUMERIC_SCALE as Size FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" . $table . "'", false);
+		$cols = $this->_execute("SELECT COLUMN_NAME as Field, DATA_TYPE as Type, COL_LENGTH('" . $table . "', COLUMN_NAME) as Length, IS_NULLABLE As [Null], COLUMN_DEFAULT as [Default], COLUMNPROPERTY(OBJECT_ID('" . $table . "'), COLUMN_NAME, 'IsIdentity') as [Key], NUMERIC_SCALE as Size FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '" . $table . "'");
+		if (!$cols) {
+			throw new CakeException(__d('cake_dev', 'Could not describe table for %s', $model->name));
+		}
 
-		$fields = false;
 		foreach ($cols as $column) {
-			$field = $column[0]['Field'];
+			$field = $column->Field;
 			$fields[$field] = array(
-				'type' => $this->column($column[0]['Type']),
-				'null' => (strtoupper($column[0]['Null']) == 'YES'),
-				'default' => preg_replace("/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/", "$1", $column[0]['Default']),
-				'length' => intval($column[0]['Length']),
-				'key' => ($column[0]['Key'] == '1') ? 'primary' : false
+				'type' => $this->column($column->Type),
+				'null' => ($column->Null === 'YES' ? true : false),
+				'default' => preg_replace("/^[(]{1,2}'?([^')]*)?'?[)]{1,2}$/", "$1", $column->Default),
+				'length' => intval($column->Length),
+				'key' => ($column->Key == '1') ? 'primary' : false
 			);
+
 			if ($fields[$field]['default'] === 'null') {
 				$fields[$field]['default'] = null;
 			} else {
 				$this->value($fields[$field]['default'], $fields[$field]['type']);
 			}
 
-			if ($fields[$field]['key'] && $fields[$field]['type'] == 'integer') {
+			if ($fields[$field]['key'] !== false && $fields[$field]['type'] == 'integer') {
 				$fields[$field]['length'] = 11;
-			} elseif (!$fields[$field]['key']) {
+			} elseif ($fields[$field]['key'] === false) {
 				unset($fields[$field]['key']);
 			}
 			if (in_array($fields[$field]['type'], array('date', 'time', 'datetime', 'timestamp'))) {
 				$fields[$field]['length'] = null;
 			}
 		}
-		$this->__cacheDescription($this->fullTableName($model, false), $fields);
+		$this->__cacheDescription($table, $fields);
+		$cols->closeCursor();
 		return $fields;
 	}
 
@@ -272,9 +235,15 @@ class DboMssql extends DboSource {
  * @param boolean $safe Whether or not numeric data should be handled automagically if no column data is provided
  * @return string Quoted and escaped data
  */
-	function value($data, $column = null, $safe = false) {
+	public function value($data, $column = null, $safe = false) {
 		$parent = parent::value($data, $column, $safe);
 
+		if ($column === 'float' && strpos($data, '.') !== false) {
+			return rtrim($data, '0');
+		}
+		if ($parent === "''" && ($column === null || $column !== 'string')) {
+			return 'NULL';
+		}
 		if ($parent != null) {
 			return $parent;
 		}
@@ -315,7 +284,7 @@ class DboMssql extends DboSource {
  * @param mixed $fields
  * @return array
  */
-	function fields($model, $alias = null, $fields = array(), $quote = true) {
+	public function fields($model, $alias = null, $fields = array(), $quote = true) {
 		if (empty($alias)) {
 			$alias = $model->alias;
 		}
@@ -331,7 +300,7 @@ class DboMssql extends DboSource {
 					$prepend = 'DISTINCT ';
 					$fields[$i] = trim(str_replace('DISTINCT', '', $fields[$i]));
 				}
-				$fieldAlias = count($this->__fieldMappings);
+				$fieldAlias = count($this->_fieldMappings);
 
 				if (!preg_match('/\s+AS\s+/i', $fields[$i])) {
 					if (substr($fields[$i], -1) == '*') {
@@ -348,12 +317,12 @@ class DboMssql extends DboSource {
 					}
 
 					if (strpos($fields[$i], '.') === false) {
-						$this->__fieldMappings[$alias . '__' . $fieldAlias] = $alias . '.' . $fields[$i];
+						$this->_fieldMappings[$alias . '__' . $fieldAlias] = $alias . '.' . $fields[$i];
 						$fieldName  = $this->name($alias . '.' . $fields[$i]);
 						$fieldAlias = $this->name($alias . '__' . $fieldAlias);
 					} else {
 						$build = explode('.', $fields[$i]);
-						$this->__fieldMappings[$build[0] . '__' . $fieldAlias] = $fields[$i];
+						$this->_fieldMappings[$build[0] . '__' . $fieldAlias] = $fields[$i];
 						$fieldName  = $this->name($build[0] . '.' . $build[1]);
 						$fieldAlias = $this->name(preg_replace("/^\[(.+)\]$/", "$1", $build[0]) . '__' . $fieldAlias);
 					}
@@ -381,7 +350,7 @@ class DboMssql extends DboSource {
  * @param mixed $conditions
  * @return array
  */
-	function create($model, $fields = null, $values = null) {
+	public function create($model, $fields = null, $values = null) {
 		if (!empty($values)) {
 			$fields = array_combine($fields, $values);
 		}
@@ -411,7 +380,7 @@ class DboMssql extends DboSource {
  * @param mixed $conditions
  * @return array
  */
-	function update($model, $fields = array(), $values = null, $conditions = null) {
+	public function update($model, $fields = array(), $values = null, $conditions = null) {
 		if (!empty($values)) {
 			$fields = array_combine($fields, $values);
 		}
@@ -425,65 +394,13 @@ class DboMssql extends DboSource {
 	}
 
 /**
- * Returns a formatted error message from previous database operation.
- *
- * @return string Error message with error number
- */
-	function lastError() {
-		if ($this->__lastQueryHadError) {
-			$error = mssql_get_last_message();
-			if ($error && !preg_match('/contexto de la base de datos a|contesto di database|changed database|contexte de la base de don|datenbankkontext/i', $error)) {
-				return $error;
-			}
-		}
-		return null;
-	}
-
-/**
- * Returns number of affected rows in previous database operation. If no previous operation exists,
- * this returns false.
- *
- * @return integer Number of affected rows
- */
-	function lastAffected() {
-		if ($this->_result) {
-			return mssql_rows_affected($this->connection);
-		}
-		return null;
-	}
-
-/**
- * Returns number of rows in previous resultset. If no previous resultset exists,
- * this returns false.
- *
- * @return integer Number of rows in resultset
- */
-	function lastNumRows() {
-		if ($this->_result) {
-			return @mssql_num_rows($this->_result);
-		}
-		return null;
-	}
-
-/**
- * Returns the ID generated from the previous INSERT operation.
- *
- * @param unknown_type $source
- * @return in
- */
-	function lastInsertId($source = null) {
-		$id = $this->fetchRow('SELECT SCOPE_IDENTITY() AS insertID', false);
-		return $id[0]['insertID'];
-	}
-
-/**
  * Returns a limit statement in the correct format for the particular database.
  *
  * @param integer $limit Limit of results returned
  * @param integer $offset Offset from which to start results
  * @return string SQL limit/offset statement
  */
-	function limit($limit, $offset = null) {
+	public function limit($limit, $offset = null) {
 		if ($limit) {
 			$rt = '';
 			if (!strpos(strtolower($limit), 'top') || strpos(strtolower($limit), 'top') === 0) {
@@ -504,7 +421,7 @@ class DboMssql extends DboSource {
  * @param string $real Real database-layer column type (i.e. "varchar(255)")
  * @return string Abstract column type (i.e. "string")
  */
-	function column($real) {
+	public function column($real) {
 		if (is_array($real)) {
 			$col = $real['name'];
 
@@ -544,33 +461,32 @@ class DboMssql extends DboSource {
 	}
 
 /**
- * Enter description here...
+ * Builds a map of the columns contained in a result
  *
- * @param unknown_type $results
+ * @param PDOStatement $results
  */
-	function resultSet(&$results) {
-		$this->results =& $results;
+	public function resultSet($results) {
 		$this->map = array();
-		$numFields = mssql_num_fields($results);
+		$numFields = $results->columnCount();
 		$index = 0;
-		$j = 0;
 
-		while ($j < $numFields) {
-			$column = mssql_field_name($results, $j);
+		while ($numFields-- > 0) {
+			$column = $results->getColumnMeta($index);
+			$name = $column['name'];
 
-			if (strpos($column, '__')) {
-				if (isset($this->__fieldMappings[$column]) && strpos($this->__fieldMappings[$column], '.')) {
-					$map = explode('.', $this->__fieldMappings[$column]);
-				} elseif (isset($this->__fieldMappings[$column])) {
-					$map = array(0, $this->__fieldMappings[$column]);
+			if (strpos($name, '__')) {
+				if (isset($this->_fieldMappings[$name]) && strpos($this->_fieldMappings[$name], '.')) {
+					$map = explode('.', $this->_fieldMappings[$name]);
+				} elseif (isset($this->_fieldMappings[$name])) {
+					$map = array(0, $this->_fieldMappings[$name]);
 				} else {
-					$map = array(0, $column);
+					$map = array(0, $name);
 				}
-				$this->map[$index++] = $map;
 			} else {
-				$this->map[$index++] = array(0, $column);
+				$map = array(0, $name);
 			}
-			$j++;
+			$map[] = ($column['sqlsrv:decl_type'] == 'bit') ? 'boolean' : $column['native_type'];
+			$this->map[$index++] = $map;
 		}
 	}
 
@@ -581,7 +497,7 @@ class DboMssql extends DboSource {
  * @param array $data Query data
  * @return string
  */
-	function renderStatement($type, $data) {
+	public function renderStatement($type, $data) {
 		switch (strtolower($type)) {
 			case 'select':
 				extract($data);
@@ -631,9 +547,8 @@ class DboMssql extends DboSource {
  *
  * @param string $order
  * @return string
- * @access private
  */
-	function __switchSort($order) {
+	private function __switchSort($order) {
 		$order = preg_replace('/\s+ASC/i', '__tmp_asc__', $order);
 		$order = preg_replace('/\s+DESC/i', ' ASC', $order);
 		return preg_replace('/__tmp_asc__/', ' DESC', $order);
@@ -644,13 +559,12 @@ class DboMssql extends DboSource {
  *
  * @param string $sql A snippet of SQL representing an ORDER or WHERE statement
  * @return string The value of $sql with field names replaced
- * @access private
  */
-	function __mapFields($sql) {
-		if (empty($sql) || empty($this->__fieldMappings)) {
+	private function __mapFields($sql) {
+		if (empty($sql) || empty($this->_fieldMappings)) {
 			return $sql;
 		}
-		foreach ($this->__fieldMappings as $key => $val) {
+		foreach ($this->_fieldMappings as $key => $val) {
 			$sql = preg_replace('/' . preg_quote($val) . '/', $this->name($key), $sql);
 			$sql = preg_replace('/' . preg_quote($this->name($val)) . '/', $this->name($key), $sql);
 		}
@@ -665,31 +579,31 @@ class DboMssql extends DboSource {
  * @param boolean $cache Enables returning/storing cached query results
  * @return array Array of resultset rows, or false if no rows matched
  */
-	function read($model, $queryData = array(), $recursive = null) {
+	public function read($model, $queryData = array(), $recursive = null) {
 		$results = parent::read($model, $queryData, $recursive);
-		$this->__fieldMappings = array();
+		$this->_fieldMappings = array();
 		return $results;
 	}
 
 /**
  * Fetches the next row from the current result set
  *
- * @return unknown
+ * @return mixed
  */
-	function fetchResult() {
-		if ($row = mssql_fetch_row($this->results)) {
+	public function fetchResult() {
+		if ($row = $this->_result->fetch()) {
 			$resultRow = array();
-			$i = 0;
-
-			foreach ($row as $index => $field) {
-				list($table, $column) = $this->map[$index];
-				$resultRow[$table][$column] = $row[$index];
-				$i++;
+			foreach ($this->map as $col => $meta) {
+				list($table, $column, $type) = $meta;
+				$resultRow[$table][$column] = $row[$col];
+				if ($type === 'boolean' && !is_null($row[$col])) {
+					$resultRow[$table][$column] = $this->boolean($resultRow[$table][$column]);
+				}
 			}
 			return $resultRow;
-		} else {
-			return false;
 		}
+		$this->_result->closeCursor();
+		return false;
 	}
 
 /**
@@ -709,7 +623,16 @@ class DboMssql extends DboSource {
 		if ($hasPrimaryKey) {
 			$this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' ON');
 		}
-		parent::insertMulti($table, $fields, $values);
+
+		$table = $this->fullTableName($table);
+		$fields = implode(', ', array_map(array(&$this, 'name'), $fields));
+		$this->begin();
+		foreach ($values as $value) {
+			$holder = implode(', ', array_map(array(&$this, 'value'), $value));
+			$this->_execute("INSERT INTO {$table} ({$fields}) VALUES ({$holder})");
+		}
+		$this->commit();
+
 		if ($hasPrimaryKey) {
 			$this->_execute('SET IDENTITY_INSERT ' . $this->fullTableName($table) . ' OFF');
 		}
@@ -722,10 +645,14 @@ class DboMssql extends DboSource {
  *   where options can be 'default', 'length', or 'key'.
  * @return string
  */
-	function buildColumn($column) {
+	public function buildColumn($column) {
 		$result = preg_replace('/(int|integer)\([0-9]+\)/i', '$1', parent::buildColumn($column));
 		if (strpos($result, 'DEFAULT NULL') !== false) {
-			$result = str_replace('DEFAULT NULL', 'NULL', $result);
+			if (isset($column['default']) && $column['default'] === '') {
+				$result = str_replace('DEFAULT NULL', "DEFAULT ''", $result);
+			} else {
+				$result = str_replace('DEFAULT NULL', 'NULL', $result);
+			}
 		} else if (array_keys($column) == array('type', 'name')) {
 			$result .= ' NULL';
 		}
@@ -739,7 +666,7 @@ class DboMssql extends DboSource {
  * @param string $table
  * @return string
  */
-	function buildIndex($indexes, $table = null) {
+	public function buildIndex($indexes, $table = null) {
 		$join = array();
 
 		foreach ($indexes as $name => $value) {
@@ -763,17 +690,14 @@ class DboMssql extends DboSource {
 /**
  * Makes sure it will return the primary key
  *
- * @param mixed $model
- * @access protected
+ * @param mixed $model Model instance of table name
  * @return string
  */
-	function _getPrimaryKey($model) {
-		if (is_object($model)) {
-			$schema = $model->schema();
-		} else {
-			$schema = $this->describe($model);
+	protected function _getPrimaryKey($model) {
+		if (!is_object($model)) {
+			$model = new Model(false, $model);
 		}
-
+		$schema = $this->describe($model);
 		foreach ($schema as $field => $props) {
 			if (isset($props['key']) && $props['key'] == 'primary') {
 				return $field;
@@ -781,4 +705,49 @@ class DboMssql extends DboSource {
 		}
 		return null;
 	}
+
+/**
+ * Returns number of affected rows in previous database operation. If no previous operation exists,
+ * this returns false.
+ *
+ * @return integer Number of affected rows
+ */
+	public function lastAffected() {
+		$affected = parent::lastAffected();
+		if ($affected === null && $this->_lastAffected !== false) {
+			return $this->_lastAffected;
+		}
+		return $affected;
+	}
+/**
+ * Executes given SQL statement.
+ *
+ * @param string $sql SQL statement
+ * @param array $params list of params to be bound to query (supported only in select)
+ * @param array $prepareOptions Options to be used in the prepare statement
+ * @return mixed PDOStatement if query executes with no problem, true as the result of a succesfull, false on error
+ * query returning no rows, suchs as a CREATE statement, false otherwise
+ */
+	protected function _execute($sql, $params = array(), $prepareOptions = array()) {
+		$this->_lastAffected = false;
+		if (strncasecmp($sql, 'SELECT', 6) == 0) {
+			$prepareOptions += array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL);
+			return parent::_execute($sql, $params, $prepareOptions);
+		}
+		try {
+			$this->_lastAffected = $this->_connection->exec($sql);
+			if ($this->_lastAffected === false) {
+				$this->_results = null;
+				$error = $this->_connection->errorInfo();
+				$this->error = $error[2];
+				return false;
+			}
+			return true;
+		} catch (PDOException $e) {
+			$this->_results = null;
+			$this->error = $e->getMessage();
+			return false;
+		}
+	}
+
 }

+ 4 - 3
lib/Cake/Model/Datasource/DboSource.php

@@ -450,10 +450,11 @@ class DboSource extends DataSource {
  *
  * @param string $sql SQL statement
  * @param array $params list of params to be bound to query
- * @return PDOStatement if query executes with no problem, true as the result of a succesfull
+ * @param array $prepareOptions Options to be used in the prepare statement
+ * @return mixed PDOStatement if query executes with no problem, true as the result of a succesfull, false on error
  * query returning no rows, suchs as a CREATE statement, false otherwise
  */
-	protected function _execute($sql, $params = array()) {
+	protected function _execute($sql, $params = array(), $prepareOptions = array()) {
 		$sql = trim($sql);
 		if (preg_match('/^(?:CREATE|ALTER|DROP)/i', $sql)) {
 			$statements = array_filter(explode(';', $sql));
@@ -464,7 +465,7 @@ class DboSource extends DataSource {
 		}
 
 		try {
-			$query = $this->_connection->prepare($sql);
+			$query = $this->_connection->prepare($sql, $prepareOptions);
 			$query->setFetchMode(PDO::FETCH_LAZY);
 			if (!$query->execute($params)) {
 				$this->_results = $query;

+ 1 - 1
lib/Cake/Test/Case/Model/Behavior/ContainableBehaviorTest.php

@@ -3420,7 +3420,7 @@ class ContainableBehaviorTest extends CakeTestCase {
 					'foreignKey'			=> 'article_id',
 					'associationForeignKey' => 'tag_id',
 					// LENGHT function mysql-only, using LIKE does almost the same
-					'conditions' 			=> 'ShortTag.tag LIKE "???"'
+					'conditions' 			=> "ShortTag.tag LIKE '???'"
 				)
 			)
 		);

+ 2 - 2
lib/Cake/Test/Case/Model/Behavior/TranslateBehaviorTest.php

@@ -431,8 +431,8 @@ class TranslateBehaviorTest extends CakeTestCase {
 		$expected = array(1 => 'Titel #1', 2 => 'Titel #2', 3 => 'Titel #3');
 		$this->assertEqual($expected, $result);
 
-		// MSSQL trigger an error and stops the page even if the debug = 0
-		if ($this->db instanceof Mssql) {
+		// SQL Server trigger an error and stops the page even if the debug = 0
+		if ($this->db instanceof Sqlserver) {
 			$debug = Configure::read('debug');
 			Configure::write('debug', 0);
 

+ 4 - 0
lib/Cake/Test/Case/Model/BehaviorCollectionTest.php

@@ -606,6 +606,8 @@ class BehaviorCollectionTest extends CakeTestCase {
  * @return void
  */
 	function testBehaviorFindCallbacks() {
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$Apple = new Apple();
 		$expected = $Apple->find('all');
 
@@ -805,6 +807,8 @@ class BehaviorCollectionTest extends CakeTestCase {
  * @return void
  */
 	function testBehaviorBelongsToFindCallbacks() {
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$Apple = new Apple();
 		$Apple->unbindModel(array('hasMany' => array('Child'), 'hasOne' => array('Sample')), false);
 		$expected = $Apple->find('all');

+ 138 - 219
lib/Cake/Test/Case/Model/Datasource/Database/MssqlTest.php

@@ -1,6 +1,6 @@
 <?php
 /**
- * DboMssqlTest file
+ * SqlserverTest file
  *
  * PHP 5
  *
@@ -17,90 +17,57 @@
  * @license       MIT License (http://www.opensource.org/licenses/mit-license.php)
  */
 
-require_once CAKE.'Model'.DS.'Model.php';
-require_once CAKE.'Model'.DS.'Datasource'.DS.'DataSource.php';
-require_once CAKE.'Model'.DS.'Datasource'.DS.'DboSource.php';
-require_once CAKE.'Model'.DS.'Datasource'.DS.'Database'.DS.'Mssql.php';
+App::uses('Model', 'Model');
+App::uses('Sqlserver', 'Model/Datasource/Database');
 
 /**
- * DboMssqlTestDb class
+ * SqlserverTestDb class
  *
  * @package       cake.tests.cases.libs.model.datasources.dbo
  */
-class DboMssqlTestDb extends DboMssql {
+class SqlserverTestDb extends Sqlserver {
 
 /**
  * simulated property
  *
  * @var array
- * @access public
  */
 	public $simulated = array();
 
 /**
- * simalate property
+ * execute results stack
  *
  * @var array
- * @access public
  */
-	public $simulate = true;
-/**
- * fetchAllResultsStack
- *
- * @var array
- * @access public
- */
-	public $fetchAllResultsStack = array();
+	public $executeResultsStack = array();
 
 /**
  * execute method
  *
  * @param mixed $sql
- * @access protected
- * @return void
+ * @return mixed
  */
-	function _execute($sql) {
-		if ($this->simulate) {
-			$this->simulated[] = $sql;
-			return null;
-		} else {
-			return parent::_execute($sql);
-		}
+	protected function _execute($sql) {
+		$this->simulated[] = $sql;
+		return empty($this->executeResultsStack) ? null : array_pop($this->executeResultsStack);
 	}
 
 /**
  * fetchAll method
  *
  * @param mixed $sql
- * @access protected
  * @return void
  */
-	function _matchRecords($model, $conditions = null) {
+	protected function _matchRecords($model, $conditions = null) {
 		return $this->conditions(array('id' => array(1, 2)));
 	}
 
 /**
- * fetchAll method
- *
- * @param mixed $sql
- * @access protected
- * @return void
- */
-	function fetchAll($sql, $cache = true, $modelName = null) {
-		$result = parent::fetchAll($sql, $cache, $modelName);
-		if (!empty($this->fetchAllResultsStack)) {
-    		return array_pop($this->fetchAllResultsStack);
-		}
-		return $result;
-	}
-
-/**
  * getLastQuery method
  *
- * @access public
- * @return void
+ * @return string
  */
-	function getLastQuery() {
+	public function getLastQuery() {
 		return $this->simulated[count($this->simulated) - 1];
 	}
 
@@ -108,43 +75,50 @@ class DboMssqlTestDb extends DboMssql {
  * getPrimaryKey method
  *
  * @param mixed $model
- * @access public
- * @return void
+ * @return string
  */
-	function getPrimaryKey($model) {
+	public function getPrimaryKey($model) {
 		return parent::_getPrimaryKey($model);
 	}
+
 /**
  * clearFieldMappings method
  *
- * @access public
  * @return void
  */
-	function clearFieldMappings() {
-		$this->__fieldMappings = array();
+	public function clearFieldMappings() {
+		$this->_fieldMappings = array();
+	}
+	
+/**
+ * describe method
+ *
+ * @param object $model
+ * @return void
+ */
+	public function describe($model) {
+		return empty($this->describe) ? parent::describe($model) : $this->describe;
 	}
 }
 
 /**
- * MssqlTestModel class
+ * SqlserverTestModel class
  *
  * @package       cake.tests.cases.libs.model.datasources
  */
-class MssqlTestModel extends Model {
+class SqlserverTestModel extends Model {
 
 /**
  * name property
  *
- * @var string 'MssqlTestModel'
- * @access public
+ * @var string 'SqlserverTestModel'
  */
-	public $name = 'MssqlTestModel';
+	public $name = 'SqlserverTestModel';
 
 /**
  * useTable property
  *
  * @var bool false
- * @access public
  */
 	public $useTable = false;
 
@@ -152,7 +126,6 @@ class MssqlTestModel extends Model {
  * _schema property
  *
  * @var array
- * @access protected
  */
 	protected $_schema = array(
 		'id'		=> array('type' => 'integer', 'null' => '', 'default' => '', 'length' => '8', 'key' => 'primary'),
@@ -179,10 +152,9 @@ class MssqlTestModel extends Model {
  * belongsTo property
  *
  * @var array
- * @access public
  */
 	public $belongsTo = array(
-		'MssqlClientTestModel' => array(
+		'SqlserverClientTestModel' => array(
 			'foreignKey' => 'client_id'
 		)
 	);
@@ -193,10 +165,9 @@ class MssqlTestModel extends Model {
  * @param mixed $fields
  * @param mixed $order
  * @param mixed $recursive
- * @access public
  * @return void
  */
-	function find($conditions = null, $fields = null, $order = null, $recursive = null) {
+	public function find($conditions = null, $fields = null, $order = null, $recursive = null) {
 		return $conditions;
 	}
 
@@ -207,50 +178,37 @@ class MssqlTestModel extends Model {
  * @param mixed $fields
  * @param mixed $order
  * @param mixed $recursive
- * @access public
- * @return void
+ * @return array
  */
-	function findAll($conditions = null, $fields = null, $order = null, $recursive = null) {
+	public function findAll($conditions = null, $fields = null, $order = null, $recursive = null) {
 		return $conditions;
 	}
-
-/**
- * setSchema method
- *
- * @param array $schema
- * @access public
- * @return void
- */
-	function setSchema($schema) {
-		$this->_schema = $schema;
-	}
 }
 
 /**
- * MssqlClientTestModel class
+ * SqlserverClientTestModel class
  *
  * @package       cake.tests.cases.libs.model.datasources
  */
-class MssqlClientTestModel extends Model {
+class SqlserverClientTestModel extends Model {
 /**
  * name property
  *
- * @var string 'MssqlAssociatedTestModel'
- * @access public
+ * @var string 'SqlserverAssociatedTestModel'
  */
-	public $name = 'MssqlClientTestModel';
+	public $name = 'SqlserverClientTestModel';
+
 /**
  * useTable property
  *
  * @var bool false
- * @access public
  */
 	public $useTable = false;
+
 /**
  * _schema property
  *
  * @var array
- * @access protected
  */
 	protected $_schema = array(
 		'id'		=> array('type' => 'integer', 'null' => '', 'default' => '', 'length' => '8', 'key' => 'primary'),
@@ -260,18 +218,32 @@ class MssqlClientTestModel extends Model {
 		'updated'	=> array('type' => 'datetime', 'null' => '1', 'default' => '', 'length' => null)
 	);
 }
+
+/**
+ * SqlserverTestResultIterator class
+ *
+ * @package       cake.tests.cases.libs.model.datasources
+ */
+class SqlserverTestResultIterator extends ArrayIterator {
+/**
+ * closeCursor method
+ *
+ * @return void
+ */
+	public function closeCursor() {}
+}
+
 /**
- * DboMssqlTest class
+ * SqlserverTest class
  *
  * @package       cake.tests.cases.libs.model.datasources.dbo
  */
-class DboMssqlTest extends CakeTestCase {
+class SqlserverTest extends CakeTestCase {
 
 /**
  * The Dbo instance to be tested
  *
  * @var DboSource
- * @access public
  */
 	public $db = null;
 
@@ -279,70 +251,46 @@ class DboMssqlTest extends CakeTestCase {
  * autoFixtures property
  *
  * @var bool false
- * @access public
  */
 	public $autoFixtures = false;
+
 /**
  * fixtures property
  *
  * @var array
- * @access public
  */
 	public $fixtures = array('core.category');
-/**
- * Skip if cannot connect to mssql
- *
- */
-	public function skip() {
-		$this->_initDb();
-		$this->skipUnless($this->db->config['driver'] == 'mssql', '%s SQL Server connection not available');
-	}
 
 /**
- * Make sure all fixtures tables are being created
- *
- */
-	public function start() {
-		$this->db->simulate = false;
-		parent::start();
-		$this->db->simulate = true;
-	}
-/**
- * Make sure all fixtures tables are being dropped
- *
- */
-	public function end() {
-		$this->db->simulate = false;
-		parent::end();
-		$this->db->simulate = true;
-	}
-/**
  * Sets up a Dbo class instance for testing
  *
  */
 	public function setUp() {
-		$db = ConnectionManager::getDataSource('test');
-		$this->db = new DboMssqlTestDb($db->config);
-		$this->model = new MssqlTestModel();
+		$this->Dbo = ConnectionManager::getDataSource('test');
+		if (!($this->Dbo instanceof Sqlserver)) {
+			$this->markTestSkipped('Please configure the test datasource to use SQL Server.');
+		}
+		$this->db = new SqlserverTestDb($this->Dbo->config);
+		$this->model = new SqlserverTestModel();
 	}
 
 /**
  * tearDown method
  *
- * @access public
  * @return void
  */
-	function tearDown() {
+	public function tearDown() {
+		unset($this->Dbo);
+		unset($this->db);
 		unset($this->model);
 	}
 
 /**
  * testQuoting method
  *
- * @access public
  * @return void
  */
-	function testQuoting() {
+	public function testQuoting() {
 		$expected = "1.2";
 		$result = $this->db->value(1.2, 'float');
 		$this->assertIdentical($expected, $result);
@@ -366,29 +314,28 @@ class DboMssqlTest extends CakeTestCase {
 /**
  * testFields method
  *
- * @access public
  * @return void
  */
-	function testFields() {
+	public function testFields() {
 		$fields = array(
-			'[MssqlTestModel].[id] AS [MssqlTestModel__0]',
-			'[MssqlTestModel].[client_id] AS [MssqlTestModel__1]',
-			'[MssqlTestModel].[name] AS [MssqlTestModel__2]',
-			'[MssqlTestModel].[login] AS [MssqlTestModel__3]',
-			'[MssqlTestModel].[passwd] AS [MssqlTestModel__4]',
-			'[MssqlTestModel].[addr_1] AS [MssqlTestModel__5]',
-			'[MssqlTestModel].[addr_2] AS [MssqlTestModel__6]',
-			'[MssqlTestModel].[zip_code] AS [MssqlTestModel__7]',
-			'[MssqlTestModel].[city] AS [MssqlTestModel__8]',
-			'[MssqlTestModel].[country] AS [MssqlTestModel__9]',
-			'[MssqlTestModel].[phone] AS [MssqlTestModel__10]',
-			'[MssqlTestModel].[fax] AS [MssqlTestModel__11]',
-			'[MssqlTestModel].[url] AS [MssqlTestModel__12]',
-			'[MssqlTestModel].[email] AS [MssqlTestModel__13]',
-			'[MssqlTestModel].[comments] AS [MssqlTestModel__14]',
-			'CONVERT(VARCHAR(20), [MssqlTestModel].[last_login], 20) AS [MssqlTestModel__15]',
-			'[MssqlTestModel].[created] AS [MssqlTestModel__16]',
-			'CONVERT(VARCHAR(20), [MssqlTestModel].[updated], 20) AS [MssqlTestModel__17]'
+			'[SqlserverTestModel].[id] AS [SqlserverTestModel__0]',
+			'[SqlserverTestModel].[client_id] AS [SqlserverTestModel__1]',
+			'[SqlserverTestModel].[name] AS [SqlserverTestModel__2]',
+			'[SqlserverTestModel].[login] AS [SqlserverTestModel__3]',
+			'[SqlserverTestModel].[passwd] AS [SqlserverTestModel__4]',
+			'[SqlserverTestModel].[addr_1] AS [SqlserverTestModel__5]',
+			'[SqlserverTestModel].[addr_2] AS [SqlserverTestModel__6]',
+			'[SqlserverTestModel].[zip_code] AS [SqlserverTestModel__7]',
+			'[SqlserverTestModel].[city] AS [SqlserverTestModel__8]',
+			'[SqlserverTestModel].[country] AS [SqlserverTestModel__9]',
+			'[SqlserverTestModel].[phone] AS [SqlserverTestModel__10]',
+			'[SqlserverTestModel].[fax] AS [SqlserverTestModel__11]',
+			'[SqlserverTestModel].[url] AS [SqlserverTestModel__12]',
+			'[SqlserverTestModel].[email] AS [SqlserverTestModel__13]',
+			'[SqlserverTestModel].[comments] AS [SqlserverTestModel__14]',
+			'CONVERT(VARCHAR(20), [SqlserverTestModel].[last_login], 20) AS [SqlserverTestModel__15]',
+			'[SqlserverTestModel].[created] AS [SqlserverTestModel__16]',
+			'CONVERT(VARCHAR(20), [SqlserverTestModel].[updated], 20) AS [SqlserverTestModel__17]'
 		);
 
 		$result = $this->db->fields($this->model);
@@ -396,7 +343,7 @@ class DboMssqlTest extends CakeTestCase {
 		$this->assertEqual($expected, $result);
 
 		$this->db->clearFieldMappings();
-		$result = $this->db->fields($this->model, null, 'MssqlTestModel.*');
+		$result = $this->db->fields($this->model, null, 'SqlserverTestModel.*');
 		$expected = $fields;
 		$this->assertEqual($expected, $result);
 
@@ -408,23 +355,22 @@ class DboMssqlTest extends CakeTestCase {
 		$this->assertEqual($expected, $result);
 
 		$this->db->clearFieldMappings();
-		$result = $this->db->fields($this->model, null, array('*', 'MssqlClientTestModel.*'));
+		$result = $this->db->fields($this->model, null, array('*', 'SqlserverClientTestModel.*'));
 		$expected = array_merge($fields, array(
-			'[MssqlClientTestModel].[id] AS [MssqlClientTestModel__18]',
-			'[MssqlClientTestModel].[name] AS [MssqlClientTestModel__19]',
-			'[MssqlClientTestModel].[email] AS [MssqlClientTestModel__20]',
-			'CONVERT(VARCHAR(20), [MssqlClientTestModel].[created], 20) AS [MssqlClientTestModel__21]',
-			'CONVERT(VARCHAR(20), [MssqlClientTestModel].[updated], 20) AS [MssqlClientTestModel__22]'));
+			'[SqlserverClientTestModel].[id] AS [SqlserverClientTestModel__18]',
+			'[SqlserverClientTestModel].[name] AS [SqlserverClientTestModel__19]',
+			'[SqlserverClientTestModel].[email] AS [SqlserverClientTestModel__20]',
+			'CONVERT(VARCHAR(20), [SqlserverClientTestModel].[created], 20) AS [SqlserverClientTestModel__21]',
+			'CONVERT(VARCHAR(20), [SqlserverClientTestModel].[updated], 20) AS [SqlserverClientTestModel__22]'));
 		$this->assertEqual($expected, $result);
 	}
 
 /**
  * testDistinctFields method
  *
- * @access public
  * @return void
  */
-	function testDistinctFields() {
+	public function testDistinctFields() {
 		$result = $this->db->fields($this->model, null, array('DISTINCT Car.country_code'));
 		$expected = array('DISTINCT [Car].[country_code] AS [Car__0]');
 		$this->assertEqual($expected, $result);
@@ -437,12 +383,11 @@ class DboMssqlTest extends CakeTestCase {
 /**
  * testDistinctWithLimit method
  *
- * @access public
  * @return void
  */
-	function testDistinctWithLimit() {
+	public function testDistinctWithLimit() {
 		$this->db->read($this->model, array(
-			'fields' => array('DISTINCT MssqlTestModel.city', 'MssqlTestModel.country'),
+			'fields' => array('DISTINCT SqlserverTestModel.city', 'SqlserverTestModel.country'),
 			'limit' => 5
 		));
 		$result = $this->db->getLastQuery();
@@ -452,23 +397,20 @@ class DboMssqlTest extends CakeTestCase {
 /**
  * testDescribe method
  *
- * @access public
  * @return void
  */
-	function testDescribe() {
-		$MssqlTableDescription = array(
-			0 => array(
-				0 => array(
-					'Default' => '((0))',
-					'Field' => 'count',
-					'Key' => 0,
-					'Length' => '4',
-					'Null' => 'NO',
-					'Type' => 'integer',
-				)
+	public function testDescribe() {
+		$SqlserverTableDescription = new SqlserverTestResultIterator(array(
+			(object) array(
+				'Default' => '((0))',
+				'Field' => 'count',
+				'Key' => 0,
+				'Length' => '4',
+				'Null' => 'NO',
+				'Type' => 'integer'
 			)
-		);
-		$this->db->fetchAllResultsStack = array($MssqlTableDescription);
+		));
+		$this->db->executeResultsStack = array($SqlserverTableDescription);
 		$dummyModel = $this->model;
 		$result = $this->db->describe($dummyModel);
 		$expected = array(
@@ -484,15 +426,15 @@ class DboMssqlTest extends CakeTestCase {
 /**
  * testBuildColumn
  *
- * @return unknown_type
+ * @return void
  */
 	public function testBuildColumn() {
-		$column = array('name' => 'id', 'type' => 'integer', 'null' => '', 'default' => '', 'length' => '8', 'key' => 'primary');
+		$column = array('name' => 'id', 'type' => 'integer', 'null' => false, 'default' => '', 'length' => '8', 'key' => 'primary');
 		$result = $this->db->buildColumn($column);
 		$expected = '[id] int IDENTITY (1, 1) NOT NULL';
 		$this->assertEqual($expected, $result);
 
-		$column = array('name' => 'client_id', 'type' => 'integer', 'null' => '', 'default' => '0', 'length' => '11');
+		$column = array('name' => 'client_id', 'type' => 'integer', 'null' => false, 'default' => '0', 'length' => '11');
 		$result = $this->db->buildColumn($column);
 		$expected = '[client_id] int DEFAULT 0 NOT NULL';
 		$this->assertEqual($expected, $result);
@@ -513,7 +455,7 @@ class DboMssqlTest extends CakeTestCase {
 		$expected = '[name] varchar(255) NULL';
 		$this->assertEqual($expected, $result);
 
-		$column = array('name' => 'name', 'type' => 'string', 'null' => '', 'default' => '', 'length' => '255');
+		$column = array('name' => 'name', 'type' => 'string', 'null' => false, 'default' => '', 'length' => '255');
 		$result = $this->db->buildColumn($column);
 		$expected = '[name] varchar(255) DEFAULT \'\' NOT NULL';
 		$this->assertEqual($expected, $result);
@@ -570,13 +512,13 @@ class DboMssqlTest extends CakeTestCase {
  * @return void
  */
 	public function testUpdateAllSyntax() {
-		$fields = array('MssqlTestModel.client_id' => '[MssqlTestModel].[client_id] + 1');
-		$conditions = array('MssqlTestModel.updated <' => date('2009-01-01 00:00:00'));
+		$fields = array('SqlserverTestModel.client_id' => '[SqlserverTestModel].[client_id] + 1');
+		$conditions = array('SqlserverTestModel.updated <' => date('2009-01-01 00:00:00'));
 		$this->db->update($this->model, $fields, null, $conditions);
 
 		$result = $this->db->getLastQuery();
-		$this->assertNoPattern('/MssqlTestModel/', $result);
-		$this->assertPattern('/^UPDATE \[mssql_test_models\]/', $result);
+		$this->assertNoPattern('/SqlserverTestModel/', $result);
+		$this->assertPattern('/^UPDATE \[sqlserver_test_models\]/', $result);
 		$this->assertPattern('/SET \[client_id\] = \[client_id\] \+ 1/', $result);
 	}
 
@@ -586,21 +528,16 @@ class DboMssqlTest extends CakeTestCase {
  * @return void
  */
 	public function testGetPrimaryKey() {
-		// When param is a model
+		$schema = $this->model->schema();
+		
+		$this->db->describe = $schema;
 		$result = $this->db->getPrimaryKey($this->model);
 		$this->assertEqual($result, 'id');
-
-		$schema = $this->model->schema();
+		
 		unset($schema['id']['key']);
-		$this->model->setSchema($schema);
+		$this->db->describe = $schema;
 		$result = $this->db->getPrimaryKey($this->model);
 		$this->assertNull($result);
-
-		// When param is a table name
-		$this->db->simulate = false;
-		$this->loadFixtures('Category');
-		$result = $this->db->getPrimaryKey('categories');
-		$this->assertEqual($result, 'id');
 	}
 
 /**
@@ -609,52 +546,34 @@ class DboMssqlTest extends CakeTestCase {
  * @return void
  */
 	public function testInsertMulti() {
+		$this->db->describe = $this->model->schema();
+		
 		$fields = array('id', 'name', 'login');
-		$values = array('(1, \'Larry\', \'PhpNut\')', '(2, \'Renan\', \'renan.saddam\')');
+		$values = array(
+			array(1, 'Larry', 'PhpNut'),
+			array(2, 'Renan', 'renan.saddam'));
 		$this->db->simulated = array();
 		$this->db->insertMulti($this->model, $fields, $values);
 		$result = $this->db->simulated;
 		$expected = array(
-			'SET IDENTITY_INSERT [mssql_test_models] ON',
-			'INSERT INTO [mssql_test_models] ([id], [name], [login]) VALUES (1, \'Larry\', \'PhpNut\')',
-    		'INSERT INTO [mssql_test_models] ([id], [name], [login]) VALUES (2, \'Renan\', \'renan.saddam\')',
-			'SET IDENTITY_INSERT [mssql_test_models] OFF'
+			'SET IDENTITY_INSERT [sqlserver_test_models] ON',
+			"INSERT INTO [sqlserver_test_models] ([id], [name], [login]) VALUES (1, 'Larry', 'PhpNut')",
+			"INSERT INTO [sqlserver_test_models] ([id], [name], [login]) VALUES (2, 'Renan', 'renan.saddam')",
+			'SET IDENTITY_INSERT [sqlserver_test_models] OFF'
 		);
 		$this->assertEqual($expected, $result);
 
 		$fields = array('name', 'login');
-		$values = array('(\'Larry\', \'PhpNut\')', '(\'Renan\', \'renan.saddam\')');
+		$values = array(
+			array('Larry', 'PhpNut'),
+			array('Renan', 'renan.saddam'));
 		$this->db->simulated = array();
 		$this->db->insertMulti($this->model, $fields, $values);
 		$result = $this->db->simulated;
 		$expected = array(
-			'INSERT INTO [mssql_test_models] ([name], [login]) VALUES (\'Larry\', \'PhpNut\')',
-    		'INSERT INTO [mssql_test_models] ([name], [login]) VALUES (\'Renan\', \'renan.saddam\')'
+			"INSERT INTO [sqlserver_test_models] ([name], [login]) VALUES ('Larry', 'PhpNut')",
+			"INSERT INTO [sqlserver_test_models] ([name], [login]) VALUES ('Renan', 'renan.saddam')",
 		);
 		$this->assertEqual($expected, $result);
 	}
-/**
- * testLastError
- *
- * @return void
- */
-	public function testLastError() {
-		$debug = Configure::read('debug');
-		Configure::write('debug', 0);
-
-		$this->db->simulate = false;
-		$query = 'SELECT [name] FROM [categories]';
-		$this->assertTrue($this->db->execute($query) !== false);
-		$this->assertNull($this->db->lastError());
-
-		$query = 'SELECT [inexistent_field] FROM [categories]';
-		$this->assertFalse($this->db->execute($query));
-		$this->assertNotNull($this->db->lastError());
-
-		$query = 'SELECT [name] FROM [categories]';
-		$this->assertTrue($this->db->execute($query) !== false);
-		$this->assertNull($this->db->lastError());
-
-		Configure::write('debug', $debug);
-	}
 }

+ 4 - 0
lib/Cake/Test/Case/Model/ModelIntegrationTest.php

@@ -634,6 +634,8 @@ class ModelIntegrationTest extends BaseModelTest {
  * @return void
  */
 	function testDeconstructFieldsTime() {
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$this->loadFixtures('Apple');
 		$TestModel = new Apple();
 
@@ -721,6 +723,8 @@ class ModelIntegrationTest extends BaseModelTest {
  * @return void
  */
 	function testDeconstructFieldsDateTime() {
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$this->loadFixtures('Apple');
 		$TestModel = new Apple();
 

+ 24 - 24
lib/Cake/Test/Case/Model/ModelReadTest.php

@@ -79,8 +79,8 @@ class ModelReadTest extends BaseModelTest {
  */
 	function testGroupBy() {
 		$db = ConnectionManager::getDataSource('test');
-		$isStrictGroupBy = $this->db instanceof Postgres || $this->db instanceof Sqlite || $this->db instanceof Oracle;
-		$message = 'Postgres and Oracle have strict GROUP BY and are incompatible with this test.';
+		$isStrictGroupBy = $this->db instanceof Postgres || $this->db instanceof Sqlite || $this->db instanceof Oracle || $this->db instanceof Sqlserver;
+		$message = 'Postgres, Oracle, SQLite and SQL Server have strict GROUP BY and are incompatible with this test.';
 
 		if ($this->skipIf($isStrictGroupBy, $message )) {
 			return;
@@ -367,13 +367,6 @@ class ModelReadTest extends BaseModelTest {
  * @return void
  */
 	function testVeryStrangeUseCase() {
-		$message = "skipping SELECT * FROM ? WHERE ? = ? AND ? = ?; prepared query.";
-		$message .= " MSSQL is incompatible with this test.";
-
-		if ($this->skipIf($this->db instanceof Mssql, $message)) {
-			return;
-		}
-
 		$this->loadFixtures('Article', 'User', 'Tag', 'ArticlesTag');
 		$Article = new Article();
 
@@ -397,6 +390,10 @@ class ModelReadTest extends BaseModelTest {
  * @return void
  */
 	function testRecursiveUnbind() {
+		if ($this->skipIf($this->db instanceof Sqlserver, 'The test of testRecursiveUnbind test is not compatible with SQL Server, because it check for time columns.')) {
+			return;
+		}
+		
 		$this->loadFixtures('Apple', 'Sample');
 		$TestModel = new Apple();
 		$TestModel->recursive = 2;
@@ -2992,7 +2989,7 @@ class ModelReadTest extends BaseModelTest {
  * @return void
  */
 	function testSelfAssociationAfterFind() {
-		$this->loadFixtures('Apple');
+		$this->loadFixtures('Apple', 'Sample');
 		$afterFindModel = new NodeAfterFind();
 		$afterFindModel->recursive = 3;
 		$afterFindData = $afterFindModel->find('all');
@@ -3651,6 +3648,10 @@ class ModelReadTest extends BaseModelTest {
  * @return void
  */
 	function testFindCombinedRelations() {
+		if ($this->skipIf($this->db instanceof Sqlserver, 'The test of testRecursiveUnbind test is not compatible with SQL Server, because it check for time columns.')) {
+			return;
+		}
+		
 		$this->loadFixtures('Apple', 'Sample');
 		$TestModel = new Apple();
 
@@ -3947,7 +3948,7 @@ class ModelReadTest extends BaseModelTest {
 
 		$TestModel = new Basket();
 		$recursive = 3;
-		$result = $TestModel->find('all', compact('conditions', 'recursive'));
+		$result = $TestModel->find('all', compact('recursive'));
 
 		$expected = array(
 			array(
@@ -6227,7 +6228,7 @@ class ModelReadTest extends BaseModelTest {
 
 		// These tests are expected to fail on SQL Server since the LIMIT/OFFSET
 		// hack can't handle small record counts.
-		if ($this->db instanceof Mssql) {
+		if (!($this->db instanceof Sqlserver)) {
 			$result = $TestModel->find('all', array('limit' => 3, 'page' => 2));
 			$expected = array(
 				array(
@@ -6621,13 +6622,9 @@ class ModelReadTest extends BaseModelTest {
  * @return void
  */
 	function testFindCountDistinct() {
-		$skip = $this->skipIf(
-			$this->db instanceof Sqlite,
-			'SELECT COUNT(DISTINCT field) is not compatible with SQLite'
-		);
-		if ($skip) {
-			return;
-		}
+		$this->skipIf($this->db instanceof Sqlite, 'SELECT COUNT(DISTINCT field) is not compatible with SQLite');
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$this->loadFixtures('Project');
 		$TestModel = new Project();
 		$TestModel->create(array('name' => 'project')) && $TestModel->save();
@@ -7398,10 +7395,13 @@ class ModelReadTest extends BaseModelTest {
 		$result = $Post->find('first');
 		$this->assertEqual($result['Post']['two'], 2);
 
-		$Post->Author->virtualFields = array('false' => '1 = 2');
-		$result = $Post->find('first');
-		$this->assertEqual($result['Post']['two'], 2);
-		$this->assertFalse((bool)$result['Author']['false']);
+		// SQL Server does not support operators in expressions
+		if (!($this->db instanceof Sqlserver)) {
+			$Post->Author->virtualFields = array('false' => '1 = 2');
+			$result = $Post->find('first');
+			$this->assertEqual($result['Post']['two'], 2);
+			$this->assertFalse((bool)$result['Author']['false']);
+		}
 
 		$result = $Post->find('first',array('fields' => array('author_id')));
 		$this->assertFalse(isset($result['Post']['two']));
@@ -7470,7 +7470,7 @@ class ModelReadTest extends BaseModelTest {
  *
  */
 	public function testVirtualFieldsMysql() {
-		if ($this->skipIf(!($this->db instanceof Mysql), 'The rest of virtualFieds test is not compatible with Postgres')) {
+		if ($this->skipIf(!($this->db instanceof Mysql), 'The rest of virtualFieds test only compatible with Mysql')) {
 			return;
 		}
 		$this->loadFixtures('Post', 'Author');

+ 11 - 2
lib/Cake/Test/Case/Model/ModelWriteTest.php

@@ -415,7 +415,12 @@ class ModelWriteTest extends BaseModelTest {
 		}
 
 		$this->loadFixtures('CategoryThread');
-		$this->db->query('ALTER TABLE '. $this->db->fullTableName('category_threads') . " ADD COLUMN child_count INTEGER");
+		$column = 'COLUMN ';
+		if ($this->db instanceof Sqlserver) {
+			$column = '';
+		}
+		$column .= $this->db->buildColumn(array('name' => 'child_count', 'type' => 'integer'));
+		$this->db->query('ALTER TABLE '. $this->db->fullTableName('category_threads') . ' ADD ' . $column);
 		$Category = new CategoryThread();
 		$result = $Category->updateAll(array('CategoryThread.name' => "'updated'"), array('CategoryThread.parent_id' => 5));
 		$this->assertFalse(empty($result));
@@ -424,7 +429,7 @@ class ModelWriteTest extends BaseModelTest {
 		$Category->belongsTo['ParentCategory']['counterCache'] = 'child_count';
 		$Category->updateCounterCache(array('parent_id' => 5));
 		$result = Set::extract($Category->find('all', array('conditions' => array('CategoryThread.id' => 5))), '{n}.CategoryThread.child_count');
-		$expected = array_fill(0, 1, 1);
+		$expected = array(1);
 		$this->assertEqual($expected, $result);
 	}
 
@@ -3829,6 +3834,8 @@ class ModelWriteTest extends BaseModelTest {
  * @return void
  */
 	function testUpdateAllEmptyValues() {
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$this->loadFixtures('Author', 'Post');
 		$model = new Author();
 		$result = $model->updateAll(array('user' => '""'));
@@ -3948,6 +3955,8 @@ class ModelWriteTest extends BaseModelTest {
  * @return void
  */
 	function testSaveAllEmptyData() {
+		$this->skipIf($this->db instanceof Sqlserver, 'This test is not compatible with SQL Server.');
+
 		$this->loadFixtures('Article', 'ProductUpdateAll', 'Comment', 'Attachment');
 		$model = new Article();
 		$result = $model->saveAll(array(), array('validate' => 'first'));

+ 2 - 2
lib/Cake/Test/Case/Network/CakeRequestTest.php

@@ -172,7 +172,7 @@ class CakeRequestTestCase extends CakeTestCase {
 	function testFILESParsing() {
 		$_FILES = array('data' => array('name' => array(
 			'File' => array(
-					array('data' => 'cake_mssql_patch.patch'),
+					array('data' => 'cake_sqlserver_patch.patch'),
 					array('data' => 'controller.diff'),
 					array('data' => ''),
 					array('data' => ''),
@@ -221,7 +221,7 @@ class CakeRequestTestCase extends CakeTestCase {
 		$expected = array(
 			'File' => array(
 				array('data' => array(
-					'name' => 'cake_mssql_patch.patch',
+					'name' => 'cake_sqlserver_patch.patch',
 					'type' => '',
 					'tmp_name' => '/private/var/tmp/phpy05Ywj',
 					'error' => 0,

+ 1 - 1
lib/Cake/Test/Fixture/ArticlesTagFixture.php

@@ -41,7 +41,7 @@ class ArticlesTagFixture extends CakeTestFixture {
 	public $fields = array(
 		'article_id' => array('type' => 'integer', 'null' => false),
 		'tag_id' => array('type' => 'integer', 'null' => false),
-		'indexes' => array('UNIQUE_TAG' => array('column'=> array('article_id', 'tag_id'), 'unique'=>1))
+		'indexes' => array('UNIQUE_TAG2' => array('column'=> array('article_id', 'tag_id'), 'unique'=>1))
 	);
 
 /**