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

View File

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

View File

@ -27,9 +27,9 @@ class Query_Parser {
* @var array * @var array
*/ */
private $match_patterns = array( private $match_patterns = array(
'function' => '`([a-zA-Z0-9_]+\((.*?)\))`', 'function' => '([a-zA-Z0-9_]+\((.*?)\))',
'identifier' => '`([a-zA-Z0-9"_-]+\.?)+`', 'identifier' => '([a-zA-Z0-9_-]+\.?)+',
'operator' => '`=|AND|&&?|~|\|\|?|\^|/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR`' 'operator' => '=|AND|&&?|~|\|\|?|\^|/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR'
); );
/** /**
@ -41,6 +41,7 @@ class Query_Parser {
'functions' => array(), 'functions' => array(),
'identifiers' => array(), 'identifiers' => array(),
'operators' => array(), 'operators' => array(),
'combined' => array(),
); );
/** /**
@ -50,9 +51,57 @@ class Query_Parser {
*/ */
public function __construct($sql = '') public function __construct($sql = '')
{ {
preg_match_all($this->match_patterns['function'], $sql, $this->matches['functions'], PREG_SET_ORDER); // Get sql clause components
preg_match_all($this->match_patterns['identifier'], $sql, $this->matches['identifiers'], PREG_SET_ORDER); preg_match_all('`'.$this->match_patterns['function'].'`', $sql, $this->matches['functions'], PREG_SET_ORDER);
preg_match_all($this->match_patterns['operator'], $sql, $this->matches['operators'], 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 { class QPTest extends UnitTestCase {
public function __construct() public function __construct()
@ -22,16 +25,33 @@ class QPTest extends UnitTestCase {
public function TestGeneric() 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() public function TestFunction()
{ {
$this->parser->__construct('table1.field1 > SUM(3+5)'); $matches = $this->parser->parse_join('table1.field1 > SUM(3+5)');
$this->assertIdentical($matches['combined'], array(
//echo '<pre>'.print_r($this->parser->matches, TRUE).'</pre>'; 'table1.field1', '>', 'SUM(3+5)'
));
} }
} }

Binary file not shown.