Commit: 530e308a6718c210e37a3e59fc3fc8bdde022f63

Author: Jason Schreckengost | Date: 2010-04-16 16:33:02 -0500
Initial commit
diff --git a/extensions/data/source/MSSql.php b/extensions/data/source/MSSql.php new file mode 100644 index 0000000..cc7c763 --- /dev/null +++ b/extensions/data/source/MSSql.php @@ -0,0 +1,174 @@ +<?php +/** + * Lithium: the most rad php framework + * + * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) + * @license http://opensource.org/licenses/bsd-license.php The BSD License + * + */ + +namespace li3_mssql\extensions\data\source; + +use \Exception; +/** +* Adapter class for the SqlSrv extension. +* +* Implements the methods connecting higher level abstractions to the specifics of Microsoft's +* own SQL Server native PHP extension. +* +* @link http://msdn.microsoft.com/en-us/library/cc296172%28SQL.90%29.aspx +*/ +class MSSql extends \li3_mssql\extensions\data\source\MicrosoftSqlServer { + + /** + * Constructs the SqlSrv adapter + * + * @link http://msdn.microsoft.com/en-us/library/cc296161(SQL.90).aspx + * @see lithium\data\source\Database::__construct() + * @see lithium\data\Source::__construct() + * @see lithium\data\Connections::add() + * @param array $config Configuration options for this class. For additional configuration, + * see `lithium\data\source\Database` and `lithium\data\Source`. + * + * Typically, these parameters are set in `Connections::add()`, when adding the adapter to the + * list of active connections. + * @return The adapter instance. + */ + public function __construct($config = array()) { + parent::__construct($config); + } + + /** + * Connects to the database using the options provided to the class constructor. + * + * + * + * @return boolean True if the database could be connected, else false. + */ + public function connect() { + $config = $this->_config; + $this->_isConnected = false; + + $this->_connection = mssql_connect($host, array_intersect_key($config, $validOptions)); + return $this->_isConnected = is_resource($this->_connection); + } + + /** + * Disconnects the adapter from the database. + * + * @return boolean True on success, else false. + */ + public function disconnect() { + if ($this->_isConnected) { + $this->_isConnected = !mssql_close($this->_connection); + return !$this->_isConnected; + } + return true; + } + + /** + * Getter/Setter for the connection's encoding + * + * @param mixed $encoding + * @return mixed. + * + * @todo Determine the best way to handle encoding - + * current idea is to disconnect, then reconnect using the proper encoding + */ + public function encoding($encoding = null) { + $encodingMap = array('UTF-8' => 'utf8'); + + if (isset($this->_config['CharacterSet'])) { + $characterSet = $this->_config['CharacterSet']; + } + + return $characterSet; + } + + /** + * Retrieves database error message and error code. + * + * @return array + */ + public function error() { + if ($error = sqlsrv_errors(SQLSRV_ERR_ERRORS)) { + return array($error[0]['code'], $error[0]['message']); + } + return null; + } + + /** + * + * + * @param string $type + * @param object $resource + * @param unknown_type $context + * @return array|null + */ + public function result($type, $resource, $context) { + if (!is_resource($resource)) { + return null; + } + + switch ($type) { + case 'next': + $result = sqlsrv_fetch_array($resource, SQLSRV_FETCH_ASSOC); + break; + case 'close': + sqlsrv_free_stmt($resource); + $result = null; + break; + default: + $result = parent::result($type, $resource, $context); + break; + } + return $result; + } + + /** + * In cases where the query is a raw string (as opposed to a `Query` object), the database must + * determine the correct column names from the result resource. + * + * @param object|string $query + * @param object $result + * @param object $context + * @return array Field names + * @filter This method can be filtered. + */ + public function schema($query, $result = null, $context = null) { + if (is_object($query)) { + return parent::schema($query, $result, $context); + } + $filter = function($self, $params, $chain) { + $return = array(); + foreach (sqlsrv_field_metadata($params['result']) as $field) { + $return[] = $field['Name']; + } + return $return; + }; + return $this->_filter(__METHOD__, compact('query', 'result', 'context'), $filter); + } + + /** + * + * @param mixed $sql + * @param Array $options + * @return unknown_type + * @filter This method can be filtered. + */ + protected function _execute($sql, $options = array()) { + $params = compact('sql', 'options'); + $conn =& $this->_connection; + + return $this->_filter(__METHOD__, $params, function($self, $params, $chain) use (&$conn) { + extract($params); + if ( !is_resource($result = sqlsrv_query($conn, $sql)) ) { + list($code, $error) = $self->error(); + throw new Exception("$sql: $error", $code); + } + return $result; + }); + } +} + +?> \ No newline at end of file diff --git a/extensions/data/source/MicrosoftSqlServer.php b/extensions/data/source/MicrosoftSqlServer.php new file mode 100644 index 0000000..726dbf8 --- /dev/null +++ b/extensions/data/source/MicrosoftSqlServer.php @@ -0,0 +1,328 @@ +<?php + +namespace li3_mssql\extensions\data\source; + +abstract class MicrosoftSqlServer extends \lithium\data\source\Database { + + /** + * SqlSrv column type definitions. + * + * @var array + */ + protected $_columns = array( + 'primary_key' => array('name' => 'IDENTITY (1, 1) NOT NULL'), + 'string' => array('name' => 'varchar', 'length' => '255'), + 'text' => array('name' => 'varchar', 'length' => 'max'), + 'integer' => array('name' => 'integer', 'length' => 11, 'formatter' => 'intval'), + 'float' => array('name' => 'float', 'formatter' => 'floatval'), + 'datetime' => array( + 'name' => 'datetime', + 'format' => 'Y-m-d H:i:s.u', + 'formatter' => 'date' + ), + 'timestamp' => array( + 'name' => 'timestamp', + 'format' => 'Y-m-d H:i:s', + 'formatter' => 'date' + ), + 'time' => array('name' => 'datetime', 'format' => 'H:i:s', 'formatter' => 'date'), + 'date' => array('name' => 'datetime', 'format' => 'Y-m-d', 'formatter' => 'date'), + 'binary' => array('name' => 'varbinary', 'length' => 'max'), + 'boolean' => array('name' => 'bit') + ); + + /** + * Contains all needed queries for rendering + * + * @var array + */ + protected $_strings = array( + 'read' => "SELECT {:limit} {:fields} From {:table} + {:joins} {:conditions} {:group} {:order} {:comment}", + 'paged' => " + SELECT * From ( + SELECT {:fields}, ROW_NUMBER() OVER({:order}) AS [__LI3_ROW_NUMBER__] + From {:table} {:joins} {:conditions} {:group} + ) a {:limit} {:comment}", + 'create' => "INSERT INTO {:table} ({:fields}) VALUES ({:values}) {:comment}", + 'update' => "UPDATE {:table} SET {:fields} {:conditions} {:comment}", + 'delete' => "DELETE {:flags} From {:table} {:aliases} {:conditions} {:comment}", + 'schema' => "CREATE TABLE {:table} (\n{:columns}\n) {:indexes} {:comment}", + 'join' => "{:type} JOIN {:table} {:constraint}" + ); + + protected function _init() { + parent::_init(); + $this->applyFilter('update', function($self, $params, $chain) { + + $query = $params['query']; + if (is_object($query)) { + $model = $query->model(); + $key = $model::meta('key'); + if (isset($query->record()->{$key})) { + unset($query->record()->{$key}); + $params['query'] = $query; + } + } + $chain->next($self, $params, $chain); + }); + } + + /** + * Quotes identifiers. + * + * @todo Currently, this method simply returns the identifier. + * + * @param string $name The identifier to quote. + * @return string The quoted identifier. + */ + public function name($name) { + return $name; + } + + /** + * Returns the list of tables in the currently-connected database. + * + * @param string $class The fully-name-spaced class name of the model object making the request. + * @return array Returns an array of objects to which models can connect. + * @filter This method can be filtered. + */ + public function entities($class = null) { + $config = $this->_config; + $method = function($self, $params, $chain) use ($config) { + return $self->read( + "SELECT TABLE_NAME FROM [INFORMATION_SCHEMA].[TABLES]", + array('return' => 'array') + ); + }; + return $this->_filter(__METHOD__, compact('class'), $method); + } + + /** + * Gets the column schema for a given SQL Server table. + * + * @param mixed $entity Specifies the table name for which the schema should be returned, or + * the class name of the model object requesting the schema, in which case the model + * class will be queried for the correct table name. + * @param array $meta + * @return array Returns an associative array describing the given table's schema, where the + * array keys are the available fields, and the values are arrays describing each + * field, containing the following keys: + * - `'type'`: The field type name + * @filter This method can be filtered. + */ + public function describe($entity, $meta = array()) { + $params = compact('entity', 'meta'); + return $this->_filter(__METHOD__, $params, function($self, $params, $chain) { + extract($params); + + $name = $entity; + $sql = "SELECT COLUMN_NAME as Field, DATA_TYPE as Type, " + . "COL_LENGTH('{$name}', COLUMN_NAME) as Length, IS_NULLABLE As [Null], " + . "COLUMN_DEFAULT as [Default], " + . "COLUMNPROPERTY(OBJECT_ID('{$name}'), COLUMN_NAME, 'IsIdentity') as [Key], " + . "NUMERIC_SCALE as Size FROM INFORMATION_SCHEMA.COLUMNS " + . "WHERE TABLE_NAME = '{$name}'"; + + $columns = $self->read($sql, array('return' => 'array')); + $fields = array(); + + foreach ($columns as $column) { + $fields[$column['Field']] = $column + array( + 'Null' => ($column['Null'] == 'YES' ? true : false), + 'Default' => ($column['Default'] === '(NULL)' ? null: $column['Default']), + ); + if ($fields[$column['Field']]['Length'] === -1) { + $fields[$column['Field']]['Length'] = 'max'; + } + } + + return $fields; + }); + } + + /** + * Converts database-layer column types to basic types. + * + * @param string $real Real database-layer column type (i.e. "varchar(255)") + * @return string Abstract column type (i.e. "string") + */ + protected function _column($real) { + + if (is_array($real)) { + $length = ''; + if (isset($real['length'])) { + $length = $real['length']; + if ($length === -1) { + $length = 'max'; + } + $length = '(' . $length . ')'; + } + return $real['type'] . $length; + } + + if (!preg_match('/(?P<type>[^(]+)(?:\((?P<length>[^)]+)\))?/', $real, $column)) { + return $real; + } + $column = array_intersect_key($column, array('type' => null, 'length' => null)); + + switch (true) { + case $column['type'] === 'datetime': + break; + case ($column['type'] == 'tinyint' && $column['length'] == '1'): + case ($column['type'] == 'bit'): + $column = array('type' => 'boolean'); + break; + case (strpos($column['type'], 'int') !== false): + $column['type'] = 'integer'; + break; + case (strpos($column['type'], 'text') !== false): + $column['type'] = 'text'; + break; + case strpos($column['type'], 'char') !== false: + if (isset($column['length']) && $column['length'] === 'max') { + $column['type'] = 'text'; + unset($column['length']); + } else { + $column['type'] = 'string'; + } + break; + case (strpos($column['type'], 'binary') !== false || $column['type'] == 'image'): + $column['type'] = 'binary'; + break; + case preg_match('/float|double|decimal/', $column['type']): + $column['type'] = 'float'; + break; + default: + $column['type'] = 'text'; + break; + } + return $column; + } + + protected function _column2($real) { + + if (is_array($real)) { + $length = ''; + if (isset($real['length'])) { + $length = $real['length']; + if ($length === -1) { + $length = 'max'; + } + $length = '(' . $length . ')'; + } + return $real['type'] . $length; + } + + if (!preg_match('/(?P<type>[^(]+)(?:\((?P<length>[^)]+)\))?/', $real, $column)) { + return $real; + } + $column = array_intersect_key($column, array('type' => null, 'length' => null)); + + $type = 'text'; + + if ($column['type'] === 'datetime') { + + } + + + switch (true) { + case ($column['type'] == 'tinyint' && $column['length'] == '1'): + case ($column['type'] == 'bit'): + $column = array('type' => 'boolean'); + break; + case (strpos($column['type'], 'int') !== false): + $column['type'] = 'integer'; + break; + case (strpos($column['type'], 'text') !== false): + $column['type'] = 'text'; + break; + case strpos($column['type'], 'char') !== false: + if (isset($column['length']) && $column['length'] === 'max') { + $column['type'] = 'text'; + unset($column['length']); + } else { + $column['type'] = 'string'; + } + break; + case (strpos($column['type'], 'binary') !== false || $column['type'] == 'image'): + $column['type'] = 'binary'; + break; + case preg_match('/float|double|decimal/', $column['type']): + $column['type'] = 'float'; + break; + default: + $column['type'] = 'text'; + break; + } + return $column; + } + + public function limit($limit, $context) { + $offset = $context->offset() ?: 0; + + if ($offset === 0) { + return "TOP {$limit}"; + } + + $limit += $offset++; + + return "WHERE __LI3_ROW_NUMBER__ between {$offset} and {$limit}"; + } + + public function renderCommand($type, $data = null, $context = null) { + if (is_object($type)) { + $context = $type; + $data = $context->export($this); + $type = $context->type(); + } + + $type = ($type == "read" && $data['page'] > 1) ? "paged" : "read"; + + return parent::renderCommand($type, $data, $context); + } + + /** + * Gets the last auto-generated ID from the query that inserted a new record. + * + * @param object $query The `Query` object associated with the query which generated + * @return mixed Returns the last inserted ID key for an auto-increment column or a column + * bound to a sequence. + */ + protected function _insertId($query) { + $resource = $this->_execute('SELECT @@identity as insertId'); + $id = $this->result('next', $resource, null); + $id = $id['insertId']; + $this->result('close', $resource, null); + + if (!empty($id) && $id !== '0') { + return $id; + } + } + + public function value($value, array $schema = array()) { + if (is_array($value)) { + return parent::value($value, $schema); + } + if ($value === null) { + return 'NULL'; + } + + switch ($type = isset($schema['type']) ? $schema['type'] : $this->_introspectType($value)) { + case 'float': + return floatval($value); + case 'integer': + return intval($value); + } + return "'" . str_replace("'", "''", $value) . "'"; + } + + public function comment($comment) { + if (!empty($comment)) { + $comment = '/* ' . str_replace('*/', '* /', $comment) . ' */'; + } + return $comment; + } +} + +?> \ No newline at end of file diff --git a/extensions/data/source/SqlSrv.php b/extensions/data/source/SqlSrv.php new file mode 100644 index 0000000..1547f9a --- /dev/null +++ b/extensions/data/source/SqlSrv.php @@ -0,0 +1,221 @@ +<?php +/** + * Lithium: the most rad php framework + * + * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) + * @license http://opensource.org/licenses/bsd-license.php The BSD License + * + */ + +namespace li3_mssql\extensions\data\source; + +use \Exception; +/** +* Adapter class for the SqlSrv extension. +* +* Implements the methods connecting higher level abstractions to the specifics of Microsoft's +* own SQL Server native PHP extension. +* +* @link http://msdn.microsoft.com/en-us/library/cc296172%28SQL.90%29.aspx +*/ +class SqlSrv extends \li3_mssql\extensions\data\source\MicrosoftSqlServer { + + /** + * Constructs the SqlSrv adapter + * + * @link http://msdn.microsoft.com/en-us/library/cc296161(SQL.90).aspx + * @see lithium\data\source\Database::__construct() + * @see lithium\data\Source::__construct() + * @see lithium\data\Connections::add() + * @param array $config Configuration options for this class. For additional configuration, + * see `lithium\data\source\Database` and `lithium\data\Source`. + * + * Typically, these parameters are set in `Connections::add()`, when adding the adapter to the + * list of active connections. + * @return The adapter instance. + */ + public function __construct($config = array()) { + parent::__construct($config); + } + + /** + * Check for required PHP extension + * + * @return boolean + */ + public static function enabled() { + return extension_loaded('sqlsrv'); + } + + public function _init() { + parent::_init(); + $this->_config += array( + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => $this->_config['persistent'], + 'Database' => $this->_config['database'] + ); + if (!empty($this->_config['login']) && !empty($this->_config['password'])) { + $this->_config['UID'] = $this->_config['login']; + $this->_config['PWD'] = $this->_config['password']; + } + if (isset($this->_config['port'])) { + $this->_config['host'] .= ', ' . $this->_config['port']; + } + } + + /** + * Connects to the database using the options provided to the class constructor. + * + * + * + * @return boolean True if the database could be connected, else false. + */ + public function connect() { + $config = $this->_config; + $this->_isConnected = false; + + $host = $config['host']; + + $validOptions = array_fill_keys(array( + 'APP', + 'CharacterSet', + 'ConnectionPooling', + 'Database', + 'Encrypt', + 'Failover_Partner', + 'LoginTimeout', + 'MultipleActiveResultSets', + 'PWD', + 'QuotedId', + 'ReturnDatesAsStrings', + 'TraceFile', + 'TraceOn', + 'TransactionIsolation', + 'TrustServerCertificate', + 'UID', + 'WSID' + ), ''); + $this->_connection = sqlsrv_connect($host, array_intersect_key($config, $validOptions)); + return $this->_isConnected = is_resource($this->_connection); + } + + /** + * Disconnects the adapter from the database. + * + * @return boolean True on success, else false. + */ + public function disconnect() { + if ($this->_isConnected) { + $this->_isConnected = !sqlsrv_close($this->_connection); + return !$this->_isConnected; + } + return true; + } + + /** + * Getter/Setter for the connection's encoding + * + * @param mixed $encoding + * @return mixed. + * + * @todo Determine the best way to handle encoding - + * current idea is to disconnect, then reconnect using the proper encoding + */ + public function encoding($encoding = null) { + $encodingMap = array('UTF-8' => 'utf8'); + + if (isset($this->_config['CharacterSet'])) { + $characterSet = $this->_config['CharacterSet']; + } + + return $characterSet; + } + + /** + * Retrieves database error message and error code. + * + * @return array + */ + public function error() { + if ($error = sqlsrv_errors(SQLSRV_ERR_ERRORS)) { + return array($error[0]['code'], $error[0]['message']); + } + return null; + } + + /** + * + * + * @param string $type + * @param object $resource + * @param unknown_type $context + * @return array|null + */ + public function result($type, $resource, $context) { + if (!is_resource($resource)) { + return null; + } + + switch ($type) { + case 'next': + $result = sqlsrv_fetch_array($resource, SQLSRV_FETCH_ASSOC); + break; + case 'close': + sqlsrv_free_stmt($resource); + $result = null; + break; + default: + $result = parent::result($type, $resource, $context); + break; + } + return $result; + } + + /** + * In cases where the query is a raw string (as opposed to a `Query` object), the database must + * determine the correct column names from the result resource. + * + * @param object|string $query + * @param object $result + * @param object $context + * @return array Field names + * @filter This method can be filtered. + */ + public function schema($query, $result = null, $context = null) { + if (is_object($query)) { + return parent::schema($query, $result, $context); + } + $filter = function($self, $params, $chain) { + $return = array(); + foreach (sqlsrv_field_metadata($params['result']) as $field) { + $return[] = $field['Name']; + } + return $return; + }; + return $this->_filter(__METHOD__, compact('query', 'result', 'context'), $filter); + } + + /** + * + * @param mixed $sql + * @param Array $options + * @return unknown_type + * @filter This method can be filtered. + */ + protected function _execute($sql, $options = array()) { + $params = compact('sql', 'options'); + $conn =& $this->_connection; + + return $this->_filter(__METHOD__, $params, function($self, $params, $chain) use (&$conn) { + extract($params); + if ( !is_resource($result = sqlsrv_query($conn, $sql)) ) { + list($code, $error) = $self->error(); + throw new Exception("$sql: $error", $code); + } + return $result; + }); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/extensions/data/source/MSSqlTest.php b/tests/cases/extensions/data/source/MSSqlTest.php new file mode 100644 index 0000000..979d455 --- /dev/null +++ b/tests/cases/extensions/data/source/MSSqlTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Lithium: the most rad php framework + * + * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) + * @license http://opensource.org/licenses/bsd-license.php The BSD License + */ + +namespace li3_mssql\tests\cases\extensions\data\source; + +use \lithium\data\Connections; +use \li3_mssql\extensions\data\source\MSSql; +use \li3_mssql\tests\mocks\extensions\data\source\MockMSSql; +use \lithium\data\model\Query; + +class MSSqlTest extends \lithium\test\Unit { + + protected $_dbConfig = array(); + + /** + * Instance of the SqlSrv adapter + * + * @var object + */ + public $db = null; + + /** + * Skip the test if a SqlSrv adapter configuration is unavailable. + * + * @return void + * @todo Tie into the Environment class to ensure that the test database is being used. + */ + public function skip() { + $message = 'MSSql extension is not available for testing the adapter.'; + $hasClass = function_exists('mssql_connect'); + $this->skipIf(!$hasClass, $message); + + $this->_dbConfig = Connections::get('test-mssql', array('config' => true)); + $hasDb = (isset($this->_dbConfig['adapter']) && $this->_dbConfig['adapter'] == 'MSSql'); + $message = 'Test database is either unavailable, or not using a MSSql adapter'; + $this->skipIf(!$hasDb, $message); + } + + public function setUp() { + $this->db = new MSSql($this->_dbConfig); + } + + /** + * Tests that the object is initialized with the correct default values. + * + * @return void + */ + public function testConstructorDefaults() { + $db = new MockSqlSrv(array('autoConnect' => false)); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'database' => 'lithium', + 'host' => 'localhost', + 'init' => true, + 'login' => 'root', + 'password' => '', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => 'lithium', + ); + $this->assertEqual($expected, $result); + + $db = new MockSqlSrv(array('autoConnect' => false)); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'database' => 'lithium', + 'host' => 'localhost', + 'init' => true, + 'login' => 'root', + 'password' => '', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => 'lithium', + ); + $this->assertEqual($expected, $result); + + $db = new MockSqlSrv(array('autoConnect' => false, 'port' => 1234)); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'port' => 1234, + 'database' => 'lithium', + 'host' => 'localhost, 1234', + 'init' => true, + 'login' => 'root', + 'password' => '', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => 'lithium', + ); + $this->assertEqual($expected, $result); + } + + /** + * Tests that this adapter can connect to the database, and that the status is properly + * persisted. + * + * @return void + */ + public function testDatabaseConnection() { + $db = new SqlSrv(array('autoConnect' => false) + $this->_dbConfig); + $this->assertTrue($db->connect()); + $this->assertTrue($db->isConnected()); + + $this->assertTrue($db->disconnect()); + $this->assertFalse($db->isConnected()); + } + + public function testDatabaseEncoding() { + $this->assertTrue($this->db->connect()); + $this->assertTrue($this->db->isConnected()); + + $this->assertTrue($this->db->encoding('UTF-8')); + $this->assertEqual('UTF-8', $this->db->encoding()); + } + + public function testExecuteException() { + $this->expectException(); + $this->db->read('SELECT deliberate syntax error'); + } +} + +?> \ No newline at end of file diff --git a/tests/cases/extensions/data/source/MicrosoftSqlServerTest.php b/tests/cases/extensions/data/source/MicrosoftSqlServerTest.php new file mode 100644 index 0000000..dbda943 --- /dev/null +++ b/tests/cases/extensions/data/source/MicrosoftSqlServerTest.php @@ -0,0 +1,182 @@ +<?php + +namespace li3_mssql\tests\cases\extensions\data\source; + +//use \lithium\data\Connections; +use \li3_mssql\tests\mocks\extensions\data\source\MockMicrosoftSqlServer; +use \lithium\data\model\Query; + +class MicrosoftSqlServerTest extends \lithium\test\Unit { + protected $_dbConfig = array(); + + /** + * Instance of the SqlSrv adapter + * + * @var object + */ + public $db = null; + + public function setUp() { + $this->db = new MockMicrosoftSqlServer($this->_dbConfig); + } + + /** + * Tests that native field types are resolved to Lithiums abstracted versions + */ + public function testColumnAbstraction() { + $result = $this->db->invokeMethod('_column', array('decimal(12,2)')); + $this->assertEqual(array('type' => 'float', 'length' => '12,2'), $result); + + $result = $this->db->invokeMethod('_column', array('int(11)')); + $this->assertEqual(array('type' => 'integer', 'length' => '11'), $result); + + $result = $this->db->invokeMethod('_column', array('varchar(max)')); + $this->assertEqual(array('type' => 'text'), $result); + + $result = $this->db->invokeMethod('_column', array('text')); + $this->assertEqual(array('type' => 'text'), $result); + + $result = $this->db->invokeMethod('_column', array('tinyint(1)')); + $this->assertEqual(array('type' => 'boolean'), $result); + + $result = $this->db->invokeMethod('_column', array('varchar')); + $this->assertEqual(array('type' => 'string'), $result); + + $result = $this->db->invokeMethod('_column', array('varchar(255)')); + $this->assertEqual(array('type' => 'string', 'length' => '255'), $result); + + $result = $this->db->invokeMethod('_column', array( + array('type' => 'varchar', 'length' => -1)) + ); + $this->assertEqual('varchar(max)', $result); + + $result = $this->db->invokeMethod('_column', array('bad_type')); + $this->assertEqual(array('type' => 'text'), $result); + + $result = $this->db->invokeMethod('_column', array('datetime')); + $this->assertEqual(array('type' => 'datetime'), $result); + + $result = $this->db->invokeMethod('_column', array('varbinary')); + $this->assertEqual(array('type' => 'binary'), $result); + + } + + public function testName() { + $this->assertEqual('Name', $this->db->name('Name')); + $this->assertEqual('li3.Test', $this->db->name('li3.Test')); + } + + public function testRenderCommand() { + $query = new Query(array( + 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost', + 'conditions' => null, + 'fields' => null, + 'order' => null, + 'limit' => 1, + 'page' => 1, + 'type' => 'read', + 'table' => 'li3_test_table', + 'comment' => 'Simulates Model::find("first")' + )); + $expected = "SELECT TOP 1 * From mock_database_posts " + . "/* Simulates Model::find(\"first\") */"; + $result = $this->db->renderCommand($query); + $this->assertEqual($expected, $result); + + $query = new Query(array( + 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost', + 'conditions' => null, + 'fields' => '*', + 'order' => null, + 'limit' => 10, + 'page' => 1, + 'type' => 'read', + 'table' => 'li3_test_table', + 'comment' => 'Simulates Model::find("all", array("limit" => 10))' + )); + $expected = "SELECT TOP 10 * From mock_database_posts " + . "/* Simulates Model::find(\"all\", array(\"limit\" => 10)) */"; + $result = $this->db->renderCommand($query); + $this->assertEqual($expected, $result); + + $query = new Query(array( + 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost', + 'conditions' => null, + 'fields' => null, + 'order' => 'id', + 'limit' => 10, + 'page' => 1, + 'type' => 'read', + 'table' => 'li3_test_table', + 'comment' => 'Simulates Model::find("all", array("limit" => 10, "order" => "id"))' + )); + $expected = "SELECT TOP 10 * From mock_database_posts ORDER BY id ASC " + . '/* Simulates Model::find("all", array("limit" => 10, "order" => "id")) */'; + $result = $this->db->renderCommand($query); + $this->assertEqual($expected, $result); + + $query = new Query(array( + 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost', + 'conditions' => null, + 'fields' => null, + 'order' => 'id asc', + 'limit' => 10, + 'page' => 2, + 'type' => 'read', + 'table' => 'li3_test_table', + 'comment' => 'Simulates Model::find("all", ' + . 'array("limit" => 10, "order" => "id", "page" => 2))' + )); + $expected = "SELECT * From (\n" + . "\t\t\t\tSELECT *, ROW_NUMBER() OVER( ORDER BY id asc) AS [__LI3_ROW_NUMBER__]\n" + . "\t\t\t\tFrom mock_database_posts\n" + . "\t\t\t) a WHERE __LI3_ROW_NUMBER__ between 11 and 20 " + . '/* Simulates Model::find("all", ' + . 'array("limit" => 10, "order" => "id", "page" => 2)) */'; + $result = $this->db->renderCommand($query); + $this->assertEqual($expected, $result); + + $query = new Query(array( + 'model' => 'lithium\tests\mocks\data\model\MockDatabasePost', + 'conditions' => null, + 'fields' => null, + 'order' => null, + 'limit' => 10, + 'page' => 2, + 'type' => 'read', + 'table' => 'li3_test_table', + 'comment' => 'Simulates Model::find("all", array("limit" => 10, "page" => 2)) ' + . '- order should default to PK' + )); + $expected = "SELECT * From (\n" + . "\t\t\t\tSELECT *, ROW_NUMBER() OVER( ORDER BY id ASC) AS [__LI3_ROW_NUMBER__]\n" + . "\t\t\t\tFrom mock_database_posts\n" + . "\t\t\t) a WHERE __LI3_ROW_NUMBER__ between 11 and 20 " + . "/* Simulates Model::find(\"all\", array(\"limit\" => 10, \"page\" => 2)) " + . "- order should default to PK */"; + $result = $this->db->renderCommand($query); + $this->assertEqual($expected, $result); + } + + public function testComment() { + $this->assertEqual('/* comment */', $this->db->comment('comment')); + $this->assertEqual('/* comment * / */', $this->db->comment('comment */')); + $this->assertEqual('/* --comment */', $this->db->comment('--comment')); + $this->assertEqual('', $this->db->comment('')); + } + + public function testEntities() { +// $expected = array( +// array('TABLE_NAME' => 'Table1'), +// array('TABLE_NAME' => 'Table2'), +// array('TABLE_NAME' => 'Table3') +// ); +// $model = '\\lithium\\tests\\mocks\\data\\MockModel'; +// $result = $this->db->entities($model); +// $this->assertEqual($expected, $result); + } + + public function testDescribe() { +// + } +} \ No newline at end of file diff --git a/tests/cases/extensions/data/source/SqlSrvTest.php b/tests/cases/extensions/data/source/SqlSrvTest.php new file mode 100644 index 0000000..e525833 --- /dev/null +++ b/tests/cases/extensions/data/source/SqlSrvTest.php @@ -0,0 +1,162 @@ +<?php +/** + * Lithium: the most rad php framework + * + * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) + * @license http://opensource.org/licenses/bsd-license.php The BSD License + */ + +namespace li3_mssql\tests\cases\extensions\data\source; + +use \lithium\data\Connections; +use \li3_mssql\extensions\data\source\SqlSrv; +use \li3_mssql\tests\mocks\extensions\data\source\MockSqlSrv; +use \lithium\data\model\Query; + +class SqlSrvTest extends \lithium\test\Unit { + + protected $_dbConfig = array(); + + /** + * Instance of the SqlSrv adapter + * + * @var object + */ + public $db = null; + + /** + * Skip the test if a SqlSrv adapter configuration is unavailable. + * + * @return void + * @todo Tie into the Environment class to ensure that the test database is being used. + */ + public function skip() { + $message = 'SqlSrv extension is not available for testing the adapter.'; + $hasClass = function_exists('sqlsrv_connect'); + $this->skipIf(!$hasClass, $message); + + $this->_dbConfig = Connections::get('test-sqlsrv', array('config' => true)); + $hasDb = (isset($this->_dbConfig['adapter']) && $this->_dbConfig['adapter'] == 'SqlSrv'); + $message = 'Test database is either unavailable, or not using a SqlSrv adapter'; + $this->skipIf(!$hasDb, $message); + } + + public function setUp() { + $this->db = new SqlSrv($this->_dbConfig); + $this->db->connect(); + } + + /** + * Tests that the object is initialized with the correct default values. + * + * @return void + */ + public function testConstructorDefaults() { + $db = new MockSqlSrv(array('autoConnect' => false)); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'database' => null, + 'host' => 'localhost', + 'init' => true, + 'login' => 'root', + 'password' => '', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => null, + ); + $this->assertEqual($expected, $result); + + $db = new MockSqlSrv(array('autoConnect' => false, 'database' => 'lithium')); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'database' => 'lithium', + 'host' => 'localhost', + 'init' => true, + 'login' => 'root', + 'password' => '', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => 'lithium', + ); + $this->assertEqual($expected, $result); + + $db = new MockSqlSrv(array( + 'autoConnect' => false, + 'login' => 'user', + 'password' => 'password' + )); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'database' => null, + 'host' => 'localhost', + 'init' => true, + 'login' => 'user', + 'password' => 'password', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => null, + 'UID' => 'user', + 'PWD' => 'password' + ); + $this->assertEqual($expected, $result); + + $db = new MockSqlSrv(array( + 'autoConnect' => false, + 'database' => 'lithium', + 'port' => 1234 + )); + $result = $db->get('_config'); + $expected = array( + 'autoConnect' => false, + 'port' => 1234, + 'database' => 'lithium', + 'host' => 'localhost, 1234', + 'init' => true, + 'login' => 'root', + 'password' => '', + 'persistent' => true, + 'APP' => 'Lithium Application', + 'CharacterSet' => 'UTF-8', + 'ConnectionPooling' => true, + 'Database' => 'lithium', + ); + $this->assertEqual($expected, $result); + } + + /** + * Tests that this adapter can connect to the database, and that the status is properly + * persisted. + * + * @return void + */ + public function testDatabaseConnection() { + $db = new SqlSrv(array('autoConnect' => false) + $this->_dbConfig); + $this->assertTrue($db->connect()); + $this->assertTrue($db->isConnected()); + + $this->assertTrue($db->disconnect()); + $this->assertFalse($db->isConnected()); + } + + public function testDatabaseEncoding() { + + $this->assertTrue($this->db->encoding('UTF-8')); + $this->assertEqual('UTF-8', $this->db->encoding()); + } + + public function testExecuteException() { + $this->expectException(); + $this->db->read('SELECT deliberate syntax error'); + } +} + +?> \ No newline at end of file diff --git a/tests/integration/extensions/data/SourceTest.php b/tests/integration/extensions/data/SourceTest.php new file mode 100644 index 0000000..d5edb41 --- /dev/null +++ b/tests/integration/extensions/data/SourceTest.php @@ -0,0 +1,65 @@ +<?php +/** + * Lithium: the most rad php framework + * + * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) + * @license http://opensource.org/licenses/bsd-license.php The BSD License + */ + +namespace li3_mssql\tests\integration\extensions\data; + +use \Exception; +use \ArrayAccess; +use \lithium\data\Connections; +use \lithium\tests\integration\data\Company; + +class SourceTest extends \lithium\tests\integration\data\SourceTest { + + /** + * Tests that a single record with a manually specified primary key can be created, persisted + * to an arbitrary data store, re-read and updated. + * + * @return void + */ + public function testSingleReadWriteWithKey() { + Company::invokeMethod('_connection')->invokeMethod('_execute', array( + 'SET IDENTITY_INSERT ' . Company::meta('source') . ' ON') + ); + + $key = Company::meta('key'); + + $new = Company::create(array($key => 12345, 'name' => 'Acme, Inc.')); + + $result = $new->data(); + $expected = array($key => 12345, 'name' => 'Acme, Inc.'); + $this->assertEqual($expected[$key], $result[$key]); + $this->assertEqual($expected['name'], $result['name']); + + $this->assertFalse($new->exists()); + $this->assertTrue($new->save()); + $this->assertTrue($new->exists()); + + $existing = Company::find(12345); + $result = $existing->data(); + $this->assertEqual($expected[$key], $result[$key]); + $this->assertEqual($expected['name'], $result['name']); + $this->assertTrue($existing->exists()); + + $existing->name = 'Big Brother and the Holding Company'; + $this->assertTrue($existing->save()); + + $existing = Company::find(12345); + $result = $existing->data(); + $expected['name'] = 'Big Brother and the Holding Company'; + $this->assertEqual($expected[$key], $result[$key]); + $this->assertEqual($expected['name'], $result['name']); + + $existing->delete(); + + Company::invokeMethod('_connection')->invokeMethod('_execute', array( + 'SET IDENTITY_INSERT ' . Company::meta('source') . ' OFF') + ); + } +} + +?> \ No newline at end of file diff --git a/tests/mocks/extensions/data/source/MockMicrosoftSqlServer.php b/tests/mocks/extensions/data/source/MockMicrosoftSqlServer.php new file mode 100644 index 0000000..2bd3ff8 --- /dev/null +++ b/tests/mocks/extensions/data/source/MockMicrosoftSqlServer.php @@ -0,0 +1,28 @@ +<?php + +namespace li3_mssql\tests\mocks\extensions\data\source; + +class MockMicrosoftSqlServer extends \li3_mssql\extensions\data\source\MicrosoftSqlServer { + public function connect() {} + public function disconnect() {} + public function encoding($encoding = null) {} + public function result($type, $resource, $context) {} + public function error() {} + protected function _execute($sql, $options = array()) {} +// public function read($query, $options) { +// switch (true) { +// case $query === 'SELECT TABLE_NAME FROM [INFORMATION_SCHEMA].[TABLES]': +// return array( +// array('TABLE_NAME' => 'Table1'), +// array('TABLE_NAME' => 'Table2'), +// array('TABLE_NAME' => 'Table3') +// ); +// break; +// case $query: +// break; +// } +// return null; +// } +} + +?> \ No newline at end of file diff --git a/tests/mocks/extensions/data/source/MockSqlSrv.php b/tests/mocks/extensions/data/source/MockSqlSrv.php new file mode 100644 index 0000000..6074ab3 --- /dev/null +++ b/tests/mocks/extensions/data/source/MockSqlSrv.php @@ -0,0 +1,18 @@ +<?php +/** + * Lithium: the most rad php framework + * + * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) + * @license http://opensource.org/licenses/bsd-license.php The BSD License + */ + +namespace li3_mssql\tests\mocks\extensions\data\source; + +class MockSqlSrv extends \li3_mssql\extensions\data\source\SqlSrv { + + public function get($var) { + return $this->{$var}; + } +} + +?> \ No newline at end of file