From c7e5256b778a6ef29e9b14f077b52e88279fc47d Mon Sep 17 00:00:00 2001 From: Edgard Lorraine Messias Date: Tue, 7 Jul 2015 21:35:00 -0300 Subject: [PATCH] First version; --- .gitattributes | 8 + .gitignore | 35 ++ .travis.yml | 62 ++++ ColumnSchema.php | 60 ++++ Command.php | 181 ++++++++++ Connection.php | 82 +++++ PdoAdapter.php | 115 +++++++ QueryBuilder.php | 227 ++++++++++++ README.md | 51 +++ Schema.php | 498 +++++++++++++++++++++++++++ Transaction.php | 191 ++++++++++ composer.json | 32 ++ phpunit.xml.dist | 13 + tests/ActiveRecordTest.php | 25 ++ tests/BatchQueryResultTest.php | 14 + tests/CommandTest.php | 120 +++++++ tests/ConnectionTest.php | 64 ++++ tests/FirebirdTestTrait.php | 59 ++++ tests/QueryBuilderTest.php | 142 ++++++++ tests/QueryTest.php | 14 + tests/SchemaTest.php | 122 +++++++ tests/bootstrap.php | 16 + tests/ci/travis/dpkg_firebird2.1.exp | 11 + tests/data/config.php | 23 ++ tests/data/source.sql | 414 ++++++++++++++++++++++ 25 files changed, 2579 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 ColumnSchema.php create mode 100644 Command.php create mode 100644 Connection.php create mode 100644 PdoAdapter.php create mode 100644 QueryBuilder.php create mode 100644 README.md create mode 100644 Schema.php create mode 100644 Transaction.php create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 tests/ActiveRecordTest.php create mode 100644 tests/BatchQueryResultTest.php create mode 100644 tests/CommandTest.php create mode 100644 tests/ConnectionTest.php create mode 100644 tests/FirebirdTestTrait.php create mode 100644 tests/QueryBuilderTest.php create mode 100644 tests/QueryTest.php create mode 100644 tests/SchemaTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/ci/travis/dpkg_firebird2.1.exp create mode 100644 tests/data/config.php create mode 100644 tests/data/source.sql diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..59ea3d7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Ignore all test and documentation for archive +/.gitattributes export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/docs export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7faba60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# phpstorm project files +.idea + +# netbeans project files +nbproject + +# zend studio for eclipse project files +.buildpath +.project +.settings + +# windows thumbnail cache +Thumbs.db + +# composer vendor dir +/vendor + +/composer.lock + +# composer itself is not needed +composer.phar + +# Mac DS_Store Files +.DS_Store + +# phpunit itself is not needed +phpunit.phar +# local phpunit config +/phpunit.xml + +# local tests configuration +/tests/data/config.local.php + +# runtime cache +/tests/runtime diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..23e2a82 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,62 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - nightly + +env: + - FB=2.1 + - FB=2.5 + +# run build against hhvm but allow them to fail +# http://docs.travis-ci.com/user/build-configuration/#Rows-That-are-Allowed-To-Fail +matrix: + fast_finish: true + allow_failures: + - php: 7.0 + - php: nightly + +# faster builds on new travis setup not using sudo +sudo: true + +# cache vendor dirs +cache: + directories: + - $HOME/.composer/cache + +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq firebird$FB-super firebird$FB-dev expect + - if [[ "$FB" == "2.1" ]]; then export DEBIAN_FRONTEND=readline; fi + - if [[ "$FB" == "2.1" ]]; then expect tests/ci/travis/dpkg_firebird2.1.exp; fi + - if [[ "$FB" == "2.1" ]]; then export DEBIAN_FRONTEND=dialog; fi + - if [[ "$FB" == "2.5" ]]; then export FIREBIRD_SERVER_CONFIG=/etc/default/firebird$FB; fi + - if [[ "$FB" == "2.5" ]]; then sudo sed /ENABLE_FIREBIRD_SERVER=/s/no/yes/ -i $FIREBIRD_SERVER_CONFIG; fi + - if [[ "$FB" == "2.5" ]]; then cat $FIREBIRD_SERVER_CONFIG | grep ENABLE_FIREBIRD_SERVER; fi + - sudo service firebird$FB-super start + - export URL_SVN_EXT=https://github.com/php/php-src/branches/PHP-$(phpenv version-name)/ext + - if [[ $(phpenv version-name) == "7.0" ]]; then export URL_SVN_EXT="https://github.com/php/php-src/branches/PHP-7.0.0/ext"; fi + - if [[ $(phpenv version-name) == "nightly" ]]; then export URL_SVN_EXT="https://github.com/php/php-src/trunk/ext"; fi + - svn checkout $URL_SVN_EXT php-ext -q + - (cd php-ext/pdo_firebird/; phpize && ./configure && make && sudo make install) + +install: + - echo "extension=pdo_firebird.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - travis_retry composer self-update && composer --version + - travis_retry composer global require "fxp/composer-asset-plugin:~1.0.0" + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - travis_retry composer install --prefer-dist --no-interaction + +before_script: + # show some versions and env information + - isql-fb -z -q -i /dev/null + # initialize databases + - echo "CREATE DATABASE 'localhost:/tmp/TEST.FDB' USER 'SYSDBA' PASSWORD 'masterkey' PAGE_SIZE 16384 DEFAULT CHARACTER SET UTF8;" > /tmp/create_test.sql + - isql-fb -i /tmp/create_test.sql + - cat /tmp/create_test.sql + +script: + - phpunit diff --git a/ColumnSchema.php b/ColumnSchema.php new file mode 100644 index 0000000..4d7bb67 --- /dev/null +++ b/ColumnSchema.php @@ -0,0 +1,60 @@ + + * @since 2.0 + */ +class ColumnSchema extends \yii\db\ColumnSchema +{ + + /** + * Converts the input value according to [[phpType]] after retrieval from the database. + * If the value is null or an [[Expression]], it will not be converted. + * @param mixed $value input value + * @return mixed converted value + * @since 2.0.3 + */ + protected function typecast($value) + { + + if ($value === '' && $this->type !== Schema::TYPE_TEXT && $this->type !== Schema::TYPE_STRING && $this->type !== Schema::TYPE_BINARY) { + return null; + } + if ($value === null || gettype($value) === $this->phpType || $value instanceof Expression) { + return $value; + } + + switch ($this->phpType) { + case 'resource': + case 'string': + if (is_resource($value)) { + return $value; + } + if (is_float($value)) { + // ensure type cast always has . as decimal separator in all locales + return str_replace(',', '.', (string) $value); + } + return (string) $value; + case 'integer': + if (is_bool($value)) { + return ($value) ? 1 : 0; + } + return (int) $value; + case 'boolean': + return (boolean) $value; + case 'double': + return (double) $value; + } + + return $value; + } +} diff --git a/Command.php b/Command.php new file mode 100644 index 0000000..c70bacc --- /dev/null +++ b/Command.php @@ -0,0 +1,181 @@ + + * @since 2.0 + */ +class Command extends \yii\db\Command +{ + + /** + * @var array pending parameters to be bound to the current PDO statement. + */ + private $_pendingParams = []; + + /** + * @var string the SQL statement that this command represents + */ + private $_sql; + + /** + * Binds a parameter to the SQL statement to be executed. + * @param string|integer $name parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form `:name`. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $value Name of the PHP variable to bind to the SQL statement parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @param integer $length length of the data type + * @param mixed $driverOptions the driver-specific options + * @return static the current command being executed + * @see http://www.php.net/manual/en/function.PDOStatement-bindParam.php + */ + public function bindParam($name, &$value, $dataType = null, $length = null, $driverOptions = null) + { + if ($dataType == \PDO::PARAM_BOOL) { + $dataType = \PDO::PARAM_INT; + } + return parent::bindParam($name, $value, $dataType, $length, $driverOptions); + } + /** + * Binds pending parameters that were registered via [[bindValue()]] and [[bindValues()]]. + * Note that this method requires an active [[pdoStatement]]. + */ + protected function bindPendingParams() + { + foreach ($this->_pendingParams as $name => $value) { +// var_dump($value); + if ($value[1] == 'blob') { + $this->pdoStatement->bindParam($name, $value[0]); + } else { + $this->pdoStatement->bindValue($name, $value[0], $value[1]); + } + } + $this->_pendingParams = []; + } + + /** + * Returns the SQL statement for this command. + * @return string the SQL statement to be executed + */ + public function getSql() + { + return $this->_sql; + } + + /** + * Specifies the SQL statement to be executed. + * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well. + * @param string $sql the SQL statement to be set. + * @return static this command instance + */ + public function setSql($sql) + { + if ($sql !== $this->_sql) { + $this->cancel(); + $this->_sql = $this->db->quoteSql($sql); + $this->_pendingParams = []; + $this->params = []; + } + + return $this; + } + + /** + * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]]. + * Note that the return value of this method should mainly be used for logging purpose. + * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders. + * @return string the raw SQL with parameter values inserted into the corresponding placeholders in [[sql]]. + */ + public function getRawSql() + { + if (empty($this->params)) { + return $this->_sql; + } + $params = []; + foreach ($this->params as $name => $value) { + if (is_string($value)) { + $params[$name] = $this->db->quoteValue($value); + } elseif ($value === null) { + $params[$name] = 'NULL'; + } elseif (!is_object($value) && !is_resource($value)) { + $params[$name] = $value; + } + } + if (!isset($params[1])) { + return strtr($this->_sql, $params); + } + $sql = ''; + foreach (explode('?', $this->_sql) as $i => $part) { + $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; + } + + return $sql; + } + + /** + * Binds a value to a parameter. + * @param string|integer $name Parameter identifier. For a prepared statement + * using named placeholders, this will be a parameter name of + * the form `:name`. For a prepared statement using question mark + * placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $value The value to bind to the parameter + * @param integer $dataType SQL data type of the parameter. If null, the type is determined by the PHP type of the value. + * @return static the current command being executed + * @see http://www.php.net/manual/en/function.PDOStatement-bindValue.php + */ + public function bindValue($name, $value, $dataType = null) + { + if ($dataType === null) { + $dataType = $this->db->getSchema()->getPdoType($value); + } + if ($dataType == \PDO::PARAM_BOOL) { + $dataType = \PDO::PARAM_INT; + } + $this->_pendingParams[$name] = [$value, $dataType]; + $this->params[$name] = $value; + + return $this; + } + + /** + * Binds a list of values to the corresponding parameters. + * This is similar to [[bindValue()]] except that it binds multiple values at a time. + * Note that the SQL data type of each value is determined by its PHP type. + * @param array $values the values to be bound. This must be given in terms of an associative + * array with array keys being the parameter names, and array values the corresponding parameter values, + * e.g. `[':name' => 'John', ':age' => 25]`. By default, the PDO type of each value is determined + * by its PHP type. You may explicitly specify the PDO type by using an array: `[value, type]`, + * e.g. `[':name' => 'John', ':profile' => [$profile, \PDO::PARAM_LOB]]`. + * @return static the current command being executed + */ + public function bindValues($values) + { + if (empty($values)) { + return $this; + } + + $schema = $this->db->getSchema(); + foreach ($values as $name => $value) { + if (is_array($value)) { + $this->_pendingParams[$name] = $value; + $this->params[$name] = $value[0]; + } else { + $type = $schema->getPdoType($value); + $this->_pendingParams[$name] = [$value, $type]; + $this->params[$name] = $value; + } + } + + return $this; + } +} diff --git a/Connection.php b/Connection.php new file mode 100644 index 0000000..e77b343 --- /dev/null +++ b/Connection.php @@ -0,0 +1,82 @@ + + * @since 2.0 + */ +class Connection extends \yii\db\Connection +{ + + /** + * @inheritdoc + */ + public $schemaMap = [ + 'firebird' => 'edgardmessias\db\firebird\Schema', // Firebird + ]; + public $pdoClass = 'edgardmessias\db\firebird\PdoAdapter'; + + /** + * @var Transaction the currently active transaction + */ + private $_transaction; + + /** + * Creates a command for execution. + * @param string $sql the SQL statement to be executed + * @param array $params the parameters to be bound to the SQL statement + * @return Command the DB command + */ + public function createCommand($sql = null, $params = []) + { + $command = new Command([ + 'db' => $this, + 'sql' => $sql, + ]); + + return $command->bindValues($params); + } + + /** + * Returns the currently active transaction. + * @return Transaction the currently active transaction. Null if no active transaction. + */ + public function getTransaction() + { + return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null; + } + + /** + * Starts a transaction. + * @param string|null $isolationLevel The isolation level to use for this transaction. + * See [[Transaction::begin()]] for details. + * @return Transaction the transaction initiated + */ + public function beginTransaction($isolationLevel = null) + { + $this->open(); + + if (($transaction = $this->getTransaction()) === null) { + $transaction = $this->_transaction = new Transaction(['db' => $this]); + } + $transaction->begin($isolationLevel); + + return $transaction; + } + + public function close() + { + if ($this->pdo !== null) { + $this->_transaction = null; + } + parent::close(); + } +} diff --git a/PdoAdapter.php b/PdoAdapter.php new file mode 100644 index 0000000..0304a81 --- /dev/null +++ b/PdoAdapter.php @@ -0,0 +1,115 @@ + + */ +class PdoAdapter extends PDO +{ + + private $inTransaction = false; + + /** + * Do some basic setup for Firebird. + * o Force use of exceptions on error. + * o Force all metadata to lower case. + * Yii will behave in unpredicatable ways if + * metadata is not lowercase. + * o Ensure that table names are not prefixed to + * fieldnames when returning metadata. + * Finally call parent constructor. + * + */ + function __construct($dsn, $username, $password, $driver_options = array()) + { + // Windows OS paths with backslashes should be changed + $dsn = str_replace("\\", "/", $dsn); + // apply error mode + $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; + // lower case column names in results are necessary for Yii ActiveRecord proper functioning + $driver_options[PDO::ATTR_CASE] = PDO::CASE_LOWER; + // ensure we only receive fieldname not tablename.fieldname. + $driver_options[PDO::ATTR_FETCH_TABLE_NAMES] = false; + parent::__construct($dsn, $username, $password, $driver_options); + } + + /** + * Initiates a transaction + * @return bool TRUE on success or FALSE on failure. + */ + public function beginTransaction($isolationLevel = null) + { + $this->setAttribute(PDO::ATTR_AUTOCOMMIT, false); + + if ($isolationLevel === false) { + $this->inTransaction = true; + return true; + } + + if ($isolationLevel === null) { + $r = $this->exec("SET TRANSACTION"); + $success = ($r !== false); + if ($success) { + $this->inTransaction = true; + } + return ($success); + } + + $r = $this->exec("SET TRANSACTION ISOLATION LEVEL $isolationLevel"); + $success = ($r !== false); + if ($success) { + $this->inTransaction = true; + } + return ($success); + } + + /** + * Commits a transaction + * @return bool TRUE on success or FALSE on failure. + */ + public function commit() + { + $r = $this->exec("COMMIT"); + $this->setAttribute(PDO::ATTR_AUTOCOMMIT, true); + $success = ($r !== false); + if ($success) { + $this->inTransaction = false; + } + return ($success); + } + + /** + * Rolls back a transaction + * @return bool TRUE on success or FALSE on failure. + */ + public function rollBack() + { + $r = $this->exec("ROLLBACK"); + $this->setAttribute(PDO::ATTR_AUTOCOMMIT, true); + $success = ($r !== false); + if ($success) { + $this->inTransaction = false; + } + return ($success); + } + + /** + * Checks if inside a transaction + * @return bool TRUE if a transaction is currently active, and FALSE if not. + */ + public function inTransaction() + { + return $this->inTransaction; + } +} diff --git a/QueryBuilder.php b/QueryBuilder.php new file mode 100644 index 0000000..d404011 --- /dev/null +++ b/QueryBuilder.php @@ -0,0 +1,227 @@ + + * @since 2.0 + */ +class QueryBuilder extends \yii\db\QueryBuilder +{ + + /** + * @var array mapping from abstract column types (keys) to physical column types (values). + */ + public $typeMap = [ + Schema::TYPE_PK => 'integer NOT NULL PRIMARY KEY', + Schema::TYPE_BIGPK => 'bigint NOT NULL PRIMARY KEY', + Schema::TYPE_STRING => 'varchar(255)', + Schema::TYPE_TEXT => 'blob sub_type text', + Schema::TYPE_SMALLINT => 'smallint', + Schema::TYPE_INTEGER => 'integer', + Schema::TYPE_BIGINT => 'bigint', + Schema::TYPE_FLOAT => 'float', + Schema::TYPE_DOUBLE => 'double precision', + Schema::TYPE_DECIMAL => 'numeric(10,0)', + Schema::TYPE_DATETIME => 'timestamp', + Schema::TYPE_TIMESTAMP => 'timestamp', + Schema::TYPE_TIME => 'time', + Schema::TYPE_DATE => 'date', + Schema::TYPE_BINARY => 'blob', + Schema::TYPE_BOOLEAN => 'smallint', + Schema::TYPE_MONEY => 'numeric(18,4)', + ]; + + public function buildSelect($columns, &$params, $distinct = false, $selectOption = null) + { + if (is_array($columns)) { + foreach ($columns as $i => $column) { + if (!is_string($column)) { + continue; + } + $matches = []; + if (preg_match("/^(COUNT|SUM|AVG|MIN|MAX)\((\w+|\*)\)$/i", $column, $matches)) { + $function = $matches[1]; + $alias = $matches[2] != '*' ? $matches[2] : 'ALL'; + + $columns[$i] = "{$column} AS {$function}_{$alias}"; + } + } + } + + return parent::buildSelect($columns, $params, $distinct, $selectOption); + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + $quotedColumns = []; + foreach ($columns as $i => $column) { + $quotedColumns[$i] = strpos($column, '(') === false ? $this->db->quoteColumnName($column) : $column; + } + $vss = []; + foreach ($values as $value) { + $vs = []; + foreach ($columns as $i => $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' = ' : ' != ') . $phName; + } else { + $vs[] = $quotedColumns[$i] . ($operator === 'IN' ? ' IS' : ' IS NOT') . ' NULL'; + } + } + $vss[] = '(' . implode($operator === 'IN' ? ' AND ' : ' OR ', $vs) . ')'; + } + return '(' . implode($operator === 'IN' ? ' OR ' : ' AND ', $vss) . ')'; + } + + public function buildOrderByAndLimit($sql, $orderBy, $limit, $offset) + { + + $orderBy = $this->buildOrderBy($orderBy); + if ($orderBy !== '') { + $sql .= $this->separator . $orderBy; + } + + $limit = $limit !== null ? intval($limit) : -1; + $offset = $offset !== null ? intval($offset) : -1; + // If ignoring both params then do nothing + if ($offset < 0 && $limit < 0) { + return $sql; + } + // If we are ignoring limit then return full result set starting + // from $offset. In Firebird this can only be done with SKIP + if ($offset >= 0 && $limit < 0) { + $count = 1; //Only do it once + $sql = preg_replace('/^SELECT /i', 'SELECT SKIP ' . (int) $offset . ' ', $sql, $count); + return $sql; + } + // If we are ignoring $offset then return $limit rows. + // ie, return the first $limit rows in the set. + if ($offset < 0 && $limit >= 0) { + $rows = $limit; + $sql .= ' ROWS ' . (int) $rows; + return $sql; + } + // Otherwise apply the params and return the amended sql. + if ($offset >= 0 && $limit >= 0) { + // calculate $rows for ROWS... + $rows = $offset + 1; + $sql .= ' ROWS ' . (int) $rows; + // calculate $to for TO... + $to = $offset + $limit; + $sql .= ' TO ' . (int) $to; + return $sql; + } + // If we have fallen through the cracks then just pass + // the sql back. + return $sql; + } + + public function insert($table, $columns, &$params) + { + $schema = $this->db->getSchema(); + $autoIncrementColumn = null; + if (($tableSchema = $schema->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + if ($tableSchema->sequenceName !== null) { + $autoIncrementColumn = $tableSchema->sequenceName; + } + } else { + $columnSchemas = []; + } + + foreach ($columns as $name => $value) { + if (in_array($columnSchemas[$name]->type, [Schema::TYPE_TEXT, Schema::TYPE_BINARY])) { + $columns[$name] = [$value, 'blob']; + } + } + $sql = parent::insert($table, $columns, $params); + + if ($autoIncrementColumn !== null) { + $sql .= ' RETURNING ' . $autoIncrementColumn; + } + + return $sql; + } + + public function update($table, $columns, $condition, &$params) + { + $schema = $this->db->getSchema(); + if (($tableSchema = $schema->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + foreach ($columns as $name => $value) { + if (in_array($columnSchemas[$name]->type, [Schema::TYPE_TEXT, Schema::TYPE_BINARY])) { + $columns[$name] = [$value, 'blob']; + } + } + return parent::update($table, $columns, $condition, $params); + } + + /** + * Generates a batch INSERT SQL statement. + * For example, + * + * ~~~ + * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [ + * ['Tom', 30], + * ['Jane', 20], + * ['Linda', 25], + * ]); + * ~~~ + * + * Note that the values in each row must match the corresponding column names. + * + * The method will properly escape the column names, and quote the values to be inserted. + * + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names + * @param array $rows the rows to be batch inserted into the table + * @return string the batch INSERT SQL statement + */ + public function batchInsert($table, $columns, $rows) + { + $schema = $this->db->getSchema(); + if (($tableSchema = $schema->getTableSchema($table)) !== null) { + $columnSchemas = $tableSchema->columns; + } else { + $columnSchemas = []; + } + + $values = []; + foreach ($rows as $row) { + $vs = []; + foreach ($row as $i => $value) { + if (isset($columns[$i], $columnSchemas[$columns[$i]]) && !is_array($value)) { + $value = $columnSchemas[$columns[$i]]->dbTypecast($value); + } + if (is_string($value)) { + $value = $schema->quoteValue($value); + } elseif ($value === false) { + $value = 0; + } elseif ($value === null) { + $value = 'NULL'; + } + $vs[] = $value; + } + $values[] = 'INSERT INTO ' . $schema->quoteTableName($table) + . ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $vs) . ');'; + } + + foreach ($columns as $i => $name) { + $columns[$i] = $schema->quoteColumnName($name); + } + + return 'EXECUTE block AS BEGIN ' . implode(' ', $values) . ' END;'; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e18526a --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +Firebird Extension for Yii 2 +========================== + +This extension adds [Firebird](http://www.firebirdsql.org/) database engine extension for the [Yii framework 2.0](http://www.yiiframework.com). + +[![Build Status](https://travis-ci.org/edgardmessias/yii2-firebird.svg?branch=master)](https://travis-ci.org/edgardmessias/yii2-firebird) +Requirements +------------ + +At least Firebird version 2.0 is required. However, in order to use all extension features. + +Not use BLOB types. [See this bug](https://bugs.php.net/bug.php?id=61183) + +Installation +------------ + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require --prefer-dist yiisoft/yii2-firebird +``` + +or add + +```json +"yiisoft/yii2-firebird": "*" +``` + +to the require section of your composer.json. + + +Configuration +------------- + +To use this extension, simply add the following code in your application configuration: + +```php +return [ + //.... + 'components' => [ + 'db' => [ + 'class' => 'edgardmessias\db\firebird\Connection', + 'dsn' => 'firebird:dbname=tests/data/TEST.FDB', + 'username' => 'username', + 'password' => 'password', + ], + ], +]; +``` diff --git a/Schema.php b/Schema.php new file mode 100644 index 0000000..b1ae2b9 --- /dev/null +++ b/Schema.php @@ -0,0 +1,498 @@ + index type. This + * property is read-only. + * @property QueryBuilder $queryBuilder The query builder for this connection. This property is read-only. + * + * @author Paul Klimov + * @since 2.0 + */ +class Schema extends \yii\db\Schema +{ + + private $_sequences = []; + private $_lastInsertID = null; + + /** + * @var array map of DB errors and corresponding exceptions + * If left part is found in DB error message exception class from the right part is used. + */ + public $exceptionMap = [ + 'SQLSTATE[23' => 'yii\db\IntegrityException', + 'SQLSTATE[HY000]: General error: -803 violation of PRIMARY' => 'yii\db\IntegrityException', + ]; + public $revervedWords = [ + 'ORDER', + 'TIME', + ]; + + /** + * @var array mapping from physical column types (keys) to abstract column types (values) + */ + public $typeMap = [ + 'bigint' => self::TYPE_BIGINT, + 'char' => self::TYPE_STRING, + 'varchar' => self::TYPE_STRING, + 'timestamp' => self::TYPE_TIMESTAMP, + 'decimal' => self::TYPE_DECIMAL, + 'float' => self::TYPE_FLOAT, + 'blob' => self::TYPE_BINARY, + 'integer' => self::TYPE_INTEGER, + 'blob sub_type text' => self::TYPE_TEXT, + 'numeric' => self::TYPE_DECIMAL, + 'double precision' => self::TYPE_DOUBLE, + 'smallint' => self::TYPE_SMALLINT, + ]; + + /** + * Creates a query builder for the database. + * This method may be overridden by child classes to create a DBMS-specific query builder. + * @return QueryBuilder query builder instance + */ + public function createQueryBuilder() + { + return new QueryBuilder($this->db); + } + + public function quoteSimpleTableName($name) + { + if (in_array(strtoupper($name), $this->revervedWords)) { + return strpos($name, '"') !== false ? $name : '"' . $name . '"'; + } + + return $name; + } + + public function quoteSimpleColumnName($name) + { + if (in_array(strtoupper($name), $this->revervedWords)) { + return parent::quoteSimpleColumnName($name); + } + return $name; + } + + protected function loadTableSchema($name) + { + $table = new TableSchema; + $this->resolveTableNames($table, $name); + if ($this->findColumns($table)) { + $this->findConstraints($table); + if (is_string($table->primaryKey) && isset($this->_sequences[$table->fullName . '.' . $table->primaryKey])) { + $table->sequenceName = $this->_sequences[$table->fullName . '.' . $table->primaryKey]; + } elseif (is_array($table->primaryKey)) { + foreach ($table->primaryKey as $pk) { + if (isset($this->_sequences[$table->fullName . '.' . $pk])) { + $table->sequenceName = $this->_sequences[$table->fullName . '.' . $pk]; + break; + } + } + } + return $table; + } + return null; + } + + public function getPdoType($data) + { + static $typeMap = [ + // php type => PDO type + 'boolean' => \PDO::PARAM_INT, + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } + + /** + * + * @param TableSchema $table + * @param string $name + */ + protected function resolveTableNames($table, $name) + { + $parts = explode('.', str_replace('"', '', $name)); + if (isset($parts[1])) { + $table->schemaName = $parts[0]; + $table->name = strtolower($parts[1]); + $table->fullName = $this->quoteTableName($table->schemaName) . '.' . $this->quoteTableName($table->name); + } else { + $table->name = strtolower($parts[0]); + $table->fullName = $this->quoteTableName($table->name); + } + } + + /** + * Collects the table column metadata. + * + * @param TableSchema $table the table metadata + * @return boolean whether the table exists in the database + */ + protected function findColumns($table) + { + // Zoggo - Converted sql to use join syntax + // robregonm - Added isAutoInc + $sql = 'SELECT + rel.rdb$field_name AS fname, + rel.rdb$default_source AS fdefault, + fld.rdb$field_type AS fcodtype, + fld.rdb$field_sub_type AS fcodsubtype, + fld.rdb$field_length AS flength, + fld.rdb$character_length AS fcharlength, + fld.rdb$field_scale AS fscale, + fld.rdb$field_precision AS fprecision, + rel.rdb$null_flag AS fnull, + fld.rdb$default_value AS fdefault_value, + (SELECT 1 FROM RDB$TRIGGERS + WHERE RDB$SYSTEM_FLAG = 0 + AND UPPER(RDB$RELATION_NAME)=UPPER(\'' . $table->name . '\') + AND RDB$TRIGGER_TYPE = 1 + AND RDB$TRIGGER_INACTIVE = 0 + AND (UPPER(REPLACE(RDB$TRIGGER_SOURCE,\' \',\'\')) LIKE \'%NEW.\'||TRIM(rel.rdb$field_name)||\'=GEN_ID%\' + OR UPPER(REPLACE(RDB$TRIGGER_SOURCE,\' \',\'\')) LIKE \'%NEW.\'||TRIM(rel.rdb$field_name)||\'=NEXTVALUEFOR%\')) + AS fautoinc + FROM + rdb$relation_fields rel + JOIN rdb$fields fld ON rel.rdb$field_source=fld.rdb$field_name + WHERE + UPPER(rel.rdb$relation_name)=UPPER(\'' . $table->name . '\') + ORDER BY + rel.rdb$field_position;'; + try { + $columns = $this->db->createCommand($sql)->queryAll(); + if (!$columns) { + return false; + } + } catch (Exception $e) { + return false; + } + $sql = 'SELECT + idx.rdb$field_name AS fname + FROM + rdb$relation_constraints rc + JOIN rdb$index_segments idx ON idx.rdb$index_name=rc.rdb$index_name + WHERE rc.rdb$constraint_type=\'PRIMARY KEY\' + AND UPPER(rc.rdb$relation_name)=UPPER(\'' . $table->name . '\')'; + try { + $pkeys = $this->db->createCommand($sql)->queryColumn(); + } catch (Exception $e) { + return false; + } + $pkeys = array_map("rtrim", $pkeys); + $pkeys = array_map("strtolower", $pkeys); + foreach ($columns as $key => $column) { + $column = array_map("strtolower", $column); + $columns[$key]['fprimary'] = in_array(rtrim($column['fname']), $pkeys); + } + foreach ($columns as $column) { + $c = $this->loadColumnSchema($column); + if ($c->autoIncrement) { + $this->_sequences[$table->fullName . '.' . $c->name] = $table->fullName . '.' . $c->name; + } + $table->columns[$c->name] = $c; + if ($c->isPrimaryKey) { + $table->primaryKey[] = $c->name; + } + } + return (count($table->columns) > 0); + } + + /** + * @return \yii\db\ColumnSchema + * @throws \yii\base\InvalidConfigException + */ + protected function createColumnSchema() + { + return \Yii::createObject('\edgardmessias\db\firebird\ColumnSchema'); + } + + /** + * Creates a table column. + * + * @param array $column column metadata + * @return ColumnSchema normalized column metadata + */ + protected function loadColumnSchema($column) + { + $c = $this->createColumnSchema(); + $c->name = strtolower(rtrim($column['fname'])); + $c->allowNull = $column['fnull'] !== '1'; + $c->isPrimaryKey = $column['fprimary']; + $c->autoIncrement = $column['fautoinc'] === '1'; + + $c->type = self::TYPE_STRING; + + $defaultValue = null; + if (!empty($column['fdefault'])) { + // remove whitespace, 'DEFAULT ' prefix and surrounding single quotes; all optional + if (preg_match("/\s*(DEFAULT\s+){0,1}('(.*)'|(.*))\s*/i", $column['fdefault'], $parts)) { + $defaultValue = array_pop($parts); + } + // handle escaped single quotes like in "funny''quoted''string" + $defaultValue = str_replace('\'\'', '\'', $defaultValue); + } + if ($defaultValue === null) { + $defaultValue = $column['fdefault_value']; + } + $dbType = ""; + $baseTypes = array( + 7 => 'SMALLINT', + 8 => 'INTEGER', + 16 => 'INT64', + 9 => 'QUAD', + 10 => 'FLOAT', + 11 => 'D_FLOAT', + 17 => 'BOOLEAN', + 27 => 'DOUBLE PRECISION', + 12 => 'DATE', + 13 => 'TIME', + 35 => 'TIMESTAMP', + 261 => 'BLOB', + 40 => 'CSTRING', + 45 => 'BLOB_ID', + ); + $baseCharTypes = array( + 37 => 'VARCHAR', + 14 => 'CHAR', + ); + if (array_key_exists((int) $column['fcodtype'], $baseTypes)) { + $dbType = $baseTypes[(int) $column['fcodtype']]; + } elseif (array_key_exists((int) $column['fcodtype'], $baseCharTypes)) { + $c->size = (int) $column['fcharlength']; + $c->precision = $c->size; + $dbType = $baseCharTypes[(int) $column['fcodtype']] . "($c->size)"; + } + switch ((int) $column['fcodtype']) { + case 7: + case 8: + switch ((int) $column['fcodsubtype']) { + case 1: + $c->precision = (int) $column['fprecision']; + $c->size = $c->precision; + $c->scale = abs((int) $column['fscale']); + $dbType = "NUMERIC({$c->precision},{$c->scale})"; + break; + case 2: + $c->precision = (int) $column['fprecision']; + $c->size = $c->precision; + $c->scale = abs((int) $column['fscale']); + $dbType = "DECIMAL({$c->precision},{$c->scale})"; + break; + } + break; + case 16: + switch ((int) $column['fcodsubtype']) { + case 1: + $c->precision = (int) $column['fprecision']; + $c->size = $c->precision; + $c->scale = abs((int) $column['fscale']); + $dbType = "NUMERIC({$c->precision},{$c->scale})"; + break; + case 2: + $c->precision = (int) $column['fprecision']; + $c->size = $c->precision; + $c->scale = abs((int) $column['fscale']); + $dbType = "DECIMAL({$c->precision},{$c->scale})"; + break; + default : + $dbType = 'BIGINT'; + break; + } + break; + case 261: + switch ((int) $column['fcodsubtype']) { + case 1: + $dbType = 'BLOB SUB_TYPE TEXT'; + $c->size = null; + break; + } + break; + } + + $c->dbType = strtolower($dbType); + + $c->type = self::TYPE_STRING; + if (preg_match('/^([\w\ ]+)(?:\(([^\)]+)\))?/', $c->dbType, $matches)) { + $type = strtolower($matches[1]); + if (isset($this->typeMap[$type])) { + $c->type = $this->typeMap[$type]; + } + } + + + $c->phpType = $this->getColumnPhpType($c); + + $c->defaultValue = null; + if ($defaultValue !== null) { + if ($c->type == self::TYPE_TIMESTAMP && trim($defaultValue) == 'CURRENT_TIMESTAMP') { + $c->defaultValue = new \yii\db\Expression('CURRENT_TIMESTAMP'); + } else { + $c->defaultValue = $c->phpTypecast($defaultValue); + } + } + + return $c; + } + + /** + * Collects the foreign key column details for the given table. + * + * @param TableSchema $table the table metadata + */ + protected function findConstraints($table) + { + // Zoggo - Converted sql to use join syntax + $sql = 'SELECT + a.rdb$constraint_name as fconstraint, + c.rdb$relation_name AS ftable, + d.rdb$field_name AS pfield, + e.rdb$field_name AS ffield + FROM + rdb$ref_constraints b + JOIN rdb$relation_constraints a ON a.rdb$constraint_name=b.rdb$constraint_name + JOIN rdb$relation_constraints c ON b.rdb$const_name_uq=c.rdb$constraint_name + JOIN rdb$index_segments d ON c.rdb$index_name=d.rdb$index_name + JOIN rdb$index_segments e ON a.rdb$index_name=e.rdb$index_name AND e.rdb$field_position = d.rdb$field_position + WHERE + a.rdb$constraint_type=\'FOREIGN KEY\' AND + UPPER(a.rdb$relation_name)=UPPER(\'' . $table->name . '\') '; + try { + $fkeys = $this->db->createCommand($sql)->queryAll(); + } catch (Exception $e) { + return false; + } + + $constraints = []; + foreach ($fkeys as $fkey) { + // Zoggo - Added strtolower here to guarantee that values are + // returned lower case. Otherwise gii generates wrong code. + $fkey = array_map("rtrim", $fkey); + $fkey = array_map("strtolower", $fkey); + + if (!isset($constraints[$fkey['fconstraint']])) { + $constraints[$fkey['fconstraint']] = [ + $fkey['ftable'] + ]; + } + $constraints[$fkey['fconstraint']][$fkey['ffield']] = $fkey['pfield']; + } + $table->foreignKeys = array_values($constraints); + } + + protected function findTableNames($schema = '') + { + $sql = 'SELECT + rdb$relation_name + FROM + rdb$relations + WHERE + (rdb$view_blr is null) AND + (rdb$system_flag is null OR rdb$system_flag=0)'; + try { + $tables = $this->db->createCommand($sql)->queryColumn(); + } catch (Exception $e) { + return false; + } + foreach ($tables as $key => $table) { + $tables[$key] = strtolower(rtrim($table)); + } + return $tables; + } + + /** + * Sets the isolation level of the current transaction. + * @param string $level The transaction isolation level to use for this transaction. + * This can be one of [[Transaction::READ_UNCOMMITTED]], [[Transaction::READ_COMMITTED]], [[Transaction::REPEATABLE_READ]] + * and [[Transaction::SERIALIZABLE]] but also a string containing DBMS specific syntax to be used + * after `SET TRANSACTION ISOLATION LEVEL`. + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + public function setTransactionIsolationLevel($level) + { + if ($level == \yii\db\Transaction::READ_UNCOMMITTED) { + parent::setTransactionIsolationLevel('READ COMMITTED RECORD_VERSION'); + } elseif ($level == \yii\db\Transaction::REPEATABLE_READ) { + parent::setTransactionIsolationLevel('SNAPSHOT'); + } elseif ($level == \yii\db\Transaction::SERIALIZABLE) { + parent::setTransactionIsolationLevel('SNAPSHOT TABLE STABILITY'); + } else { + parent::setTransactionIsolationLevel($level); + } + } + + /** + * Executes the INSERT command, returning primary key values. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column data (name => value) to be inserted into the table. + * @return array primary key values or false if the command fails + * @since 2.0.4 + */ + public function insert($table, $columns) + { + + $tableSchema = $this->getTableSchema($table); + + $command = $this->db->createCommand()->insert($table, $columns); + + if ($tableSchema->sequenceName !== null) { + $this->_lastInsertID = $command->queryScalar(); + if ($this->_lastInsertID === false) { + return false; + } + } else { + if (!$command->execute()) { + return false; + } + } + $result = []; + foreach ($tableSchema->primaryKey as $name) { + if ($tableSchema->columns[$name]->autoIncrement) { + $result[$name] = $this->getLastInsertID($tableSchema->sequenceName); + break; + } else { + $result[$name] = isset($columns[$name]) ? $columns[$name] : $tableSchema->columns[$name]->defaultValue; + } + } + return $result; + } + + /** + * Returns the ID of the last inserted row or sequence value. + * @param string $sequenceName name of the sequence object (required by some DBMS) + * @return string the row ID of the last row inserted, or the last value retrieved from the sequence object + * @throws InvalidCallException if the DB connection is not active + * @see http://www.php.net/manual/en/function.PDO-lastInsertId.php + */ + public function getLastInsertID($sequenceName = '') + { + if (!$this->db->isActive) { + throw new InvalidCallException('DB Connection is not active.'); + } + + if ($this->_lastInsertID !== false) { + return $this->_lastInsertID; + } + return null; + } +} diff --git a/Transaction.php b/Transaction.php new file mode 100644 index 0000000..aa64b90 --- /dev/null +++ b/Transaction.php @@ -0,0 +1,191 @@ +beginTransaction(); + * try { + * $connection->createCommand($sql1)->execute(); + * $connection->createCommand($sql2)->execute(); + * //.... other SQL executions + * $transaction->commit(); + * } catch (Exception $e) { + * $transaction->rollBack(); + * } + * ~~~ + * + * @property boolean $isActive Whether this transaction is active. Only an active transaction can [[commit()]] + * or [[rollBack()]]. This property is read-only. + * @property string $isolationLevel The transaction isolation level to use for this transaction. This can be + * one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but also a string + * containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. This property is + * write-only. + * + * @author Edgard Lorraine Messias + * @since 1.0 + */ +class Transaction extends \yii\db\Transaction +{ + + /** + * @var integer the nesting level of the transaction. 0 means the outermost level. + */ + private $_level = 0; + + /** + * Returns a value indicating whether this transaction is active. + * @return boolean whether this transaction is active. Only an active transaction + * can [[commit()]] or [[rollBack()]]. + */ + public function getIsActive() + { + return $this->_level > 0 && $this->db && $this->db->isActive; + } + + /** + * Begins a transaction. + * @param string|null $isolationLevel The [isolation level][] to use for this transaction. + * This can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but + * also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. + * If not specified (`null`) the isolation level will not be set explicitly and the DBMS default will be used. + * + * > Note: This setting does not work for PostgreSQL, where setting the isolation level before the transaction + * has no effect. You have to call [[setIsolationLevel()]] in this case after the transaction has started. + * + * > Note: Some DBMS allow setting of the isolation level only for the whole connection so subsequent transactions + * may get the same isolation level even if you did not specify any. When using this feature + * you may need to set the isolation level for all transactions explicitly to avoid conflicting settings. + * At the time of this writing affected DBMS are MSSQL and SQLite. + * + * [isolation level]: http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + * @throws InvalidConfigException if [[db]] is `null`. + */ + public function begin($isolationLevel = null) + { + if ($this->db === null) { + throw new InvalidConfigException('Transaction::db must be set.'); + } + $this->db->open(); + + if ($this->_level == 0) { + Yii::trace('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__); + + $this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION); + if ($isolationLevel !== null) { + $this->db->pdo->beginTransaction(false); + $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel); + } else { + $this->db->pdo->beginTransaction(); + } + $this->_level = 1; + + return; + } + + $schema = $this->db->getSchema(); + if ($schema->supportsSavepoint()) { + Yii::trace('Set savepoint ' . $this->_level, __METHOD__); + $schema->createSavepoint('LEVEL' . $this->_level); + } else { + Yii::info('Transaction not started: nested transaction not supported', __METHOD__); + } + $this->_level++; + } + + /** + * Commits a transaction. + * @throws Exception if the transaction is not active + */ + public function commit() + { + if (!$this->getIsActive()) { + throw new Exception('Failed to commit transaction: transaction was inactive.'); + } + + $this->_level--; + if ($this->_level == 0) { + Yii::trace('Commit transaction', __METHOD__); + $this->db->pdo->commit(); + $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION); + return; + } + + $schema = $this->db->getSchema(); + if ($schema->supportsSavepoint()) { + Yii::trace('Release savepoint ' . $this->_level, __METHOD__); + $schema->releaseSavepoint('LEVEL' . $this->_level); + } else { + Yii::info('Transaction not committed: nested transaction not supported', __METHOD__); + } + } + + /** + * Rolls back a transaction. + * @throws Exception if the transaction is not active + */ + public function rollBack() + { + if (!$this->getIsActive()) { + // do nothing if transaction is not active: this could be the transaction is committed + // but the event handler to "commitTransaction" throw an exception + return; + } + + $this->_level--; + if ($this->_level == 0) { + Yii::trace('Roll back transaction', __METHOD__); + $this->db->pdo->rollBack(); + $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION); + return; + } + + $schema = $this->db->getSchema(); + if ($schema->supportsSavepoint()) { + Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__); + $schema->rollBackSavepoint('LEVEL' . $this->_level); + } else { + Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__); + // throw an exception to fail the outer transaction + throw new Exception('Roll back failed: nested transaction not supported.'); + } + } + + /** + * Sets the transaction isolation level for this transaction. + * + * This method can be used to set the isolation level while the transaction is already active. + * However this is not supported by all DBMS so you might rather specify the isolation level directly + * when calling [[begin()]]. + * @param string $level The transaction isolation level to use for this transaction. + * This can be one of [[READ_UNCOMMITTED]], [[READ_COMMITTED]], [[REPEATABLE_READ]] and [[SERIALIZABLE]] but + * also a string containing DBMS specific syntax to be used after `SET TRANSACTION ISOLATION LEVEL`. + * @throws Exception if the transaction is not active + * @see http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels + */ + public function setIsolationLevel($level) + { + if (!$this->getIsActive()) { + throw new Exception('Failed to set isolation level: transaction was inactive.'); + } + Yii::trace('Setting transaction isolation level to ' . $level, __METHOD__); + $this->db->getSchema()->setTransactionIsolationLevel($level); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..d2fb371 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "edgardmessias/yii2-firebird", + "description": "Firebird connector for Yii2 framework", + "keywords": ["yii2", "firebird", "active-record"], + "type": "yii2-extension", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/edgardmessias/yii2-firebird/issues", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/edgardmessias/yii2-firebird" + }, + "authors": [ + { + "name": "Edgard Lorraine Messias", + "email": "edgardmessias@gmail.com" + } + ], + "require": { + "yiisoft/yii2": ">=2.0.4", + "ext-pdo": "*", + "ext-pdo_firebird": "*" + }, + "require-dev": { + "yiisoft/yii2-dev": ">=2.0.4", + "phpunit/phpunit": "~4.5" + }, + "autoload": { + "psr-4": { "edgardmessias\\db\\firebird\\": "" } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..ac5ca5e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + ./tests + + + diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php new file mode 100644 index 0000000..1937e4c --- /dev/null +++ b/tests/ActiveRecordTest.php @@ -0,0 +1,25 @@ +select(['{{customer}}.*', '([[status]]*2) AS [[status2]]']) + ->where(['name' => 'user3'])->one(); + $this->assertEquals(3, $customer->id); + $this->assertEquals(4, $customer->status2); + } +} diff --git a/tests/BatchQueryResultTest.php b/tests/BatchQueryResultTest.php new file mode 100644 index 0000000..29c8a77 --- /dev/null +++ b/tests/BatchQueryResultTest.php @@ -0,0 +1,14 @@ +getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT id, t.name FROM customer t", $command->sql); + } + + public function testColumnCase() + { + $this->markTestSkipped('Test for travis with exit code 139'); + return; + + $db = $this->getConnection(false); + + //Force to use LOWER CASE + $this->assertEquals(\PDO::CASE_LOWER, $db->slavePdo->getAttribute(\PDO::ATTR_CASE)); + + $sql = 'SELECT [[customer_id]], [[total]] FROM {{order}}'; + $rows = $db->createCommand($sql)->queryAll(); + $this->assertTrue(isset($rows[0])); + $this->assertTrue(isset($rows[0]['customer_id'])); + $this->assertTrue(isset($rows[0]['total'])); + + $db->slavePdo->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_LOWER); + $rows = $db->createCommand($sql)->queryAll(); + $this->assertTrue(isset($rows[0])); + $this->assertTrue(isset($rows[0]['customer_id'])); + $this->assertTrue(isset($rows[0]['total'])); + + $db->slavePdo->setAttribute(\PDO::ATTR_CASE, \PDO::CASE_UPPER); + $rows = $db->createCommand($sql)->queryAll(); + $this->assertTrue(isset($rows[0])); + $this->assertTrue(isset($rows[0]['CUSTOMER_ID'])); + $this->assertTrue(isset($rows[0]['TOTAL'])); + } + + public function testBindParamValue() + { + $db = $this->getConnection(); + + // bindParam + $sql = 'INSERT INTO {{customer}}([[email]], [[name]], [[address]]) VALUES (:email, :name, :address)'; + $command = $db->createCommand($sql); + $email = 'user4@example.com'; + $name = 'user4'; + $address = 'address4'; + $command->bindParam(':email', $email); + $command->bindParam(':name', $name); + $command->bindParam(':address', $address); + $command->execute(); + + $sql = 'SELECT [[name]] FROM {{customer}} WHERE [[email]] = :email'; + $command = $db->createCommand($sql); + $command->bindParam(':email', $email); + $this->assertEquals($name, $command->queryScalar()); + + $sql = <<createCommand($sql); + $intCol = 123; + $charCol = str_repeat('abc', 33) . 'x'; // a 100 char string + $boolCol = false; + $command->bindParam(':int_col', $intCol, \PDO::PARAM_INT); + $command->bindParam(':char_col', $charCol); + $command->bindParam(':bool_col', $boolCol, \PDO::PARAM_BOOL); + + $floatCol = 1.23; + $numericCol = '1.23'; + $blobCol = "\x10\x11\x12"; + $command->bindParam(':float_col', $floatCol); + $command->bindParam(':numeric_col', $numericCol); + $command->bindParam(':blob_col', $blobCol); + + $this->assertEquals(1, $command->execute()); + + $command = $db->createCommand('SELECT [[int_col]], [[char_col]], [[float_col]], [[blob_col]], [[numeric_col]], [[bool_col]] FROM {{type}}'); + + //For Firebird + $command->prepare(); + $command->pdoStatement->bindColumn('blob_col', $blobCol, \PDO::PARAM_LOB); + + $row = $command->queryOne(); + $this->assertEquals($intCol, $row['int_col']); + $this->assertEquals($charCol, $row['char_col']); + $this->assertEquals($floatCol, $row['float_col']); + + $this->assertEquals($blobCol, $row['blob_col']); + $this->assertEquals($numericCol, $row['numeric_col']); + $this->assertEquals($boolCol, (int) $row['bool_col']); + + // bindValue + $sql = 'INSERT INTO {{customer}}([[email]], [[name]], [[address]]) VALUES (:email, \'user5\', \'address5\')'; + $command = $db->createCommand($sql); + $command->bindValue(':email', 'user5@example.com'); + $command->execute(); + + $sql = 'SELECT [[email]] FROM {{customer}} WHERE [[name]] = :name'; + $command = $db->createCommand($sql); + $command->bindValue(':name', 'user5'); + $this->assertEquals('user5@example.com', $command->queryScalar()); + } +} diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php new file mode 100644 index 0000000..83515c0 --- /dev/null +++ b/tests/ConnectionTest.php @@ -0,0 +1,64 @@ +getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + public function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals('table', $connection->quoteTableName('table')); + $this->assertEquals('"table"', $connection->quoteTableName('"table"')); + $this->assertEquals('schema.table', $connection->quoteTableName('schema.table')); + $this->assertEquals('schema."table"', $connection->quoteTableName('schema."table"')); + $this->assertEquals('"schema"."table"', $connection->quoteTableName('"schema"."table"')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + + $this->assertEquals('"order"', $connection->quoteTableName('order')); + $this->assertEquals('"order"', $connection->quoteTableName('"order"')); + $this->assertEquals('schema."order"', $connection->quoteTableName('schema.order')); + $this->assertEquals('schema."order"', $connection->quoteTableName('schema."order"')); + $this->assertEquals('"schema"."order"', $connection->quoteTableName('"schema"."order"')); + $this->assertEquals('{{order}}', $connection->quoteTableName('{{order}}')); + $this->assertEquals('(order)', $connection->quoteTableName('(order)')); + } + + public function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('column', $connection->quoteColumnName('column')); + $this->assertEquals('"column"', $connection->quoteColumnName('"column"')); + $this->assertEquals('table.column', $connection->quoteColumnName('table.column')); + $this->assertEquals('table."column"', $connection->quoteColumnName('table."column"')); + $this->assertEquals('"table"."column"', $connection->quoteColumnName('"table"."column"')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + + $this->assertEquals('"time"', $connection->quoteColumnName('time')); + $this->assertEquals('"time"', $connection->quoteColumnName('"time"')); + $this->assertEquals('"order"."time"', $connection->quoteColumnName('order.time')); + $this->assertEquals('"order"."time"', $connection->quoteColumnName('order."time"')); + $this->assertEquals('"order"."time"', $connection->quoteColumnName('"order"."time"')); + $this->assertEquals('[[time]]', $connection->quoteColumnName('[[time]]')); + $this->assertEquals('{{time}}', $connection->quoteColumnName('{{time}}')); + $this->assertEquals('(time)', $connection->quoteColumnName('(time)')); + } +} diff --git a/tests/FirebirdTestTrait.php b/tests/FirebirdTestTrait.php new file mode 100644 index 0000000..743c03f --- /dev/null +++ b/tests/FirebirdTestTrait.php @@ -0,0 +1,59 @@ +traitDbs as $db) { + $db->close(); + } + parent::tearDown(); + } + + public function prepareDatabase($config, $fixture, $open = true) + { + if (!isset($config['class'])) { + $config['class'] = '\edgardmessias\db\firebird\Connection'; + } + /* @var $db \edgardmessias\db\firebird\Connection */ + $db = \Yii::createObject($config); + + $this->traitDbs[] = $db; + + if (!$open) { + return $db; + } + $db->open(); + if ($fixture !== null) { + $lines = explode('-- SQL', file_get_contents($fixture)); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + //Unlock resources of table modification. + $db->close(); + $db->open(); + foreach ($this->traitDbs as $db) { + if ($db->pdo !== null) { + $db->close(); + $db->open(); + } + } + } + return $db; + } +} diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php new file mode 100644 index 0000000..b413dfa --- /dev/null +++ b/tests/QueryBuilderTest.php @@ -0,0 +1,142 @@ +driverName) { + case 'firebird': + return new \edgardmessias\db\firebird\QueryBuilder($this->getConnection(true, false)); + } + throw new \Exception('Test is not implemented for ' . $this->driverName); + } + + /** + * adjust dbms specific escaping + * @param $sql + * @return mixed + */ + protected function replaceQuotes($sql) + { + if (!in_array($this->driverName, ['mssql', 'mysql', 'sqlite'])) { + return str_replace('`', '', $sql); + } + return $sql; + } + + /** + * this is not used as a dataprovider for testGetColumnType to speed up the test + * when used as dataprovider every single line will cause a reconnect with the database which is not needed here + */ + public function columnTypes() + { + return [ + [Schema::TYPE_PK, 'integer NOT NULL PRIMARY KEY'], + [Schema::TYPE_PK . '(8)', 'integer NOT NULL PRIMARY KEY'], + [Schema::TYPE_PK . ' CHECK (value > 5)', 'integer NOT NULL PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_PK . '(8) CHECK (value > 5)', 'integer NOT NULL PRIMARY KEY CHECK (value > 5)'], + [Schema::TYPE_STRING, 'varchar(255)'], + [Schema::TYPE_STRING . '(32)', 'varchar(32)'], + [Schema::TYPE_STRING . " CHECK (value LIKE 'test%')", "varchar(255) CHECK (value LIKE 'test%')"], + [Schema::TYPE_STRING . "(32) CHECK (value LIKE 'test%')", "varchar(32) CHECK (value LIKE 'test%')"], + [Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'], + [Schema::TYPE_TEXT, 'blob sub_type text'], + [Schema::TYPE_TEXT . '(255)', 'blob sub_type text'], + [Schema::TYPE_TEXT . " CHECK (value LIKE 'test%')", "blob sub_type text CHECK (value LIKE 'test%')"], + [Schema::TYPE_TEXT . "(255) CHECK (value LIKE 'test%')", "blob sub_type text CHECK (value LIKE 'test%')"], + [Schema::TYPE_TEXT . ' NOT NULL', 'blob sub_type text NOT NULL'], + [Schema::TYPE_TEXT . '(255) NOT NULL', 'blob sub_type text NOT NULL'], + [Schema::TYPE_SMALLINT, 'smallint'], + [Schema::TYPE_SMALLINT . '(8)', 'smallint'], + [Schema::TYPE_INTEGER, 'integer'], + [Schema::TYPE_INTEGER . '(8)', 'integer'], + [Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'], + [Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'], + [Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'], + [Schema::TYPE_BIGINT, 'bigint'], + [Schema::TYPE_BIGINT . '(8)', 'bigint'], + [Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'], + [Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'], + [Schema::TYPE_FLOAT, 'float'], + [Schema::TYPE_FLOAT . '(16,5)', 'float'], + [Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'float CHECK (value > 5.6)'], + [Schema::TYPE_FLOAT . ' NOT NULL', 'float NOT NULL'], + [Schema::TYPE_DOUBLE, 'double precision'], + [Schema::TYPE_DOUBLE . '(16,5)', 'double precision'], + [Schema::TYPE_DOUBLE . ' CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'], + [Schema::TYPE_DOUBLE . '(16,5) CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'], + [Schema::TYPE_DOUBLE . ' NOT NULL', 'double precision NOT NULL'], + [Schema::TYPE_DECIMAL, 'numeric(10,0)'], + [Schema::TYPE_DECIMAL . '(12,4)', 'numeric(12,4)'], + [Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'numeric(10,0) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'numeric(12,4) CHECK (value > 5.6)'], + [Schema::TYPE_DECIMAL . ' NOT NULL', 'numeric(10,0) NOT NULL'], + [Schema::TYPE_DATETIME, 'timestamp'], + [Schema::TYPE_DATETIME . " CHECK (value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK (value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATETIME . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIMESTAMP, 'timestamp'], + [Schema::TYPE_TIMESTAMP . " CHECK (value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK (value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'], + [Schema::TYPE_TIME, 'time'], + [Schema::TYPE_TIME . " CHECK (value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK (value BETWEEN '12:00:00' AND '13:01:01')"], + [Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'], + [Schema::TYPE_DATE, 'date'], + [Schema::TYPE_DATE . " CHECK (value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK (value BETWEEN '2011-01-01' AND '2013-01-01')"], + [Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'], + [Schema::TYPE_BINARY, 'blob'], + [Schema::TYPE_BOOLEAN, 'smallint'], + [Schema::TYPE_BOOLEAN . ' DEFAULT 1 NOT NULL', 'smallint DEFAULT 1 NOT NULL'], + [Schema::TYPE_MONEY, 'numeric(18,4)'], + [Schema::TYPE_MONEY . '(16,2)', 'numeric(16,2)'], + [Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'numeric(18,4) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'numeric(16,2) CHECK (value > 0.0)'], + [Schema::TYPE_MONEY . ' NOT NULL', 'numeric(18,4) NOT NULL'], + ]; + } + + public function conditionProvider() + { + $conditions = parent::conditionProvider(); + + $conditions[46] = [ ['=', 'date', (new Query())->select('max(date)')->from('test')->where(['id' => 5])], 'date = (SELECT max(date) AS max_date FROM test WHERE id=:qp0)', [':qp0' => 5] ]; + $conditions[49] = [ ['in', ['id', 'name'], [['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar']]], '((id = :qp0 AND name = :qp1) OR (id = :qp2 AND name = :qp3))', [':qp0' => 1, ':qp1' => 'foo', ':qp2' => 2, ':qp3' => 'bar']]; + $conditions[50] = [ ['not in', ['id', 'name'], [['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar']]], '((id != :qp0 OR name != :qp1) AND (id != :qp2 OR name != :qp3))', [':qp0' => 1, ':qp1' => 'foo', ':qp2' => 2, ':qp3' => 'bar']]; + + return $conditions; + } + + public function testSelectSubquery() + { + $subquery = (new Query()) + ->select('COUNT(*)') + ->from('operations') + ->where('account_id = accounts.id'); + $query = (new Query()) + ->select('*') + ->from('accounts') + ->addSelect(['operations_count' => $subquery]); + list ($sql, $params) = $this->getQueryBuilder()->build($query); + $expected = $this->replaceQuotes('SELECT *, (SELECT COUNT(*) AS COUNT_ALL FROM `operations` WHERE account_id = accounts.id) AS `operations_count` FROM `accounts`'); + $this->assertEquals($expected, $sql); + $this->assertEmpty($params); + } +} diff --git a/tests/QueryTest.php b/tests/QueryTest.php new file mode 100644 index 0000000..bdf0f9b --- /dev/null +++ b/tests/QueryTest.php @@ -0,0 +1,14 @@ +getConnection()->schema; + $tables = $schema->getTableNames(); + $this->assertTrue(in_array('customer', $tables)); + $this->assertTrue(in_array('category', $tables)); + $this->assertTrue(in_array('item', $tables)); + $this->assertTrue(in_array('order', $tables)); + $this->assertTrue(in_array('order_item', $tables)); + $this->assertTrue(in_array('type', $tables)); + $this->assertTrue(in_array('animal', $tables)); +// $this->assertTrue(in_array('animal_view', $tables)); + } + + public function testSingleFk() + { + /* @var $schema Schema */ + $schema = $this->getConnection()->schema; + $table = $schema->getTableSchema('order_item'); + $this->assertCount(2, $table->foreignKeys); + $this->assertTrue(isset($table->foreignKeys[0])); + $this->assertEquals('order', $table->foreignKeys[0][0]); + $this->assertEquals('id', $table->foreignKeys[0]['order_id']); + $this->assertTrue(isset($table->foreignKeys[1])); + $this->assertEquals('item', $table->foreignKeys[1][0]); + $this->assertEquals('id', $table->foreignKeys[1]['item_id']); + } + + public function getExpectedColumns() + { + $columns = parent::getExpectedColumns(); + unset($columns['enum_col']); + $columns['int_col']['dbType'] = 'integer'; + $columns['int_col']['size'] = null; + $columns['int_col']['precision'] = null; + $columns['int_col2']['dbType'] = 'integer'; + $columns['int_col2']['size'] = null; + $columns['int_col2']['precision'] = null; + $columns['smallint_col']['dbType'] = 'smallint'; + $columns['smallint_col']['size'] = null; + $columns['smallint_col']['precision'] = null; + + /** + * Removed blob support + * @see https://bugs.php.net/bug.php?id=61183 + */ +// $columns['char_col3']['dbType'] = 'blob sub_type text'; + + $columns['char_col3']['dbType'] = 'varchar(255)'; + $columns['char_col3']['type'] = 'string'; + $columns['char_col3']['size'] = 255; + $columns['char_col3']['precision'] = 255; + $columns['blob_col']['dbType'] = 'varchar(255)'; + $columns['blob_col']['phpType'] = 'string'; + $columns['blob_col']['type'] = 'string'; + $columns['blob_col']['size'] = 255; + $columns['blob_col']['precision'] = 255; + + $columns['float_col']['dbType'] = 'double precision'; + $columns['float_col']['size'] = null; + $columns['float_col']['precision'] = null; + $columns['float_col']['scale'] = null; + $columns['float_col2']['dbType'] = 'double precision'; + $columns['float_col2']['size'] = null; + $columns['float_col2']['precision'] = null; + $columns['float_col2']['scale'] = null; + $columns['bool_col']['dbType'] = 'smallint'; + $columns['bool_col']['size'] = null; + $columns['bool_col']['precision'] = null; + $columns['bool_col2']['dbType'] = 'smallint'; + $columns['bool_col2']['size'] = null; + $columns['bool_col2']['precision'] = null; + $columns['bit_col']['type'] = 'smallint'; + $columns['bit_col']['dbType'] = 'smallint'; + $columns['bit_col']['size'] = null; + $columns['bit_col']['precision'] = null; + return $columns; + } + + public function testGetPDOType() + { + $values = [ + [null, \PDO::PARAM_NULL], + ['', \PDO::PARAM_STR], + ['hello', \PDO::PARAM_STR], + [0, \PDO::PARAM_INT], + [1, \PDO::PARAM_INT], + [1337, \PDO::PARAM_INT], + [true, \PDO::PARAM_INT], + [false, \PDO::PARAM_INT], + [$fp = fopen(__FILE__, 'rb'), \PDO::PARAM_LOB], + ]; + + /* @var $schema Schema */ + $schema = $this->getConnection()->schema; + + foreach ($values as $value) { + $this->assertEquals($value[1], $schema->getPdoType($value[0]), 'type for value ' . print_r($value[0], true) . ' does not match.'); + } + fclose($fp); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..ceef793 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,16 @@ + [ + 'firebird' => [ + 'dsn' => 'firebird:dbname=localhost:/tmp/TEST.FDB;charset=ISO8859_1', + 'username' => 'SYSDBA', + 'password' => 'masterkey', + 'fixture' => __DIR__ . '/source.sql', + ], + ], +]; + +if (is_file(__DIR__ . '/config.local.php')) { + include(__DIR__ . '/config.local.php'); +} + +return $config; diff --git a/tests/data/source.sql b/tests/data/source.sql new file mode 100644 index 0000000..a4f5f97 --- /dev/null +++ b/tests/data/source.sql @@ -0,0 +1,414 @@ +/** + * This is the MySQL database schema for creation of the test Firebird index sources. + */ +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'composite_fk')) THEN + EXECUTE STATEMENT 'DROP TABLE composite_fk;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'order_item')) THEN + EXECUTE STATEMENT 'DROP TABLE order_item;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'order_item_with_null_fk')) THEN + EXECUTE STATEMENT 'DROP TABLE order_item_with_null_fk;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'item')) THEN + EXECUTE STATEMENT 'DROP TABLE item;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'order')) THEN + EXECUTE STATEMENT 'DROP TABLE "order";'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'order_with_null_fk')) THEN + EXECUTE STATEMENT 'DROP TABLE order_with_null_fk;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'category')) THEN + EXECUTE STATEMENT 'DROP TABLE category;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'customer')) THEN + EXECUTE STATEMENT 'DROP TABLE customer;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'profile')) THEN + EXECUTE STATEMENT 'DROP TABLE profile;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'type')) THEN + EXECUTE STATEMENT 'DROP TABLE type;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'null_values')) THEN + EXECUTE STATEMENT 'DROP TABLE null_values;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'constraints')) THEN + EXECUTE STATEMENT 'DROP TABLE constraints;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'animal')) THEN + EXECUTE STATEMENT 'DROP TABLE animal;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'default_pk')) THEN + EXECUTE STATEMENT 'DROP TABLE default_pk;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'document')) THEN + EXECUTE STATEMENT 'DROP TABLE document;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$relations WHERE LOWER(rdb$relation_name) = 'animal_view')) THEN + EXECUTE STATEMENT 'DROP VIEW animal_view;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_animal_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_animal_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_profile_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_profile_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_customer_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_customer_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_category_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_category_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_item_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_item_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_order_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_order_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_order_with_null_fk_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_order_with_null_fk_id;'; +END; +-- SQL +EXECUTE block AS +BEGIN + IF (EXISTS(SELECT 1 FROM rdb$generators WHERE LOWER(rdb$generator_name) = 'gen_document_id')) THEN + EXECUTE STATEMENT 'DROP GENERATOR gen_document_id;'; +END; +-- SQL +CREATE TABLE constraints ( + id INTEGER NOT NULL, + field1 varchar(255) +); +-- SQL +CREATE TABLE profile ( + id INTEGER NOT NULL, + description varchar(128) NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_profile_id; +-- SQL +CREATE TRIGGER tr_profile FOR profile +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_profile_id, 1); +END +-- SQL +CREATE TABLE customer ( + id INTEGER NOT NULL, + email varchar(128) NOT NULL, + name varchar(128), + address varchar(255), + status INTEGER DEFAULT 0, + profile_id INTEGER, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_customer_id; +-- SQL +CREATE TRIGGER tr_customer FOR customer +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_customer_id, 1); +END +-- SQL +CREATE TABLE category ( + id INTEGER NOT NULL, + name varchar(128) NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_category_id; +-- SQL +CREATE TRIGGER tr_category FOR category +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_category_id, 1); +END +-- SQL +CREATE TABLE item ( + id INTEGER NOT NULL, + name varchar(128) NOT NULL, + category_id INTEGER NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_item_id; +-- SQL +CREATE TRIGGER tr_item FOR item +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_item_id, 1); +END +-- SQL +CREATE TABLE "order" ( + id INTEGER NOT NULL, + customer_id INTEGER NOT NULL, + created_at INTEGER NOT NULL, + total decimal(10,0) NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_order_id; +-- SQL +CREATE TRIGGER tr_order FOR "order" +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_order_id, 1); +END +-- SQL +CREATE TABLE order_with_null_fk ( + id INTEGER NOT NULL, + customer_id INTEGER, + created_at INTEGER NOT NULL, + total decimal(10,0) NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_order_with_null_fk_id; +-- SQL +CREATE TRIGGER tr_order_with_null_fk FOR order_with_null_fk +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_order_with_null_fk_id, 1); +END +-- SQL +CREATE TABLE order_item ( + order_id INTEGER NOT NULL, + item_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + subtotal decimal(10,0) NOT NULL, + PRIMARY KEY (order_id, item_id), + CONSTRAINT FK_single_fk_order FOREIGN KEY (order_id) REFERENCES "order" (id) ON DELETE CASCADE, + CONSTRAINT FK_single_fk_item FOREIGN KEY (item_id) REFERENCES item (id) ON DELETE CASCADE +); +-- SQL +CREATE TABLE order_item_with_null_fk ( + order_id INTEGER, + item_id INTEGER, + quantity INTEGER NOT NULL, + subtotal decimal(10,0) NOT NULL +); +-- SQL +CREATE TABLE composite_fk ( + id INT NOT NULL, + order_id INT NOT NULL, + item_id INT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT FK_composite_fk_order_item FOREIGN KEY (order_id, item_id) REFERENCES order_item (order_id, item_id) ON DELETE CASCADE +); +-- SQL +CREATE TABLE null_values ( + id INTEGER PRIMARY KEY NOT NULL, + var1 INTEGER, + var2 INTEGER, + var3 INTEGER DEFAULT NULL, + stringcol VARCHAR(32) DEFAULT NULL +); +-- SQL +CREATE TABLE type ( + int_col INTEGER NOT NULL, + int_col2 INTEGER DEFAULT '1', + smallint_col SMALLINT DEFAULT '1', + char_col char(100) NOT NULL, + char_col2 varchar(100) DEFAULT 'something', + char_col3 varchar(255), + float_col DOUBLE PRECISION NOT NULL, + float_col2 DOUBLE PRECISION DEFAULT '1.23', + blob_col varchar(255), + numeric_col decimal(5,2) DEFAULT '33.22', + "time" TIMESTAMP DEFAULT '2002-01-01 00:00:00' NOT NULL, + bool_col SMALLINT NOT NULL, + bool_col2 SMALLINT DEFAULT '1', + ts_default TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + bit_col SMALLINT DEFAULT '130' NOT NULL + +); +-- SQL +CREATE TABLE animal ( + id INTEGER NOT NULL, + type VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_animal_id; +-- SQL +CREATE TRIGGER tr_animal FOR animal +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_animal_id, 1); +END +-- SQL +CREATE TABLE default_pk ( + id INTEGER DEFAULT 5 NOT NULL , + type VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +); +-- SQL +CREATE TABLE document ( + id INTEGER NOT NULL, + title VARCHAR(255) NOT NULL, + content varchar(255), + version INTEGER DEFAULT '0' NOT NULL , + PRIMARY KEY (id) +); +-- SQL +CREATE GENERATOR gen_document_id; +-- SQL +CREATE TRIGGER tr_document FOR document +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + if (NEW.ID is NULL) then NEW.ID = GEN_ID(gen_document_id, 1); +END +-- SQL +CREATE VIEW animal_view AS SELECT * FROM animal; +-- SQL +EXECUTE block AS BEGIN + INSERT INTO animal (type) VALUES ('yiiunit\data\ar\Cat'); + INSERT INTO animal (type) VALUES ('yiiunit\data\ar\Dog'); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO profile (description) VALUES ('profile customer 1'); + INSERT INTO profile (description) VALUES ('profile customer 3'); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO customer (email, name, address, status, profile_id) VALUES ('user1@example.com', 'user1', 'address1', 1, 1); + INSERT INTO customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1); + INSERT INTO customer (email, name, address, status, profile_id) VALUES ('user3@example.com', 'user3', 'address3', 2, 2); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO category (name) VALUES ('Books'); + INSERT INTO category (name) VALUES ('Movies'); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO item (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); + INSERT INTO item (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); + INSERT INTO item (name, category_id) VALUES ('Ice Age', 2); + INSERT INTO item (name, category_id) VALUES ('Toy Story', 2); + INSERT INTO item (name, category_id) VALUES ('Cars', 2); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO "order" (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); + INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); + INSERT INTO "order" (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO order_with_null_fk (customer_id, created_at, total) VALUES (1, 1325282384, 110.0); + INSERT INTO order_with_null_fk (customer_id, created_at, total) VALUES (2, 1325334482, 33.0); + INSERT INTO order_with_null_fk (customer_id, created_at, total) VALUES (2, 1325502201, 40.0); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO order_item (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); + INSERT INTO order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); + INSERT INTO order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); + INSERT INTO order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); + INSERT INTO order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); + INSERT INTO order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO order_item_with_null_fk (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); + INSERT INTO order_item_with_null_fk (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); + INSERT INTO order_item_with_null_fk (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); + INSERT INTO order_item_with_null_fk (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); + INSERT INTO order_item_with_null_fk (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); + INSERT INTO order_item_with_null_fk (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); +END; +-- SQL +EXECUTE block AS +BEGIN + INSERT INTO document (title, content, version) VALUES ('Yii 2.0 guide', 'This is Yii 2.0 guide', 0); +END;