views:

475

answers:

3

There are several ActiveRecord styled query builder libraries out there. Some are stand alone and some come built into frameworks. However, they really have trouble with WHERE and HAVING clauses when it comes to complex SQL. Setting other databases aside - I am trying to come up with a MySQL and PostgreSQL compatible WHERE() method that could fix these current method downfalls.

What follows is a long list of ideas and examples showing the best I could come up with so far. However, I can't seem to solve all of the use cases and I feel my partial solution is sloppy. Anyone that can answer with something that solves all of these problems will not only answer this question - but a will be responsible for fixing a problem that has hunted PHP implementations for several years now.

Common Operators

    = Equal
    <> Not Equal
    > Greater Than
    < Less Than
    >= Greater Than Or Equal
    <= Less Than Or Equal
    BETWEEN between values on right 
    NOT logical NOT 
    AND logical AND 
    OR logical OR

Example Where clauses

SELECT ... FROM table...
    WHERE column = 5
    WHERE column > 5
    WHERE column IS NULL
    WHERE column IN (1, 2, 3)
    WHERE column NOT IN (1, 2, 3)
    WHERE column IN (SELECT column FROM t2)
    WHERE column IN (SELECT c3 FROM t2 WHERE c2 = table.column + 10)
    WHERE column BETWEEN 32 AND 34
    WHERE column BETWEEN (SELECT c3 FROM t2 WHERE c2 = table.column + 10) AND 100
    WHERE EXISTS (SELECT column FROM t2 WHERE c2 > table.column)

There are many common ActiveRecord formats that the where() clause uses in the different current libraries.

$this->db->where(array('session_id' => '?', 'username' => '?'));
$this->db->fetch(array($id, $username));

// vs with is_int($key)
$this->db->where(array('session_id', 'username'));
$this->db->fetch(array($id, $username));

// vs with is_string($where)
$this->db->where('session_id', '?');
$this->db->where('username');
$this->db->fetch(array($id, $username));

// vs with is_array($value)
$this->db->where('session_id', '?');
$this->db->where('username', array('Sam', 'Bob'));
$this->db->fetch(array($id));

Here is the final format that I have so far. It should handle grouping (...) AND (...) as well as prepared statement bound params ("?" & ":name").

function where($column, $op = '=', $value = '?', $group = FALSE){}


// Single line

$this->db->where('column > 5');
$this->db->where('column IS NULL');

// Column + condition

$this->db->where('column', '=');
// WHERE column = ?  (prepared statement)
$this->db->where('column', '<>');
// WHERE column <> ?    (prepared statement)

// Column + condition + values

$this->db->where('column', '=', 5);
// // WHERE column = 5
$this->db->where('column', 'IN', '(SELECT column FROM t2)');
// WHERE column IN (SELECT column FROM t2)
$this->db->where('column', 'IN', array(1,2,3));
// WHERE column IN (1, 2, 3)
$this->db->where('column', 'NOT IN', array(1,2,3));
// WHERE column NOT IN (1, 2, 3)

// column + condition + values + group
$this->db->where(
    array(
     array('column', '<', 20), 
     array('column', '>', 10)
    ),
    NULL,
    NULL,
    $group = TRUE
);
// WHERE (column < 20 AND column > 10)

:UPDATE:

Over the course of my question I came to realize that WHERE and HAVING conditions only get more complex the deeper you go. Trying to abstract even 80% of the features would result in a massive library just for WHERE and HAVING. As Bill points out, that just isn't reasonable for a scripting language like PHP.

The solution is just to hand craft the WHERE portion of your query. As long as you use " around your columns you can use the same WHERE query in Postgre, SQLite, and MySQL since they use almost the same SQL syntax. (For MySQL you must str_replace() them with a tick`).

There comes a point where abstraction hurts more than it helps, WHERE conditions are one such place.

A: 

SQLAlchemy's API is the best one I've worked with so far. It's a Python-library, but you can still be inspired by it. It's not only for WHERE-clauses --- the entire SQL query (be it a select or DML) is expressed with a data structure that is easily modifiable.

(I'm referring to its SQL-toolkit, not the ORM-parts. :-)

Alex Brasetvik
Not being a Python dev I'm having a bit of trouble following the API when it comes to the [WHERE clause structure][1]. Would you mind adding an example to your question? [1]:http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/expressions.html#sqlalchemy.sql.expression.ClauseElement
Xeoncross
http://www.sqlalchemy.org/docs/06/sqlexpression.html covers it.
Alex Brasetvik
+2  A: 

I worked quite a bit on the Zend_Db library, which includes a PHP class for constructing SQL queries. I decided to punt on trying to handle every imaginable SQL syntax in WHERE and HAVING clauses, for several reasons:

  • PHP is a scripting language that parses and compiles code on every request (unless you use a bytecode cache). So the PHP environment is sensitive to bulky code libraries -- more so than Java or C# or Python or what have you. It's therefore a high priority to keep libraries as lean as we can.

    All of the Zend_Db library I worked on was about 2,000 lines of PHP code. By contrast, Java Hibernate is on the order of 118K lines of code. But that's not so much of an issue since a Java library is precompiled and doesn't have to be loaded on every request.

  • SQL expressions follow a generative grammar that is more compact, and easier to read and maintain that any of the PHP-based construction you showed. Learning the SQL expression grammar is far easier than learning an API that can simulate it. You end up supporting a "simplified grammar." Or else you start out that way, and find yourself coerced by your user community into Feature Creep until your API is unusably complex.

  • To debug an application that used such an API, you'd inevitably need access to the final SQL expression, so it's about the leakiest abstraction you can have.

  • The only advantage to using a PHP-based interface for SQL expressions would be that it assists code-completion in smart editors and IDE's. But when so many of the operators and operands use string constants like '>=', you spoil any code-completion intelligence.


update: I just read a good blog article "A Farewell to ORMs." The writer, Aldo Cortesi, suggests using the SQL Expression Language in Python's SQLAlchemy. Syntactic sugar and operator overloading that is standard in Python (but not supported in PHP) make this a very effective query-generating solution.

You might also look at Perl's DBIx::Class, but it ends up being pretty ugly.

Bill Karwin
I get the feeling that your right. The more we try to abstract some things, the harder our jobs can become. A case in point is the forced use of BBCode, textile, or markdown because programmers didn't want to properly filter HTML.
Xeoncross
I thought simple markup formats exist because typical users can't balance their HTML tags. :)
Bill Karwin
Actually, you `*have*` to `[b]balance[/b]` your `<tags>anyway</tags>`. It's just that early programmers didn't understand encoding and/or escaping output so they didn't know what to do with HTML in comments - *so they deleted it and made users learn something else*!
Xeoncross
A: 

This is part of my ActiveRecord class, I don't handle sub queries (I don't even bother):

public function Having($data, $operator = 'LIKE', $merge = 'AND')
{
    if (array_key_exists('query', $this->sql) === true)
    {
     foreach ($data as $key => $value)
     {
      $this->sql['having'][] = ((empty($this->sql['having']) === true) ? 'HAVING' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value);
     }
    }

    return $this;
}

public function Where($data, $operator = 'LIKE', $merge = 'AND')
{
    if (array_key_exists('query', $this->sql) === true)
    {
     foreach ($data as $key => $value)
     {
      $this->sql['where'][] = ((empty($this->sql['where']) === true) ? 'WHERE' : $merge) . ' ' . $this->Tick($key) . ' ' . $operator . ' ' . $this->Quote($value);
     }
    }

    return $this;
}

One other thing that you may consider is having a customHaving() and customWhere() methods.

Alix Axel
Thanks for sharing. However, things like `BETWEEN ? AND ?`, functions, grouped operations `(? OR (? AND ?))`, also don't work.
Xeoncross
Take a look at my somewhat related question: http://stackoverflow.com/questions/1332217/backticking-mysql-entities maybe you could implement a similar thing for the Quote() method.
Alix Axel