D7 ships with pdo and a serie of query classes (SelectQuery, DeleteQuery,...) in the db layer. So you write queries by using objects.
Before D7 it was possible to write queries compatible for both D5&6. With the growing adoption for D7 in drush, all queries must be written twice, and this involves a lot of spaguetti when the query is not a predefined one but needs to be dinamically built.
Well, there aren't so much database queries in drush, but most complex are in drush_core_watchdog_* and I'm struggling my head with watchdog :) ..... also other queries could be introduced in a future... and it can be a benefit for drush contribs.
option 1
-------
Easiest queries are SELECT, as D7 provides db_query() for select statements.
Main difference with D5&6 is placeholders. D7 accepts unnamed placeholders (with ?) or named, with :column.
So a hypothetical function drush_db_query() accepting queries with named placeholders and replacing them if running on D5&6 will help.
This is a quick and dirty approach to replacing placeholders (not so dirty if you know your query args are ok) with some code copied from D7's db code:
<?php
function _drush_replace_query_placeholders($where, $args) {
foreach ($args as $key => $data) {
if (is_array($data)) {
$new_keys = array();
foreach ($data as $i => $value) {
$new_keys[$key . '_' . $i] = $value;
}
$where = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
unset($args[$key]);
$args += $new_keys;
}
}
foreach ($args as $key => $data) {
$where = str_replace($key, $data, $where);
}
return $where;
}
??>
... but this is only useful for selects.
option 2
-------
D7 provides also db_select(), db_delete(), etc. those return Query objects. An option is defining those in d5,6 environments and also a "fake" class with the same interface but running db_query() on ->execute(). Actually it won't be a fake but perhaps classes inheriting from SelectQuery, DeleteQuery, etc. In any case this is perhaps a unnecessary overhead for drush (at least in terms of # lines of code).
With this option all queries will be written for D7 and will be converted as needed if running D5|6.
option 3
-------
this is an extension to option #1. D7 Query classes have a method for WHERE predicates with placeholders. In fact, you can do
db_select($table)->fields()->where('id = :id', array(':id' => $id));
So the idea here is to provide wrapper functions: drush_db_select(), drush_db_delete(), etc.
This is a running code for drush_db_select() that I've developed and tested while working on #170583: watchdog fixes and improvements:
<?php
function drush_db_select($table, $fields = '*', $where = NULL, $args = NULL, $start = 0, $length = NULL, $order_by_field = NULL, $order_by_direction = 'ASC') {
if (drush_drupal_major_version() >= 7) {
if ($fields == '*') {
$fields = array();
}
$query = db_select($table, $table)
->fields($table, $fields);
if (!empty($where)) {
$query = $query->where($where, $args);
}
if (!is_null($length)) {
$query = $query->range($start, $length);
}
if (!is_null($order_by_field)) {
$query = $query->orderBy($order_by_field, $order_by_direction);
}
return $query->execute();
}
else {
if ($fields != '*') {
$fields = implode(',', $fields);
}
$query = "SELECT $fields FROM {$table}";
if (!empty($where)) {
$where = _drush_replace_query_placeholders($where, $args);
$query .= " WHERE ".$where;
}
if (!is_null($count)) {
$limit = " LIMIT $count";
if (!is_null($start)) {
$limit .= " OFFSET $start";
}
$query .= $limit;
}
if (!is_null($order_by_field)) {
$query .= "ORDER BY $order_by_field $order_by_direction";
}
return db_query($query, $args);
}
}
?>
I also have drush_db_delete() and no use case for update or inserts. All of this still needs some work (ej: limit/offset/range), and some improvements can be added (ej: DISTINCT), but first I'd like to know if you see this of interest or is acceptable to write twice wherever a query is needed.
| Comment | File | Size | Author |
|---|---|---|---|
| #15 | 700608.patch | 3.06 KB | jonhattan |
| #9 | 700608.patch | 4.22 KB | jonhattan |
| #7 | 700608.patch | 4.24 KB | jonhattan |
| #5 | 700608.patch | 4.12 KB | jonhattan |
| #3 | 700608.patch | 3.79 KB | jonhattan |
Comments
Comment #1
moshe weitzman commentedOption #3 looks good to me. I noticed that $length and $count are used for same thing. Also, it does not handle joins at all. Would it be hard to add that?
Comment #2
jonhattanHere is a patch with the relevant part of a major patch in #170583-5: watchdog fixes and improvements. It still needs drush_db_delete().
I'd like to focus on implementing the required functionality for #170583 to be commited and afterwards continue with joins and/or insert/update queries.
Comment #3
jonhattanNew patch with drush_db_delete().
Sadly DatabaseQuery doesn't implement orderBy() nor range(). I've not found any issue on this.
Adding this to the class do the work but I think a major change is needed in db api and anyway it is not to be accepted in a short terrm :(
Comment #4
moshe weitzman commented"A db_select() that works for any version of Drupal.". thats on top of delete function as well.
whats the issue with orderBy and range methods? I don't see how those make sense for delete queries.
Comment #5
jonhattanorderBy and range are useful for watchdog-delete to remove N items from the top or the bottom of the watchdog stack. It can be accomplished with a select query and deleting the result.
New patch removing all orderby/range for db_delete() and improved documentation.
Comment #6
moshe weitzman commentedThis looks good. One last optimization is to use db_truncate() when you have a delete with no where on D7.
Comment #7
jonhattanpatch including truncate for all versions.
Comment #8
moshe weitzman commentedTRUNCATE is a mysql specific feature. Lets just use it on D7 where each database driver implements it as they see fit. Namely, they all do DELETE FROM except mysql which does the faster TRUNCATE
Comment #9
jonhattanpatch with truncate for D7 only.
A new addition: drush_db_delete() now returns # of affected rows on success.
In D5,6 you can use db_affected_rows() after db_query(). In D7 db_delete($table)->execute() already returns the number of affected rows. I can't see an alternative to db_affected_rows() for D7.
Comment #10
moshe weitzman commentedI don't quite grok "In D7 db_delete($table)->execute() already returns the number of affected rows. I can't see an alternative to db_affected_rows() for D7.". Is there an inconsistency in the APi for D7
Comment #11
jonhattanI think I finally understood what "grok" means (not in my dictionaries!).
db_affected_rows() is now method rowCount() in PDOStatement or classes implementing DatabaseStatementInterface. I was referring to the fact that DeleteQuery does not return a database statement (as SelectQuery does) but the number of affected rows.
Constructor of DeleteQuery hardcode what is to be returned by execute():
$options['return'] = Database::RETURN_AFFECTED;.For reference:
* DeleteQuery::__constructor() at line 879 of query.inc
* DatabaseConnection::query() at line 539 of database.inc
Comment #12
moshe weitzman commentedI think this is ready. Shall we commit this separately, or refine watchdog patch and commit there. I can do whatever is easier for you.
Comment #13
jonhattanYou can commit this separately.
Comment #14
moshe weitzman commentedcommitted. thanks. are there places beyond watchdog where we should be using this?
Comment #15
jonhattanvariable.drush.inc can use it. Also drush_cache_clear_theme_registry().
Probably batch_6.inc and batch_7.inc could share some code. There're other queries in update_6.inc, environment_5.inc and environment_6.inc that can stand there.
Patch attached fixes for variable and cache commands. It also modifies drush_db_select() to allow passing a single field instead of an array.
Comment #16
moshe weitzman commentedcommitted. thanks.
Comment #17
moshe weitzman commentedi should add that dbtng has been backported to d6 at http://drupal.org/project/dbtng. might be useful when building up insert/update code.