Parsed join conditions fixes #2

This commit is contained in:
Timothy Warren 2012-08-09 16:15:36 +00:00
parent 6a89b48fe8
commit 90c6760196
5 changed files with 204 additions and 115 deletions

View File

@ -29,21 +29,21 @@ abstract class DB_PDO extends PDO {
* @var mixed
*/
protected $statement;
/**
* Character to escape identifiers
*
* @var string
*/
protected $escape_char = '"';
/**
* Reference to sql sub class
*
* @var Object
*/
public $sql;
/**
* Reference to util sub class
*
@ -62,11 +62,11 @@ abstract class DB_PDO extends PDO {
public function __construct($dsn, $username=NULL, $password=NULL, $driver_options=array())
{
parent::__construct($dsn, $username, $password, $driver_options);
// Load the sql class for the driver
$class = get_class($this)."_sql";
$this->sql = new $class();
// Load the util class for the driver
$class = get_class($this)."_util";
$this->util = new $class($this);
@ -210,7 +210,7 @@ abstract class DB_PDO extends PDO {
{
return array_map(array($this, 'quote_ident'), $ident);
}
// If the string is already quoted, return the string
if (($pos = strpos($ident, $this->escape_char)) !== FALSE && $pos === 0)
{
@ -223,9 +223,9 @@ abstract class DB_PDO extends PDO {
// Return the re-compiled string
return implode('.', array_map(array($this, '_quote'), $hiers));
}
// --------------------------------------------------------------------------
/**
* Helper method for quote_ident
*
@ -238,7 +238,7 @@ abstract class DB_PDO extends PDO {
{
return $str;
}
return "{$this->escape_char}{$str}{$this->escape_char}";
}
@ -366,9 +366,9 @@ abstract class DB_PDO extends PDO {
{
return $this->driver_query($this->sql->system_table_list());
}
// --------------------------------------------------------------------------
/**
* Retrieve column information for the current database table
*
@ -379,9 +379,9 @@ abstract class DB_PDO extends PDO {
{
return $this->driver_query($this->sql->column_list($table), FALSE);
}
// --------------------------------------------------------------------------
/**
* Retrieve list of data types for the database
*
@ -408,7 +408,7 @@ abstract class DB_PDO extends PDO {
{
return FALSE;
}
// Return predefined data
if (is_array($sql))
{
@ -422,7 +422,7 @@ abstract class DB_PDO extends PDO {
return ($filtered_index) ? db_filter($all, 0) : $all;
}
// --------------------------------------------------------------------------
/**
@ -432,8 +432,8 @@ abstract class DB_PDO extends PDO {
*/
public function num_rows()
{
return isset($this->statement) && is_object($this->statement)
? $this->statement->rowCount()
return isset($this->statement) && is_object($this->statement)
? $this->statement->rowCount()
: FALSE;
}

View File

@ -52,35 +52,35 @@ class Query_Builder {
* @var string
*/
private $select_string;
/**
* Compiled 'from' clause
*
* @var string
*/
*/
private $from_string;
/**
* Compiled arguments for insert / update
*
* @var string
*/
private $set_string;
/**
* Order by clause
*
* @var string
*/
private $order_string;
/**
* Group by clause
*
* @var string
*/
private $group_string;
// --------------------------------------------------------------------------
// ! SQL Clause Arrays
// --------------------------------------------------------------------------
@ -91,21 +91,21 @@ class Query_Builder {
* @var array
*/
private $set_array_keys;
/**
/**
* Key/val pairs for order by clause
*
* @var array
*/
private $order_array;
/**
* Key/val pairs for group by clause
*
* @var array
*/
private $group_array;
// --------------------------------------------------------------------------
// ! Other Class vars
// --------------------------------------------------------------------------
@ -123,7 +123,7 @@ class Query_Builder {
* @var int
*/
private $limit;
/**
* Value for offset in limit string
*
@ -138,7 +138,7 @@ class Query_Builder {
*/
public $sql;
/**
/**
* Query component order mapping
* for complex select queries
*
@ -153,7 +153,7 @@ class Query_Builder {
* @var array
*/
private $query_map;
/**
* Map for having clause
*
@ -161,6 +161,13 @@ class Query_Builder {
*/
private $having_map;
/**
* Query parser to safely escape conditions
*
* @var object
*/
private $parser;
/**
* Convenience property for connection management
*
@ -179,33 +186,33 @@ class Query_Builder {
if (is_array($params))
{
$p = new stdClass();
foreach($params as $k => $v)
{
$p->$k = $v;
}
$params = $p;
}
// Let the connection work with 'conn_db' or 'database'
// Let the connection work with 'conn_db' or 'database'
if (isset($params->database))
{
$params->conn_db = $params->database;
}
$params->type = strtolower($params->type);
$dbtype = ($params->type !== 'postgresql') ? $params->type : 'pgsql';
$dsn = '';
// Add the driver type to the dsn
if ($dbtype !== 'firebird' && $dbtype !== 'sqlite')
{
$dsn = strtolower($dbtype).':'.$dsn;
}
// Make sure the class exists
if ( ! class_exists($dbtype))
{
@ -227,7 +234,7 @@ class Query_Builder {
{
$dsn .= ";port={$params->port}";
}
break;
case "sqlite":
@ -238,9 +245,9 @@ class Query_Builder {
$dsn = "{$params->host}:{$params->file}";
break;
}
try
try
{
// Create the database connection
if ( ! empty($params->user))
@ -263,6 +270,8 @@ class Query_Builder {
$this->conn_name = $params->name;
}
// Instantiate the Query Parser
$this->parser = new Query_Parser();
// Make things just slightly shorter
$this->sql =& $this->db->sql;
@ -314,9 +323,9 @@ class Query_Builder {
return $this;
}
// --------------------------------------------------------------------------
/**
* Method to simplify select_ methods
*
@ -328,16 +337,16 @@ class Query_Builder {
{
// Escape the identifiers
$field = $this->quote_ident($field);
$as = ($as !== FALSE)
$as = ($as !== FALSE)
? $this->quote_ident($as)
: $field;
return "({$field}) AS {$as} ";
}
// --------------------------------------------------------------------------
/**
* Selects the maximum value of a field from a query
*
@ -351,9 +360,9 @@ class Query_Builder {
$this->select_string .= $this->sql->max().$this->_select($field, $as);
return $this;
}
// --------------------------------------------------------------------------
/**
* Selects the minimum value of a field from a query
*
@ -362,14 +371,14 @@ class Query_Builder {
* @return $this
*/
public function select_min($field, $as=FALSE)
{
{
// Create the select string
$this->select_string .= $this->sql->min().$this->_select($field, $as);
return $this;
}
// --------------------------------------------------------------------------
/**
* Selects the average value of a field from a query
*
@ -383,9 +392,9 @@ class Query_Builder {
$this->select_string .= $this->sql->avg().$this->_select($field, $as);
return $this;
}
// --------------------------------------------------------------------------
/**
* Selects the sum of a field from a query
*
@ -401,7 +410,7 @@ class Query_Builder {
}
// --------------------------------------------------------------------------
/**
* Adds the 'distinct' keyword to a query
*
@ -413,7 +422,7 @@ class Query_Builder {
$this->select_string = $this->sql->distinct() . $this->select_string;
return $this;
}
// --------------------------------------------------------------------------
/**
@ -440,7 +449,7 @@ class Query_Builder {
// --------------------------------------------------------------------------
// ! 'Like' methods
// --------------------------------------------------------------------------
/**
* Simplify 'like' methods
*
@ -470,19 +479,19 @@ class Query_Builder {
{
$val = "%{$val}%";
}
$this->query_map[] = array(
'type' => 'like',
'conjunction' => (empty($this->query_map)) ? ' WHERE ' : " {$conj} ",
'string' => $l
);
// Add to the values array
$this->values[] = $val;
return $this;
}
}
// --------------------------------------------------------------------------
/**
@ -542,11 +551,11 @@ class Query_Builder {
{
return $this->_like($field, $val, $pos, 'NOT LIKE', 'OR');
}
// --------------------------------------------------------------------------
// ! Having methods
// --------------------------------------------------------------------------
/**
* Simplify building having clauses
*
@ -558,7 +567,7 @@ class Query_Builder {
private function _having($key, $val=array(), $conj='AND')
{
$where = $this->_where($key, $val);
// Create key/value placeholders
foreach($where as $f => &$val)
{
@ -577,12 +586,12 @@ class Query_Builder {
'string' => $item
);
}
return $this;
}
// --------------------------------------------------------------------------
/**
* Generates a 'Having' clause
*
@ -594,9 +603,9 @@ class Query_Builder {
{
return $this->_having($key, $val, 'AND');
}
// --------------------------------------------------------------------------
/**
* Generates a 'Having' clause prefixed with 'OR'
*
@ -642,9 +651,9 @@ class Query_Builder {
return $where;
}
// --------------------------------------------------------------------------
/**
* Simplify generating where string
*
@ -679,9 +688,9 @@ class Query_Builder {
return $this;
}
// --------------------------------------------------------------------------
/**
* Simplify where_in methods
*
@ -708,7 +717,7 @@ class Query_Builder {
'conjunction' => ( ! empty($this->query_map)) ? " {$conj} " : ' WHERE ',
'string' => $string
);
return $this;
}
@ -801,7 +810,7 @@ class Query_Builder {
// --------------------------------------------------------------------------
// ! Other Query Modifier methods
// --------------------------------------------------------------------------
/**
* Sets values for inserts / updates / deletes
*
@ -837,7 +846,7 @@ class Query_Builder {
return $this;
}
// --------------------------------------------------------------------------
/**
@ -850,15 +859,26 @@ class Query_Builder {
*/
public function join($table, $condition, $type='')
{
// TODO make able to handle operators without spaces
$table = implode(" ", array_map(array($this->db, 'quote_ident'), explode(' ', trim($table))));
//$condition = preg_replace('`(\W)`', " $1 ", $condition);
$cond_array = explode(' ', trim($condition));
$cond_array = array_map('trim', $cond_array);
$condition = $table . ' ON ' . $this->quote_ident($cond_array[0]) . $cond_array[1] .
' ' . $this->quote_ident($cond_array[2]);
$parser = new query_parser();
// Parse out the join condition
$parts = $parser->parse_join($condition);
$count = count($parts['identifiers']);
// Go through and quote the identifiers
for($i=0; $i <= $count; $i++)
{
if (in_array($parts['combined'][$i], $parts['identifiers']) && ! is_numeric($parts['combined'][$i]))
{
$parts['combined'][$i] = $this->quote_ident($parts['combined'][$i]);
}
}
$parsed_condition = implode(' ', $parts['combined']);
$condition = $table . ' ON ' . $parsed_condition;
$this->query_map[] = array(
'type' => 'join',
@ -1022,7 +1042,7 @@ class Query_Builder {
return $this;
}
// --------------------------------------------------------------------------
// ! Query execution methods
// --------------------------------------------------------------------------
@ -1070,7 +1090,7 @@ class Query_Builder {
}
// --------------------------------------------------------------------------
/**
* Convience method for get() with a where clause
*
@ -1084,13 +1104,13 @@ class Query_Builder {
{
// Create the where clause
$this->where($where);
// Return the result
return $this->get($table, $limit, $offset);
}
// --------------------------------------------------------------------------
/**
* Retreive the number of rows in the selected table
*
@ -1103,9 +1123,9 @@ class Query_Builder {
$res = $this->query($sql);
return (int) count($res->fetchAll());
}
// --------------------------------------------------------------------------
/**
* Retrieve the number of results for the generated query - used
* in place of the get() method
@ -1120,7 +1140,7 @@ class Query_Builder {
{
$this->from($table);
}
$sql = $this->_compile();
// Do prepared statements for anything involving a "where" clause
@ -1136,12 +1156,12 @@ class Query_Builder {
// Reset for next query
$this->_reset();
$rows = $result->fetchAll();
return (int) count($rows);
}
// --------------------------------------------------------------------------
/**
@ -1276,7 +1296,7 @@ class Query_Builder {
// Set empty arrays
$this->values = array();
// Set select string as an empty string, for proper handling
// of the 'distinct' keyword
$this->select_string = '';
@ -1295,7 +1315,7 @@ class Query_Builder {
private function _compile($type='', $table='')
{
$sql = '';
$table = $this->quote_ident($table);
switch($type)
@ -1324,7 +1344,7 @@ class Query_Builder {
{
$sql .= $this->group_string;
}
// Set the having string
if ( ! empty($this->having_map))
{
@ -1350,7 +1370,7 @@ class Query_Builder {
case "insert":
$param_count = count($this->set_array_keys);
$params = array_fill(0, $param_count, '?');
$sql = "INSERT INTO {$table} ("
$sql = "INSERT INTO {$table} ("
. implode(', ', $this->set_array_keys) .
') VALUES ('.implode(', ', $params).')';
break;
@ -1382,7 +1402,7 @@ class Query_Builder {
break;
}
//echo $sql . '<br />';
return $sql;

View File

@ -27,9 +27,9 @@ class Query_Parser {
* @var array
*/
private $match_patterns = array(
'function' => '`([a-zA-Z0-9_]+\((.*?)\))`',
'identifier' => '`([a-zA-Z0-9"_-]+\.?)+`',
'operator' => '`=|AND|&&?|~|\|\|?|\^|/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR`'
'function' => '([a-zA-Z0-9_]+\((.*?)\))',
'identifier' => '([a-zA-Z0-9_-]+\.?)+',
'operator' => '=|AND|&&?|~|\|\|?|\^|/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR'
);
/**
@ -41,6 +41,7 @@ class Query_Parser {
'functions' => array(),
'identifiers' => array(),
'operators' => array(),
'combined' => array(),
);
/**
@ -50,9 +51,57 @@ class Query_Parser {
*/
public function __construct($sql = '')
{
preg_match_all($this->match_patterns['function'], $sql, $this->matches['functions'], PREG_SET_ORDER);
preg_match_all($this->match_patterns['identifier'], $sql, $this->matches['identifiers'], PREG_SET_ORDER);
preg_match_all($this->match_patterns['operator'], $sql, $this->matches['operators'], PREG_SET_ORDER);
// Get sql clause components
preg_match_all('`'.$this->match_patterns['function'].'`', $sql, $this->matches['functions'], PREG_SET_ORDER);
preg_match_all('`'.$this->match_patterns['identifier'].'`', $sql, $this->matches['identifiers'], PREG_SET_ORDER);
preg_match_all('`'.$this->match_patterns['operator'].'`', $sql, $this->matches['operators'], PREG_SET_ORDER);
// Get everything at once for ordering
$full_pattern = '`'.$this->match_patterns['function'].'+|'.$this->match_patterns['identifier'].'|('.$this->match_patterns['operator'].')+`i';
preg_match_all($full_pattern, $sql, $this->matches['combined'], PREG_SET_ORDER);
// Go through the matches, and get the most relevant matches
$this->matches = array_map(array($this, 'filter_array'), $this->matches);
}
// --------------------------------------------------------------------------
/**
* Public parser method for seting the parse string
*
* @param string
*/
public function parse_join($sql)
{
$this->__construct($sql);
return $this->matches;
}
// --------------------------------------------------------------------------
/**
* Returns a more useful match array
*
* @param array
* @return array
*/
private function filter_array($array)
{
$new_array = array();
foreach($array as $row)
{
if (is_array($row))
{
$new_array[] = $row[0];
}
else
{
$new_array[] = $row;
}
}
return $new_array;
}
}

View File

@ -13,6 +13,9 @@
// --------------------------------------------------------------------------
/**
* Tests for the Query Parser
*/
class QPTest extends UnitTestCase {
public function __construct()
@ -22,16 +25,33 @@ class QPTest extends UnitTestCase {
public function TestGeneric()
{
$this->parser->__construct('table1.field1=table2.field2');
$matches = $this->parser->parse_join('table1.field1=table2.field2');
$this->assertIdentical($matches['combined'], array(
'table1.field1', '=', 'table2.field2'
));
}
//echo '<pre>'.print_r($this->parser->matches, TRUE).'</pre>';
public function TestGeneric2()
{
$matches = $this->parser->parse_join('db1.table1.field1!=db2.table2.field2');
$this->assertIdentical($matches['combined'], array(
'db1.table1.field1','!=','db2.table2.field2'
));
}
public function TestWUnderscore()
{
$matches = $this->parser->parse_join('table_1.field1 = tab_le2.field_2');
$this->assertIdentical($matches['combined'], array(
'table_1.field1', '=', 'tab_le2.field_2'
));
}
public function TestFunction()
{
$this->parser->__construct('table1.field1 > SUM(3+5)');
//echo '<pre>'.print_r($this->parser->matches, TRUE).'</pre>';
$matches = $this->parser->parse_join('table1.field1 > SUM(3+5)');
$this->assertIdentical($matches['combined'], array(
'table1.field1', '>', 'SUM(3+5)'
));
}
}

Binary file not shown.