Issue #2477853 by ingaro, daffie, bzrudi71, mradcliffe, alexpott: PostgreSQL: Add support for reserved field/column names

8.0.x
Alex Pott 2015-07-01 12:49:16 +01:00
parent d433aecf0a
commit 40335e9773
9 changed files with 123 additions and 2 deletions

View File

@ -31,6 +31,27 @@ class Connection extends DatabaseConnection {
*/
const DATABASE_NOT_FOUND = 7;
/**
* The list of PostgreSQL reserved key words.
*
* @see http://www.postgresql.org/docs/9.4/static/sql-keywords-appendix.html
*/
protected $postgresqlReservedKeyWords = ['all', 'analyse', 'analyze', 'and',
'any', 'array', 'as', 'asc', 'asymmetric', 'authorization', 'binary', 'both',
'case', 'cast', 'check', 'collate', 'collation', 'column', 'concurrently',
'constraint', 'create', 'cross', 'current_catalog', 'current_date',
'current_role', 'current_schema', 'current_time', 'current_timestamp',
'current_user', 'default', 'deferrable', 'desc', 'distinct', 'do', 'else',
'end', 'except', 'false', 'fetch', 'for', 'foreign', 'freeze', 'from', 'full',
'grant', 'group', 'having', 'ilike', 'in', 'initially', 'inner', 'intersect',
'into', 'is', 'isnull', 'join', 'lateral', 'leading', 'left', 'like', 'limit',
'localtime', 'localtimestamp', 'natural', 'not', 'notnull', 'null', 'offset',
'on', 'only', 'or', 'order', 'outer', 'over', 'overlaps', 'placing',
'primary', 'references', 'returning', 'right', 'select', 'session_user',
'similar', 'some', 'symmetric', 'table', 'then', 'to', 'trailing', 'true',
'union', 'unique', 'user', 'using', 'variadic', 'verbose', 'when', 'where',
'window', 'with'];
/**
* Constructs a connection object.
*/
@ -167,6 +188,10 @@ class Connection extends DatabaseConnection {
// Quote the field name for case-sensitivity.
$escaped = '"' . $escaped . '"';
}
elseif (in_array(strtolower($escaped), $this->postgresqlReservedKeyWords)) {
// Quote the field name for PostgreSQL reserved key words.
$escaped = '"' . $escaped . '"';
}
return $escaped;
}
@ -181,6 +206,10 @@ class Connection extends DatabaseConnection {
if (preg_match('/[A-Z]/', $escaped)) {
$escaped = '"' . $escaped . '"';
}
elseif (in_array(strtolower($escaped), $this->postgresqlReservedKeyWords)) {
// Quote the alias name for PostgreSQL reserved key words.
$escaped = '"' . $escaped . '"';
}
return $escaped;
}
@ -195,6 +224,10 @@ class Connection extends DatabaseConnection {
if (preg_match('/[A-Z]/', $escaped)) {
$escaped = '"' . $escaped . '"';
}
elseif (in_array(strtolower($escaped), $this->postgresqlReservedKeyWords)) {
// Quote the table name for PostgreSQL reserved key words.
$escaped = '"' . $escaped . '"';
}
return $escaped;
}

View File

@ -117,6 +117,8 @@ class Insert extends QueryInsert {
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$insert_fields = array_map(function($f) { return $this->connection->escapeField($f); }, $insert_fields);
// If we're selecting from a SelectQuery, finish building the query and
// pass it back, as any remaining options are irrelevant.
if (!empty($this->fromQuery)) {
@ -154,4 +156,5 @@ class Insert extends QueryInsert {
return $query;
}
}

View File

@ -256,13 +256,13 @@ class Update extends Query implements ConditionInterface {
$data['expression']->compile($this->connection, $this);
$data['expression'] = ' (' . $data['expression'] . ')';
}
$update_fields[] = $field . '=' . $data['expression'];
$update_fields[] = $this->connection->escapeField($field) . '=' . $data['expression'];
unset($fields[$field]);
}
$max_placeholder = 0;
foreach ($fields as $field => $value) {
$update_fields[] = $field . '=:db_update_placeholder_' . ($max_placeholder++);
$update_fields[] = $this->connection->escapeField($field) . '=:db_update_placeholder_' . ($max_placeholder++);
}
$query = $comments . 'UPDATE {' . $this->connection->escapeTable($this->table) . '} SET ' . implode(', ', $update_fields);

View File

@ -138,4 +138,23 @@ class ConnectionTest extends DatabaseTestBase {
}
}
/**
* Test the escapeTable(), escapeField() and escapeAlias() methods with all possible reserved words in PostgreSQL.
*/
public function testPostgresqlReservedWords() {
if (Database::getConnection()->databaseType() !== 'pgsql') {
return;
}
$db = Database::getConnection('default', 'default');
$stmt = $db->query("SELECT word FROM pg_get_keywords() WHERE catcode IN ('R', 'T')");
$stmt->execute();
foreach ($stmt->fetchAllAssoc('word') as $word => $row) {
$expected = '"' . $word . '"';
$this->assertIdentical($db->escapeTable($word), $expected, format_string('The reserved word %word was correctly escaped when used as a table name.', array('%word' => $word)));
$this->assertIdentical($db->escapeField($word), $expected, format_string('The reserved word %word was correctly escaped when used as a column name.', array('%word' => $word)));
$this->assertIdentical($db->escapeAlias($word), $expected, format_string('The reserved word %word was correctly escaped when used as an alias.', array('%word' => $word)));
}
}
}

View File

@ -30,6 +30,7 @@ abstract class DatabaseTestBase extends KernelTestBase {
'test_task',
'test_null',
'test_serialized',
'test_special_columns',
));
self::addSampleData();
}
@ -138,5 +139,12 @@ abstract class DatabaseTestBase extends KernelTestBase {
'priority' => 3,
))
->execute();
db_insert('test_special_columns')
->fields(array(
'id' => 1,
'offset' => 'Offset value 1',
))
->execute();
}
}

View File

@ -70,4 +70,19 @@ class DeleteTruncateTest extends DatabaseTestBase {
$num_records_after = db_query("SELECT COUNT(*) FROM {test}")->fetchField();
$this->assertEqual(0, $num_records_after, 'Truncate really deletes everything.');
}
/**
* Confirms that we can delete a single special column name record successfully.
*/
function testSpecialColumnDelete() {
$num_records_before = db_query('SELECT COUNT(*) FROM {test_special_columns}')->fetchField();
$num_deleted = db_delete('test_special_columns')
->condition('id', 1)
->execute();
$this->assertIdentical($num_deleted, 1, 'Deleted 1 special column record.');
$num_records_after = db_query('SELECT COUNT(*) FROM {test_special_columns}')->fetchField();
$this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.');
}
}

View File

@ -182,4 +182,17 @@ class InsertTest extends DatabaseTestBase {
$this->assertIdentical($saved_age, '30', 'Can retrieve after inserting.');
}
/**
* Tests that we can INSERT INTO a special named column.
*/
function testSpecialColumnInsert() {
$id = db_insert('test_special_columns')
->fields(array(
'id' => 2,
'offset' => 'Offset value 2',
))
->execute();
$saved_value = db_query('SELECT "offset" FROM {test_special_columns} WHERE id = :id', array(':id' => 2))->fetchField();
$this->assertIdentical($saved_value, 'Offset value 2', 'Can retrieve special column name value after inserting.');
}
}

View File

@ -145,4 +145,18 @@ class UpdateTest extends DatabaseTestBase {
$saved_name= db_query('SELECT name FROM {test} WHERE id = :id', array(':id' => 42))->fetchField();
$this->assertIdentical($saved_name, 'John', 'Updated primary key successfully.');
}
/**
* Confirm that we can update values in a column with special name.
*/
function testSpecialColumnUpdate() {
$num_updated = db_update('test_special_columns')
->fields(array('offset' => 'New offset value'))
->condition('id', 1)
->execute();
$this->assertIdentical($num_updated, 1, 'Updated 1 special column record.');
$saved_value = db_query('SELECT "offset" FROM {test_special_columns} WHERE id = :id', array(':id' => 1))->fetchField();
$this->assertIdentical($saved_value, 'New offset value', 'Updated special column name value successfully.');
}
}

View File

@ -274,5 +274,21 @@ function database_test_schema() {
'primary key' => array('name', 'age'),
);
$schema['test_special_columns'] = array(
'description' => 'A simple test table with special column names.',
'fields' => array(
'id' => array(
'description' => 'Simple unique ID.',
'type' => 'int',
'not null' => TRUE,
),
'offset' => array(
'description' => 'A column with preserved name.',
'type' => 'text',
),
),
'primary key' => array('id'),
);
return $schema;
}