drupal_write_record is not using backticks (`) for column names. This leads to SQL syntax errors when using reserved keywords as column names.

This behaviour was observed with 'loop' as column name with mysql-5.1.
Querying using backticks around the column name (eg. "SELECT `loop` …") works normally.

Proposed solution would be for drupal_write_record to use backticks for all column names.

As I do not know if the solution would have to be made in drupal_write_record itself or somewhere 'further down' in the database system, I set the priority to major, since it could turn up anywhere in that case.
If you do happen to know that this issue is limited to drupal_write_record, feel free to set the priority back to 'normal'.

CommentFileSizeAuthor
#66 core-mysql-backticks-1426084-66.patch20.63 KBkriboogh
#64 core-mysql-backticks-1426084-64.patch20.64 KBkriboogh
#62 core-mysql-backticks-1426084-62.patch20.63 KBkriboogh
#58 core-mysql-backticks-1426084-58.patch16.08 KBkriboogh
#56 core-mysql-backticks-1426084-56.patch14.87 KBkriboogh
#54 core-1426084-mysql-backticks-54.patch14.62 KBkriboogh
#51 core-mysql-backticks-dashed-table-names-1426084-51.patch13.77 KBkriboogh
#49 core-mysql-backticks-dashed-table-names-1426084-49.patch9.28 KBkriboogh
#46 core-mysql-backticks-dashed-table-names-1426084-46.diff9.28 KBkriboogh
#44 core-mysql-backticks-dashed-table-names-1426084-44.patch9.28 KBkriboogh
#41 core-mysql-backticks-dashed-table-names-1426084-41.patch1.9 KBkriboogh
#39 core-mysql-backticks-dashed-table-names-1426084-39.patch1.89 KBkriboogh
#32 core-mysql-backticks-dashed-table-names-1426084-32.patch1.06 KBkriboogh
#25 core-mysql-backticks-dashed-table-names-1426084-25.patch992 byteskriboogh
#20 core-mysql-backticks-dashed-table-names-1426084-20.patch760 byteskriboogh
#11 1426084_2.patch4.32 KBchrisrockwell
#10 1426084_1.patch4.79 KBchrisrockwell
#4 1426084.patch629 bytesbleen
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

xjm’s picture

Category: bug » support
Priority: Major » Normal
Status: Active » Closed (works as designed)

Backticks are not used because they are MySQL-only. Instead, you should not use ANSI reserved words as table or field names.

References:
http://drupal.org/node/141051
http://drupal.org/node/555514

bleen’s picture

Status: Closed (works as designed) » Active

Given that we have abstracted out the DB layer completely, why can't we do this for mysql independently of PostgreSQL (etc.)?

xjm’s picture

Title: drupal_write_record not using backticks for column names, resulting in Syntax error with reserved keywords as column name. » Provide backtick escaping for MySQL in DB abstraction later
Version: 7.10 » 8.x-dev
Category: support » feature

Recategorizing based on my IRC conversation with @bleen18. I'm personally -1 on this, but I don't know the technical implications, so hopefully someone with more in-depth knowledge can evaluate the request.

bleen’s picture

Status: Active » Needs review
FileSize
629 bytes

Chew on this testbot...

Status: Needs review » Needs work

The last submitted patch, 1426084.patch, failed testing.

bleen’s picture

... and that's why we pay testbot the big bucks. The patch in #4 creates queries like this: "UPDATE {`system`} SET schema_version..." which is clearly not good. Ill take a deeper look.

chrisrockwell’s picture

Has there been any progress on this? Maybe I can help out as I need this with 7.x. I've inherited a MySQL table that is the new back-end for an Access front-end. The Access structure has spaces in field names all over the place.

bleen’s picture

clrockwell: I got uber stuck on making this work ... you're welcome to take a swing

chrisrockwell’s picture

I'm all backwards I think. I need this to work in D7 and my issue doesn't involve drupal_write_record, sorry. Nonetheless, maybe the following can help. Note that db_query() works fine with backticks, but $query->execute will not, which I'm still trying to track down why.

Here is the relevant module code:

function _exhibitor_search($search_text = '', $search_category = NULL, $exclude = NULL) {
  db_set_active('exhibitors');
  //$results = db_query("SELECT `Product Category` FROM Exhibitors WHERE `Product Category` = 20"); WORKS without patch

  $search_category = array('20','21'); // give a default for testing
  $search_fields = array('Company', 'Booth#', 'Product');
  if (!is_null($exclude)) {
    // a field needs to be excluded from the query
    if (!is_array($exclude)) : (array)$exclude; endif; // makes sure is_array for array_diff
    $search_fields = array_diff($search_fields, $exclude); // new query fields with exclude removed
  }
  $query = db_select('Exhibitors', 'e');
  //$query->fields = array('e', array('Company', 'City', 'State', 'Web Link', 'Booth#', 'Product', 'Product Category'));
  $db_or = db_or();
  foreach ($search_fields as $field) {
    $db_or->condition('`' . $field . '`', "%$searchtext%", 'LIKE'); 
  }
  // search category need an AND
  if (!is_null($search_category)) {
    $db_and = db_and();
    $db_and->condition(db_like('`Product Category`'), $search_category, 'IN');
    $db_and->condition($db_or);
  }
  $query->condition($db_and);
  die ((string)$query); // looks good after applied patch
		
  db_set_active();
}

I added the following code to database/mysql/database.inc

/**
   * Escapes a table name string.
   *
   * Force all table names to be strictly alphanumeric-plus-underscore.
   * For some database drivers, it may also wrap the table name in
   * database-specific escape characters.
   *
   * @return
   *   The sanitized table name string.
   */
   /* From http://drupal.org/node/1600670#comment-6096740 */
  public function escapeTable($table) {
    $escaped = preg_replace('/[^A-Za-z0-9_ ]+/', '', $table);

    // Check for beginning and ending backtick.
    if (preg_match('/^[`]/', $table) && preg_match('/[`]$/', $table)) {
      $escaped = '`' . $escaped . '`';
    }
    return $escaped;
  }

  /**
   * Escapes a field name string.
   *
   * Force all field names to be strictly alphanumeric-plus-underscore.
   * For some database drivers, it may also wrap the field name in
   * database-specific escape characters.
   *
   * @return
   *   The sanitized field name string.
   */
  public function escapeField($field) {
    $escaped = preg_replace('/[^A-Za-z0-9_ ]+/', '', $field);

    // Check for beginning and ending backtick.
    if (preg_match('/^[`]/', $field) && preg_match('/[`]$/', $field)) {
      $escaped = '`' . $escaped . '`';
    }
    return $escaped;
  }

I reference in the comments where I got the code from. This also allows for spaces in the table name. If drupal_write_record prepares it's queries this way, it should solve your issue as well. I'm fairly new to really diving into Drupal so I haven't quite figured out how to submit a proper patch.

Edit: note that the above 'patch' overrides the functions in database/database.inc

chrisrockwell’s picture

FileSize
4.79 KB

First try

chrisrockwell’s picture

FileSize
4.32 KB

Sorry, bad coding, didn't realize those tabs were in there.

chrisrockwell’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 1426084_2.patch, failed testing.

nevergone’s picture

In Drupal 7.15, the next query is failure:

$query = Database::getConnection('default', 'joomla_migrate')
->select('jos_content', 'jc')
->fields('jc', array('id', 'title', 'introtext', 'fulltext'));

because "fulltext" is MySQL reserved word.

The error message:
PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'fulltext FROM jos_content jc' at line 1: SELECT jc.id AS id, jc.title AS title, jc.introtext AS introtext, jc.fulltext AS fulltext FROM {jos_content} jc;

cafuego’s picture

I'm fiddling with a diver for the Drizzle database (based on MySQL) and that fails with a bunch of core modules using reserved keywords as column names. Thus, I've also been looking at escapeTable() and escapeField().

It looks as if the database layer uses escapeTable() but pretty much never uses escapeField(), which it should really run over each field name!

Anyway, having modified the base database.inc to add backticks to field names, it seems that the DB layer adds backticks around the whole `tablealias.fieldname` combination (usually used in conditions), which breaks queries completely (but I notice you've addressed that :-)

The backticks get added *inside* the { } table name placeholders, does this mean that with a database prefix the table would be turned into prefix_`users` or is the prefix adding function smart enough?

In addition, core seems to contain a bunch of db_query() statements with hardcoded field names that cannot be escaped without rewriting their queries, so I've opened #1776016: Database layer does not escape table and field strings everywhere

nevergone’s picture

Little workaround for #14

$query = Database::getConnection('default', 'joomla_migrate')
->select('jos_content', 'jc')
->fields('jc', array('id', 'title', 'introtext', 'created', 'modified'));
$query->addExpression('`fulltext`', 'full');

idacrobert’s picture

Thanks - had exact same problem here

idacrobert’s picture

Issue summary: View changes

Minor typo in issue description fixed.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kriboogh’s picture

Apparently this is a 4year old issue.

Anyway, reviving this, as I recently had to make '-' dashes in a table names to work, we are migrating some external tables into drupal 8. Dashes are allowed in MySql as long as you backtick the name. Why backticking everything isn't done by default in the MySQL layer is beyond me. Attached is a patch that allows dashes. Maybe someone from core needs to pick this up again.

Obvious things always seem to hit you in the head with Drupal. I also had problems with table names being reserved keywords and not being backticked.

daffie’s picture

Status: Needs work » Needs review

Lets see what the testbot thinks.

Status: Needs review » Needs work

The last submitted patch, 20: core-mysql-backticks-dashed-table-names-1426084-20.patch, failed testing.

daffie’s picture

Version: 8.2.x-dev » 8.3.x-dev

This issue is a feature request and those go to the developer branch of Drupal core and that is 8.3.

The last submitted patch, 20: core-mysql-backticks-dashed-table-names-1426084-20.patch, failed testing.

daffie’s picture

Status: Needs work » Needs review

Lets see what the testbot thinks.

Status: Needs review » Needs work

The last submitted patch, 25: core-mysql-backticks-dashed-table-names-1426084-25.patch, failed testing.

The last submitted patch, 25: core-mysql-backticks-dashed-table-names-1426084-25.patch, failed testing.

kriboogh’s picture

What do I need to change to this patch to make it work for the testbot?

daffie’s picture

@kriboogh: Which version of Drupal 8 are you using for creating your patches? Did you do follow these instructions: https://www.drupal.org/node/3060/git-instructions/8.3.x/nonmaintainer?

kriboogh’s picture

Our project is currently on version 8.2.x, maybe that's the problem. I'll try to do it on 8.3 if I get the time.
(I'm using the phpstorm create patch menu to generate the file, but don't think that would be the issue)

kriboogh’s picture

Status: Needs work » Needs review

Testbot go!

Status: Needs review » Needs work

The last submitted patch, 32: core-mysql-backticks-dashed-table-names-1426084-32.patch, failed testing.

kriboogh’s picture

Not sure what the problem is with the tests. I just installed a fresh drupal 8.3 install with this patch without any hickups...

daffie’s picture

@kriboogh: Your patch applied correctly. So that is good. The testbot ran for Drupal core with your patch applied all the tests and that resulted in 5315 fails. If you want to see what the testbot failures are you can click on the text "PHP 5.5 & MySQL 5.5 15,844 pass, 5,315 fail". What I see is that your patch results in SQL syntax errors:

exception: [Uncaught exception] Line 685 of core/lib/Drupal/Core/Database/Connection.php:
Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42000]: Syntax error or access violation: 1064 
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '`config`
WHERE (`collection` = '') AND (`name` = 'core.extension')' at line 2: SELECT 1 AS `expression`
FROM 
{`config`} `config`
WHERE (`collection` = :db_condition_placeholder_0) AND (`name` = :db_condition_placeholder_1); Array
(
    [:db_condition_placeholder_0] => 
    [:db_condition_placeholder_1] => core.extension
)
chrisrockwell’s picture

I want to look into this more, but at first glance I would think the issue is that db_query accepts {table_name} where as the other db_* do not (i.e. db_select('table_name')). So you may want to look into the preparation of a query and if, because we're putting backticks in there, {`table_name`} isn't escaped, resulting it in being used literally in the query e.g. "SELECT * FROM {`table_name`}...".

To make this easier for testing, you might enable simpletest and then pick a couple relevant database tests to run locally. The SchemaTest runs relatively quickly and was failing on this.

Hope that helps and I'll try to find time to come back and contribute some more.

kriboogh’s picture

It might be related to prefixing tables, on a normal setup I don't have any problems. My guess is the simpletest tables are also prefixed which is why they fail.
BTW, I'm unable to xdebug the exception, any idea how I enable this on simple tests? Tried XDEBUG cookies or altering the query string options in drupalGet, but none seem to work. The simple test run doesn't seem to be picked up by phpstorm.

kriboogh’s picture

Fixed table prefixes.

Status: Needs review » Needs work

The last submitted patch, 39: core-mysql-backticks-dashed-table-names-1426084-39.patch, failed testing.

kriboogh’s picture

Fixed EXISTS condition empty field being back ticked.

Status: Needs review » Needs work

The last submitted patch, 41: core-mysql-backticks-dashed-table-names-1426084-41.patch, failed testing.

kriboogh’s picture

Almost there, still a couple of tests failing.

Things still to look at:
- Some tests use placeholders to insert table names in a query. This results in `'tablename'` being generated when the arguments to a query are replaced.
- There are tests failing on `11` as a table name, not sure what the original purpose is for the test. Maybe the writer of the test can have a look.
- The failure in PreviewUI needs some more insight.

kriboogh’s picture

Small extra patch, the schema also uses tables for creating, lookup ect that aren't back ticked.

Status: Needs review » Needs work

The last submitted patch, 44: core-mysql-backticks-dashed-table-names-1426084-44.patch, failed testing.

kriboogh’s picture

kriboogh’s picture

Status: Needs work » Needs review
kriboogh’s picture

kriboogh’s picture

Renamed the file to patch.

Status: Needs review » Needs work

The last submitted patch, 49: core-mysql-backticks-dashed-table-names-1426084-49.patch, failed testing.

kriboogh’s picture

Found another problem with {} replacement. We were running an custom scripts that executes a sql file onto the drupal database. This basically executes each line of the file through $connection->query($line, array()). In one of the insert statements a value contained some serialised json, which has curly braces in it. The braces were replaced by the table prefix functionality. I replaced the str_replace method with a preg_replace statement, which as a bonus also deals with backticks, so the changes to the mysql driver could be simplified as well.

Hopefully fixed some failed test too.

Attached is a patch agains 8.2.5.

Status: Needs review » Needs work

The last submitted patch, 51: core-mysql-backticks-dashed-table-names-1426084-51.patch, failed testing.

nevergone’s picture

Title: Provide backtick escaping for MySQL in DB abstraction later » Provide backtick escaping for MySQL in DB abstraction layer

typo in title

kriboogh’s picture

Status: Needs work » Needs review
FileSize
14.62 KB

Re-upload patch file

Status: Needs review » Needs work

The last submitted patch, 54: core-1426084-mysql-backticks-54.patch, failed testing.

kriboogh’s picture

Version: 8.3.x-dev » 8.2.x-dev
Status: Needs work » Needs review
FileSize
14.87 KB

Fixed regex expression.

Status: Needs review » Needs work

The last submitted patch, 56: core-mysql-backticks-1426084-56.patch, failed testing.

kriboogh’s picture

Status: Needs work » Needs review
FileSize
16.08 KB

Fixed some failing tests.

Status: Needs review » Needs work

The last submitted patch, 58: core-mysql-backticks-1426084-58.patch, failed testing.

kriboogh’s picture

I keep getting PHPUnit_Framework_Exception: sh: : command not found errors on my local install, so tackling those last failed test is going to be difficult. If someone feels up to the task please do.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kriboogh’s picture

Status: Needs work » Needs review
FileSize
20.63 KB

Finally was able to pin point the remaining problem. Apparently this bug was already present in the code, but was never revealed because by chance the test succeeded. The problem was the extra condition being added in case a entity query is performed with a delta higher than 1 on a single value field. In this case the '1<>1' condition is added as a field to the query. This 'field' is then transformed into '11' due to the escapeField function in the connection object. Before the backticks, this didn't raised an error, as 11 = NULL returns false anyway. but with the backticks added `11`= NULL did. The correct way is to add a where clause in stead of a condition, so the field isn't parsed through escapeField.

Status: Needs review » Needs work

The last submitted patch, 62: core-mysql-backticks-1426084-62.patch, failed testing.

kriboogh’s picture

Forgot to backtick a test case.

Status: Needs review » Needs work

The last submitted patch, 64: core-mysql-backticks-1426084-64.patch, failed testing.

kriboogh’s picture

Sorry about the noise, test still failed due to braces being different.

mradcliffe’s picture

Status: Needs review » Needs work

A test failure in views_ui asserting a query string seems related.

Also it really helps to review changes in patches if you can create an interdiff between your patches.

+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php
@@ -204,11 +204,14 @@ public function query($query, array $args = array(), $options = array()) {
+    // To make the ILIKE operator work, we type-cast bytea fields into text.
+    // @todo This workaround only affects bytea fields, but the involved field
+    //   types involved in the query are unknown, so there is no way to
+    //   conditionally execute this for affected queries only.
+    return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE) /i', ' ${1}::text ${2} ', $query));

For instance I'm not sure where this change came from and why.

kriboogh’s picture

Hmm, were did you get that change from? At some point I switched from 8.2 to 8.3 so maybe that snook in. I never touched that file pgsql/Connection.php for this backtick fix.
I will add the interdiffs in the future. Alhough it might take a while now as the project we are using this for is about to round up.

joachim’s picture

Version: 8.3.x-dev » 8.4.x-dev
Category: Feature request » Bug report
Issue tags: +Needs issue summary update

Marking these as duplicates:

- #2186291: Standardise escaping of table, field and aliases in database layer -- has a patch which should be looked at to compare it to this one, and use the best bits of both
- #2767595: Support "-" in database table names
- #2845333: can't JOIN to a foreign database whose name contains a hyphen

This issue needs a summary update, as that still refers to D7. Also changing this to a bug, as the other issues all refer to this as a bug -- queries fail when they shouldn't, and without a proper explanation of why.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Lennard Westerveld’s picture

Hi,

Bump, is there any update on this matter? because i have issues with it with a contentEntity with a field in it named key.

For example:
INSERT INTO viewed (type, user, key, counter, created, changed) VALUES ('video', 1, '56dg211', 0, 1526384920, 1526384920);

This throws a error, because key is a function in MYSQL? This i literally a copy of the ContentEntity::create([])->save() query execution.

Escaping it with backtick and it will work again!
INSERT INTO viewed (`type`, `user`, `key`, `counter`, `created`, `changed`) VALUES ('video', 1, '56dg211', 0, 1526384920, 1526384920);

daffie’s picture

@Lennard Westerveld: The word "KEY" is a reserved keyword for MySQL. That is why it only works with backticks.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

alexpott’s picture

Status: Needs work » Closed (duplicate)
Related issues: +#2986452: Database reserved keywords need to be quoted as per the ANSI standard

This issue is superseded by #2986452: Database reserved keywords need to be quoted as per the ANSI standard which escapes columns for all database drivers. Going to mark as duplicate and credit people that worked on and review this issue on that one.