SQLite patch. From: Damien Tournoud --- CHANGELOG.txt | 1 includes/database/sqlite/._database.inc | Bin includes/database/sqlite/._query.inc | Bin includes/database/sqlite/database.inc | 261 ++++++++++++++ includes/database/sqlite/install.inc | 15 + includes/database/sqlite/query.inc | 108 ++++++ includes/database/sqlite/schema.inc | 572 +++++++++++++++++++++++++++++++ 7 files changed, 957 insertions(+), 0 deletions(-) create mode 100644 includes/database/sqlite/._database.inc create mode 100644 includes/database/sqlite/._query.inc create mode 100644 includes/database/sqlite/database.inc create mode 100644 includes/database/sqlite/install.inc create mode 100644 includes/database/sqlite/query.inc create mode 100644 includes/database/sqlite/schema.inc diff --git CHANGELOG.txt CHANGELOG.txt index b2b64f6..b802fdc 100644 --- CHANGELOG.txt +++ CHANGELOG.txt @@ -7,6 +7,7 @@ Drupal 7.0, xxxx-xx-xx (development version) * Added query builders for INSERT, UPDATE, DELETE, MERGE, and SELECT queries. * Support for master/slave replication, transactions, multi-insert queries, delayed inserts, and other features. + * Added support for the SQLite database engine. - Security: * Protected cron.php -- cron will only run if the proper key is provided. * Implemented much stronger password hashes that are also compatible with the diff --git includes/database/sqlite/._database.inc includes/database/sqlite/._database.inc new file mode 100644 index 0000000000000000000000000000000000000000..d19d31692b60ba8fdf581c57e867a491d9e848db GIT binary patch literal 4183 zcmZQz6=P>$V!#Bbwqs!6+W@42YWUtUFfhtBuCjOB&e)( z?OUMkM?j@BfLH}h3nM~YG&w(4FE=r{C_gu`B(+#CIk70UgrS;C0SJ=wb4qjb6l@hN zEUm!;IhlE>K(3*Yfi+hxnwg{A(GVC7fzc2c4S~@R7!85Z5Eu=C(GVC7fzc2c4S^9J z0-(+&2!nxKNJeI{LQ!gQerZv1szO?RQMN*6UP)?RNoIatVopw_LQZO0i9%9NVqP|| z2MX0Sq$yPY!@VNIAou@2q@S&Xtq-mQ>}V@h$K;`O#7zvXAtJDzxS$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103v`Wj<& z45EW!22i;YnifVNA1W@IoS&Oi^t%8{~ zn4goGmkQ(=T3U0}qM10#9Swoe5Eu=C(GVC7fzc2c4S~@R7!85Z5Eu=C(GVE%Apq)Z zf-o4!g=AzFD-@*`=a&{Grz)i77iBAC=9Q%8m1O4UCFbN*D&(Z5l_(_TB<5uUd!SHV RLz+VMKin%a408Yf0|4jVFB1R& literal 0 HcmV?d00001 diff --git includes/database/sqlite/database.inc includes/database/sqlite/database.inc new file mode 100644 index 0000000..9c90a20 --- /dev/null +++ includes/database/sqlite/database.inc @@ -0,0 +1,261 @@ +transactionSupport = isset($connection_options['transactions']) ? $connection_options['transactions'] : TRUE; + + parent::__construct('sqlite:'. $connection_options['database'], '', '', $connection_options); + + $this->exec('PRAGMA encoding="UTF-8"'); + + // Create functions needed by SQLite. + $this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf')); + $this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest')); + $this->sqliteCreateFunction('pow', 'pow', 2); + $this->sqliteCreateFunction('length', 'strlen', 1); + $this->sqliteCreateFunction('concat', array($this, 'sqlFunctionConcat')); + $this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3); + $this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand')); + } + + /** + * SQLite compatibility implementation for the IF() SQL function. + */ + public function sqlFunctionIf($condition, $expr1, $expr2 = NULL) { + return $condition ? $expr1 : $expr2; + } + + /** + * SQLite compatibility implementation for the GREATEST() SQL function. + */ + public function sqlFunctionGreatest() { + $args = func_get_args(); + foreach ($args as $k => $v) { + if (is_null($v)) { + unset($args); + } + } + if (count($args)) { + return max($args); + } + else { + return NULL; + } + } + + /** + * SQLite compatibility implementation for the CONCAT() SQL function. + */ + public function sqlFunctionConcat() { + $args = func_get_args(); + return implode('', $args); + } + + /** + * SQLite compatibility implementation for the SUBSTRING() SQL function. + */ + public function sqlFunctionSubstring($string, $from, $length) { + return substr($string, $from - 1, $length); + } + + /** + * SQLite compatibility implementation for the RAND() SQL function. + */ + public function sqlFunctionRand($seed = NULL) { + if (isset($seed)) { + mt_srand($seed); + } + return mt_rand() / mt_getrandmax(); + } + + /** + * SQLite-specific implementation of DatabaseConnection::prepare(). + * + * We don't use prepared statements at all at this stage. We just create + * a DatabaseStatement_sqlite object, that will create a PDOStatement + * using the semi-private PDOPrepare() method below. + */ + public function prepare($query, Array $options = array()) { + return new DatabaseStatement_sqlite($this, $query, $options); + } + + /** + * NEVER CALL THIS FUNCTION: YOU MIGHT DEADLOCK YOUR PHP PROCESS. + * + * This is a wrapper around the parent PDO::prepare method. However, as + * the PDO SQLite driver only closes SELECT statements when the PDOStatement + * destructor is called and SQLite does not allow data change (INSERT, + * UPDATE etc) on a table which has open SELECT statements, you should never + * call this function and keep a PDOStatement object alive as that can lead + * to a deadlock. This really, really should be private, but as + * DatabaseStatement_sqlite needs to call it, we have no other choice but to + * expose this function to the world. + */ + public function PDOPrepare($query, Array $options = array()) { + return parent::prepare($query, $options); + } + + public function queryRange($query, Array $args, $from, $count, Array $options = array()) { + return $this->query($query . ' LIMIT ' . $from . ', ' . $count, $args, $options); + } + + public function queryTemporary($query, Array $args, $tablename, Array $options = array()) { + return $this->query(preg_replace('/^SELECT/i', 'CREATE TEMPORARY TABLE ' . $tablename . ' AS SELECT', $query), $args, $options); + } + + public function driver() { + return 'sqlite'; + } + + public function databaseType() { + return 'sqlite'; + } + + public function supportsTransactions() { + return $this->transactionSupport; + } + + public function mapConditionOperator($operator) { + // We don't want to override any of the defaults. + return NULL; + } + + protected function prepareQuery($query) { + // In makes no sense to use the static prepared statement cache here, + // because all the work in our implementation is done in + // DatabaseStatement_sqlite::execute() and cannot be cached. + return $this->prepare($this->prefixTables($query)); + } + + /** + * @todo Remove this as soon as db_rewrite_sql() has been exterminated. + */ + public function distinctField($table, $field, $query) { + $field_to_select = 'DISTINCT(' . $table . '.' . $field . ')'; + // (?= :count) fail. + * We replace numeric placeholders in the query ourself to workaround that bug. + * + * See http://bugs.php.net/bug.php?id=45259 for more details. + */ + protected function getStatement($query, &$args = array()) { + if (count($args)) { + // Check if $args is a simple numeric array. + if (range(0, count($args) - 1) === array_keys($args)) { + // In that case, we have unnamed placeholders. + $count = 0; + $new_args = array(); + foreach ($args as $value) { + if (is_numeric($value)) { + $query = substr_replace($query, $value, strpos($query, '?'), 1); + } + else { + $placeholder = ':db_statement_placeholder_' . $count++; + $query = substr_replace($query, $placeholder, strpos($query, '?'), 1); + $new_args[$placeholder] = $value; + } + } + $args = $new_args; + } + else { + // Else, this is using named placeholders. + foreach ($args as $placeholder => $value) { + if (is_numeric($value)) { + $query = str_replace($placeholder, $value, $query); + unset($args[$placeholder]); + } + } + } + } + + return $this->dbh->PDOPrepare($query); + } + + public function execute($args, $options) { + try { + $return = parent::execute($args, $options); + } + catch (PDOException $e) { + if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { + // The schema has changed. SQLite specifies that we must resend the query. + $return = parent::execute($args, $options); + } + else { + // Rethrow the exception. + throw $e; + } + } + + // In some weird cases, SQLite will prefix some column names by the name + // of the table. We post-process the data, by renaming the column names + // using the same convention as MySQL and PostgreSQL. + $rename_columns = array(); + foreach ($this->columnNames as $k => $column) { + if (preg_match("/^.*\.(.*)$/", $column, $matches)) { + $rename_columns[$column] = $matches[1]; + $this->columnNames[$k] = $matches[1]; + } + } + if ($rename_columns) { + foreach ($this->data as $k => $row) { + foreach ($rename_columns as $old_column => $new_column) { + $this->data[$k][$new_column] = $this->data[$k][$old_column]; + unset($this->data[$k][$old_column]); + } + } + } + + // This is necessary to ensure that the array with start in a clean state. + reset($this->data); + + return $return; + } + +} + +/** + * @} End of "ingroup database". + */ diff --git includes/database/sqlite/install.inc includes/database/sqlite/install.inc new file mode 100644 index 0000000..096c209 --- /dev/null +++ includes/database/sqlite/install.inc @@ -0,0 +1,15 @@ +insertFields) + count($this->defaultFields) == 0) { + return NULL; + } + if (count($this->insertFields)) { + return parent::execute(); + } + else { + return $this->connection->query('INSERT INTO {'. $this->table .'} DEFAULT VALUES', array(), $this->queryOptions); + } + } + + public function __toString() { + // Produce as many generic placeholders as necessary. + $placeholders = array_fill(0, count($this->insertFields), '?'); + return 'INSERT INTO {'. $this->table .'} ('. implode(', ', $this->insertFields) .') VALUES ('. implode(', ', $placeholders) .')'; + } + +} + +/** + * SQLite specific implementation of UpdateQuery. + * + * SQLite counts all the rows that match the conditions as modified, even if they + * will not be affected by the query. We workaround this by ensuring that + * we don't select those rows. + * + * A query like this one: + * UPDATE test SET name = 'newname' WHERE tid = 1 + * will become: + * UPDATE test SET name = 'newname' WHERE tid = 1 AND name <> 'newname' + */ +class UpdateQuery_sqlite extends UpdateQuery { + + /** + * Helper function that remove the fields that are already in a condition. + * + * @param $fields + * The fields. + * @param QueryConditionInterface $condition + * A database condition. + */ + protected function removeFieldsInCondition(&$fields, QueryConditionInterface $condition) { + foreach ($condition->conditions() as $child_condition) { + if ($child_condition['field'] instanceof QueryConditionInterface) { + $this->removeFieldsInCondition($fields, $child_condition['field']); + } + else { + unset($fields[$child_condition['field']]); + } + } + } + + public function execute() { + // Get the fields use in the update query, and remove those that are already + // in the condition. + $fields = $this->expressionFields + $this->fields; + $this->removeFieldsInCondition($fields, $this->condition); + + // Add the inverse of the fields to the condition. + $condition = db_or(); + foreach ($fields as $field => $data) { + if (is_array($data)) { + // The field is an expression. + $condition->condition($field, $data['expression'], '<>'); + // The IS NULL operator is badly managed by DatabaseCondition. + $condition->where($field . ' IS NULL'); + } + else if (is_null($data)) { + // The field will be set to NULL. + // The IS NULL operator is badly managed by DatabaseCondition. + $condition->where($field . ' IS NOT NULL'); + } + else { + $condition->condition($field, $data, '<>'); + // The IS NULL operator is badly managed by DatabaseCondition. + $condition->where($field . ' IS NULL'); + } + } + if (count($condition)) { + $condition->compile($this->connection); + $this->condition->where((string) $condition, $condition->arguments()); + } + return parent::execute(); + } + +} + +/** + * @} End of "ingroup database". + */ diff --git includes/database/sqlite/schema.inc includes/database/sqlite/schema.inc new file mode 100644 index 0000000..03a3641 --- /dev/null +++ includes/database/sqlite/schema.inc @@ -0,0 +1,572 @@ +connection->query("SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE '{" . $table . "}'", array(), array())->fetchField(); + } + + public function columnExists($table, $column) { + $schema = $this->introspectSchema($table); + return !empty($schema['fields'][$column]); + } + + /** + * Generate SQL to create a new table from a Drupal schema definition. + * + * @param $name + * The name of the table to create. + * @param $table + * A Schema API table definition array. + * @return + * An array of SQL statements to create the table. + */ + public function createTableSql($name, $table) { + $sql = array(); + $sql[] = "CREATE TABLE {". $name ."} (\n". $this->createColumsSql($name, $table) ."\n);\n"; + return array_merge($sql, $this->createIndexSql($name, $table)); + } + + /** + * Build the SQL expression for indexes. + */ + protected function createIndexSql($tablename, $schema) { + $sql = array(); + if (!empty($schema['unique keys'])) { + foreach ($schema['unique keys'] as $key => $fields) { + $sql[] = 'CREATE UNIQUE INDEX {'. $tablename .'}_'. $key .' ON {'. $tablename .'} ('. $this->createKeySql($fields) ."); \n"; + } + } + if (!empty($schema['indexes'])) { + foreach ($schema['indexes'] as $index => $fields) { + $sql[] = 'CREATE INDEX {'. $tablename .'}_'. $index .' ON {'. $tablename .'} ('. $this->createKeySql($fields) ."); \n"; + } + } + return $sql; + } + + /** + * Build the SQL expression for creating columns. + */ + protected function createColumsSql($tablename, $schema) { + $sql = ""; + + // Add the SQL statement for each field. + foreach ($schema['fields'] as $name => $field) { + if ($field['type'] == 'serial') { + if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== false) { + unset($schema['primary key'][$key]); + } + } + $sql .= $this->createFieldSql($name, $this->processField($field)) .", \n"; + } + + // Process keys. + if (!empty($schema['primary key'])) { + $sql .= " PRIMARY KEY (". $this->createKeySql($schema['primary key']) ."), \n"; + } + + // Remove the last comma and space. + $sql = substr($sql, 0, -3); + return $sql; + } + + /** + * Build the SQL expression for keys. + */ + protected function createKeySql($fields) { + $ret = array(); + foreach ($fields as $field) { + if (is_array($field)) { + $ret[] = $field[0]; + } + else { + $ret[] = $field; + } + } + return implode(', ', $ret); + } + + /** + * Set database-engine specific properties for a field. + * + * @param $field + * A field description array, as specified in the schema documentation. + */ + protected function processField($field) { + if (!isset($field['size'])) { + $field['size'] = 'normal'; + } + // Set the correct database-engine specific datatype. + if (!isset($field['sqlite_type'])) { + $map = $this->getFieldTypeMap(); + $field['sqlite_type'] = $map[$field['type'] .':'. $field['size']]; + } + + if ($field['type'] == 'serial') { // this should check if field is a primary key + $field['auto_increment'] = TRUE; + } + + return $field; + } + + /** + * Create an SQL string for a field to be used in table creation or alteration. + * + * Before passing a field out of a schema definition into this function it has + * to be processed by db_processField(). + * + * @param $name + * Name of the field. + * @param $spec + * The field specification, as per the schema data structure format. + */ + protected function createFieldSql($name, $spec) { + if (!empty($spec['auto_increment'])) { + $sql = "". $name ." INTEGER PRIMARY KEY AUTOINCREMENT"; + } + else { + $sql = "". $name ." ". $spec['sqlite_type']; + + if (isset($spec['length'])) { + $sql .= '('. $spec['length'] .')'; + } + + if (!empty($spec['not null'])) { + $sql .= ' NOT NULL'; + } + + if (isset($spec['default'])) { + if (is_string($spec['default'])) { + $spec['default'] = "'". $spec['default'] ."'"; + } + $sql .= ' DEFAULT '. $spec['default']; + } + + if (empty($spec['not null']) && !isset($spec['default'])) { + $sql .= ' DEFAULT NULL'; + } + } + return $sql; + } + + /** + * This maps a generic data type in combination with its data size + * to the engine-specific data type. + */ + public function getFieldTypeMap() { + // Put :normal last so it gets preserved by array_flip. This makes + // it much easier for modules (such as schema.module) to map + // database types back into schema types. + $map = array( + 'varchar:normal' => 'VARCHAR', + + 'text:tiny' => 'TEXT', + 'text:small' => 'TEXT', + 'text:medium' => 'TEXT', + 'text:big' => 'TEXT', + 'text:normal' => 'TEXT', + + 'serial:tiny' => 'INTEGER', + 'serial:small' => 'INTEGER', + 'serial:medium' => 'INTEGER', + 'serial:big' => 'INTEGER', + 'serial:normal' => 'INTEGER', + + 'int:tiny' => 'INTEGER', + 'int:small' => 'INTEGER', + 'int:medium' => 'INTEGER', + 'int:big' => 'INTEGER', + 'int:normal' => 'INTEGER', + + 'float:tiny' => 'FLOAT', + 'float:small' => 'FLOAT', + 'float:medium' => 'FLOAT', + 'float:big' => 'FLOAT', + 'float:normal' => 'FLOAT', + + 'numeric:normal' => 'NUMERIC', + + 'blob:big' => 'BLOB', + 'blob:normal' => 'BLOB', + + 'datetime:normal' => 'TIMESTAMP', + ); + return $map; + } + + /** + * Rename a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be renamed. + * @param $new_name + * The new name for the table. + */ + public function renameTable(&$ret, $table, $new_name) { + $ret[] = update_sql('ALTER TABLE {'. $table .'} RENAME TO {'. $new_name .'}'); + } + + /** + * Drop a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be dropped. + */ + public function dropTable(&$ret, $table) { + $ret[] = update_sql('DROP TABLE {' . $table . '}'); + } + + /** + * Add a new field to a table. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * Name of the table to be altered. + * @param $field + * Name of the field to be added. + * @param $spec + * The field specification array, as taken from a schema definition. + */ + public function addField(&$ret, $table, $field, $spec, $keys_new = array()) { + // TODO: $keys_new is not supported yet. + $query = 'ALTER TABLE {'. $table .'} ADD '; + $query .= $this->createFieldSql($field, $this->processField($spec)); + $ret[] = update_sql($query); + } + + /** + * Recreate a table with a new schema with the old content. + * + * As SQLite does not support ALTER TABLE (with a few exceptions) it is + * necessary to create a new table and copy over the old content. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * Name of the table to be altered. + * @param $new_schema + * The new schema array for the table. + */ + protected function alterTable(&$ret, $table, $new_schema) { + $i = 0; + do { + $new_table = $table . '_' . $i++; + } while ($this->tableExists($new_table)); + $this->createTable($ret, $new_table, $new_schema); + $fields = implode(', ', array_keys($new_schema['fields'])); + $ret[] = update_sql('INSERT INTO {'. $new_table ."} ($fields) SELECT $fields FROM {". $table . '}'); + $old_count = db_query('SELECT COUNT(*) FROM {'. $table .'}')->fetchField(); + $new_count = db_query('SELECT COUNT(*) FROM {'. $new_table .'}')->fetchField(); + if ($old_count == $new_count) { + do { + $temp_table = $table . '_' . $i++; + } while ($this->tableExists($temp_table)); + $this->renameTable($ret, $table, $temp_table); + $this->renameTable($ret, $new_table, $table); + $this->dropTable($ret, $temp_table); + } + } + + /** + * Find out the schema of a table. + * + * This function uses introspection methods provided by the database to + * create a schema array. This is useful for examnple during update when + * the old schema is not available. + * + * @param $table + * Name of the table. + * @return + * An array representing the schema, @see drupal_get_schema. + */ + protected function introspectSchema($table) { + $mapped_fields = array_flip($this->getFieldTypeMap()); + $schema = array(); + foreach (db_query("PRAGMA table_info('{" . $table . "}')") as $row) { + if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) { + $type = $matches[1]; + $length = $matches[2]; + } + else { + $type = $row->type; + $length = NULL; + } + if (isset($mapped_fields[$type])) { + list($type, $size) = explode(':', $mapped_fields[$type]); + $schema['fields'][$row->name] = array( + 'type' => $type, + 'size' => $size, + 'not null' => !empty($row->notnull), + 'default' => trim($row->dflt_value, "'"), + ); + if ($length) { + $schema['fields'][$row->name]['length'] = $length; + } + if ($row->pk) { + $schema['primary key'][] = $row->name; + } + } + else { + new Exception("Unable to parse the column type " . $row->type); + } + } + $indexes = array(); + foreach (db_query("PRAGMA index_list('{" . $table . "}')") as $row) { + if (strpos($row->name, 'sqlite_autoindex_') !== 0) { + $indexes[] = array( + 'schema_key' => $row->unique ? 'unique keys' : 'indexes', + 'name' => $row->name, + ); + } + } + $n = strlen($table) + 1; + foreach ($indexes as $index) { + $name = $index['name']; + $index_name = substr($name, $n); + foreach (db_query("PRAGMA index_info('$name')") as $row) { + $schema[$index['schema_key']][$index_name][] = $row->name; + } + } + return $schema; + } + + /** + * Drop a field. + * + * This implementation can't use ALTER TABLE directly, because SQLite only + * supports a limited subset of that command. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be dropped. + */ + public function dropField(&$ret, $table, $field) { + $new_schema = $this->introspectSchema($table); + unset($new_schema['fields'][$field]); + foreach ($new_schema['indexes'] as $index => $fields) { + foreach ($fields as $key => $field_name) { + if ($field_name == $field) { + unset($new_schema['indexes'][$index][$key]); + } + } + // If this index has no more fields then remove it. + if (empty($new_schema['indexes'][$index])) { + unset($new_schema['indexes'][$index]); + } + } + $this->alterTable($ret, $table, $new_schema); + } + + /** + * Change a field definition. + * + * This implementation can't use ALTER TABLE directly, because SQLite only + * supports a limited subset of that command. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * Name of the table. + * @param $field + * Name of the field to change. + * @param $field_new + * New name for the field (set to the same as $field if you don't want to change the name). + * @param $spec + * The field specification for the new field. + * @param $keys_new + * Optional keys and indexes specification to be created on the + * table along with changing the field. The format is the same as a + * table specification but without the 'fields' element. + */ + public function changeField(&$ret, $table, $field, $field_new, $spec, $keys_new = array()) { + $new_schema = $this->introspectSchema($table); + unset($new_schema['fields'][$field]); + $new_schema['fields'][$field_new] = $spec; + if (isset($keys_new['primary keys'])) { + $new_schema['primary keys'] = $keys_new['primary keys']; + $keys_new['primary keys']; + } + foreach (array('unique keys', 'indexes') as $k) { + if (!empty($keys_new[$k])) { + $new_schema[$k] = $keys_new[$k] + $new_schema[$k]; + } + } + $this->alterTable($ret, $table, $new_schema); + } + + /** + * Add an index. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + * @param $fields + * An array of field names. + */ + public function addIndex(&$ret, $table, $name, $fields) { + $schema['indexes'][$name] = $fields; + $ret[] = update_sql($this->createIndexSql($table, $schema)); + } + + /** + * Drop an index. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the index. + */ + public function dropIndex(&$ret, $table, $name) { + $ret[] = update_sql('DROP INDEX ' . '{' . $table . '}_' . $name); + } + + /** + * Add a unique key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + * @param $fields + * An array of field names. + */ + public function addUniqueKey(&$ret, $table, $name, $fields) { + $schema['unique keys'][$name] = $fields; + $ret[] = update_sql($this->createIndexSql($table, $schema)); + + } + + /** + * Drop a unique key. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $name + * The name of the key. + */ + public function dropUniqueKey(&$ret, $table, $name) { + $ret[] = update_sql('DROP INDEX ' . '{' . $table . '}_' . $name); + } + + /** + * Add a primary key. + * + * This implementation can't use ALTER TABLE directly, because SQLite only + * supports a limited subset of that command. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $fields + * Fields for the primary key. + */ + public function addPrimaryKey(&$ret, $table, $fields) { + $new_schema = $this->introspectSchema($table); + $new_schema['primary key'] = $fields; + $this->alterTable($ret, $table, $new_schema); + } + + /** + * Drop the primary key. + * + * This implementation can't use ALTER TABLE directly, because SQLite only + * supports a limited subset of that command.` + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + */ + public function dropPrimaryKey(&$ret, $table) { + $new_schema = $this->introspectSchema($table); + unset($new_schema['primary key']); + $this->alterTable($ret, $table, $new_schema); + } + + /** + * Set the default value for a field. + * + * This implementation can't use ALTER TABLE directly, because SQLite only + * supports a limited subset of that command. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + * @param $default + * Default value to be set. NULL for 'default NULL'. + */ + public function fieldSetDefault(&$ret, $table, $field, $default) { + $new_schema = $this->introspectSchema($table); + $new_schema['fields'][$field]['default'] = $default; + $this->alterTable($ret, $table, $new_schema); + } + + /** + * Set a field to have no default value. + * + * This implementation can't use ALTER TABLE directly, because SQLite only + * supports a limited subset of that command. + * + * @param $ret + * Array to which query results will be added. + * @param $table + * The table to be altered. + * @param $field + * The field to be altered. + */ + public function fieldSetNoDefault(&$ret, $table, $field) { + $new_schema = $this->introspectSchema($table); + unset($new_schema['fields'][$field]['default']); + $this->alterTable($ret, $table, $new_schema); + } + + /** + * Find all tables that are like the specified base table name. + * + * @param $table_expression + * An SQL expression, for example "simpletest%" (without the quotes). + * BEWARE: this is not prefixed, the caller should take care of that. + * @return + * Array, both the keys and the values are the matching tables. + */ + public function findTables($table_expression) { + $result = db_query("SELECT name FROM sqlite_master WHERE name LIKE :table_name", array( + ':table_name' => $table_expression, + )); + return $result->fetchAllKeyed(0, 0); + } +}