In #337926: Force connection with PDO::CASE_LOWER, a default PDO database connection attribute was added that converts all fetched field/column names into lowercase. That is, for no particular reason.

This turns integration with third-party web services into a pain.

Schema functions, however, still create fields with their natural letter casing.

Example:

db_create_table('foo');
db_add_field('foo', 'contentId', array(
  'type' => 'int',
  'not null' => FALSE,
));
db_insert('foo')
  ->fields(array('contentId' => 3)
  ->execute();

debug(db_query('SELECT * FROM {foo} WHERE contentId = :id', array(':id' => 3))->fetchObject());

stdClass::__set_state(array(
   'contentid' => '3',
))

Note that the fetched column name is lowercase, contentid instead of contentId.

The only ugly workaround for the moment:

function foo_db_query($query, array $args = array(), array $options = array()) {
  if (empty($options['target'])) {
    $options['target'] = 'default';
  }

  $connection = Database::getConnection($options['target']);

  // Backup and restore PDO::ATTR_CASE afterwards, 'cause it sticks.
  $backup = $connection->getAttribute(PDO::ATTR_CASE);
  $connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);

  // Execute the query statement.
  $result = $connection->queryRange($query, $from, $count, $args, $options);

  // Restore the attribute.
  $connection->setAttribute(PDO::ATTR_CASE, $backup);

  return $result;
}

It might be possible to set PDO::ATTR_CASE on a statement only (instead of the connection). At least there's http://php.net/manual/en/pdostatement.setattribute.php, living right next to http://www.php.net/manual/en/pdostatement.setfetchmode.php, which we already support through $options = array('fetch' => PDO::FETCH_...)

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

catch’s picture

Status: Active » Needs review
Issue tags: +Needs backport to D7
FileSize
1.82 KB

That was a messy issue, but this patch should be a straight rollback of the change that caused the actual bug.

Status: Needs review » Needs work
Issue tags: -Needs backport to D7

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

catch’s picture

Status: Needs work » Needs review

#1: attr_rollback.patch queued for re-testing.

Status: Needs review » Needs work

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

sun’s picture

Status: Needs work » Needs review

#1: attr_rollback.patch queued for re-testing.

Status: Needs review » Needs work
Issue tags: +Needs backport to D7

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

sun’s picture

So the issue is that schema queries return information with always varying letter-cases (also experienced this flu in Demo module, it's different from mysql to mysqli to pdo_mysql to PHP5.2 to PHP5.3 to WHATNOT... not. fun.)

array (
  'Table' => 'simpletest824275node',
  'Non_unique' => '1',
  'Key_name' => 'node_created',
  'Seq_in_index' => '1',
  'Column_name' => 'created',
  'Collation' => 'A',
  'Cardinality' => '0',
  'Sub_part' => NULL,
  'Packed' => NULL,
  'Null' => '',
  'Index_type' => 'BTREE',
  'Comment' => '',
  'Index_comment' => '',
)
sun’s picture

Status: Needs work » Needs review
FileSize
3.77 KB

This would work, but I've no idea whether the implementation is correct. Thus, only one converted one example, which should lead to one test failure less.

A potential issue with this approach is that a query throwing an exception would possibly not revert the PDO::ATTR_CASE attribute on the connection. While the exception may be caught upstream, the database connection attribute won't be reverted. :-/

Crell’s picture

Status: Needs review » Needs work

Per the previous issue, I don't see why this should be per-query. It should be per-connection, and should be a connection option to disable the case folding.

I'm not sure why that ended up getting left out of the previous patch, but that is, IMO, the correct solution.

sun’s picture

Status: Needs work » Needs review

@Crell: Changing it on the connection is basically what both #1 and #8 are doing: Columns are fetched in their unchanged natural casing again.

However, in the very same database connection, we are also querying database schema information -- and especially MySQL is extremely inconsistent in what casing it returns for schema info. All of the keys in the array in #7 are currently expected to be all-lowercase throughout Drupal core. Unless you enforce columns to be fetched in lowercase, you'll get varying results depending on platform and version.

I don't think we want to instantiate an entirely new database connection when querying schema information...? Instead, only a handful schema queries want to enforce lowercase column names to ensure that consistent array keys are available and accessed in subsequent code.

Possibly I didn't fully understand what you meant though. If you meant with "change it per connection" to invoke some magic DatabaseConnection::setConnectionOptions() method (which doesn't seem to exist), then I could use some more detailed pointers to what you've thought of ;)

Crell’s picture

I was thinking something along the lines of:

$databases = array(
  // ...
);
$databases['extra']['default'] = array(
  'driver' => 'mysql',
  'user' => 'hello',
  'pass' => 'world',
  'name' => 'mystupiddb',
  'case_folding' => FALSE, // defaults to TRUE
);

Or alternatively we could allow a key to override arbitrary PDO options, although that might cause more trouble than its worth.

As for MySQL and its schema... *expletive deleted*.

sun’s picture

Apparently, my message didn't come through. Trying once again:

  1. We need to remove the hard-coded case-folding.
  2. We could make case-folding a database connection option. (but no one asked for that)
  3. All queries performed in schema.inc must fetch column names in lowercase, regardless of connection and current connection options.

If this is still unclear, please ping me in IRC. ;)

xjm’s picture

Tagging issues not yet using summary template.

Sylvain Lecoy’s picture

I fully support this as I created a similar issue #1315330: entity_load does not respect case on entity keys without finding this one.

It is effectively a pain in the ass for services integration. Here is a work-around for now:

<?php
/**
 * Implements hook_cron().
 */
function wow_guild_cron() {
  $guilds = wow_guild_load_multiple(FALSE);
  foreach ($guilds as $guild) {

    // this is crap... @see: #1171866
    $guild->lastModified = $guild->lastmodified;
    $guild->achivementPoints = $guild->achievementpoints;
    unset($guild->lastmodified);
    unset($guild->achievementPoints);

    try {
      $guild = wow_guild_fetch_guild($guild, array('members', 'achievements'));
      wow_guild_save($guild);
    }
    catch (WoWException $exception) {
      if ($exception->getCode() == 304) {
        // Not Modified. Do nothing.
      }
      else {
        watchdog_exception('wow_guild', $exception);
      }
    }

  }
}
?>
catch’s picture

Assigned: Unassigned » Crell

Looks like we need Crell's feedback on approach here.

Crell’s picture

Status: Needs review » Needs work

Now that #1309278: Make PDO connection options configurable is in for Drupal 8, does that change anything here? (Basically item 2 from #12 just happened anyway.)

That patch would have broken the latest patch here anyway, so marking back to needs work.

sun’s picture

Status: Needs work » Needs review
FileSize
3.91 KB

Just so everyone gets it:

All queries performed in schema.inc must fetch column names in lowercase, regardless of connection options.

Re-rolled against latest branch HEAD.

I don't really care whether case-folding is configurable, as long as we have natural casing by default. People overriding it to lowercase in their connection options just simply won't be able to use modules relying on natural casing.

In other words, the actual fix in this patch is that schema queries enforce the lowercase, and everything else remains natural. In concert with that, I'd personally recommend to hard-code and enforce natural casing on everyone.

chx’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

I tried to review #337926: Force connection with PDO::CASE_LOWER but all I could find is another case of hswong3i's irrational demands. So doing this looks good to me. The backport, alas, will be much less, we can only change schema.inc but we can not change connection options. We can make them configurable yes and that willl help you but the change to database.inc is not backportable.

catch’s picture

Configurable connection options is on its way #1309278: Make PDO connection options configurable.

Crell’s picture

With the connection options configurable now I don't think we need to have the separate "case" option. We just change the default options. People can still change them if they really really need, give or take the warning that they could break everything by doing so. I'm fine at this point with moving back to "native" casing as the default.

As for MySQL, <expletive deleted>. If we have to force lowercase there, we should document inline why we're doing so.

sun’s picture

My last known revision on why schema queries need to return lowercase keys/results is because there are differences between MySQL 5.0 and 5.1+ as well as differences between PDO and non-PDO:

http://drupalcode.org/project/demo.git/blob/refs/heads/7.x-1.x:/database...

sun’s picture

Status: Needs work » Needs review
FileSize
2.77 KB

I keep on running into this issue in contributed modules for D7.

To make them work at all, I'm committing more and more _mymodule_db_query() helper functions to the respective modules. This is insane.

Attached patch is the most simple fix:

It removes the bogus lowercase case-folding and makes the MySQL driver look for the original, natural-cased 'Key_name' that's returned by SHOW INDEX.

I really really hope that this can be backported.

Lastly, I don't really see how to write tests for this... the test failures for #1 show that the case-folding option is implicitly tested by the database system's schema functionality already.

daniel.hunt’s picture

Sun, would you mind pasting an example of what your _mymodule_db_query() functions contain?

sphism’s picture

Glad this is being fixed for drupal 8, was a right pain in drupal 7

Thanks for that.

Crell’s picture

Status: Needs review » Reviewed & tested by the community

Let's just go ahead and do #22. I don't know if we can backport it, but I am OK with doing so if webchick is.

meustrus’s picture

If you remove the case folding in the PDO fetch mode, please be ABSOLUTELY SURE that every database driver will behave consistently with mixed-case column names. This shouldn't affect schema install/uninstall, updates, or insertions; PDO::ATTR_CASE only affects returned result sets, AFAIK. I read somewhere that the PostgreSQL driver itself returns case folded column names, even if the PDO::ATTR_CASE is set to PDO::CASE_NATURAL; note the PDO documentation:

PDO::CASE_NATURAL: Leave column names as returned by the database driver.

I am pleased enough to see that the database connection is being set to intentionally case fold in a consistent manner with the current setup. Not sure why the options are at the driver level since it's a PDO function and not a driver function. The tables are created with the correct case, and I just need to use case folded column names when fetching data.

webchick’s picture

This looks a bit too scary for committing this close to 7.13. I can take another look after Wednesday.

Crell’s picture

This patch is against 8.x anyway, so it has to go there first. :-) Once that's done let us know if you're OK with a D7 backport and we can get xjm and her minions to handle that.

catch’s picture

Version: 8.x-dev » 7.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

I've committed/pushed this to 8.x. Moving to 7.x for backport. I think it makes sense to commit this just after the next stable 7.x release rather than just before too.

catch’s picture

Assigned: Crell » Unassigned
Issue tags: +Novice

Unassigning Crell since he's signed off on this, and tagging novice for the backport.

Alan Evans’s picture

I realise this has been signed-off, but for the record here are a few checks and verifications I did last week (hoping to add confidence to the backport):

  • fix does not break automated testing. Seems a good case for relying on automated testing, as it will be far more efficient than me in testing this. We can have confidence that at least one core test would break if this were introducing issues
  • checked simpletest codebase for core tests that might invoke the issue ... looking for anything that checks an index; general database tests ... YES there are tests that explicitly check indexExists in schema.test and field_sql_storage
  • run those simpletests and read through what they test ... All passed
  • used a modified version of the example script to test behaviour .... looks good, works as designed on mysql/pdo
  • how would it break contrib?
    • I can't think of any reasonable behaviour in contrib that would break with this patch. If anything in contrib breaks with this patch, then that needs fixing in contrib.
  • check backporting to D7 ...
    • key_name change should be the same: it's OK
    • database.inc change was noted by chx as not backportable because of the fact that it changes case on a per-query basis which we're not doing here. Should be ok
    • The main changes should have the same effect in D7 - they all have easily corresponding points in D7 code
  • checked for instances on d8. There are 3 of them, one for each of mysql, pgsql and sqlite
    • checked for all 3 instances covered in the patch ... looks good
    • apply patch and check for instances still ... looks good
    • TODO do we need it testing on pgsql and sqlite?
    • TODO backport latest to D7 - IMO backport should be harmless: see above
    Alan Evans’s picture

    Status: Patch (to be ported) » Needs review
    FileSize
    3.27 KB

    Attaching the backport ...

    bvirtual’s picture

    Long Beach Drupal Meetup is going to test the D7 patch tonight, March 28. We have 5 members working on this. 3 experienced, and 2 novice.

    frob’s picture

    Status: Needs review » Reviewed & tested by the community

    rtbced #32

    Tested patch 32 and verified with help from btmash.

    Used this code to create table:
    http://drupalbin.com/21097

    then added a data to the table and pulled the table structure with:
    debug(db_query("SELECT * FROM {foo} where formatId = 0")->fetchObject());

    bvirtual’s picture

    Status: Reviewed & tested by the community » Needs review

    RTBCED #32 wrapping up the Long Beach Contrib Meeting. We had 3 members test the same way, on 2 different OS (linux and mac). I tested on Gentoo with PHP 5.2.17 and mysql 5.1.56. I did test using a lower case name for the SELECT Frank posted in #34. It worked.

    Ashok points out this patch could be reviewed in D7 using sqlite, SQL, and I thought what my MySQL was switched to case sensitive mode for field names (does it still have that option?). Ashok's opinion was the code was all the same for all these databases, and should work.

    Alan Evans’s picture

    @frob/@bvirtual: wow, that's some seriously efficient reviewing work going on there :) Nice work ...

    @bvirtual: did you intend to switch the status back? That looks to me like it may have been a concurrent edit.

    Crell’s picture

    Sweet jebus that's a lot of testing. :-)

    Different databases have different case folding rules. Technically this patch reverts back to "let the database do whatever it's going to do", for better or worse. So a quick verification on SQLite and Postgres that nothing explodes is probably a good idea, especially for D7. A quick run through some subset of the database unit tests should be sufficient, since otherwise testing SQLite and Postgres takes a LONG time. :-(

    Alan Evans’s picture

    Couple of odd fails on D8/postgresql. Will come back to them once I've been through the rest. Given it's 2/2.5K fails/success, I doubt it's caused by this patch.

    RESULTS
    2688 passes, 1 fail, 1 exception, and 81 debug messages
    Table test_task created successfully.	Other	database_test.test	63	DatabaseTestCase->installTables()	
    PDOException: SQLSTATE[42803]: Grouping error: 7 ERROR: column "test.age" must appear in the GROUP BY clause or be used in an aggregate function LINE 3: (SELECT test.age AS age, age + 1 AS expression, 1 AS express... ^: SELECT COUNT(*) AS expression FROM (SELECT test.age AS age, age + 1 AS expression, 1 AS expression_2 FROM {test} test HAVING (age + 1 > 0) ) subquery; Array ( ) in PagerDefault->execute() (line 77 of /Users/alan/Sites/d8/core/includes/pager.inc).	Uncaught exception	Statement.php	58	PDOStatement->execute()	
    Enabled modules: database_test	Other	database_test.test	34	DatabaseTestCase->setUp()	
    Table test created successfully.	Other	database_test.test	63	DatabaseTestCase->installTables()	
    Table test_task created successfully.	Other	database_test.test	63	DatabaseTestCase->installTables()	
    FAIL Number of affected rows are returned.	Other	database_test.test	818	DatabaseUpdateTestCase->testExpressionUpdate()	

    Ran all of database and Field API (for field_sql_storage). The fails are reliable.

    Alan Evans’s picture

    D8 sqlite:
    2664 passes, 2 fails, 1 exception, and 81 debug messages

    Table test_task created successfully.	Other	database_test.test	63	DatabaseTestCase->installTables()	
    FAIL The whole transaction is rolled back when a duplicate key insert occurs.	Other	database_test.test	3185	DatabaseInvalidDataTestCase->testInsertDuplicateData()	
    The rest of the insert aborted as expected.	Other	database_test.test	3198	DatabaseInvalidDataTestCase->testInsertDuplicateData()	
    Table test_task created successfully.	Other	database_test.test	63	DatabaseTestCase->installTables()	
    PDOException: SQLSTATE[HY000]: General error: 1 a GROUP BY clause is required before HAVING: SELECT COUNT(*) AS expression FROM (SELECT test.age AS age, age + 1 AS expression, 1 AS expression_2 FROM {test} test HAVING (age + 1 > 0) ) subquery; Array ( ) in PagerDefault->execute() (line 77 of /Users/alan/Sites/d8/core/includes/pager.inc).	Uncaught exception	Connection.php	243	PDO->prepare()	
    Enabled modules: database_test	Other	database_test.test	34	DatabaseTestCase->setUp()	
    Enabled modules: field_sql_storage, field, field_test, text, number	Other	field_sql_storage.test	26	FieldSqlStorageTestCase->setUp()	
    FAIL Update succeeded.	Other	field_sql_storage.test	323	FieldSqlStorageTestCase->testFieldUpdateFailure()	
    Table field_data_test_text exists.	Other	field_sql_storage.test	331	FieldSqlStorageTestCase->testFieldUpdateFailure()	

    The exception is the same between sqlite/postgresql, the fails are different.

    Alan Evans’s picture

    D8/sqlite tests fail in the same places when patch is reverted, so no regression here ...

    Alan Evans’s picture

    D8/pgsql also has the same fails when reverted, so ok there too...

    Will come back to the actual fails/exception in a separate issue.

    Alan Evans’s picture

    Prior to patching, pgsql shows similar fails on d7

    PDOException: SQLSTATE[42803]: Grouping error: 7 ERROR: column "test.age" must appear in the GROUP BY clause or be used in an aggregate function LINE 3: (SELECT test.age AS age, age + 1 AS expression, 1 AS express... ^: SELECT COUNT(*) AS expression FROM (SELECT test.age AS age, age + 1 AS expression, 1 AS expression_2 FROM {test} test HAVING (age + 1 > 0) ) subquery; Array ( ) in PagerDefault->execute() (line 74 of /Users/alan/Sites/d7/includes/pager.inc). Uncaught exception database.inc 2136 PDOStatement->execute()

    Number of affected rows are returned. Other database_test.test 810 DatabaseUpdateTestCase->testExpressionUpdate()

    And sqlite also:

    PDOException: SQLSTATE[HY000]: General error: 1 a GROUP BY clause is required before HAVING: SELECT COUNT(*) AS expression FROM (SELECT test.age AS age, age + 1 AS expression, 1 AS expression_2 FROM {test} test HAVING (age + 1 > 0) ) subquery; Array ( ) in PagerDefault->execute() (line 74 of /Users/alan/Sites/d7/includes/pager.inc). Uncaught exception database.inc 240 PDO->prepare()

    Update succeeded. Other field_sql_storage.test 321 FieldSqlStorageTestCase->testFieldUpdateFailure()

    So, first of all looking that no new fails are introduced by the patch...

    Alan Evans’s picture

    sqlite shows the same fails as prior to patching.

    pgsql also.

    I'll check again what mysql has to say about the couple of fails I've seen, but I'm not expecting to see any problems. I think we're probably good here.

    Alan Evans’s picture

    FWIW all clear on that same small subset of tests D7/8 MySQL

    Crell’s picture

    So... if I understand Alan Evans correctly, SQLite and Postgres are broken now in D7 and D8, and are no more or less broken with this patch in either D7 or D8. Correct?

    If that's the case, then I think we can go ahead and commit the backport to D7 (webchick-permitting) and then separately try and figure out why those DBs are broken again.

    Alan Evans’s picture

    Right - MySQL is all clear with the patch (which we know anyway from the testbot), sqlite and postgres show the same fails/exceptions with and without the patch. I haven't run a full battery of tests on either to see what else might be lurking, just all of "Database" and "Field API" groups.

    My first guess on the PDO Exception is just some MySQL-specific SQL within the test. The failed field API update is more of a concern. The nested transaction issue is also not good. I'll report them separately. On postgres I'd also wonder if there's a server version-specific issue involved (I just downloaded and installed the latest).

    Crell’s picture

    Status: Needs review » Reviewed & tested by the community

    OK, then I think the D7 version of this is ready. Thanks Alan and crew for the super-detailed testing!

    Alan Evans’s picture

    Thanks Crell!

    webchick’s picture

    Status: Reviewed & tested by the community » Fixed

    Agreed; thank you so much for all of your great testing!

    Committed and pushed to 7.x.

    sphism’s picture

    Yay, I can finally remove the only core hack on one of my sites :)

    Thanks for all the hard work.

    meustrus’s picture

    Great, now I have to go through all my code and change the case folded names back to normal.

    onelittleant’s picture

    This is a very disruptive core change. This breaks all migrations that use the migration module and sql data sources with column names that use uppercase letters. I would have considered this more of a questionable architectural decision than a bug. If your sql migrations are broken after an update to 7.14 as mine were, this is likely the reason. You will need to modify your migrations to use the proper source column names with capital letters where before this change you were required to convert all column names to lowercase. I would expect that this change will break a lot of custom integration code that relies on DBTNG to access legacy or 3rd party databases.

    mikeryan’s picture

    Yes, from the migration standpoint (and if you check the original issue, that's not the only reason for this change) this is going to be disruptive in the short term. But think back to when you were first implementing those migrations from a mixed-case DB - how long did it take you, after initially doing the obvious thing and using the original case, to figure out you had to lower-case everything? Didn't you say, WTF;)? Future generations of migrators will be grateful to express their mappings in a natural way.

    Since users of the Migrate module are most likely to run into trouble with this upgrade, I've added a note to the project page. Hopefully at least some people will be forewarned before upgrading.

    onelittleant’s picture

    I did say WTF, but then just assumed that it was an arbitrary decision to default to PDO::CASE_LOWER and dealt with it. I wrote a number of migrations from many camelcased tables and was only alterted to the fact that this was considered a bug when my migrations broke with a core update. My only suggestion is that this late in the game (7.14) it may have been better to:

    1) Keep the PDO::CASE_LOWER default in D7.
    2) Provide an option in D7 to change to CASE_NATURAL programtically.
    3) Change the default to CASE_NATURAL in D8.

    sphism’s picture

    Oh I thought it was just going to be optional, maybe youcould define a constant in settings.php and use that for the pdo case ???

    Heine’s picture

    I think you can opt for pdo options in the db conf array in settings.php in 7.14 and possibly per query.

    That said; This change did come as a nasty surprise though as it was quite buried in the release notes:

    // ... lots of changes
    #1171866 by sun, catch, Alan Evans, bvirtual, frob, btmash: Fixed Enforced fetching of fields/columns in lowercase breaks third-party integration.
    // ... lots of changes
    

    Additional communication about the change (its presence, impact, workarounds) in the release announcements and additional docs similar to a Drupal 6.1 API change would have been nice.

    mfer’s picture

    I agree entirely with Heine on this one. This change broke backup and migrate among other things. Had no idea where to look to deal with this. #1576812: Could not complete the backup.

    webchick’s picture

    Well, I definitely welcome any and all help keeping an eye on the RTBC queue/commit log to spot things that break backwards compatibility. We never intentionally break APIs.

    catch’s picture

    Title: Enforced fetching of fields/columns in lowercase breaks third-party integration » Change notice for: Enforced fetching of fields/columns in lowercase breaks third-party integration
    Category: bug » task
    Priority: Major » Critical
    Status: Fixed » Active

    Yes this could have done with a change notice, re-opening for that.

    Leeteq’s picture

    For reference: ("Known issues" at the release statement) http://drupal.org/drupal-7.14

    mikeryan’s picture

    Regarding the drupal-7.14 release notes, they currently say:

    Solution for Migrate is to rename tables in scripts back to their proper names.

    Actually, it's not the table names, it's field names in mappings. Better:

    The solution for Migrate is to name source fields in mappings to match their case in the source database.

    sun’s picture

    Title: Change notice for: Enforced fetching of fields/columns in lowercase breaks third-party integration » Enforced fetching of fields/columns in lowercase breaks third-party integration
    Category: task » bug
    Priority: Critical » Major
    Status: Active » Fixed

    Added a change notice: http://drupal.org/node/1595218

    Status: Fixed » Closed (fixed)

    Automatically closed -- issue fixed for 2 weeks with no activity.

    sun’s picture

    Dave Cohen’s picture

    I'm new to this thread and realize it's already closed. Still, I want to express my emphatic agreement with comment #54. A change that will obviously break modules should be in a major release. What is this, drupal 4.x?

    I have a module broken by this change. As far as I can tell, I need to create a new branch to support < 7.14 vs >=7.14.