While converting my exercises from Drupal 6 to 7 I've tried to stay loyal to the new DB-abstraction layer. One of the exercises has some advanced mysql. In an attempt to make this query work, I've encountered several issues that make me question if I've discovered some incompleteness in the DB-abstraction layer or if I'm incompetent and do not understanding the DB-abstraction layer.
Let me start with the query, it is used for data analysis between users. The query will populate a table based on how often a user has replied to another user. The key of user_interaction is (sid, rid), which allows me to write one query to populate the table:
INSERT INTO user_interaction (sid, rid, count)
SELECT s.uid as sid, r.uid as rid, 1 as count
FROM comments s
INNER JOIN comments r ON s.pid = r.cid
ON DUPLICATE KEY UPDATE count = count + 1
From the DB documentation I understand that the above code is not exactly according to the SQL standard. The "INSERT ..ON DUPLICATE KEY UPDATE" means we are combining an insert an an update query and so need to use db_merge. However, this query is also an "INSERT ... FROM", which is demonstrated here. How I think the query should look like is:
$query = db_select('comment', 's');
$query->join('comment', 'r', 'r.nid = s.nid');
$query->addField('s','uid', 'sid');
$query->addField('r','uid', 'rid');
$query->addField('',1, 'count');
$tmp = db_merge('user_interaction')
->from($query)
->expression('count', 'count + :inc', array(':inc' => 1))
->execute();
Normally the db_merge needs a "key" and "fields", but we expect this should be replaceable by "from".
So time to dig into the DB abstraction layer to understand what is happening.
We see that both InsertQuery and MergeQuery inherit from Query and of course the from method is located at "InsertQuery::from". Logical as from only is relevant for insert and should not be part of Query.
The evaluation of the "from" method can be found in the "execute", were a the actual object is asked to transform the query to a string:
if (!empty($this->fromQuery)) {
$sql = (string) $this;
// The SelectQuery may contain arguments, load and pass them through.
return $this->connection->query($sql, $this->fromQuery->getArguments(), $this->queryOptions);
}
With Query being an abstract class and the string transformation is and abstract method (abstract public Query::__toString();
), we see how (string) $this
only becomes executable in the concrete InsertQuery (e.g. InsertQuery_mysql, InsertQuery_pgsql, ...).
So I got a warm and fussy feeling about the design and tried debugging my code by calling (string) $tmp
, but MergeQuery does not implement the string transformation properly it does: public function __toString() { }
... byby warmth, hello cold shower ...
MergeQuery does not seem to be implemented properly, but to suggest a solution, I need to have a look at some software architecture patterns in the DB abstraction layer. By putting some code behind db_insert together we get:
$class = $this->getDriverClass('InsertQuery', array('query.inc'));
return new $class($this, $table, $options);
//and inside getDriverClass($class, $driver);
$this->driverClasses[$class] = $class . '_' . $driver;
As a small remark, I'm not sure why the class creation needs to be outside the getDriverClass, as separate method calls (insert, update, delete and merge) all create the class. More importantly, this is a creational patter (I'm guessing the builder). The breaking of the "abstract class/method" structure seems a validation of the architectural patter.Two possible change could be done to MergeQuery:
1) If we do not need unique syntax for can MergeQuery, we may use a structural pattern (something like the wraper). This would make MergeQuery a simple object and have all default method calls go to InsertQuery, so that we only need to regulate the update case ... I'm not sure if this is possible
2) If we cannot wrap the object than we need MergeQuery to follow the creational structure (e.g. MergeQuery_mysql, MergeQuery_pgsql, ...)
Considering that I'm bringing this problem up, I'm more than happy to try and solve it. I would actually prefer to solve it as this would be a good exercise for me to contribute code. Still I could use some support from experienced DB abstraction layer architects. It is also possible that I misunderstood the whole problem, in that case I hope you will educate me.