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.

Comments

moshe weitzman’s picture

Option #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?

jonhattan’s picture

Status: Active » Needs work
StatusFileSize
new2.5 KB

Here 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.

jonhattan’s picture

StatusFileSize
new3.79 KB

New 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 :(

  public function orderBy($field, $direction = 'ASC') {
    $this->query->orderBy($field, $direction);
    return $this;
  }

  public function range($start = NULL, $length = NULL) {
    $this->query->range($start, $length);
    return $this;
  }
moshe weitzman’s picture

"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.

jonhattan’s picture

Status: Needs work » Needs review
StatusFileSize
new4.12 KB

orderBy 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.

moshe weitzman’s picture

Status: Needs review » Needs work

This looks good. One last optimization is to use db_truncate() when you have a delete with no where on D7.

jonhattan’s picture

Status: Needs work » Needs review
StatusFileSize
new4.24 KB

patch including truncate for all versions.

moshe weitzman’s picture

TRUNCATE 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

jonhattan’s picture

StatusFileSize
new4.22 KB

patch 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.

moshe weitzman’s picture

I 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

jonhattan’s picture

I 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

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

I think this is ready. Shall we commit this separately, or refine watchdog patch and commit there. I can do whatever is easier for you.

jonhattan’s picture

You can commit this separately.

moshe weitzman’s picture

Status: Reviewed & tested by the community » Fixed

committed. thanks. are there places beyond watchdog where we should be using this?

jonhattan’s picture

Status: Fixed » Needs review
StatusFileSize
new3.06 KB

variable.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.

moshe weitzman’s picture

Status: Needs review » Fixed

committed. thanks.

moshe weitzman’s picture

i should add that dbtng has been backported to d6 at http://drupal.org/project/dbtng. might be useful when building up insert/update code.

Status: Fixed » Closed (fixed)

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