summaryrefslogtreecommitdiff
path: root/includes/database/sqlite
diff options
context:
space:
mode:
authorAngie Byron <webchick@24967.no-reply.drupal.org>2008-11-23 06:06:15 +0000
committerAngie Byron <webchick@24967.no-reply.drupal.org>2008-11-23 06:06:15 +0000
commit9de3b9cb74fa8a3cd70fff00c7198d291f2f88b3 (patch)
tree830c233f8b2d47e68b9cfe0cff10495a6ebfa3f0 /includes/database/sqlite
parent842a0fae20f3aec089b4d99adb540a440c67d10d (diff)
downloadbrdo-9de3b9cb74fa8a3cd70fff00c7198d291f2f88b3.tar.gz
brdo-9de3b9cb74fa8a3cd70fff00c7198d291f2f88b3.tar.bz2
#67349 by chx, Damien Tournoud, and paranojik: SQLite support in core! Yeah! :D
Diffstat (limited to 'includes/database/sqlite')
-rw-r--r--includes/database/sqlite/database.inc263
-rw-r--r--includes/database/sqlite/install.inc15
-rw-r--r--includes/database/sqlite/query.inc131
-rw-r--r--includes/database/sqlite/schema.inc570
4 files changed, 979 insertions, 0 deletions
diff --git a/includes/database/sqlite/database.inc b/includes/database/sqlite/database.inc
new file mode 100644
index 000000000..769653f59
--- /dev/null
+++ b/includes/database/sqlite/database.inc
@@ -0,0 +1,263 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Database interface code for SQLite embedded database engine.
+ */
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+include_once DRUPAL_ROOT . '/includes/database/prefetch.inc';
+
+/**
+ * Specific SQLite implementation of DatabaseConnection.
+ */
+class DatabaseConnection_sqlite extends DatabaseConnection {
+
+ /**
+ * Indicates that this connection supports transactions.
+ *
+ * @var bool
+ */
+ protected $transactionSupport = TRUE;
+
+ public function __construct(Array $connection_options = array()) {
+ // We don't need a specific PDOStatement class here, we simulate it below.
+ $connection_options['statement_class'] = FALSE;
+
+ $this->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) {
+ // It 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 . ')';
+ // (?<!text) is a negative look-behind (no need to rewrite queries that already use DISTINCT).
+ return preg_replace('/(SELECT.*)(?:' . $table . '\.|\s)(?<!DISTINCT\()(?<!DISTINCT\(' . $table . '\.)' . $field . '(.*FROM )/AUsi', '\1 ' . $field_to_select . '\2', $query);
+ }
+}
+
+/**
+ * Specific SQLite implementation of DatabaseConnection.
+ *
+ * @see DatabaseConnection_sqlite::PDOPrepare for reasons why we must prefetch
+ * the data instead of using PDOStatement.
+ */
+class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
+
+ /**
+ * SQLite specific implementation of getStatement().
+ *
+ * The PDO SQLite layer doesn't replace numeric placeholders in queries
+ * correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
+ * fail. We replace numeric placeholders in the query ourselves to work
+ * around this 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]);
+ }
+ }
+ }
+
+ // We will iterate this array so we need to make sure the array pointer is
+ // at the beginning.
+ reset($this->data);
+
+ return $return;
+ }
+
+}
+
+/**
+ * @} End of "ingroup database".
+ */
diff --git a/includes/database/sqlite/install.inc b/includes/database/sqlite/install.inc
new file mode 100644
index 000000000..096c20901
--- /dev/null
+++ b/includes/database/sqlite/install.inc
@@ -0,0 +1,15 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * SQLite specific install functions
+ */
+
+class DatabaseInstaller_sqlite extends DatabaseInstaller {
+ protected $pdoDriver = 'sqlite';
+ public function name() {
+ return 'SQLite';
+ }
+}
+
diff --git a/includes/database/sqlite/query.inc b/includes/database/sqlite/query.inc
new file mode 100644
index 000000000..364b618b4
--- /dev/null
+++ b/includes/database/sqlite/query.inc
@@ -0,0 +1,131 @@
+<?php
+// $Id $
+
+/**
+ * @ingroup database
+ * @{
+ */
+
+/**
+ * SQLite specific implementation of InsertQuery.
+ *
+ * We ignore all the default fields and use the clever SQLite syntax:
+ * INSERT INTO table DEFAULT VALUES
+ * for degenerated "default only" queries.
+ */
+class InsertQuery_sqlite extends InsertQuery {
+
+ public function execute() {
+ if (count($this->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 removes 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 used 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();
+ }
+
+}
+
+/**
+ * SQLite specific implementation of DeleteQuery.
+ *
+ * When the WHERE is omitted from a DELETE statement and the table being deleted
+ * has no triggers, SQLite uses an optimization to erase the entire table content
+ * without having to visit each row of the table individually.
+ *
+ * Prior to SQLite 3.6.5, SQLite does not return the actual number of rows deleted
+ * by that optimized "truncate" optimization.
+ */
+class DeleteQuery_sqlite extends DeleteQuery {
+ public function execute() {
+ if (!count($this->condition)) {
+ $total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField();
+ parent::execute();
+ return $total_rows;
+ }
+ else {
+ return parent::execute();
+ }
+ }
+}
+
+/**
+ * @} End of "ingroup database".
+ */
diff --git a/includes/database/sqlite/schema.inc b/includes/database/sqlite/schema.inc
new file mode 100644
index 000000000..92baeaf18
--- /dev/null
+++ b/includes/database/sqlite/schema.inc
@@ -0,0 +1,570 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Database schema code for SQLite databases.
+ */
+
+
+/**
+ * @ingroup schemaapi
+ * @{
+ */
+
+class DatabaseSchema_sqlite extends DatabaseSchema {
+
+ public function tableExists($table) {
+ return (bool) $this->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_array = array();
+
+ // 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_array[] = $this->createFieldSql($name, $this->processField($field));
+ }
+
+ // Process keys.
+ if (!empty($schema['primary key'])) {
+ $sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']);
+ }
+
+ return implode(", \n", $sql_array);
+ }
+
+ /**
+ * 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') {
+ $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);
+ }
+
+ /**
+ * Create a table with a new schema containing 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 example, 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);
+ }
+}