2
0
Просмотр исходного кода

ZF-7396
- Promoting Zend_Db SQL Server adapter to trunk

git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@17184 44c647ce-9c0f-0410-b52a-842ac1e357ba

ralph 16 лет назад
Родитель
Сommit
ca09c59ca7

+ 53 - 0
documentation/manual/en/module_specs/Zend_Db_Adapter.xml

@@ -1945,6 +1945,59 @@ if (!is_null($version)) {
             </itemizedlist>
         </sect3>
 
+		<sect3 id="zend.db.adapter.adapter-notes.sqlsrv">
+			<title>Microsoft SQL Server</title>
+			<itemizedlist>
+				<listitem>
+					<para>
+						Specify this Adapter to the <methodname>factory()</methodname> method with the name 'Sqlsrv'.
+					</para>
+				</listitem>
+				<listitem>
+					<para>
+						This Adapter uses the PHP extension sqlsrv
+					</para>
+				</listitem>
+				<listitem>
+					<para>
+						Microsoft SQL Server does not support sequences, so <methodname>lastInsertId()</methodname> 
+						ignores primary key argument and returns the last value generated for an auto-increment 
+						key if a table name is specified or a last insert query returned id. The 
+						<methodname>lastSequenceId()</methodname> method returns <constant>NULL</constant>.
+					</para>
+				</listitem>
+				<listitem>
+					<para>
+						<classname>Zend_Db_Adapter_Sqlsrv</classname> sets <constant>QUOTED_IDENTIFIER ON</constant> 
+						immediately after connecting to a SQL Server database. This makes the driver use the standard 
+						SQL identifier delimiter symbol (<code>"</code>) instead of the proprietary square-brackets 
+						syntax SQL Server uses for delimiting identifiers.
+					</para>
+				</listitem>
+				<listitem>
+					<para>
+						You can specify <code>driver_options</code> as a key in the options array. The value can be 
+						a anything from here <ulink url="http://msdn.microsoft.com/en-us/library/cc296161(SQL.90).aspx">http://msdn.microsoft.com/en-us/library/cc296161(SQL.90).aspx</ulink>.
+					</para>
+				</listitem>
+				<listitem>
+					<para>
+						You can use <code>setTransactionIsolationLevel</code> to set isolation level for current 
+						connection. The value can be <constant>SQLSRV_TXN_READ_UNCOMMITTED</constant>, 
+						<constant>SQLSRV_TXN_READ_COMMITTED</constant>, <constant>SQLSRV_TXN_REPEATABLE_READ</constant>, 
+						<constant>SQLSRV_TXN_SNAPSHOT</constant> or <constant>SQLSRV_TXN_SERIALIZABLE</constant>.
+					</para>
+				</listitem>
+                <listitem>
+                    <para>
+                        As of <acronym>ZF</acronym> 1.9, the minimal supported build of the PHP SQL Server extension
+                        from Microsoft is 1.0.1924.0. and the MS SQL Server Native Client version 9.00.3042.00.
+                    </para>
+                </listitem>
+			</itemizedlist>
+		</sect3>
+
+
         <sect3 id="zend.db.adapter.adapter-notes.pdo-ibm">
             <title>PDO for IBM DB2 and Informix Dynamic Server (IDS)</title>
             <itemizedlist>

+ 624 - 0
library/Zend/Db/Adapter/Sqlsrv.php

@@ -0,0 +1,624 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Loader
+ */
+require_once 'Zend/Loader.php';
+
+/**
+ * @see Zend_Db_Adapter_Abstract
+ */
+require_once 'Zend/Db/Adapter/Abstract.php';
+
+/**
+ * @see Zend_Db_Statement_Sqlsrv
+ */
+require_once 'Zend/Db/Statement/Sqlsrv.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Sqlsrv extends Zend_Db_Adapter_Abstract
+{
+    /**
+     * User-provided configuration.
+     *
+     * Basic keys are:
+     *
+     * username => (string) Connect to the database as this username.
+     * password => (string) Password associated with the username.
+     * dbname   => Either the name of the local Oracle instance, or the
+     *             name of the entry in tnsnames.ora to which you want to connect.
+     *
+     * @var array
+     */
+    protected $_config = array(
+        'dbname'       => null,
+        'username'     => null,
+        'password'     => null,
+    );
+
+    /**
+     * Last insert id from INSERT query
+     *
+     * @var int
+     */
+    protected $_lastInsertId;
+
+    /**
+     * Query used to fetch last insert id
+     *
+     * @var string
+     */
+    protected $_lastInsertSQL = 'SELECT SCOPE_IDENTITY() as Current_Identity';
+
+    /**
+     * Keys are UPPERCASE SQL datatypes or the constants
+     * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE.
+     *
+     * Values are:
+     * 0 = 32-bit integer
+     * 1 = 64-bit integer
+     * 2 = float or decimal
+     *
+     * @var array Associative array of datatypes to values 0, 1, or 2.
+     */
+    protected $_numericDataTypes = array(
+        Zend_Db::INT_TYPE    => Zend_Db::INT_TYPE,
+        Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE,
+        Zend_Db::FLOAT_TYPE  => Zend_Db::FLOAT_TYPE,
+        'INT'                => Zend_Db::INT_TYPE,
+        'SMALLINT'           => Zend_Db::INT_TYPE,
+        'TINYINT'            => Zend_Db::INT_TYPE,
+        'BIGINT'             => Zend_Db::BIGINT_TYPE,
+        'DECIMAL'            => Zend_Db::FLOAT_TYPE,
+        'FLOAT'              => Zend_Db::FLOAT_TYPE,
+        'MONEY'              => Zend_Db::FLOAT_TYPE,
+        'NUMERIC'            => Zend_Db::FLOAT_TYPE,
+        'REAL'               => Zend_Db::FLOAT_TYPE,
+        'SMALLMONEY'         => Zend_Db::FLOAT_TYPE,
+    );
+
+    /**
+     * Default class name for a DB statement.
+     *
+     * @var string
+     */
+    protected $_defaultStmtClass = 'Zend_Db_Statement_Sqlsrv';
+
+    /**
+     * Creates a connection resource.
+     *
+     * @return void
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+    protected function _connect()
+    {
+        if (is_resource($this->_connection)) {
+            // connection already exists
+            return;
+        }
+
+        if (!extension_loaded('sqlsrv')) {
+            /**
+             * @see Zend_Db_Adapter_Sqlsrv_Exception
+             */
+            require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+            throw new Zend_Db_Adapter_Sqlsrv_Exception('The Sqlsrv extension is required for this adapter but the extension is not loaded');
+        }
+
+        $serverName = $this->_config['host'];
+        if (isset($this->_config['port'])) {
+            $port        = (integer) $this->_config['port'];
+            $serverName .= ', ' . $port;
+        } 
+
+        $connectionInfo = array(
+            'Database' => $this->_config['dbname'],
+            'UID'      => $this->_config['username'],
+            'PWD'      => $this->_config['password'],
+        );
+
+        if (!empty($this->_config['driver_options'])) {
+            foreach ($this->_config['driver_options'] as $option => $value) {
+                // A value may be a constant.
+                if (is_string($value)) {
+                    $constantValue = @constant(strtoupper($value));
+                    if ($constantValue === null) {
+                        $connectionInfo[$option] = $value;
+                    } else {
+                        $connectionInfo[$option] = $constantValue;
+                    }
+                }
+            }
+        }
+
+        $this->_connection = sqlsrv_connect($serverName, $connectionInfo);
+
+        if (!$this->_connection) {
+            /**
+             * @see Zend_Db_Adapter_Sqlsrv_Exception
+             */
+            require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+            throw new Zend_Db_Adapter_Sqlsrv_Exception(sqlsrv_errors());
+        }
+
+        sqlsrv_query($this->_connection, 'SET QUOTED_IDENTIFIER ON');
+    }
+
+    /**
+     * Set the transaction isoltion level.
+     *
+     * @param integer|null $level A fetch mode from SQLSRV_TXN_*.
+     * @return true
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+    public function setTransactionIsolationLevel($level = null)
+    {
+        $this->_connect();
+        $sql = null;
+
+        // Default transaction level in sql server
+        if ($level === null)
+        {
+            $level = SQLSRV_TXN_READ_COMMITTED;
+        }
+
+        switch ($level) {
+            case SQLSRV_TXN_READ_UNCOMMITTED:
+                $sql = "READ UNCOMMITTED";
+                break;
+            case SQLSRV_TXN_READ_COMMITTED:
+                $sql = "READ COMMITTED";
+                break;
+            case SQLSRV_TXN_REPEATABLE_READ:
+                $sql = "REPEATABLE READ";
+                break;
+            case SQLSRV_TXN_SNAPSHOT:
+                $sql = "SNAPSHOT";
+                break;
+            case SQLSRV_TXN_SERIALIZABLE:
+                $sql = "SERIALIZABLE";
+                break;
+            default:
+                require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+                throw new Zend_Db_Adapter_Sqlsrv_Exception("Invalid transaction isolation level mode '$level' specified");
+        }
+
+        if (!sqlsrv_query($this->_connection, "SET TRANSACTION ISOLATION LEVEL $sql;")) {
+            require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+            throw new Zend_Db_Adapter_Sqlsrv_Exception("Transaction cannot be changed to '$level'");
+        }
+
+        return true;
+    }
+
+    /**
+     * Test if a connection is active
+     *
+     * @return boolean
+     */
+    public function isConnected()
+    {
+        return (is_resource($this->_connection)
+                && (get_resource_type($this->_connection) == 'SQL Server Connection')
+        );
+    }
+
+    /**
+     * Force the connection to close.
+     *
+     * @return void
+     */
+    public function closeConnection()
+    {
+        if ($this->isConnected()) {
+            sqlsrv_close($this->_connection);
+        }
+        $this->_connection = null;
+    }
+
+    /**
+     * Returns an SQL statement for preparation.
+     *
+     * @param string $sql The SQL statement with placeholders.
+     * @return Zend_Db_Statement_Sqlsrv
+     */
+    public function prepare($sql)
+    {
+        $this->_connect();
+        $stmtClass = $this->_defaultStmtClass;
+
+        if (!class_exists($stmtClass)) {
+            require_once 'Zend/Loader.php';
+            Zend_Loader::loadClass($stmtClass);
+        }
+
+        $stmt = new $stmtClass($this, $sql);
+        $stmt->setFetchMode($this->_fetchMode);
+        return $stmt;
+    }
+
+    /**
+     * Quote a raw string.
+     *
+     * @param string $value     Raw string
+     * @return string           Quoted string
+     */
+    protected function _quote($value)
+    {
+        if (is_int($value)) {
+            return $value;
+        } elseif (is_float($value)) {
+            return sprintf('%F', $value);
+        }
+
+        return "'" . str_replace("'", "''", $value) . "'";
+    }
+
+    /**
+     * Gets the last ID generated automatically by an IDENTITY/AUTOINCREMENT column.
+     *
+     * As a convention, on RDBMS brands that support sequences
+     * (e.g. Oracle, PostgreSQL, DB2), this method forms the name of a sequence
+     * from the arguments and returns the last id generated by that sequence.
+     * On RDBMS brands that support IDENTITY/AUTOINCREMENT columns, this method
+     * returns the last value generated for such a column, and the table name
+     * argument is disregarded.
+     *
+     * @param string $tableName   OPTIONAL Name of table.
+     * @param string $primaryKey  OPTIONAL Name of primary key column.
+     * @return string
+     */
+    public function lastInsertId($tableName = null, $primaryKey = null)
+    {
+        if ($tableName) {
+            $tableName = $this->quote($tableName);
+            $sql       = 'SELECT IDENT_CURRENT (' . $tableName . ') as Current_Identity';
+            return (string) $this->fetchOne($sql);
+        }
+        
+        if ($this->_lastInsertId > 0) {
+            return (string) $this->_lastInsertId;
+        }
+
+        $sql = $this->_lastInsertSQL;
+        return (string) $this->fetchOne($sql);
+    }
+
+    /**
+     * Inserts a table row with specified data.
+     *
+     * @param mixed $table The table to insert data into.
+     * @param array $bind Column-value pairs.
+     * @return int The number of affected rows.
+     */
+    public function insert($table, array $bind)
+    {
+        // extract and quote col names from the array keys
+        $cols = array();
+        $vals = array();
+        foreach ($bind as $col => $val) {
+            $cols[] = $this->quoteIdentifier($col, true);
+            if ($val instanceof Zend_Db_Expr) {
+                $vals[] = $val->__toString();
+                unset($bind[$col]);
+            } else {
+                $vals[] = '?';
+            }
+        }
+
+        // build the statement
+        $sql = "INSERT INTO "
+             . $this->quoteIdentifier($table, true)
+             . ' (' . implode(', ', $cols) . ') '
+             . 'VALUES (' . implode(', ', $vals) . ')'
+             . ' ' . $this->_lastInsertSQL;
+
+        // execute the statement and return the number of affected rows
+        $stmt   = $this->query($sql, array_values($bind));
+        $result = $stmt->rowCount();
+
+        $stmt->nextRowset();
+
+        $this->_lastInsertId = $stmt->fetchColumn();
+
+        return $result;
+    }
+    
+    /**
+     * Returns a list of the tables in the database.
+     *
+     * @return array
+     */
+    public function listTables()
+    {
+        $this->_connect();
+        $sql = "SELECT name FROM sysobjects WHERE type = 'U' ORDER BY name";
+        return $this->fetchCol($sql);
+    }
+
+    /**
+     * Returns the column descriptions for a table.
+     *
+     * The return value is an associative array keyed by the column name,
+     * as returned by the RDBMS.
+     *
+     * The value of each array element is an associative array
+     * with the following keys:
+     *
+     * SCHEMA_NAME      => string; name of schema
+     * TABLE_NAME       => string;
+     * COLUMN_NAME      => string; column name
+     * COLUMN_POSITION  => number; ordinal position of column in table
+     * DATA_TYPE        => string; SQL datatype name of column
+     * DEFAULT          => string; default expression of column, null if none
+     * NULLABLE         => boolean; true if column can have nulls
+     * LENGTH           => number; length of CHAR/VARCHAR
+     * SCALE            => number; scale of NUMERIC/DECIMAL
+     * PRECISION        => number; precision of NUMERIC/DECIMAL
+     * UNSIGNED         => boolean; unsigned property of an integer type
+     * PRIMARY          => boolean; true if column is part of the primary key
+     * PRIMARY_POSITION => integer; position of column in primary key
+     * IDENTITY         => integer; true if column is auto-generated with unique values
+     *
+     * @todo Discover integer unsigned property.
+     *
+     * @param string $tableName
+     * @param string $schemaName OPTIONAL
+     * @return array
+     */
+    public function describeTable($tableName, $schemaName = null)
+    {
+        /**
+         * Discover metadata information about this table.
+         */
+        $sql    = "exec sp_columns @table_name = " . $this->quoteIdentifier($tableName, true);
+        $stmt   = $this->query($sql);
+        $result = $stmt->fetchAll(Zend_Db::FETCH_NUM);
+
+        $owner           = 1;
+        $table_name      = 2;
+        $column_name     = 3;
+        $type_name       = 5;
+        $precision       = 6;
+        $length          = 7;
+        $scale           = 8;
+        $nullable        = 10;
+        $column_def      = 12;
+        $column_position = 16;
+
+        /**
+         * Discover primary key column(s) for this table.
+         */
+        $tableOwner = $result[0][$owner];
+        $sql        = "exec sp_pkeys @table_owner = " . $tableOwner 
+                    . ", @table_name = " . $this->quoteIdentifier($tableName, true);
+        $stmt       = $this->query($sql);
+
+        $primaryKeysResult = $stmt->fetchAll(Zend_Db::FETCH_NUM);
+        $primaryKeyColumn  = array();
+
+        // Per http://msdn.microsoft.com/en-us/library/ms189813.aspx, 
+        // results from sp_keys stored procedure are:
+        // 0=TABLE_QUALIFIER 1=TABLE_OWNER 2=TABLE_NAME 3=COLUMN_NAME 4=KEY_SEQ 5=PK_NAME
+
+        $pkey_column_name = 3;
+        $pkey_key_seq     = 4;
+        foreach ($primaryKeysResult as $pkeysRow) {
+            $primaryKeyColumn[$pkeysRow[$pkey_column_name]] = $pkeysRow[$pkey_key_seq];
+        }
+
+        $desc = array();
+        $p    = 1;
+        foreach ($result as $key => $row) {
+            $identity = false;
+            $words    = explode(' ', $row[$type_name], 2);
+            if (isset($words[0])) {
+                $type = $words[0];
+                if (isset($words[1])) {
+                    $identity = (bool) preg_match('/identity/', $words[1]);
+                }
+            }
+
+            $isPrimary = array_key_exists($row[$column_name], $primaryKeyColumn);
+            if ($isPrimary) {
+                $primaryPosition = $primaryKeyColumn[$row[$column_name]];
+            } else {
+                $primaryPosition = null;
+            }
+
+            $desc[$this->foldCase($row[$column_name])] = array(
+                'SCHEMA_NAME'      => null, // @todo
+                'TABLE_NAME'       => $this->foldCase($row[$table_name]),
+                'COLUMN_NAME'      => $this->foldCase($row[$column_name]),
+                'COLUMN_POSITION'  => (int) $row[$column_position],
+                'DATA_TYPE'        => $type,
+                'DEFAULT'          => $row[$column_def],
+                'NULLABLE'         => (bool) $row[$nullable],
+                'LENGTH'           => $row[$length],
+                'SCALE'            => $row[$scale],
+                'PRECISION'        => $row[$precision],
+                'UNSIGNED'         => null, // @todo
+                'PRIMARY'          => $isPrimary,
+                'PRIMARY_POSITION' => $primaryPosition,
+                'IDENTITY'         => $identity,
+            );
+        }
+
+        return $desc;
+    }
+
+    /**
+     * Leave autocommit mode and begin a transaction.
+     *
+     * @return void
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+    protected function _beginTransaction()
+    {
+        if (!sqlsrv_begin_transaction($this->_connection)) {
+            require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+            throw new Zend_Db_Adapter_Sqlsrv_Exception(sqlsrv_errors());
+        }
+    }
+
+    /**
+     * Commit a transaction and return to autocommit mode.
+     *
+     * @return void
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+    protected function _commit()
+    {
+        if (!sqlsrv_commit($this->_connection)) {
+            require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+            throw new Zend_Db_Adapter_Sqlsrv_Exception(sqlsrv_errors());
+        }
+    }
+
+    /**
+     * Roll back a transaction and return to autocommit mode.
+     *
+     * @return void
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+    protected function _rollBack()
+    {
+        if (!sqlsrv_rollback($this->_connection)) {
+            require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+            throw new Zend_Db_Adapter_Sqlsrv_Exception(sqlsrv_errors());
+        }
+    }
+
+    /**
+     * Set the fetch mode.
+     *
+     * @todo Support FETCH_CLASS and FETCH_INTO.
+     *
+     * @param integer $mode A fetch mode.
+     * @return void
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+    public function setFetchMode($mode)
+    {
+        switch ($mode) {
+            case Zend_Db::FETCH_NUM:   // seq array
+            case Zend_Db::FETCH_ASSOC: // assoc array
+            case Zend_Db::FETCH_BOTH:  // seq+assoc array
+            case Zend_Db::FETCH_OBJ:   // object
+                $this->_fetchMode = $mode;
+                break;
+            case Zend_Db::FETCH_BOUND: // bound to PHP variable
+                require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+                throw new Zend_Db_Adapter_Sqlsrv_Exception('FETCH_BOUND is not supported yet');
+                break;
+            default:
+                require_once 'Zend/Db/Adapter/Sqlsrv/Exception.php';
+                throw new Zend_Db_Adapter_Sqlsrv_Exception("Invalid fetch mode '$mode' specified");
+                break;
+        }
+    }
+
+    /**
+     * Adds an adapter-specific LIMIT clause to the SELECT statement.
+     *
+     * @param string $sql
+     * @param integer $count
+     * @param integer $offset OPTIONAL
+     * @return string
+     * @throws Zend_Db_Adapter_Sqlsrv_Exception
+     */
+     public function limit($sql, $count, $offset = 0)
+     {
+        $count = intval($count);
+        if ($count <= 0) {
+            require_once 'Zend/Db/Adapter/Exception.php';
+            throw new Zend_Db_Adapter_Exception("LIMIT argument count=$count is not valid");
+        }
+
+        $offset = intval($offset);
+        if ($offset < 0) {
+            /** @see Zend_Db_Adapter_Exception */
+            require_once 'Zend/Db/Adapter/Exception.php';
+            throw new Zend_Db_Adapter_Exception("LIMIT argument offset=$offset is not valid");
+        }
+
+        $orderby = stristr($sql, 'ORDER BY');
+        if ($orderby !== false) {
+            $sort  = (stripos($orderby, ' desc') !== false) ? 'desc' : 'asc';
+            $order = str_ireplace('ORDER BY', '', $orderby);
+            $order = trim(preg_replace('/\bASC\b|\bDESC\b/i', '', $order));
+        }
+
+        $sql = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . ($count+$offset) . ' ', $sql);
+
+        $sql = 'SELECT * FROM (SELECT TOP ' . $count . ' * FROM (' . $sql . ') AS inner_tbl';
+        if ($orderby !== false) {
+            $sql .= ' ORDER BY ' . $order . ' ';
+            $sql .= (stripos($sort, 'asc') !== false) ? 'DESC' : 'ASC';
+        }
+        $sql .= ') AS outer_tbl';
+        if ($orderby !== false) {
+            $sql .= ' ORDER BY ' . $order . ' ' . $sort;
+        }
+
+        return $sql;
+    }
+
+    /**
+     * Check if the adapter supports real SQL parameters.
+     *
+     * @param string $type 'positional' or 'named'
+     * @return bool
+     */
+    public function supportsParameters($type)
+    {
+        if ($type == 'positional') {
+            return true;
+        }
+
+        // if its 'named' or anything else
+        return false;
+    }
+
+    /**
+     * Retrieve server version in PHP style
+     *
+     * @return string
+     */
+    public function getServerVersion()
+    {
+        $this->_connect();
+        $version = sqlsrv_client_info($this->_connection);
+
+        if ($version !== false) {
+            return $version['DriverVer'];
+        }
+
+        return null;
+    }
+}

+ 63 - 0
library/Zend/Db/Adapter/Sqlsrv/Exception.php

@@ -0,0 +1,63 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ *
+ */
+
+/**
+ * Zend
+ */
+require_once 'Zend/Db/Adapter/Exception.php';
+
+/**
+ * Zend_Db_Adapter_Sqlsrv_Exception
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Adapter
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_Sqlsrv_Exception extends Zend_Db_Adapter_Exception
+{
+    /**
+     * Constructor
+     *
+     * If $message is an array, the assumption is that the return value of 
+     * sqlsrv_errors() was provided. If so, it then retrieves the most recent
+     * error from that stack, and sets the message and code based on it.
+     *
+     * @param null|array|string $message
+     * @param null|int $code
+     */
+    public function __construct($message = null, $code = 0) 
+    {
+       if (is_array($message)) {
+            // Error should be array of errors
+            // We only need first one (?)
+            if (isset($message[0])) {
+                $message = $message[0];
+            }
+
+            $code    = (int)    $message['code'];
+            $message = (string) $message['message'];
+       } 
+       parent::__construct($message, new Exception($message, $code));
+   }
+}

+ 414 - 0
library/Zend/Db/Statement/Sqlsrv.php

@@ -0,0 +1,414 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Statement
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Db_Statement
+ */
+require_once 'Zend/Db/Statement.php';
+
+/**
+ * Extends for Microsoft SQL Server Driver for PHP
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Statement
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Statement_Sqlsrv extends Zend_Db_Statement
+{
+    /**
+     * The connection_stmt object.
+     */
+    protected $_stmt;
+
+    /**
+     * The connection_stmt object original string.
+     */
+    protected $_originalSQL;
+
+    /**
+     * Column names.
+     */
+    protected $_keys;
+
+    /**
+     * Query executed
+     */
+    protected $_executed = false;
+
+    /**
+     * Prepares statement handle
+     *
+     * @param string $sql
+     * @return void
+     * @throws Zend_Db_Statement_Sqlsrv_Exception
+     */
+    protected function _prepare($sql)
+    {
+        $connection = $this->_adapter->getConnection();
+
+        $this->_stmt = sqlsrv_prepare($connection, $sql);
+
+        if (!$this->_stmt) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception(sqlsrv_errors());
+        }
+
+        $this->_originalSQL = $sql;
+    }
+
+    /**
+     * Binds a parameter to the specified variable name.
+     *
+     * @param mixed $parameter Name the parameter, either integer or string.
+     * @param mixed $variable  Reference to PHP variable containing the value.
+     * @param mixed $type      OPTIONAL Datatype of SQL parameter.
+     * @param mixed $length    OPTIONAL Length of SQL parameter.
+     * @param mixed $options   OPTIONAL Other options.
+     * @return bool
+     * @throws Zend_Db_Statement_Exception
+     */
+    protected function _bindParam($parameter, &$variable, $type = null, $length = null, $options = null)
+    {
+        //Sql server doesn't support bind by name
+        return true;
+    }
+
+    /**
+     * Closes the cursor, allowing the statement to be executed again.
+     *
+     * @return bool
+     */
+    public function closeCursor()
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        sqlsrv_free_stmt($this->_stmt);
+        $this->_stmt = false;
+        return true;
+    }
+
+    /**
+     * Returns the number of columns in the result set.
+     * Returns null if the statement has no result set metadata.
+     *
+     * @return int The number of columns.
+     */
+    public function columnCount()
+    {
+        if ($this->_stmt && $this->_executed) {
+            return sqlsrv_num_fields($this->_stmt);
+        }
+
+        return 0;
+    }
+
+
+    /**
+     * Retrieves the error code, if any, associated with the last operation on
+     * the statement handle.
+     *
+     * @return string error code.
+     */
+    public function errorCode()
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        $error = sqlsrv_errors();
+        if (!$error) {
+            return false;
+        }
+
+        return $error[0]['code'];
+    }
+
+
+    /**
+     * Retrieves an array of error information, if any, associated with the
+     * last operation on the statement handle.
+     *
+     * @return array
+     */
+    public function errorInfo()
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        $error = sqlsrv_errors();
+        if (!$error) {
+            return false;
+        }
+
+        return array(
+            $error[0]['code'],
+            $error[0]['message'],
+        );
+    }
+
+
+    /**
+     * Executes a prepared statement.
+     *
+     * @param array $params OPTIONAL Values to bind to parameter placeholders.
+     * @return bool
+     * @throws Zend_Db_Statement_Exception
+     */
+    public function _execute(array $params = null)
+    {
+        $connection = $this->_adapter->getConnection();
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        if ($params !== null) {
+            if (!is_array($params)) {
+                $params = array($params);
+            }
+            $error = false;
+
+            // make all params passed by reference
+            $params_ = array();
+            $temp    = array();
+            $i       = 1;
+            foreach ($params as $param) {
+                $temp[$i]  = $param;
+                $params_[] = &$temp[$i];
+                $i++;
+            }
+            $params = $params_;
+        }
+
+        $this->_stmt = sqlsrv_query($connection, $this->_originalSQL, $params);
+
+        if (!$this->_stmt) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception(sqlsrv_errors());
+        }
+
+        $this->_executed = true;
+
+        return (!$this->_stmt);
+    }
+
+    /**
+     * Fetches a row from the result set.
+     *
+     * @param  int $style  OPTIONAL Fetch mode for this fetch operation.
+     * @param  int $cursor OPTIONAL Absolute, relative, or other.
+     * @param  int $offset OPTIONAL Number for absolute or relative cursors.
+     * @return mixed Array, object, or scalar depending on fetch mode.
+     * @throws Zend_Db_Statement_Exception
+     */
+    public function fetch($style = null, $cursor = null, $offset = null)
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        if (null === $style) {
+            $style = $this->_fetchMode;
+        }
+
+        $values = sqlsrv_fetch_array($this->_stmt, SQLSRV_FETCH_ASSOC);
+
+        if (!$values && (null !== $error = sqlsrv_errors())) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception($error);
+        }
+
+        if (null === $values) {
+            return null;
+        }
+
+        if (!$this->_keys) {
+            foreach ($values as $key => $value) {
+                $this->_keys[] = $this->_adapter->foldCase($key);
+            }
+        }
+
+        $values = array_values($values);
+
+        $row = false;
+        switch ($style) {
+            case Zend_Db::FETCH_NUM:
+                $row = $values;
+                break;
+            case Zend_Db::FETCH_ASSOC:
+                $row = array_combine($this->_keys, $values);
+                break;
+            case Zend_Db::FETCH_BOTH:
+                $assoc = array_combine($this->_keys, $values);
+                $row   = array_merge($values, $assoc);
+                break;
+            case Zend_Db::FETCH_OBJ:
+                $row = (object) array_combine($this->_keys, $values);
+                break;
+            case Zend_Db::FETCH_BOUND:
+                $assoc = array_combine($this->_keys, $values);
+                $row   = array_merge($values, $assoc);
+                $row   = $this->_fetchBound($row);
+                break;
+            default:
+                require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+                throw new Zend_Db_Statement_Sqlsrv_Exception("Invalid fetch mode '$style' specified");
+                break;
+        }
+
+        return $row;
+    }
+
+    /**
+     * Returns a single column from the next row of a result set.
+     *
+     * @param int $col OPTIONAL Position of the column to fetch.
+     * @return string
+     * @throws Zend_Db_Statement_Exception
+     */
+    public function fetchColumn($col = 0)
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        if (!sqlsrv_fetch($this->_stmt)) {
+            if (null !== $error = sqlsrv_errors()) {
+                require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+                throw new Zend_Db_Statement_Sqlsrv_Exception($error);
+            }
+
+            // If no error, there is simply no record
+            return false;
+        }
+
+        $data = sqlsrv_get_field($this->_stmt, $col); //0-based
+        if ($data === false) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception(sqlsrv_errors());
+        }
+
+        return $data;
+    }
+
+    /**
+     * Fetches the next row and returns it as an object.
+     *
+     * @param string $class  OPTIONAL Name of the class to create.
+     * @param array  $config OPTIONAL Constructor arguments for the class.
+     * @return mixed One object instance of the specified class.
+     * @throws Zend_Db_Statement_Exception
+     */
+    public function fetchObject($class = 'stdClass', array $config = array())
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        $obj = sqlsrv_fetch_object($this->_stmt);
+
+        if ($error = sqlsrv_errors()) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception($error);
+        }
+
+        /* @todo XXX handle parameters */
+
+        if (null === $obj) {
+            return false;
+        }
+
+        return $obj;
+    }
+
+    /**
+     * Returns metadata for a column in a result set.
+     *
+     * @param int $column
+     * @return mixed
+     * @throws Zend_Db_Statement_Sqlsrv_Exception
+     */
+    public function getColumnMeta($column)
+    {
+        $fields = sqlsrv_field_metadata($this->_stmt);
+
+        if (!$fields) {
+            throw new Zend_Db_Statement_Sqlsrv_Exception('Column metadata can not be fetched');
+        }
+
+        if (!isset($fields[$column])) {
+            throw new Zend_Db_Statement_Sqlsrv_Exception('Column index does not exist in statement');
+        }
+
+        return $fields[$column];
+    }
+
+    /**
+     * Retrieves the next rowset (result set) for a SQL statement that has
+     * multiple result sets.  An example is a stored procedure that returns
+     * the results of multiple queries.
+     *
+     * @return bool
+     * @throws Zend_Db_Statement_Exception
+     */
+    public function nextRowset()
+    {
+        if (sqlsrv_next_result($this->_stmt) === false) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception(sqlsrv_errors());
+        }
+
+        //else - moved to next (or there are no more rows)
+    }
+
+    /**
+     * Returns the number of rows affected by the execution of the
+     * last INSERT, DELETE, or UPDATE statement executed by this
+     * statement object.
+     *
+     * @return int     The number of rows affected.
+     * @throws Zend_Db_Statement_Exception
+     */
+    public function rowCount()
+    {
+        if (!$this->_stmt) {
+            return false;
+        }
+
+        if (!$this->_executed) {
+            return 0;
+        }
+
+        $num_rows = sqlsrv_rows_affected($this->_stmt);
+
+        // Strict check is necessary; 0 is a valid return value
+        if ($num_rows === false) {
+            require_once 'Zend/Db/Statement/Sqlsrv/Exception.php';
+            throw new Zend_Db_Statement_Sqlsrv_Exception(sqlsrv_errors());
+        }
+
+        return $num_rows;
+    }
+}

+ 59 - 0
library/Zend/Db/Statement/Sqlsrv/Exception.php

@@ -0,0 +1,59 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @package    Zend_Db
+ * @subpackage Statement
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Db_Statement_Exception
+ */
+require_once 'Zend/Db/Statement/Exception.php';
+
+/**
+ * @package    Zend_Db
+ * @subpackage Statement
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Statement_Sqlsrv_Exception extends Zend_Db_Statement_Exception
+{
+    /**
+     * Constructor
+     *
+     * If $message is an array, the assumption is that the return value of 
+     * sqlsrv_errors() was provided. If so, it then retrieves the most recent
+     * error from that stack, and sets the message and code based on it.
+     *
+     * @param null|array|string $message
+     * @param null|int $code
+     */
+    public function __construct($message = null, $code = 0) 
+    {
+       if (is_array($message)) {
+            // Error should be array of errors
+            // We only need first one (?)
+            if (isset($message[0])) {
+                $message = $message[0];
+            }
+
+            $code    = (int)    $message['code'];
+            $message = (string) $message['message'];
+       } 
+       parent::__construct($message, $code);
+   }
+}
+

+ 12 - 0
tests/TestConfiguration.php.dist

@@ -156,6 +156,18 @@ define('TESTS_ZEND_DB_ADAPTER_DB2_PASSWORD', null);
 define('TESTS_ZEND_DB_ADAPTER_DB2_DATABASE', 'sample');
 
 /**
+ * Zend_Db_Adapter_Sqlsrv
+ * Note: Make sure that you create the "test" database and set a 
+ * username and password
+ *
+ */
+define('TESTS_ZEND_DB_ADAPTER_SQLSRV_ENABLED',  false);
+define('TESTS_ZEND_DB_ADAPTER_SQLSRV_HOSTNAME', 'localhost\SQLEXPRESS');
+define('TESTS_ZEND_DB_ADAPTER_SQLSRV_USERNAME', null);
+define('TESTS_ZEND_DB_ADAPTER_SQLSRV_PASSWORD', null);
+define('TESTS_ZEND_DB_ADAPTER_SQLSRV_DATABASE', 'test');
+
+/**
  * Zend_Feed_Reader tests
  *
  * If the ONLINE_ENABLED property is false, only tests that can be executed

+ 438 - 0
tests/Zend/Db/Adapter/SqlsrvTest.php

@@ -0,0 +1,438 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: $
+ */
+
+/**
+ * @see Zend_Db_Adapter_TestCommon
+ */
+require_once 'Zend/Db/Adapter/TestCommon.php';
+
+/**
+ * @see Zend_Db_Adapter_Sqlsrv
+ */
+require_once 'Zend/Db/Adapter/Sqlsrv.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Adapter_SqlsrvTest extends Zend_Db_Adapter_TestCommon
+{
+    protected $_numericDataTypes = array(
+        Zend_Db::INT_TYPE    => Zend_Db::INT_TYPE,
+        Zend_Db::BIGINT_TYPE => Zend_Db::BIGINT_TYPE,
+        Zend_Db::FLOAT_TYPE  => Zend_Db::FLOAT_TYPE,
+        'INT'                => Zend_Db::INT_TYPE,
+        'SMALLINT'           => Zend_Db::INT_TYPE,
+        'TINYINT'            => Zend_Db::INT_TYPE,
+        'BIGINT'             => Zend_Db::BIGINT_TYPE,
+        'DECIMAL'            => Zend_Db::FLOAT_TYPE,
+        'FLOAT'              => Zend_Db::FLOAT_TYPE,
+        'MONEY'              => Zend_Db::FLOAT_TYPE,
+        'NUMERIC'            => Zend_Db::FLOAT_TYPE,
+        'REAL'               => Zend_Db::FLOAT_TYPE,
+        'SMALLMONEY'         => Zend_Db::FLOAT_TYPE
+    );
+    
+    /**
+     * Test AUTO_QUOTE_IDENTIFIERS option
+     * Case: Zend_Db::AUTO_QUOTE_IDENTIFIERS = true
+     */
+    public function testAdapterAutoQuoteIdentifiersTrue()
+    {
+        $params = $this->_util->getParams();
+
+        $params['options'] = array(
+            Zend_Db::AUTO_QUOTE_IDENTIFIERS => true
+        );
+        $db = Zend_Db::factory($this->getDriver(), $params);
+        $db->getConnection();
+
+        $select = $this->_db->select();
+        $select->from('zfproducts');
+        $stmt = $this->_db->query($select);
+        $result = $stmt->fetchAll();
+        $this->assertEquals(3, count($result), 'Expected 3 rows in first query result');
+
+        $this->assertEquals(1, $result[0]['product_id']);
+    }
+
+    /**
+     * Test the Adapter's insert() method.
+     * This requires providing an associative array of column=>value pairs.
+     */
+    public function testAdapterInsert()
+    {
+        $row = array (
+            'bug_description' => 'New bug',
+            'bug_status'      => 'NEW',
+            'created_on'      => '2007-04-02',
+            'updated_on'      => '2007-04-02',
+            'reported_by'     => 'micky',
+            'assigned_to'     => 'goofy',
+            'verified_by'     => 'dduck'
+        );
+
+        $rowsAffected = $this->_db->insert('zfbugs', $row);
+        $this->assertEquals(1, $rowsAffected);
+
+        $lastInsertId = $this->_db->lastInsertId();
+        $this->assertType('string', $lastInsertId);
+        $this->assertEquals('5', (string) $lastInsertId,
+            'Expected new id to be 5');
+
+        $lastInsertId = $this->_db->lastInsertId('zfbugs');
+        $this->assertEquals('5', (string) $lastInsertId,
+            'Expected new id to be 5, selecting by table');
+    }
+
+    /**
+     * Test the Adapter's insert() method.
+     * This requires providing an associative array of column=>value pairs.
+     * Multiple rows are insert in one query
+     */
+    public function testAdapterMultipleInsert()
+    {
+        $row = array (
+            'bug_description' => 'New bug',
+            'bug_status'      => 'NEW',
+            'created_on'      => '2007-04-02',
+            'updated_on'      => '2007-04-02',
+            'reported_by'     => 'micky',
+            'assigned_to'     => 'goofy',
+            'verified_by'     => 'dduck'
+        );
+        
+        $bugs = $this->_db->quoteIdentifier('zfbugs');
+
+        $values = '(?, ?, ?, ?, ?, ?, ?)';
+
+        $query = 'INSERT INTO ' . $bugs . ' VALUES ' . implode(',', array($values, $values, $values));
+
+        $data = array();
+
+        for ($i = 0; $i < 3; $i++) {
+            foreach ($row as $value) {
+                $data[] = $value;
+            }
+        }
+
+        $stmt = $this->_db->query($query, $data);
+        $rowsAffected = $stmt->rowCount();
+        $this->assertEquals(3, $rowsAffected);
+    }
+
+    public function testAdapterDescribeTableAttributeColumn()
+    {
+        $desc = $this->_db->describeTable('zfproducts');
+
+        $this->assertEquals('zfproducts',   $desc['product_name']['TABLE_NAME']);
+        $this->assertEquals('product_name', $desc['product_name']['COLUMN_NAME']);
+        $this->assertEquals(2,              $desc['product_name']['COLUMN_POSITION']);
+        $this->assertRegExp('/varchar/i',   $desc['product_name']['DATA_TYPE']);
+        $this->assertEquals('',             $desc['product_name']['DEFAULT']);
+        $this->assertTrue($desc['product_name']['NULLABLE'], 'Expected product_name to be nullable');
+        $this->assertNull($desc['product_name']['SCALE'], 'scale is not 0');
+
+        // MS SQL Server reports varchar length in the PRECISION field.  Whaaa?!?
+        $this->assertEquals(100, $desc['product_name']['PRECISION'], 'precision is not 100');
+        $this->assertFalse($desc['product_name']['PRIMARY'], 'Expected product_name not to be a primary key');
+        $this->assertNull($desc['product_name']['PRIMARY_POSITION'], 'Expected product_name to return null for PRIMARY_POSITION');
+        $this->assertFalse($desc['product_name']['IDENTITY'], 'Expected product_name to return false for IDENTITY');
+    }
+
+    public function testAdapterDescribeTablePrimaryKeyColumn()
+    {
+        $desc = $this->_db->describeTable('zfproducts');
+
+        $this->assertEquals('zfproducts', $desc['product_id']['TABLE_NAME']);
+        $this->assertEquals('product_id', $desc['product_id']['COLUMN_NAME']);
+        $this->assertEquals(1,            $desc['product_id']['COLUMN_POSITION']);
+        $this->assertEquals('',           $desc['product_id']['DEFAULT']);
+        $this->assertFalse($desc['product_id']['NULLABLE'], 'Expected product_id not to be nullable');
+        $this->assertEquals(0,            $desc['product_id']['SCALE'], 'scale is not 0');
+        $this->assertEquals(10,           $desc['product_id']['PRECISION'], 'precision is not 10');
+        $this->assertTrue($desc['product_id']['PRIMARY'], 'Expected product_id to be a primary key');
+        $this->assertEquals(1,            $desc['product_id']['PRIMARY_POSITION']);
+    }
+
+    /**
+     * Test that quote() takes an array and returns
+     * an imploded string of comma-separated, quoted elements.
+     */
+    public function testAdapterQuoteArray()
+    {
+        $array = array("it's", 'all', 'right!');
+        $value = $this->_db->quote($array);
+        $this->assertEquals("'it''s', 'all', 'right!'", $value);
+    }
+
+    /**
+     * test that quote() escapes a double-quote
+     * character in a string.
+     */
+    public function testAdapterQuoteDoubleQuote()
+    {
+        $string = 'St John"s Wort';
+        $value  = $this->_db->quote($string);
+        $this->assertEquals("'St John\"s Wort'", $value);
+    }
+
+    /**
+     * test that quote() escapes a single-quote
+     * character in a string.
+     */
+    public function testAdapterQuoteSingleQuote()
+    {
+        $string = "St John's Wort";
+        $value  = $this->_db->quote($string);
+        $this->assertEquals("'St John''s Wort'", $value);
+    }
+
+    /**
+     * test that quoteInto() escapes a double-quote
+     * character in a string.
+     */
+    public function testAdapterQuoteIntoDoubleQuote()
+    {
+        $string = 'id=?';
+        $param  = 'St John"s Wort';
+        $value  = $this->_db->quoteInto($string, $param);
+        $this->assertEquals("id='St John\"s Wort'", $value);
+    }
+
+    /**
+     * test that quoteInto() escapes a single-quote
+     * character in a string.
+     */
+    public function testAdapterQuoteIntoSingleQuote()
+    {
+        $string = 'id = ?';
+        $param  = 'St John\'s Wort';
+        $value  = $this->_db->quoteInto($string, $param);
+        $this->assertEquals("id = 'St John''s Wort'", $value);
+    }
+
+    public function testAdapterInsertSequence()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support sequences.');
+    }
+
+    public function testAdapterInsertDbExpr()
+    {
+        $bugs   = $this->_db->quoteIdentifier('zfbugs');
+        $bug_id = $this->_db->quoteIdentifier('bug_id');
+        $expr   = new Zend_Db_Expr('2+3');
+
+        $row = array (
+            'bug_id'          => $expr,
+            'bug_description' => 'New bug',
+            'bug_status'      => 'NEW',
+            'created_on'      => '2007-04-02',
+            'updated_on'      => '2007-04-02',
+            'reported_by'     => 'micky',
+            'assigned_to'     => 'goofy',
+            'verified_by'     => 'dduck'
+        );
+
+        $this->_db->query("SET IDENTITY_INSERT $bugs ON");
+
+        $rowsAffected = $this->_db->insert('zfbugs', $row);
+        $this->assertEquals(1, $rowsAffected);
+
+        $this->_db->query("SET IDENTITY_INSERT $bugs OFF");
+
+        $value = $this->_db->fetchOne("SELECT $bug_id FROM $bugs WHERE $bug_id = 5");
+        $this->assertEquals(5, $value);
+    }
+
+    /**
+     * @group ZF-1541
+     */
+    public function testCharacterSetUtf8()
+    {
+        // Create a new adapter
+        $params = $this->_util->getParams();
+
+        $params['charset'] = 'utf8';
+
+        $db = Zend_Db::factory($this->getDriver(), $params);
+
+         // create a new util object, with the new db adapter
+        $driver    = $this->getDriver();
+        $utilClass = "Zend_Db_TestUtil_{$driver}";
+        $util      = new $utilClass();
+        $util->setAdapter($db);
+
+        // create test table using no identifier quoting
+        $util->createTable('charsetutf8', array(
+            'id'    => 'IDENTITY',
+            'stuff' => 'VARCHAR(32)'
+        ));
+        $tableName = $this->_util->getTableName('charsetutf8');
+
+        $table = $db->quoteIdentifier('charsetutf8');
+
+        $db->query("SET IDENTITY_INSERT $table ON");
+
+        // insert into the table
+        $numRows = $db->insert($tableName, array(
+            'id'    => 1,
+            'stuff' => 'äöüß'
+        ));
+
+        // check if the row was inserted as expected
+        $select = $db->select()->from($tableName, array('id', 'stuff'));
+
+        $stmt = $db->query($select);
+        $fetched = $stmt->fetchAll(Zend_Db::FETCH_NUM);
+        $a = array(
+            0 => array(0 => 1, 1 => 'äöüß')
+        );
+        $this->assertEquals($a, $fetched,
+            'result of query not as expected');
+
+        $db->query("SET IDENTITY_INSERT $table OFF");
+
+        // clean up
+        unset($stmt);
+        $util->dropTable($tableName);
+    }
+
+    public function testAdapterTransactionCommit()
+    {
+        $bugs   = $this->_db->quoteIdentifier('zfbugs');
+        $bug_id = $this->_db->quoteIdentifier('bug_id');
+
+        // notice the number of rows in connection 2
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(4, $count, 'Expecting to see 4 rows in bugs table (step 1)');
+
+        // start an explicit transaction in connection 1
+        $this->_db->beginTransaction();
+
+        // delete a row in connection 1
+        $rowsAffected = $this->_db->delete(
+            'zfbugs',
+            "$bug_id = 1"
+        );
+        $this->assertEquals(1, $rowsAffected);
+
+        // we should still see all rows in connection 2
+        // because the DELETE has not been committed yet
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(3, $count, 'Expecting to still see 4 rows in bugs table (step 2); perhaps Adapter is still in autocommit mode?');
+
+        // commit the DELETE
+        $this->_db->commit();
+
+        // now we should see one fewer rows in connection 2
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(3, $count, 'Expecting to see 3 rows in bugs table after DELETE (step 3)');
+
+        // delete another row in connection 1
+        $rowsAffected = $this->_db->delete(
+            'zfbugs',
+            "$bug_id = 2"
+        );
+        $this->assertEquals(1, $rowsAffected);
+
+        // we should see results immediately, because
+        // the db connection returns to auto-commit mode
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(2, $count);
+    }
+
+    public function testAdapterTransactionRollback()
+    {
+        $bugs   = $this->_db->quoteIdentifier('zfbugs');
+        $bug_id = $this->_db->quoteIdentifier('bug_id');
+
+        // notice the number of rows in connection 2
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(4, $count, 'Expecting to see 4 rows in bugs table (step 1)');
+
+        // start an explicit transaction in connection 1
+        $this->_db->beginTransaction();
+
+        // delete a row in connection 1
+        $rowsAffected = $this->_db->delete(
+            'zfbugs',
+            "$bug_id = 1"
+        );
+        $this->assertEquals(1, $rowsAffected);
+
+        // we should still see all rows in connection 2
+        // because the DELETE has not been committed yet
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(3, $count, 'Expecting to still see 4 rows in bugs table (step 2); perhaps Adapter is still in autocommit mode?');
+
+        // rollback the DELETE
+        $this->_db->rollback();
+
+        // now we should see the same number of rows
+        // because the DELETE was rolled back
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(4, $count, 'Expecting to still see 4 rows in bugs table after DELETE is rolled back (step 3)');
+
+        // delete another row in connection 1
+        $rowsAffected = $this->_db->delete(
+            'zfbugs',
+            "$bug_id = 2"
+        );
+        $this->assertEquals(1, $rowsAffected);
+
+        // we should see results immediately, because
+        // the db connection returns to auto-commit mode
+        $count = $this->_db->fetchOne("SELECT COUNT(*) FROM $bugs");
+        $this->assertEquals(3, $count, 'Expecting to see 3 rows in bugs table after DELETE (step 4)');
+    }
+
+    public function testCanChangeIsolationLevel()
+    {
+        $db = $this->_db;
+
+        // All of these should work
+        $this->assertTrue($db->setTransactionIsolationLevel(SQLSRV_TXN_READ_UNCOMMITTED));
+        $this->assertTrue($db->setTransactionIsolationLevel(SQLSRV_TXN_READ_COMMITTED));
+        $this->assertTrue($db->setTransactionIsolationLevel(SQLSRV_TXN_REPEATABLE_READ));
+        $this->assertTrue($db->setTransactionIsolationLevel(SQLSRV_TXN_SNAPSHOT));
+        $this->assertTrue($db->setTransactionIsolationLevel(SQLSRV_TXN_SERIALIZABLE));
+
+        try {
+            $db->setTransactionIsolationLevel('not existing isolation level');
+            $this->fail("Not existing isolation types are allowed to set");
+        } catch (Zend_Db_Adapter_Sqlsrv_Exception $e) {
+        }
+
+        $this->assertTrue($db->setTransactionIsolationLevel(), "Setting to default should work by passsing null or nothing");
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 52 - 0
tests/Zend/Db/Profiler/SqlsrvTest.php

@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id$
+ */
+
+
+/**
+ * @see Zend_Db_Profiler_TestCommon
+ */
+require_once 'Zend/Db/Profiler/TestCommon.php';
+
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Profiler_SqlsrvTest extends Zend_Db_Profiler_TestCommon
+{
+    public function testProfilerPreparedStatementWithBoundParams()
+    {
+        $this->markTestIncomplete($this->getDriver() . ' is having trouble with binding params');
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 57 - 0
tests/Zend/Db/Select/SqlsrvTest.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Select/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+class Zend_Db_Select_SqlsrvTest extends Zend_Db_Select_TestCommon
+{
+    public function testSelectQueryWithBinds()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support binding by name.');
+    }
+
+    public function testSelectColumnWithColonQuotedParameter()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support selecting int columns by varchar param.');
+    }
+
+    public function testSelectFromForUpdate()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support for update.');
+    }
+
+    public function testSelectFromQualified()
+    {
+        $this->markTestIncomplete($this->getDriver() . ' needs more syntax for qualified table names.');
+    }
+
+    public function testSelectJoinQualified()
+    {
+        $this->markTestIncomplete($this->getDriver() . ' needs more syntax for qualified table names.');
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 120 - 0
tests/Zend/Db/Statement/SqlsrvTest.php

@@ -0,0 +1,120 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Statement/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_Statement_SqlsrvTest extends Zend_Db_Statement_TestCommon
+{
+    // http://msdn.microsoft.com/en-us/library/cc296197(SQL.90).aspx
+    protected $_getColumnMetaKeys = array(
+        'Name' , 'Type', 'Size', 'Precision', 'Scale', 'Nullable'
+    );
+
+    public function testStatementExecuteWithParams()
+    {
+        $products = $this->_db->quoteIdentifier('zfproducts');
+
+        // Make IDENTITY column accept explicit value.
+        // This can be done in only one table in a given session.
+        sqlsrv_query($this->_db->getConnection(), "SET IDENTITY_INSERT $products ON");
+        parent::testStatementExecuteWithParams();
+        sqlsrv_query($this->_db->getConnection(), "SET IDENTITY_INSERT $products OFF");
+    }
+
+    public function testStatementBindParamByName()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support bind by name.');
+    }
+
+    public function testStatementBindValueByName()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support bind by name.');
+    }
+
+    public function testStatementBindParamByPosition()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support bind by position.');
+    }
+
+    public function testStatementBindValueByPosition()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support bind by position.');
+    }
+
+    public function testStatementNextRowset()
+    {
+        $products   = $this->_db->quoteIdentifier('zfproducts');
+        $product_id = $this->_db->quoteIdentifier('product_id');
+
+        $query = "SELECT * FROM $products WHERE $product_id > 1 ORDER BY $product_id ASC";
+        $stmt  = $this->_db->query($query . ';' . $query);
+
+        $result1 = $stmt->fetchAll();
+
+        $stmt->nextRowset();
+
+        $result2 = $stmt->fetchAll();
+
+        $this->assertEquals(count($result1), count($result2));
+        $this->assertEquals($result1, $result2);
+        
+        $stmt->closeCursor();
+    }
+
+    public function testStatementErrorInfo()
+    {
+        $products   = $this->_db->quoteIdentifier('zfproducts');
+        $product_id = $this->_db->quoteIdentifier('product_id');
+
+        $query = "INVALID SELECT * FROM INVALID TABLE WHERE $product_id > 1 ORDER BY $product_id ASC";
+        $stmt  = new Zend_Db_Statement_Sqlsrv($this->_db, $query);
+
+        try {
+            $stmt->fetchAll();
+            $this->fail("Invalid query should have throw an error");
+        } catch (Zend_Db_Statement_Sqlsrv_Exception $e) {
+            // Exception is thrown, nothing to worry about
+            $this->assertEquals(-11, $e->getCode());
+        }
+
+        $this->assertNotSame(false, $stmt->errorCode());
+        $this->assertEquals(-11, $stmt->errorCode());
+
+        $errors = $stmt->errorInfo();
+        $this->assertEquals(2, count($errors));
+        $this->assertEquals($stmt->errorCode(), $errors[0]);
+        $this->assertType('string', $errors[1]);
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 43 - 0
tests/Zend/Db/Table/Relationships/SqlsrvTest.php

@@ -0,0 +1,43 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Table/Relationships/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+class Zend_Db_Table_Relationships_SqlsrvTest extends Zend_Db_Table_Relationships_TestCommon
+{
+
+    public function testTableRelationshipCascadingUpdateUsageBasicString()
+    {
+        $this->markTestSkipped($this->getDriver() . ' cannot update identity columns.');
+    }
+
+    public function testTableRelationshipCascadingUpdateUsageInvalidNoop()
+    {
+        $this->markTestSkipped($this->getDriver() . ' cannot update identity columns.');
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 48 - 0
tests/Zend/Db/Table/Row/SqlsrvTest.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Table/Row/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+class Zend_Db_Table_Row_SqlsrvTest extends Zend_Db_Table_Row_TestCommon
+{
+
+    public function testTableRowSaveInsert()
+    {
+        $this->markTestIncomplete($this->getDriver() . ': DEFAULT or NULL are not allowed as explicit identity values.');
+    }
+
+    public function testTableRowSetPrimaryKey()
+    {
+        $this->markTestSkipped($this->getDriver() . ' cannot update identity columns.');
+    }
+
+    public function testTableRowSaveInsertSequence()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support sequences');
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 32 - 0
tests/Zend/Db/Table/Rowset/SqlsrvTest.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Table/Rowset/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+class Zend_Db_Table_Rowset_SqlsrvTest extends Zend_Db_Table_Rowset_TestCommon
+{
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 57 - 0
tests/Zend/Db/Table/Select/SqlsrvTest.php

@@ -0,0 +1,57 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Table/Select/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+class Zend_Db_Table_Select_SqlsrvTest extends Zend_Db_Table_Select_TestCommon
+{
+    public function testSelectQueryWithBinds()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support binding by name.');
+    }
+
+    public function testSelectColumnWithColonQuotedParameter()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support selecting int columns by varchar param.');
+    }
+
+    public function testSelectFromForUpdate()
+    {
+        $this->markTestSkipped($this->getDriver() . ' does not support for update.');
+    }
+    
+    public function testSelectFromQualified()
+    {
+        $this->markTestIncomplete($this->getDriver() . ' needs more syntax for qualified table names.');
+    }
+
+    public function testSelectJoinQualified()
+    {
+        $this->markTestIncomplete($this->getDriver() . ' needs more syntax for qualified table names.');
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 42 - 0
tests/Zend/Db/Table/SqlsrvTest.php

@@ -0,0 +1,42 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'Zend/Db/Table/TestCommon.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+class Zend_Db_Table_SqlsrvTest extends Zend_Db_Table_TestCommon
+{
+    public function testTableInsertSequence()
+    {
+        $this->markTestSkipped($this->getDriver().' does not support sequences.');
+    }
+
+    public function testTableCascadeUpdate()
+    {
+        $this->markTestSkipped($this->getDriver() . ' cannot update identity columns.');
+    }
+
+    public function getDriver()
+    {
+        return 'Sqlsrv';
+    }
+}

+ 142 - 0
tests/Zend/Db/TestUtil/Sqlsrv.php

@@ -0,0 +1,142 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Mysqli.php 14401 2009-03-20 16:18:03Z mikaelkael $
+ */
+
+/**
+ * @see Zend_Db_TestUtil_Common
+ */
+require_once 'Zend/Db/TestUtil/Common.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__);
+
+/**
+ * @category   Zend
+ * @package    Zend_Db
+ * @subpackage Table
+ * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Db_TestUtil_Sqlsrv extends Zend_Db_TestUtil_Common
+{
+    public function getParams(array $constants = array())
+    {
+        $constants = array(
+            'host'     => 'TESTS_ZEND_DB_ADAPTER_SQLSRV_HOSTNAME',
+            'username' => 'TESTS_ZEND_DB_ADAPTER_SQLSRV_USERNAME',
+            'password' => 'TESTS_ZEND_DB_ADAPTER_SQLSRV_PASSWORD',
+            'dbname'   => 'TESTS_ZEND_DB_ADAPTER_SQLSRV_DATABASE',
+        );
+
+        $constants = parent::getParams($constants);
+
+        return $constants;
+    }
+
+    public function getSqlType($type)
+    {
+        if ($type == 'IDENTITY') {
+            return 'INT NOT NULL IDENTITY PRIMARY KEY';
+        }
+        return $type;
+    }
+
+    protected function _getColumnsDocuments()
+    {
+        return array(
+            'doc_id'       => 'INTEGER NOT NULL',
+            'doc_clob'     => 'VARCHAR(8000)',
+            'doc_blob'     => 'VARCHAR(8000)',
+            'PRIMARY KEY'  => 'doc_id',
+        );
+    }
+
+    protected function _getColumnsBugs()
+    {
+        return array(
+            'bug_id'          => 'IDENTITY',
+            'bug_description' => 'VARCHAR(100) NULL',
+            'bug_status'      => 'VARCHAR(20) NULL',
+            'created_on'      => 'DATETIME NULL',
+            'updated_on'      => 'DATETIME NULL',
+            'reported_by'     => 'VARCHAR(100) NULL',
+            'assigned_to'     => 'VARCHAR(100) NULL',
+            'verified_by'     => 'VARCHAR(100) NULL',
+        );
+    }
+
+    protected function _getSqlCreateTable($tableName)
+    {
+        $sql       = "exec sp_tables @table_name = " . $this->_db->quoteIdentifier($tableName, true);
+        $stmt      = $this->_db->query($sql);
+        $tableList = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        if (count($tableList) > 0 && $tableName == $tableList[0]['TABLE_NAME']) {
+            return null;
+        }
+        return 'CREATE TABLE ' . $this->_db->quoteIdentifier($tableName);
+    }
+
+    protected function _getSqlDropElement($elementName, $typeElement = 'TABLE')
+    {
+        $sql         = "exec sp_tables @table_name = " . $this->_db->quoteIdentifier($elementName, true);
+        $stmt        = $this->_db->query($sql);
+        $elementList = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        if (count($elementList) > 0 && $elementName == $elementList[0]['TABLE_NAME']) {
+            return "DROP $typeElement " . $this->_db->quoteIdentifier($elementName);
+        }
+        return null;
+    }
+
+    protected function _getSqlDropTable($tableName)
+    {
+        return $this->_getSqlDropElement($tableName);
+    }
+
+    protected function _getSqlDropView($viewName)
+    {
+        return $this->_getSqlDropElement($viewName, 'VIEW');
+    }
+
+    public function getSchema()
+    {
+        $desc = $this->_db->describeTable('zfproducts');
+        return $desc['product_id']['SCHEMA_NAME'];
+    }
+
+    public function createView()
+    {
+        parent::dropView();
+        parent::createView();
+    }
+
+    protected function _rawQuery($sql)
+    {
+        $sqlsrv = $this->_db->getConnection();
+        $retval = sqlsrv_query($sqlsrv, $sql);
+        if (!$retval) {
+            $e = sqlsrv_errors();
+            $e = $e[0]['message'];
+            require_once 'Zend/Db/Exception.php';
+            throw new Zend_Db_Exception("SQL error for \"$sql\": $e");
+        }
+    }
+}