From 30b776544b0911a857ab87a4a62797c18a2329b8 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2009 18:20:51 +0000 Subject: [PATCH] BackwardsCompatibilityBreak - removed support for date function translation in fDatabase/fSQLTranslation. InternalBackwardsCompatibilityBreak - Removed fORMDatabase::addTableToKeys(), fORMDatabase::addTableToValues(), fORMDatabase::escapeBySchema() and fORMDatabase::escapeByType(); rewrote fORMDatabase::createHavingClause() to fORMDatabase::addHavingClause(); rewrote fORMDatabase::createOrderByClause() to fORMDatabase::addOrderByClause(); rewrote fORMDatabase::insertFromAndGroupByClauses() to fORMDatabase::injectFromAndGroupByClauses(); added the `$schema` parameter to the beginning of fORMSchema::getRouteName(), fORMSchema::getRouteName(), fORMSchema::getRoutes() and fORMSchema::isOneToOne(); added the `$class` parameter to the beginning of fORMRelated::storeManyToMany(). Fixed tickets #319 and #308. Rewrote the ORM to use quoted identifiers and placeholder escaping. Added support for PostgreSQL, Oracle and SQL Server schemas. Added tests for fActiveRecord, fRecordSet, fORMRelated and fORMOrdering --- classes/fActiveRecord.php | 279 ++++++---- classes/fDatabase.php | 409 +++++++++----- classes/fORMColumn.php | 44 +- classes/fORMDatabase.php | 1031 ++++++++++++++++++----------------- classes/fORMOrdering.php | 298 +++++++--- classes/fORMRelated.php | 273 ++++++---- classes/fORMSchema.php | 79 ++- classes/fORMValidation.php | 135 +++-- classes/fRecordSet.php | 367 +++++++------ classes/fSQLTranslation.php | 314 ++++------- classes/fSchema.php | 518 ++++++++---------- 11 files changed, 2060 insertions(+), 1687 deletions(-) diff --git a/classes/fActiveRecord.php b/classes/fActiveRecord.php index fc6ccc07..ca465a65 100644 --- a/classes/fActiveRecord.php +++ b/classes/fActiveRecord.php @@ -15,7 +15,8 @@ * @package Flourish * @link http://flourishlib.com/fActiveRecord * - * @version 1.0.0b45 + * @version 1.0.0b46 + * @changes 1.0.0b46 Changed SQL statements to use value placeholders and identifier escaping [wb, 2009-10-22] * @changes 1.0.0b45 Added support for `!~`, `&~`, `><` and OR comparisons to ::checkConditions(), made object handling in ::checkConditions() more robust [wb, 2009-09-21] * @changes 1.0.0b44 Updated code for new fValidationException API [wb, 2009-09-18] * @changes 1.0.0b43 Updated code for new fRecordSet API [wb, 2009-09-16] @@ -769,8 +770,9 @@ public function __call($method_name, $parameters) $plural = FALSE; // one-to-many relationships need to use plural forms - if (in_array($subject, fORMSchema::retrieve()->getTables())) { - if (fORMSchema::isOneToOne($table, $subject, $route)) { + $schema = fORMSchema::retrieve(); + if (in_array($subject, $schema->getTables())) { + if (fORMSchema::isOneToOne($schema, $table, $subject, $route)) { throw new fProgrammerException( 'The table %1$s is not in a %2$srelationship with the table %3$s', $table, @@ -856,8 +858,9 @@ public function __call($method_name, $parameters) $route = isset($parameters[0]) ? $parameters[0] : NULL; // one-to-many relationships need to use plural forms - if (in_array($subject, fORMSchema::retrieve()->getTables())) { - if (fORMSchema::isOneToOne($table, $subject, $route)) { + $schema = fORMSchema::retrieve(); + if (in_array($subject, $schema->getTables())) { + if (fORMSchema::isOneToOne($schema, $table, $subject, $route)) { throw new fProgrammerException( 'The table %1$s is not in a%2$srelationship with the table %3$s', $table, @@ -947,10 +950,11 @@ public function __clone() } // If we have a single auto incrementing primary key, remove the value + $schema = fORMSchema::retrieve(); $table = fORM::tablize($class); - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); + $pk_columns = $schema->getKeys($table, 'primary'); - if (sizeof($pk_columns) == 1 && fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { + if (sizeof($pk_columns) == 1 && $schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { $this->values[$pk_columns[0]] = NULL; unset($this->old_values[$pk_columns[0]]); } @@ -967,7 +971,8 @@ public function __clone() */ public function __construct($key=NULL) { - $class = get_class($this); + $class = get_class($this); + $schema = fORMSchema::retrieve(); // If the features of this class haven't been set yet, do it if (!isset(self::$configured[$class])) { @@ -975,7 +980,7 @@ public function __construct($key=NULL) self::$configured[$class] = TRUE; $table = fORM::tablize($class); - if (!fORMSchema::retrieve()->getKeys($table, 'primary')) { + if (!$schema->getKeys($table, 'primary')) { throw new fProgrammerException( 'The database table %1$s (being modelled by the class %2$s) does not appear to have a primary key defined. %3$s and %4$s will not work properly without a primary key.', $table, @@ -1022,12 +1027,12 @@ public function __construct($key=NULL) } elseif ($key !== NULL) { $table = fORM::tablize($class); - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); + $pk_columns = $schema->getKeys($table, 'primary'); // If the primary key does not look properly formatted, check to see if it is a UNIQUE key $is_unique_key = FALSE; if (is_array($key) && (sizeof($pk_columns) == 1 || array_diff(array_keys($key), $pk_columns))) { - $unique_keys = fORMSchema::retrieve()->getKeys($table, 'unique'); + $unique_keys = $schema->getKeys($table, 'unique'); $key_keys = array_keys($key); foreach ($unique_keys as $unique_key) { if (!array_diff($key_keys, $unique_key)) { @@ -1075,7 +1080,7 @@ public function __construct($key=NULL) // Create an empty array for new objects } else { - $column_info = fORMSchema::retrieve()->getColumnInfo(fORM::tablize($class)); + $column_info = $schema->getColumnInfo(fORM::tablize($class)); foreach ($column_info as $column => $info) { $this->values[$column] = NULL; if ($info['default'] !== NULL) { @@ -1145,51 +1150,87 @@ protected function configure() /** - * Creates the SQL to insert this record + * Creates the fDatabase::translatedQuery() insert statement params * - * @param array $sql_values The SQL-formatted values for this record - * @return string The SQL insert statement + * @param boolean $new_autoincrementing_record If the record is new and has an auto-incrementing primary key + * @param string $pk_column The auto-incrementing primary key column for a new record + * @return array The parameters for an fDatabase::translatedQuery() SQL insert statement */ - protected function constructInsertSQL($sql_values) + protected function constructInsertParams($new_autoincrementing_record, $pk_column) { - $sql = 'INSERT INTO ' . fORM::tablize(get_class($this)) . ' ('; + $columns = array(); + $values = array(); - $columns = ''; - $values = ''; + $column_placeholders = array(); + $value_placeholders = array(); - $column_num = 0; - foreach ($sql_values as $column => $sql_value) { - if ($column_num) { $columns .= ', '; $values .= ', '; } - $columns .= $column; - $values .= $sql_value; - $column_num++; + $class = get_class($this); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); + $column_info = $schema->getColumnInfo($table); + foreach ($column_info as $column => $info) { + // Most databases don't like the auto incrementing primary key to be set to NULL + if ($new_autoincrementing_record && $pk_column == $column && $this->values[$pk_column] === NULL) { + continue; + } + + $value = fORM::scalarize($class, $column, $this->values[$column]); + if ($value === NULL && $info['not_null'] && $info['default'] !== NULL) { + $value = $info['default']; + } + + $columns[] = $column; + $values[] = $value; + + $column_placeholders[] = '%r'; + $value_placeholders[] = $info['placeholder']; } - $sql .= $columns . ') VALUES (' . $values . ')'; - return $sql; + + $sql = 'INSERT INTO %r (' . join(', ', $column_placeholders) . ') VALUES (' . join(', ', $value_placeholders) . ')'; + $params = array($sql, $table); + $params = array_merge($params, $columns); + $params = array_merge($params, $values); + + return $params; } /** - * Creates the SQL to update this record + * Creates the fDatabase::translatedQuery() update statement params * - * @param array $sql_values The SQL-formatted values for this record - * @return string The SQL update statement + * @return array The parameters for an fDatabase::translatedQuery() SQL update statement */ - protected function constructUpdateSQL($sql_values) + protected function constructUpdateParams() { - $table = fORM::tablize(get_class($this)); + $class = get_class($this); + $schema = fORMSchema::retrieve(); + + $table = fORM::tablize($class); + $column_info = $schema->getColumnInfo($table); - $sql = 'UPDATE ' . $table . ' SET '; - $column_num = 0; - foreach ($sql_values as $column => $sql_value) { - if ($column_num) { $sql .= ', '; } - $sql .= $column . ' = ' . $sql_value; - $column_num++; + $assignments = array(); + $params = array($table); + + foreach ($column_info as $column => $info) { + if ($info['auto_increment'] && !fActiveRecord::changed($this->values, $this->old_values, $column)) { + continue; + } + + $assignments[] = '%r = ' . $info['placeholder']; + + $value = fORM::scalarize($class, $column, $this->values[$column]); + if ($value === NULL && $info['not_null'] && $info['default'] !== NULL) { + $value = $info['default']; + } + + $params[] = $column; + $params[] = $value; } - $sql .= ' WHERE ' . fORMDatabase::createPrimaryKeyWhereClause($table, $table, $this->values, $this->old_values); + $sql = 'UPDATE %r SET ' . join(', ', $assignments) . ' WHERE '; + array_unshift($params, $sql); - return $sql; + return fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values); } @@ -1215,6 +1256,9 @@ public function delete() ); } + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + fORM::callHookCallbacks( $this, 'pre::delete()', @@ -1226,12 +1270,12 @@ public function delete() $table = fORM::tablize($class); - $inside_db_transaction = fORMDatabase::retrieve()->isInsideTransaction(); + $inside_db_transaction = $db->isInsideTransaction(); try { if (!$inside_db_transaction) { - fORMDatabase::retrieve()->translatedQuery('BEGIN'); + $db->translatedQuery('BEGIN'); } fORM::callHookCallbacks( @@ -1244,8 +1288,8 @@ public function delete() ); // Check to ensure no foreign dependencies prevent deletion - $one_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'one-to-many'); - $many_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'many-to-many'); + $one_to_many_relationships = $schema->getRelationships($table, 'one-to-many'); + $many_to_many_relationships = $schema->getRelationships($table, 'many-to-many'); $relationships = array_merge($one_to_many_relationships, $many_to_many_relationships); $records_sets_to_delete = array(); @@ -1294,8 +1338,10 @@ public function delete() // Delete this record - $sql = 'DELETE FROM ' . $table . ' WHERE ' . fORMDatabase::createPrimaryKeyWhereClause($table, $table, $this->values, $this->old_values); - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $params = array('DELETE FROM %r WHERE ', $table); + $params = fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values); + + $result = call_user_func_array($db->translatedQuery, $params); // Delete related records @@ -1317,7 +1363,7 @@ public function delete() ); if (!$inside_db_transaction) { - fORMDatabase::retrieve()->translatedQuery('COMMIT'); + $db->translatedQuery('COMMIT'); } fORM::callHookCallbacks( @@ -1332,7 +1378,7 @@ public function delete() } catch (fException $e) { if (!$inside_db_transaction) { - fORMDatabase::retrieve()->translatedQuery('ROLLBACK'); + $db->translatedQuery('ROLLBACK'); } fORM::callHookCallbacks( @@ -1382,8 +1428,8 @@ public function delete() // If we just deleted an object that has an auto-incrementing primary key, // lets delete that value from the object since it is no longer valid - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); - if (sizeof($pk_columns) == 1 && fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { + $pk_columns = $schema->getKeys($table, 'primary'); + if (sizeof($pk_columns) == 1 && $schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { $this->values[$pk_columns[0]] = NULL; unset($this->old_values[$pk_columns[0]]); } @@ -1416,8 +1462,9 @@ protected function encode($column, $formatting=NULL) ); } + $schema = fORMSchema::retrieve(); $table = fORM::tablize(get_class($this)); - $column_type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $column_type = $schema->getColumnInfo($table, $column, 'type'); // Ensure the programmer is calling the function properly if ($column_type == 'blob') { @@ -1458,7 +1505,7 @@ protected function encode($column, $formatting=NULL) // Make sure we don't mangle a non-float value if ($column_type == 'float' && is_numeric($value)) { - $column_decimal_places = fORMSchema::retrieve()->getColumnInfo($table, $column, 'decimal_places'); + $column_decimal_places = $schema->getColumnInfo($table, $column, 'decimal_places'); // If the user passed in a formatting value, use it if ($formatting !== NULL && is_numeric($formatting)) { @@ -1500,7 +1547,9 @@ public function exists() return $this->__call('exists', array()); } - $pk_columns = fORMSchema::retrieve()->getKeys(fORM::tablize($class), 'primary'); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); + $pk_columns = $schema->getKeys($table, 'primary'); $exists = FALSE; foreach ($pk_columns as $pk_column) { @@ -1525,20 +1574,28 @@ public function exists() protected function fetchResultFromUniqueKey($values) { $class = get_class($this); + + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + try { if ($values === array_combine(array_keys($values), array_fill(0, sizeof($values), NULL))) { throw new fExpectedException('The values specified for the unique key are all NULL'); } - $table = fORM::tablize($class); - $sql = 'SELECT * FROM ' . $table . ' WHERE '; + $table = fORM::tablize($class); + $params = array('SELECT * FROM %r WHERE ', $table); + $conditions = array(); foreach ($values as $column => $value) { - $conditions[] = $column . fORMDatabase::escapeBySchema($table, $column, $value, '='); + $conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '=', $value); + $params[] = $column; + $params[] = $value; } - $sql .= join(' AND ', $conditions); + + $params[0] .= join(' AND ', $conditions); - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $result = call_user_func_array($db->translatedQuery, $params); $result->tossIfNoRows(); } catch (fExpectedException $e) { @@ -1586,7 +1643,10 @@ protected function inspect($column, $element=NULL) ); } - $info = fORMSchema::retrieve()->getColumnInfo(fORM::tablize(get_class($this)), $column); + $class = get_class($this); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + $info = $schema->getColumnInfo($table, $column); if (!in_array($info['type'], array('varchar', 'char', 'text'))) { unset($info['valid_values']); @@ -1629,7 +1689,9 @@ protected function inspect($column, $element=NULL) */ public function load() { - $class = get_class($this); + $class = get_class($this); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); if (fORM::getActiveRecordMethod($class, 'load')) { return $this->__call('load', array()); @@ -1637,9 +1699,10 @@ public function load() try { $table = fORM::tablize($class); - $sql = 'SELECT * FROM ' . $table . ' WHERE ' . fORMDatabase::createPrimaryKeyWhereClause($table, $table, $this->values, $this->old_values); + $params = array('SELECT * FROM %r WHERE ', $table); + $params = fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values); - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $result = call_user_func_array($db->translatedQuery, $params); $result->tossIfNoRows(); } catch (fExpectedException $e) { @@ -1664,15 +1727,16 @@ public function load() */ protected function loadFromResult($result, $ignore_identity_map=FALSE) { - $class = get_class($this); - $table = fORM::tablize($class); - $row = $result->current(); + $class = get_class($this); + $table = fORM::tablize($class); + $row = $result->current(); - $db = fORMDatabase::retrieve(); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); if (!isset(self::$unescape_map[$class])) { self::$unescape_map[$class] = array(); - $column_info = fORMSchema::retrieve()->getColumnInfo($table); + $column_info = $schema->getColumnInfo($table); foreach ($column_info as $column => $info) { if (in_array($info['type'], array('blob', 'boolean', 'date', 'time', 'timestamp'))) { @@ -1681,7 +1745,7 @@ protected function loadFromResult($result, $ignore_identity_map=FALSE) } } - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); + $pk_columns = $schema->getKeys($table, 'primary'); foreach ($pk_columns as $column) { $value = $row[$column]; if ($value !== NULL && isset(self::$unescape_map[$class][$column])) { @@ -1776,9 +1840,10 @@ public function populate() $this->cache ); - $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); - $column_info = fORMSchema::retrieve()->getColumnInfo($table); + $column_info = $schema->getColumnInfo($table); foreach ($column_info as $column => $info) { if (fRequest::check($column)) { $method = 'set' . fGrammar::camelize($column, TRUE); @@ -1824,7 +1889,11 @@ protected function prepare($column, $formatting=NULL) ); } - $column_info = fORMSchema::retrieve()->getColumnInfo(fORM::tablize(get_class($this)), $column); + $class = get_class($this); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + + $column_info = $schema->getColumnInfo($table, $column); $column_type = $column_info['type']; // Ensure the programmer is calling the function properly @@ -1914,7 +1983,9 @@ public function reflect($include_doc_comments=FALSE) $signatures = array(); $class = get_class($this); - $columns_info = fORMSchema::retrieve()->getColumnInfo(fORM::tablize($class)); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + $columns_info = $schema->getColumnInfo($table); foreach ($columns_info as $column => $column_info) { $camelized_column = fGrammar::camelize($column, TRUE); @@ -2178,9 +2249,10 @@ public function replicate($related_class=NULL) { fActiveRecord::$replicate_level++; - $class = get_class($this); - $hash = self::hash($this->values, $class); - $table = fORM::tablize($class); + $class = get_class($this); + $hash = self::hash($this->values, $class); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); // If the object has not been replicated yet, do it now if (!isset(fActiveRecord::$replicate_map[$class])) { @@ -2190,8 +2262,8 @@ public function replicate($related_class=NULL) fActiveRecord::$replicate_map[$class][$hash] = clone $this; // We need the primary key to get a hash, otherwise certain recursive relationships end up losing members - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); - if (sizeof($pk_columns) == 1 && fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { + $pk_columns = $schema->getKeys($table, 'primary'); + if (sizeof($pk_columns) == 1 && $schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { fActiveRecord::$replicate_map[$class][$hash]->values[$pk_columns[0]] = $this->values[$pk_columns[0]]; } @@ -2201,8 +2273,8 @@ public function replicate($related_class=NULL) $parameters = func_get_args(); $recursive = FALSE; - $many_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'many-to-many'); - $one_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'one-to-many'); + $many_to_many_relationships = $schema->getRelationships($table, 'many-to-many'); + $one_to_many_relationships = $schema->getRelationships($table, 'one-to-many'); // When just TRUE is passed we recursively replicate all related records @@ -2231,7 +2303,7 @@ public function replicate($related_class=NULL) } else { $related_class = fGrammar::singularize($parameter); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table); + $route = fORMSchema::getRouteName($schema, $table, $related_table); } // Determine the kind of relationship @@ -2286,8 +2358,8 @@ public function replicate($related_class=NULL) // This removes the primary keys we had added back in for proper duplicate detection foreach (fActiveRecord::$replicate_map as $class => $records) { $table = fORM::tablize($class); - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); - if (sizeof($pk_columns) != 1 || !fORMSchema::retrieve()->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { + $pk_columns = $schema->getKeys($table, 'primary'); + if (sizeof($pk_columns) != 1 || !$schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) { continue; } foreach ($records as $hash => $record) { @@ -2327,8 +2399,9 @@ protected function set($column, $value) // Float and int columns that look like numbers with commas will have the commas removed if (is_string($value)) { - $table = fORM::tablize($class); - $type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + $type = $schema->getColumnInfo($table, $column, 'type'); if (in_array($type, array('integer', 'float')) && preg_match('#^(\d+,)+\d+(\.\d+)?$#', $value)) { $value = str_replace(',', '', $value); } @@ -2367,25 +2440,28 @@ public function store() $this->cache ); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + try { - $table = fORM::tablize($class); - $column_info = fORMSchema::retrieve()->getColumnInfo($table); + $table = fORM::tablize($class); // New auto-incrementing records require lots of special stuff, so we'll detect them here $new_autoincrementing_record = FALSE; if (!$this->exists()) { - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); + $pk_columns = $schema->getKeys($table, 'primary'); + $pk_column = $pk_columns[0]; + $pk_auto_incrementing = $schema->getColumnInfo($table, $pk_column, 'auto_increment'); - if (sizeof($pk_columns) == 1 && $column_info[$pk_columns[0]]['auto_increment'] && !$this->values[$pk_columns[0]]) { + if (sizeof($pk_columns) == 1 && $pk_auto_incrementing && !$this->values[$pk_column]) { $new_autoincrementing_record = TRUE; - $pk_column = $pk_columns[0]; } } - $inside_db_transaction = fORMDatabase::retrieve()->isInsideTransaction(); + $inside_db_transaction = $db->isInsideTransaction(); if (!$inside_db_transaction) { - fORMDatabase::retrieve()->translatedQuery('BEGIN'); + $db->translatedQuery('BEGIN'); } fORM::callHookCallbacks( @@ -2410,23 +2486,12 @@ public function store() // Storing main table - $sql_values = array(); - foreach ($column_info as $column => $info) { - $value = fORM::scalarize($class, $column, $this->values[$column]); - $sql_values[$column] = fORMDatabase::escapeBySchema($table, $column, $value); - } - - // Most databases don't like the auto incrementing primary key to be set to NULL - if ($new_autoincrementing_record && $sql_values[$pk_column] == 'NULL') { - unset($sql_values[$pk_column]); - } - if (!$this->exists()) { - $sql = $this->constructInsertSQL($sql_values); + $params = $this->constructInsertParams($new_autoincrementing_record, $pk_column); } else { - $sql = $this->constructUpdateSQL($sql_values); + $params = $this->constructUpdateParams(); } - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $result = call_user_func_array($db->translatedQuery, $params); // If there is an auto-incrementing primary key, grab the value from the database @@ -2449,7 +2514,7 @@ public function store() ); if (!$inside_db_transaction) { - fORMDatabase::retrieve()->translatedQuery('COMMIT'); + $db->translatedQuery('COMMIT'); } fORM::callHookCallbacks( @@ -2464,7 +2529,7 @@ public function store() } catch (fException $e) { if (!$inside_db_transaction) { - fORMDatabase::retrieve()->translatedQuery('ROLLBACK'); + $db->translatedQuery('ROLLBACK'); } fORM::callHookCallbacks( diff --git a/classes/fDatabase.php b/classes/fDatabase.php index d154926b..8dea7e74 100644 --- a/classes/fDatabase.php +++ b/classes/fDatabase.php @@ -46,7 +46,8 @@ * @package Flourish * @link http://flourishlib.com/fDatabase * - * @version 1.0.0b18 + * @version 1.0.0b19 + * @changes 1.0.0b19 Added support for escaping identifiers (column and table names) to ::escape(), added support for database schemas, rewrote internal SQL string spliting [wb, 2009-10-22] * @changes 1.0.0b18 Updated the class for the new fResult and fUnbufferedResult APIs, fixed ::unescape() to not touch NULLs [wb, 2009-08-12] * @changes 1.0.0b17 Added the ability to pass an array of all values as a single parameter to ::escape() instead of one value per parameter [wb, 2009-08-11] * @changes 1.0.0b16 Fixed PostgreSQL and Oracle from trying to get auto-incrementing values on inserts when explicit values were given [wb, 2009-08-06] @@ -534,6 +535,7 @@ private function connectToDatabase() $this->determineCharacterSet(); } $this->query('SET TEXTSIZE 65536'); + $this->query('SET QUOTED_IDENTIFIER ON'); } // Make PostgreSQL use UTF-8 @@ -784,6 +786,7 @@ public function enableSlowQueryWarnings($threshold) * - `'boolean'` * - `'date'` * - `'float'` + * - `'identifier'` * - `'integer'` * - `'string'` (also varchar, char or text) * - `'varchar'` @@ -799,6 +802,7 @@ public function enableSlowQueryWarnings($threshold) * - `%b` for a boolean * - `%d` for a date * - `%f` for a float + * - `%r` for an indentifier (table or column name) * - `%i` for an integer * - `%s` for a string * - `%t` for a time @@ -868,6 +872,10 @@ public function escape($sql_or_type, $value) case '%f': $callback = $this->escapeFloat; break; + case 'identifier': + case '%r': + $callback = $this->escapeIdentifier; + break; case 'integer': case '%i': $callback = $this->escapeInteger; @@ -906,100 +914,30 @@ public function escape($sql_or_type, $value) } // Separate the SQL from quoted values - preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql_or_type, $matches); + $parts = $this->splitSQL($sql_or_type); $temp_sql = ''; $strings = array(); - // Replace strings with a placeholder so they don't mess use the regex parsing - foreach ($matches[0] as $match) { - if ($match[0] == "'") { - $strings[] = $match; - $match = ':string_' . (sizeof($strings)-1); + // Replace strings with a placeholder so they don't mess up the regex parsing + foreach ($parts as $part) { + if ($part[0] == "'") { + $strings[] = $part; + $part = ':string_' . (sizeof($strings)-1); } - $temp_sql .= $match; + $temp_sql .= $part; } - $pieces = preg_split('#(%[lbdfistp])\b#', $temp_sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); - $sql = ''; - // If the values were passed as a single array, this handles that - if (count($values) == 0 && is_array($value)) { - $placeholders = 0; - foreach ($pieces as $piece) { - if (strlen($piece) == 2 && $piece[0] == '%') { - $placeholders++; - } - } - - if ($placeholders == count($value)) { - $values = $value; - $value = array_shift($values); - } - } - - $missing_values = -1; - - foreach ($pieces as $piece) { - switch ($piece) { - case '%l': - $callback = $this->escapeBlob; - break; - case '%b': - $callback = $this->escapeBoolean; - break; - case '%d': - $callback = $this->escapeDate; - break; - case '%f': - $callback = $this->escapeFloat; - break; - case '%i': - $callback = $this->escapeInteger; - break; - case '%s': - $callback = $this->escapeString; - break; - case '%t': - $callback = $this->escapeTime; - break; - case '%p': - $callback = $this->escapeTimestamp; - break; - default: - $sql .= $piece; - continue 2; - } - - if (is_array($value)) { - $sql .= join(', ', array_map($callback, $value)); - } else { - $sql .= call_user_func($callback, $value); - } - - if (sizeof($values)) { - $value = array_shift($values); - } else { - $value = NULL; - $missing_values++; - } + $placeholders = preg_match_all('#%[lbdfristp]\b#', $temp_sql, $trash); + if (count($values) == 0 && is_array($value) && count($value) == $placeholders) { + $values = $value; + $value = array_shift($values); } - if ($missing_values > 0) { - throw new fProgrammerException( - '%1$s value(s) are missing for the placeholders in: %2$s', - $missing_values, - $sql_or_type - ); - } + array_unshift($values, $value); - if (sizeof($values)) { - throw new fProgrammerException( - '%1$s extra value(s) were passed for the placeholders in: %2$s', - sizeof($values), - $sql_or_type - ); - } + $sql = $this->escapeSQL($temp_sql, $values); $string_number = 0; foreach ($strings as $string) { @@ -1123,6 +1061,26 @@ private function escapeFloat($value) } + /** + * Escapes an identifier for use in SQL, necessary for reserved words + * + * @param string $value The identifier to escape + * @return string The escaped identifier + */ + private function escapeIdentifier($value) + { + $value = '"' . str_replace( + array('"', '.'), + array('', '"."'), + $value + ) . '"'; + if ($this->type == 'oracle') { + $value = strtoupper($value); + } + return $value; + } + + /** * Escapes an integer for use in SQL * @@ -1139,10 +1097,10 @@ private function escapeInteger($value) if (!strlen($value)) { return 'NULL'; } - if (!preg_match('#^[+\-]?[0-9]+$#D', $value)) { + if (!preg_match('#^([+\-]?[0-9]+)(\.[0-9]*)?$#D', $value, $matches)) { return 'NULL'; } - return (string) $value; + return str_replace('+', '', $matches[1]); } @@ -1242,6 +1200,91 @@ private function escapeString($value) } + /** + * Takes a SQL string and an array of values and replaces the placeholders with the value + * + * @param string $sql The SQL string containing placeholders + * @param array $values An array of values to escape into the SQL + * @return string The SQL with the values escaped into it + */ + private function escapeSQL($sql, $values) + { + $original_sql = $sql; + $pieces = preg_split('#(%[lbdfristp])\b#', $sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY); + + $sql = ''; + $value = array_shift($values); + + $missing_values = -1; + + foreach ($pieces as $piece) { + switch ($piece) { + case '%l': + $callback = $this->escapeBlob; + break; + case '%b': + $callback = $this->escapeBoolean; + break; + case '%d': + $callback = $this->escapeDate; + break; + case '%f': + $callback = $this->escapeFloat; + break; + case '%r': + $callback = $this->escapeIdentifier; + break; + case '%i': + $callback = $this->escapeInteger; + break; + case '%s': + $callback = $this->escapeString; + break; + case '%t': + $callback = $this->escapeTime; + break; + case '%p': + $callback = $this->escapeTimestamp; + break; + default: + $sql .= $piece; + continue 2; + } + + if (is_array($value)) { + $sql .= join(', ', array_map($callback, $value)); + } else { + $sql .= call_user_func($callback, $value); + } + + if (sizeof($values)) { + $value = array_shift($values); + } else { + $value = NULL; + $missing_values++; + } + } + + if ($missing_values > 0) { + throw new fProgrammerException( + '%1$s value(s) are missing for the placeholders in: %2$s', + $missing_values, + $original_sql + ); + } + + if (sizeof($values)) { + throw new fProgrammerException( + '%1$s extra value(s) were passed for the placeholders in: %2$s', + sizeof($values), + $original_sql + ); + } + + return $sql; + } + + /** * Escapes a time for use in SQL, includes surrounding quotes * @@ -1599,31 +1642,43 @@ public function getUsername() */ private function handleAutoIncrementedValue($result) { - if (!preg_match('#^\s*INSERT\s+INTO\s+(?:`|"|\[)?(\w+)(?:`|"|\])?#i', $result->getSQL(), $table_match)) { + if (!preg_match('#^\s*INSERT\s+INTO\s+(?:`|"|\[)?(["\w.]+)(?:`|"|\])?#i', $result->getSQL(), $table_match)) { $result->setAutoIncrementedValue(NULL); return; } - $table = strtolower($table_match[1]); + $quoted_table = $table_match[1]; + $table = str_replace('"', '', strtolower($table_match[1])); $insert_id = NULL; if ($this->type == 'oracle') { if (!isset($this->schema_info['sequences'])) { $sql = "SELECT - TABLE_NAME, + LOWER(OWNER) AS \"SCHEMA\", + LOWER(TABLE_NAME) AS \"TABLE\", TRIGGER_BODY FROM - USER_TRIGGERS + ALL_TRIGGERS WHERE TRIGGERING_EVENT = 'INSERT' AND STATUS = 'ENABLED' AND - TRIGGER_NAME NOT LIKE 'BIN\$%'"; + TRIGGER_NAME NOT LIKE 'BIN\$%' AND + OWNER IN (SELECT + username + FROM + dba_users + WHERE + default_tablespace NOT IN ('SYSTEM', 'SYSAUX'))"; $this->schema_info['sequences'] = array(); foreach ($this->query($sql) as $row) { - if (preg_match('#SELECT\s+(\w+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) { - $this->schema_info['sequences'][strtolower($row['table_name'])] = array('sequence' => $matches[1], 'column' => $matches[2]); + if (preg_match('#SELECT\s+(["\w.]+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) { + $table_name = $row['table']; + if ($row['schema'] != strtolower($this->username)) { + $table_name = $row['schema'] . '.' . $table_name; + } + $this->schema_info['sequences'][$table_name] = array('sequence' => $matches[1], 'column' => str_replace('"', '', $matches[2])); } } @@ -1632,7 +1687,7 @@ private function handleAutoIncrementedValue($result) } } - if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '\b#i', $result->getSQL())) { + if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($quoted_table, '#') . '\s+\([^\)]*?(\b|")' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '(\b|")#i', $result->getSQL())) { return; } @@ -1642,11 +1697,13 @@ private function handleAutoIncrementedValue($result) if ($this->type == 'postgresql') { if (!isset($this->schema_info['sequences'])) { $sql = "SELECT - pg_class.relname AS table_name, + pg_namespace.nspname AS \"schema\", + pg_class.relname AS \"table\", pg_attribute.attname AS column FROM pg_attribute INNER JOIN pg_class ON pg_attribute.attrelid = pg_class.oid INNER JOIN + pg_namespace ON pg_class.relnamespace = pg_namespace.oid INNER JOIN pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND pg_attribute.attnum = pg_attrdef.adnum WHERE NOT pg_attribute.attisdropped AND @@ -1655,7 +1712,11 @@ private function handleAutoIncrementedValue($result) $this->schema_info['sequences'] = array(); foreach ($this->query($sql) as $row) { - $this->schema_info['sequences'][strtolower($row['table_name'])] = $row['column']; + $table_name = strtolower($row['table']); + if ($row['schema'] != 'public') { + $table_name = $row['schema'] . '.' . $table_name; + } + $this->schema_info['sequences'][$table_name] = $row['column']; } if ($this->cache) { @@ -1663,7 +1724,7 @@ private function handleAutoIncrementedValue($result) } } - if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table], '#') . '\b#i', $result->getSQL())) { + if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($quoted_table, '#') . '\s+\([^\)]*?(\b|")' . preg_quote($this->schema_info['sequences'][$table], '#') . '(\b|")#i', $result->getSQL())) { return; } } @@ -1978,13 +2039,6 @@ private function prepareSQL($sql, $values, $translate) throw new fProgrammerException('No SQL statement passed'); } - if ($values) { - $sql = call_user_func_array( - $this->escape, - array_merge(array($sql), $values) - ); - } - // Fix \' in MySQL and PostgreSQL if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql, '\\') !== FALSE) { $sql = preg_replace("#(?splitSQL($sql); + + foreach ($parts as $part) { + // We split out all strings except for empty ones because Oracle + // has to translate empty strings to NULL + if ($part[0] == "'" && $part != "''") { $queries[$number] .= ':string_' . sizeof($strings[$number]); - $strings[$number][] = $match; + $strings[$number][] = $part; } else { - $split_queries = preg_split('#(?escapeIdentifier, $value)); + } else { + $new_sql .= $this->escapeIdentifier($value); + } + $value_number++; + + // Other placeholder/value combos just get added + } else { + $placeholders++; + $value_number++; + $new_sql .= $piece; + $chunked_values[$number][] = $value; + } + + // A piece of SQL + } else { + $new_sql .= $piece; + } + } + + $queries[$number] = $new_sql; + } + + // Translate the SQL queries if ($translate) { - $output = $this->getSQLTranslation()->translate($queries, $strings); + $queries = $this->getSQLTranslation()->translate($queries); + } + + $output = array(); + foreach (array_keys($queries) as $number => $key) { + $query = $queries[$key]; - // For untranslated queries we need to unescape and reinsert strings - } else { - $output = array(); - foreach ($queries as $number => $query) { - // Unescape literal semicolons in the queries - $query = preg_replace('#(?escapeSQL($query, $chunked_values[$number]); + } + + // Unescape literal semicolons in the queries + $query = preg_replace('#(? $string) { $string = strtr($string, array('\\' => '\\\\', '$' => '\\$')); $query = preg_replace('#:string_' . $index . '\b#', $string, $query, 1); } - $output[] = $query; - } - } + } + + $output[$key] = $query; + } return $output; } @@ -2221,6 +2339,51 @@ private function setReturnedRows($result) } + /** + * Splits SQL into pieces of SQL and quoted strings + * + * @param string $sql The SQL to split + * @return array The pieces + */ + private function splitSQL($sql) + { + $parts = array(); + $temp_sql = $sql; + $start_pos = 0; + $inside_string = FALSE; + do { + $pos = strpos($temp_sql, "'", $start_pos); + if ($pos !== FALSE) { + if (!$inside_string) { + $parts[] = substr($temp_sql, 0, $pos); + $temp_sql = substr($temp_sql, $pos); + $start_pos = 1; + $inside_string = TRUE; + + } elseif ($pos == strlen($temp_sql)) { + $parts[] = $temp_sql; + $temp_sql = ''; + $pos = FALSE; + + } elseif (strlen($temp_sql) > $pos+1 && $temp_sql[$pos+1] == "'") { + $start_pos = $pos+2; + + } else { + $parts[] = substr($temp_sql, 0, $pos+1); + $temp_sql = substr($temp_sql, $pos+1); + $start_pos = 0; + $inside_string = FALSE; + } + } + } while ($pos !== FALSE); + if ($temp_sql) { + $parts[] = $temp_sql; + } + + return $parts; + } + + /** * Translates the SQL statement using fSQLTranslation and executes it * @@ -2522,4 +2685,4 @@ private function unescapeTimestamp($value) * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */ \ No newline at end of file + */ diff --git a/classes/fORMColumn.php b/classes/fORMColumn.php index b70acf7e..7e77cb0e 100644 --- a/classes/fORMColumn.php +++ b/classes/fORMColumn.php @@ -9,7 +9,8 @@ * @package Flourish * @link http://flourishlib.com/fORMColumn * - * @version 1.0.0b5 + * @version 1.0.0b6 + * @changes 1.0.0b6 Changed SQL statements to use value placeholders, identifier escaping and schema support [wb, 2009-10-22] * @changes 1.0.0b5 Updated to use new fORM::registerInspectCallback() method [wb, 2009-07-13] * @changes 1.0.0b4 Updated code for new fORM API [wb, 2009-06-15] * @changes 1.0.0b3 Updated code to use new fValidationException::formatField() method [wb, 2009-06-04] @@ -99,7 +100,8 @@ static public function configureEmailColumn($class, $column) { $class = fORM::getClass($class); $table = fORM::tablize($class); - $data_type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $schema = fORMSchema::retrieve(); + $data_type = $schema->getColumnInfo($table, $column, 'type'); $valid_data_types = array('varchar', 'char', 'text'); if (!in_array($data_type, $valid_data_types)) { @@ -136,7 +138,8 @@ static public function configureLinkColumn($class, $column) { $class = fORM::getClass($class); $table = fORM::tablize($class); - $data_type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $schema = fORMSchema::retrieve(); + $data_type = $schema->getColumnInfo($table, $column, 'type'); $valid_data_types = array('varchar', 'char', 'text'); if (!in_array($data_type, $valid_data_types)) { @@ -180,7 +183,8 @@ static public function configureNumberColumn($class, $column) { $class = fORM::getClass($class); $table = fORM::tablize($class); - $data_type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $schema = fORMSchema::retrieve(); + $data_type = $schema->getColumnInfo($table, $column, 'type'); $valid_data_types = array('integer', 'float'); if (!in_array($data_type, $valid_data_types)) { @@ -231,7 +235,8 @@ static public function configureRandomColumn($class, $column, $type, $length) { $class = fORM::getClass($class); $table = fORM::tablize($class); - $data_type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $schema = fORMSchema::retrieve(); + $data_type = $schema->getColumnInfo($table, $column, 'type'); $valid_data_types = array('varchar', 'char', 'text'); if (!in_array($data_type, $valid_data_types)) { @@ -298,7 +303,9 @@ static public function encodeNumberColumn($object, &$values, &$old_values, &$rel list ($action, $column) = fORM::parseMethod($method_name); $class = get_class($object); - $column_info = fORMSchema::retrieve()->getColumnInfo(fORM::tablize($class), $column); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); + $column_info = $schema->getColumnInfo($table, $column); $value = $values[$column]; if ($value instanceof fNumber) { @@ -332,24 +339,24 @@ static public function generate($object, &$values, &$old_values, &$related_recor { list ($action, $column) = fORM::parseMethod($method_name); - $class = get_class($object); - $table = fORM::tablize($class); + $class = get_class($object); + $table = fORM::tablize($class); + + $schema = fORMSchema::retrieve(); + $db = fORMDatabase::retrieve(); $settings = self::$random_columns[$class][$column]; // Check to see if this is a unique column - $unique_keys = fORMSchema::retrieve()->getKeys($table, 'unique'); + $unique_keys = $schema->getKeys($table, 'unique'); $is_unique_column = FALSE; foreach ($unique_keys as $unique_key) { if ($unique_key == array($column)) { $is_unique_column = TRUE; + $sql = "SELECT %r FROM %r WHERE %r = %s"; do { $value = fCryptography::randomString($settings['length'], $settings['type']); - - // See if this is unique - $sql = "SELECT " . $column . " FROM " . $table . " WHERE " . $column . " = " . fORMDatabase::retrieve()->escape('string', $value); - - } while (fORMDatabase::retrieve()->query($sql)->countReturnedRows()); + } while ($db->query($sql, $column, $table, $column, $value)->countReturnedRows()); } } @@ -472,7 +479,9 @@ static public function prepareNumberColumn($object, &$values, &$old_values, &$re list ($action, $column) = fORM::parseMethod($method_name); $class = get_class($object); - $column_info = fORMSchema::retrieve()->getColumnInfo(fORM::tablize($class), $column); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + $column_info = $schema->getColumnInfo($table, $column); $value = $values[$column]; if ($value instanceof fNumber) { @@ -527,11 +536,12 @@ static public function reflect($class, &$signatures, $include_doc_comments) if (isset(self::$number_columns[$class])) { - $table = fORM::tablize($class); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); foreach(self::$number_columns[$class] as $column => $enabled) { $camelized_column = fGrammar::camelize($column, TRUE); - $type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $type = $schema->getColumnInfo($table, $column, 'type'); // Get and set methods $signature = ''; diff --git a/classes/fORMDatabase.php b/classes/fORMDatabase.php index 6ef345ea..436b7e96 100644 --- a/classes/fORMDatabase.php +++ b/classes/fORMDatabase.php @@ -10,7 +10,8 @@ * @package Flourish * @link http://flourishlib.com/fORMDatabase * - * @version 1.0.0b15 + * @version 1.0.0b16 + * @changes 1.0.0b16 Internal Backwards Compatibility Break - Renamed methods and significantly changed parameters and functionality for SQL statements to use value placeholders, identifier escaping and to handle schemas [wb, 2009-10-22] * @changes 1.0.0b15 Streamlined intersection operator SQL and added support for the second value being NULL [wb, 2009-09-21] * @changes 1.0.0b14 Added support for the intersection operator `><` to ::createWhereClause() [wb, 2009-07-13] * @changes 1.0.0b13 Added support for the `AND LIKE` operator `&~` to ::createWhereClause() [wb, 2009-07-09] @@ -30,17 +31,14 @@ class fORMDatabase { // The following constants allow for nice looking callbacks to static methods - const addTableToKeys = 'fORMDatabase::addTableToKeys'; - const addTableToValues = 'fORMDatabase::addTableToValues'; + const addHavingClause = 'fORMDatabase::addHavingClause'; + const addOrderByClause = 'fORMDatabase::addOrderByClause'; + const addPrimaryKeyWhereParams = 'fORMDatabase::addPrimaryKeyWhereParams'; + const addWhereClause = 'fORMDatabase::addWhereClause'; const attach = 'fORMDatabase::attach'; - const createFromClauseFromJoins = 'fORMDatabase::createFromClauseFromJoins'; - const createHavingClause = 'fORMDatabase::createHavingClause'; - const createOrderByClause = 'fORMDatabase::createOrderByClause'; - const createPrimaryKeyWhereClause = 'fORMDatabase::createPrimaryKeyWhereClause'; - const createWhereClause = 'fORMDatabase::createWhereClause'; - const escapeBySchema = 'fORMDatabase::escapeBySchema'; - const escapeByType = 'fORMDatabase::escapeByType'; - const insertFromAndGroupByClauses = 'fORMDatabase::insertFromAndGroupByClauses'; + const injectFromAndGroupByClauses = 'fORMDatabase::injectFromAndGroupByClauses'; + const makeCondition = 'fORMDatabase::makeCondition'; + const parseSearchTerms = 'fORMDatabase::parseSearchTerms'; const reset = 'fORMDatabase::reset'; const retrieve = 'fORMDatabase::retrieve'; const splitHavingConditions = 'fORMDatabase::splitHavingConditions'; @@ -54,52 +52,6 @@ class fORMDatabase static private $database_object = NULL; - /** - * Prepends the table to the keys of the array - * - * @internal - * - * @param string $table The table to prepend - * @param array $array The array to modify - * @return array The modified array - */ - static public function addTableToKeys($table, $array) - { - $modified_array = array(); - foreach ($array as $key => $value) { - if (preg_match('#^\w+$#D', $key)) { - $modified_array[$table . '.' . $key] = $value; - } else { - $modified_array[$key] = $value; - } - } - return $modified_array; - } - - - /** - * Prepends the table to the values of the array - * - * @internal - * - * @param string $table The table to prepend - * @param array $array The array to modify - * @return array The modified array - */ - static public function addTableToValues($table, $array) - { - $modified_array = array(); - foreach ($array as $key => $value) { - if (preg_match('#^\w+$#D', $value)) { - $modified_array[$key] = $table . '.' . $value; - } else { - $modified_array[$key] = $value; - } - } - return $modified_array; - } - - /** * Allows attaching an fDatabase-compatible object as the database singleton for ORM code * @@ -115,73 +67,109 @@ static public function attach($database) /** * Translated the where condition for a single column into a SQL clause * - * @param string $table The table to create the condition for - * @param string $column The column to store the value in, may also be shorthand column name like `table.column` or `table=>related_table.column` - * @param mixed $values The value(s) to escape - * @param string $operator Should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'` - * @return string The SQL clause for the column, values and operator specified + * @param fDatabase $db The database the query will be run on + * @param fSchema $schema The schema for the database + * @param array $params The parameters for the fDatabase::query() call + * @param string $table The table to create the condition for + * @param string $column The column to store the value in, may also be shorthand column name like `table.column` or `table=>related_table.column` + * @param string $operator Should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'` + * @param mixed $values The value(s) to escape + * @param string $escaped_column The escaped column to use in the SQL + * @param string $placeholder This allows overriding the placeholder + * @return array The modified parameters */ - static private function createColumnCondition($table, $column, $values, $operator) + static private function addColumnCondition($db, $schema, $params, $table, $column, $operator, $values, $escaped_column=NULL, $placeholder=NULL) { - settype($values, 'array'); + // Some objects when cast to an array turn the members into array keys + if (!is_object($values)) { + settype($values, 'array'); + } else { + $values = array($values); + } + // Make sure we have an array with something in it to compare to + if (!$values) { $values = array(NULL); } + + // If the table and column specified are real and not some combination + $real_column = $escaped_column === NULL && $placeholder === NULL; + + if ($escaped_column === NULL) { + $escaped_column = $db->escape('%r', (strpos($column, '.') === FALSE) ? $table . '.' . $column : $column); + } + + list($table, $column) = self::getTableAndColumn($schema, $table, $column); + + if ($placeholder === NULL) { + $placeholder = $schema->getColumnInfo($table, $column, 'placeholder'); + } // More than one value if (sizeof($values) > 1) { switch ($operator) { case '=': - $condition = array(); + $non_null_values = array(); $has_null = FALSE; foreach ($values as $value) { if ($value === NULL) { $has_null = TRUE; continue; } - $condition[] = self::escapeBySchema($table, $column, $value); + $non_null_values[] = $value; + } + if ($has_null) { + $params[0] .= '(' . $escaped_column . ' IS NULL OR '; } - $sql = $column . ' IN (' . join(', ', $condition) . ')'; + $params[0] .= $escaped_column . ' IN (' . $placeholder . ')'; + $params[] = $non_null_values; if ($has_null) { - $sql = '(' . $column . ' IS NULL OR ' . $sql . ')'; + $params[0] .= ')'; } break; case '!': - $condition = array(); + $non_null_values = array(); $has_null = FALSE; foreach ($values as $value) { if ($value === NULL) { $has_null = TRUE; continue; } - $condition[] = self::escapeBySchema($table, $column, $value); + $non_null_values[] = $value; + } + if ($has_null) { + $params[0] .= '(' . $escaped_column . ' IS NOT NULL AND '; } - $sql = $column . ' NOT IN (' . join(', ', $condition) . ')'; + $params[0] .= $escaped_column . ' NOT IN (' . $placeholder . ')'; + $params[] = $non_null_values; if ($has_null) { - $sql = '(' . $column . ' IS NOT NULL AND ' . $sql . ')'; + $params[0] .= ')'; } break; case '~': $condition = array(); foreach ($values as $value) { - $condition[] = $column . self::retrieve()->escape(' LIKE %s', '%' . $value . '%'); + $condition[] = $escaped_column . ' LIKE %s'; + $params[] = '%' . $value . '%'; } - $sql = '(' . join(' OR ', $condition) . ')'; + $params[0] .= '(' . join(' OR ', $condition) . ')'; break; case '&~': $condition = array(); foreach ($values as $value) { - $condition[] = $column . self::retrieve()->escape(' LIKE %s', '%' . $value . '%'); + $condition[] = $escaped_column . ' LIKE %s'; + $params[] = '%' . $value . '%'; } - $sql = '(' . join(' AND ', $condition) . ')'; + $params[0] .= '(' . join(' AND ', $condition) . ')'; break; case '!~': $condition = array(); foreach ($values as $value) { - $condition[] = $column . self::retrieve()->escape(' NOT LIKE %s', '%' . $value . '%'); + $condition[] = $escaped_column . ' NOT LIKE %s'; + $params[] = '%' . $value . '%'; } - $sql = '(' . join(' AND ', $condition) . ')'; + $params[0] .= '(' . join(' AND ', $condition) . ')'; break; default: @@ -199,30 +187,46 @@ static private function createColumnCondition($table, $column, $values, $operato } else { $value = current($values); } - + switch ($operator) { case '=': - case '<': - case '<=': - case '>': - case '>=': - $sql = $column . self::escapeBySchema($table, $column, $value, $operator); + if ($value === NULL) { + $operator = 'IS'; + } + $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder; + $params[] = $value; break; case '!': + $operator = '<>'; if ($value !== NULL) { - $sql = '(' . $column . self::escapeBySchema($table, $column, $value, '<>') . ' OR ' . $column . ' IS NULL)'; + $params[0] .= '('; } else { - $sql = $column . self::escapeBySchema($table, $column, $value, '<>'); + $operator = 'IS NOT'; + } + $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder; + $params[] = $value; + if ($value !== NULL) { + $params[0] .= ' OR ' . $escaped_column . ' IS NULL)'; } break; + case '<': + case '<=': + case '>': + case '>=': + $params[0] .= $escaped_column . ' ' . $operator . ' ' . $placeholder; + $params[] = $value; + break; + case '~': - $sql = $column . self::retrieve()->escape(' LIKE %s', '%' . $value . '%'); + $params[0] .= $escaped_column . ' LIKE %s'; + $params[] = '%' . $value . '%'; break; case '!~': - $sql = $column . self::retrieve()->escape(' NOT LIKE %s', '%' . $value . '%'); + $params[0] .= $escaped_column . ' NOT LIKE %s'; + $params[] = '%' . $value . '%'; break; default: @@ -234,48 +238,7 @@ static private function createColumnCondition($table, $column, $values, $operato } } - return $sql; - } - - - /** - * Creates a `FROM` clause from a join array - * - * @internal - * - * @param array $joins The joins to create the `FROM` clause out of - * @return string The from clause (does not include the word `'FROM'`) - */ - static public function createFromClauseFromJoins($joins) - { - $sql = ''; - - foreach ($joins as $join) { - // Here we handle the first table in a join - if ($join['join_type'] == 'none') { - $sql .= $join['table_name']; - if ($join['table_alias'] != $join['table_name']) { - $sql .= ' AS ' . $join['table_alias']; - } - - // Here we handle all other joins - } else { - $sql .= ' ' . strtoupper($join['join_type']) . ' ' . $join['table_name']; - if ($join['table_alias'] != $join['table_name']) { - $sql .= ' AS ' . $join['table_alias']; - } - if (isset($join['on_clause_type'])) { - if ($join['on_clause_type'] == 'simple_equation') { - $sql .= ' ON ' . $join['on_clause_fields'][0] . ' = ' . $join['on_clause_fields'][1]; - - } else { - $sql .= ' ON ' . $join['on_clause']; - } - } - } - } - - return $sql; + return $params; } @@ -283,15 +246,23 @@ static public function createFromClauseFromJoins($joins) * Creates a `HAVING` clause from an array of conditions * * @internal - * - * @param array $conditions The array of conditions - see fRecordSet::build() for format - * @return string The SQL `HAVING` clause + * + * @param fDatabase $db The database the query will be executed on + * @param fSchema $schema The schema for the database + * @param array $params The params for the fDatabase::query() call + * @param string $table The table the query is being executed on + * @param array $conditions The array of conditions - see fRecordSet::build() for format + * @return array The params with the `HAVING` clause added */ - static public function createHavingClause($conditions) + static public function addHavingClause($db, $schema, $params, $table, $conditions) { - $sql = array(); - + $i = 0; foreach ($conditions as $expression => $value) { + if ($i) { + $params[0] .= ' AND '; + } + + // Splits the operator off of the end of the expression if (in_array(substr($expression, -2), array('<=', '>=', '!=', '<>'))) { $operator = strtr( substr($expression, -2), @@ -306,180 +277,41 @@ static public function createHavingClause($conditions) $expression = substr($expression, 0, -1); } - if (is_object($value)) { - if (is_callable(array($value, '__toString'))) { - $value = $value->__toString(); - } else { - $value = (string) $value; - } - } + // Quotes the identifier in the expression + preg_match('#^([^(]+\()\s*([^\s]+)\s*(\))$#D', $expression, $parts); + $expression = $parts[1] . $db->escape('%r', $parts[2]) . $parts[3]; - if (is_array($value)) { - - switch ($operator) { - case '=': - $condition = array(); - foreach ($values as $value) { - $condition[] = self::escapeByType($value); - } - $sql[] = $expression . ' IN (' . join(', ', $condition) . ')'; - break; - - case '!': - $condition = array(); - foreach ($values as $value) { - $condition[] = self::escapeByType($value); - } - $sql[] = $expression . ' NOT IN (' . join(', ', $condition) . ')'; - break; - - default: - throw new fProgrammerException( - 'An invalid array comparison operator, %s, was specified', - $operator - ); - break; - } - - } else { - - if (!in_array($operator, array('=', '!', '~', '<', '<=', '>', '>='))) { - throw new fProgrammerException( - 'An invalid comparison operator, %s, was specified', - $operator - ); - } - - $sql[] = $expression . self::escapeByType($value, $operator); - } - } - - return join(' AND ', $sql); - } - - - /** - * Creates join information for the table shortcut provided - * - * @internal - * - * @param string $table The primary table - * @param string $table_alias The primary table alias - * @param string $related_table The related table - * @param string $route The route to the related table - * @param array &$joins The names of the joins that have been created - * @param array &$used_aliases The aliases that have been used - * @return string The name of the significant join created - */ - static private function createJoin($table, $table_alias, $related_table, $route, &$joins, &$used_aliases) - { - $routes = fORMSchema::getRoutes($table, $related_table); - - if (!isset($routes[$route])) { - throw new fProgrammerException( - 'An invalid route, %1$s, was specified for the relationship from %2$s to %3$s', - $route, - $table, - $related_table - ); - } - - if (isset($joins[$table . '_' . $related_table . '{' . $route . '}'])) { - return $table . '_' . $related_table . '{' . $route . '}'; - } - - // If the route uses a join table - if (isset($routes[$route]['join_table'])) { - $join = array( - 'join_type' => 'LEFT JOIN', - 'table_name' => $routes[$route]['join_table'], - 'table_alias' => self::createNewAlias($routes[$route]['join_table'], $used_aliases), - 'on_clause_type' => 'simple_equation', - 'on_clause_fields' => array() - ); - - $join2 = array( - 'join_type' => 'LEFT JOIN', - 'table_name' => $related_table, - 'table_alias' => self::createNewAlias($related_table, $used_aliases), - 'on_clause_type' => 'simple_equation', - 'on_clause_fields' => array() - ); - - if ($table != $related_table) { - $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column']; - $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column']; - $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column']; - $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column']; - } else { - $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column']; - $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column']; - $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column']; - $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column']; - } - - $joins[$table . '_' . $related_table . '{' . $route . '}_join'] = $join; - $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join2; - - // If the route is a direct join - } else { + // The AVG, SUM and COUNT functions all return a number + $function = strtolower(substr($parts[2], 0, -1)); + $placeholder = (in_array($function, array('avg', 'sum', 'count'))) ? '%f' : NULL; - $join = array( - 'join_type' => 'LEFT JOIN', - 'table_name' => $related_table, - 'table_alias' => self::createNewAlias($related_table, $used_aliases), - 'on_clause_type' => 'simple_equation', - 'on_clause_fields' => array() - ); + // This removes stray quoting inside of {route} specified for shorthand column names + $expression = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $expression); - $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column']; - $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['related_column']; - - $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join; - - } + $params = self::addColumnCondition($db, $schema, $params, $table, $parts[2], $operator, $value, $expression, $placeholder); - return $table . '_' . $related_table . '{' . $route . '}'; - } - - - /** - * Creates a new table alias - * - * @internal - * - * @param string $table The table to create an alias for - * @param array &$used_aliases The aliases that have been used - * @return string The alias to use for the table - */ - static private function createNewAlias($table, &$used_aliases) - { - if (!in_array($table, $used_aliases)) { - $used_aliases[] = $table; - return $table; - } - $i = 1; - while(in_array($table . $i, $used_aliases)) { $i++; } - $used_aliases[] = $table . $i; - return $table . $i; + + return $params; } /** - * Creates an `ORDER BY` clause from an array of columns/expressions and directions + * Adds an `ORDER BY` clause to an array of params for an fDatabase::query() call * * @internal * - * @param string $table The table any ambigious column references will refer to - * @param array $order_bys The array of order bys to use - see fRecordSet::build() for format - * @return string The SQL `ORDER BY` clause + * @param fDatabase $db The database the query will be executed on + * @param fSchema $schema The schema object for the database the query will be executed on + * @param array $params The parameters for the fDatabase::query() call + * @param string $table The table any ambigious column references will refer to + * @param array $order_bys The array of order bys to use - see fRecordSet::build() for format + * @return array The params with a SQL `ORDER BY` clause added */ - static public function createOrderByClause($table, $order_bys) + static public function addOrderByClause($db, $schema, $params, $table, $order_bys) { - $order_bys = self::addTableToKeys($table, $order_bys); - $sql = array(); + $expressions = array(); foreach ($order_bys as $column => $direction) { if ((!is_string($column) && !is_object($column) && !is_numeric($column)) || !strlen(trim($column))) { @@ -497,73 +329,96 @@ static public function createOrderByClause($table, $order_bys) ); } - if (preg_match('#^(?:\w+(?:\{\w+\})?=>)?(\w+)(?:\{\w+\})?\.(\w+)$#D', $column, $matches)) { - $column_type = fORMSchema::retrieve()->getColumnInfo($matches[1], $matches[2], 'type'); + if (preg_match('#^((?:max|min|avg|sum|count)\()?((?:(?:(?:"?\w+"?\.)?"?\w+(?:\{[\w.]+\})?"?=>)?"?(?:(?:\w+"?\."?)?\w+)(?:\{[\w.]+\})?"?\.)?"?(?:\w+)"?)(?:\))?$#D', $column, $matches)) { + + // Parse the expression and get a table and column to determine the data type + list ($clause_table, $clause_column) = self::getTableAndColumn($schema, $table, $matches[2]); + $column_type = $schema->getColumnInfo($clause_table, $clause_column, 'type'); + + // Make sure each column is qualified with a table name + if (strpos($matches[2], '.') === FALSE) { + $matches[2] = $table . '.' . $matches[2]; + } + + $matches[2] = $db->escape('%r', $matches[2]); + + // Text columns are converted to lowercase for more accurate sorting if (in_array($column_type, array('varchar', 'char', 'text'))) { - $sql[] = 'LOWER(' . $column . ') ' . $direction; + $expression = 'LOWER(' . $matches[2] . ')'; } else { - $sql[] = $column . ' ' . $direction; + $expression = $matches[2]; } + + // If the column is in an aggregate function, add the function back in + if ($matches[1]) { + $expression = $matches[1] . $expression . ')'; + } + + $expressions[] = $expression . ' ' . $direction; + } else { - $sql[] = $column . ' ' . $direction; + $expressions[] = $column . ' ' . $direction; } } - return join(', ', $sql); + $params[0] .= join(', ', $expressions); + + return $params; } /** - * Creates a `WHERE` clause condition for primary keys of the table specified - * - * This method requires the `$primary_keys` parameter to be one of: - * - * - A scalar value for a single-column primary key - * - An array of values for a single-column primary key - * - An associative array of values for a multi-column primary key (`column => value`) - * - An array of associative arrays of values for a multi-column primary key (`key => array(column => value)`) - * - * If you are looking to build a primary key where clause from the `$values` - * and `$old_values` arrays, please see ::createPrimaryKeyWhereClause() + * Add the appropriate SQL and params for a `WHERE` clause condition for primary keys of the table specified * * @internal * - * @param string $table The table to build the where clause for - * @param string $table_alias The alias for the table - * @param array &$values The values array for the fActiveRecord object - * @param array &$old_values The old values array for the fActiveRecord object - * @return string The `WHERE` clause that will specify the fActiveRecord as it currently exists in the database + * @param fSchema $schema The schema for the database the query will be run on + * @param array $params The currently constructed params for fDatabase::query() - the first param should be a SQL statement + * @param string $table The table to build the where clause for + * @param string $table_alias The alias for the table + * @param array &$values The values array for the fActiveRecord object + * @param array &$old_values The old values array for the fActiveRecord object + * @return array The params to pass to fDatabase::query(), including the new primary key where condition */ - static public function createPrimaryKeyWhereClause($table, $table_alias, &$values, &$old_values) + static public function addPrimaryKeyWhereParams($schema, $params, $table, $table_alias, &$values, &$old_values) { - $primary_keys = fORMSchema::retrieve()->getKeys($table, 'primary'); + $pk_columns = $schema->getKeys($table, 'primary'); - $sql = ''; - foreach ($primary_keys as $primary_key) { - if ($sql) { $sql .= " AND "; } + $conditions = array(); + foreach ($pk_columns as $pk_column) { + $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]); - $value = (isset($old_values[$primary_key])) ? $old_values[$primary_key][0] : $values[$primary_key]; + $params[] = $table_alias . '.' . $pk_column; + $params[] = $value; - $sql .= $table . '.' . $primary_key . fORMDatabase::escapeBySchema($table, $primary_key, $value, '='); + $conditions[] = self::makeCondition($schema, $table, $pk_column, '=', $value); } - return $sql; + $params[0] .= join(' AND ', $conditions); + + return $params; } /** - * Creates a `WHERE` clause from an array of conditions + * Adds a `WHERE` clause, from an array of conditions, to the parameters for an fDatabase::query() call * * @internal * - * @param string $table The table any ambigious column references will refer to - * @param array $conditions The array of conditions - see fRecordSet::build() for format - * @return string The SQL `WHERE` clause + * @param fDatabase $db The database the query will be executed on + * @param fSchema $schema The schema for the database + * @param array $params The parameters for the fDatabase::query() call + * @param string $table The table any ambigious column references will refer to + * @param array $conditions The array of conditions - see fRecordSet::build() for format + * @return array The params with the SQL `WHERE` clause added */ - static public function createWhereClause($table, $conditions) + static public function addWhereClause($db, $schema, $params, $table, $conditions) { - $sql = array(); + $i = 0; foreach ($conditions as $column => $values) { + if ($i) { + $params[0] .= ' AND '; + } if (in_array(substr($column, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '><'))) { $operator = strtr( @@ -619,7 +474,15 @@ static public function createWhereClause($table, $conditions) } $operators[] = $operator; - $columns = self::addTableToValues($table, $columns); + // Make sure every column is qualified by a table name + $new_columns = array(); + foreach ($columns as $column) { + if (strpos($column, '.') === FALSE) { + $column = $table . '.' . $column; + } + $new_columns[] = $column; + } + $columns = $new_columns; if (sizeof($operators) == 1) { @@ -635,11 +498,12 @@ static public function createWhereClause($table, $conditions) foreach ($values as $value) { $sub_condition = array(); foreach ($columns as $column) { - $sub_condition[] = $column . self::retrieve()->escape(' LIKE %s', '%' . $value . '%'); + $sub_condition[] = $db->escape('%r', $column) . ' LIKE %s'; + $params[] = '%' . $value . '%'; } $condition[] = '(' . join(' OR ', $sub_condition) . ')'; } - $sql[] = ' (' . join(' AND ', $condition) . ') '; + $params[0] .= ' (' . join(' AND ', $condition) . ') '; // Handle intersection } elseif ($operator == '><') { @@ -650,16 +514,36 @@ static public function createWhereClause($table, $conditions) $operator ); } + + $escaped_columns = array( + $db->escape('%r', $columns[0]), + $db->escape('%r', $columns[1]) + ); + + list($column_1_table, $column_1) = self::getTableAndColumn($schema, $table, $columns[0]); + list($column_2_table, $column_2) = self::getTableAndColumn($schema, $table, $columns[1]); + $placeholders = array( + $schema->getColumnInfo($column_1_table, $column_1, 'placeholder'), + $schema->getColumnInfo($column_2_table, $column_2, 'placeholder') + ); if ($values[1] === NULL) { - $part_1 = '(' . $columns[1] . ' IS NULL AND ' . $columns[0] . ' = ' . self::escapeBySchema($table, $columns[0], $values[0]) . ')'; - $part_2 = '(' . $columns[1] . ' IS NOT NULL AND ' . $columns[0] . ' <= ' . self::escapeBySchema($table, $columns[0], $values[0]) . ' AND ' . $columns[1] . ' >= ' . self::escapeBySchema($table, $columns[1], $values[0]) . ')'; + $part_1 = '(' . $escaped_columns[1] . ' IS NULL AND ' . $escaped_columns[0] . ' = ' . $placeholders[0] . ')'; + $part_2 = '(' . $escaped_columns[1] . ' IS NOT NULL AND ' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ' AND ' . $escaped_columns[1] . ' >= ' . $placeholders[1] . ')'; + $params[] = $values[0]; + $params[] = $values[0]; + $params[] = $values[0]; + } else { - $part_1 = '(' . $columns[0] . ' <= ' . self::escapeBySchema($table, $columns[0], $values[0]) . ' AND ' . $columns[1] . ' >= ' . self::escapeBySchema($table, $columns[1], $values[0]) . ')'; - $part_2 = '(' . $columns[0] . ' >= ' . self::escapeBySchema($table, $columns[0], $values[0]) . ' AND ' . $columns[0] . ' <= ' . self::escapeBySchema($table, $columns[0], $values[1]) . ')'; + $part_1 = '(' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ' AND ' . $escaped_columns[1] . ' >= ' . $placeholders[1] . ')'; + $part_2 = '(' . $escaped_columns[0] . ' >= ' . $placeholders[0] . ' AND ' . $escaped_columns[0] . ' <= ' . $placeholders[0] . ')'; + $params[] = $values[0]; + $params[] = $values[0]; + $params[] = $values[0]; + $params[] = $values[1]; } - $sql[] = ' (' . $part_1 . ' OR ' . $part_2 . ') '; + $params[0] .= ' (' . $part_1 . ' OR ' . $part_2 . ') '; } else { throw new fProgrammerException( @@ -686,161 +570,236 @@ static public function createWhereClause($table, $conditions) ); } - $conditions = array(); + $params[0] .= ' ('; $iterations = sizeof($columns); - for ($i=0; $i<$iterations; $i++) { - $conditions[] = self::createColumnCondition($table, $columns[$i], $values[$i], $operators[$i]); + for ($j=0; $j<$iterations; $j++) { + if ($j) { + $params[0] .= ' OR '; + } + $params = self::addColumnCondition($db, $schema, $params, $table, $columns[$j], $operators[$j], $values[$j]); } - $sql[] = ' (' . join(' OR ', $conditions) . ') '; + $params[0] .= ') '; } - // Single column condition - } else { + // Concatenated columns + } elseif (strpos($column, '||') !== FALSE) { - $columns = self::addTableToValues($table, array($column)); - $column = $columns[0]; + $parts = explode('||', $column); + $new_parts = array(); + foreach ($parts as $part) { + $part = trim($part); + if ($part[0] != "'") { + $new_parts[] = $db->escape('%r', $part); + } else { + $new_parts[] = $part; + } + } + $escaped_column = join('||', $new_parts); + $params = self::addColumnCondition($db, $schema, $params, $table, $column, $operator, $values, $escaped_column, '%s'); + + // Single column condition + } else { - $sql[] = self::createColumnCondition($table, $column, $values, $operator); + $params = self::addColumnCondition($db, $schema, $params, $table, $column, $operator, $values); } + + $i++; } - return join(' AND ', $sql); + return $params; } /** - * Escapes a value for a DB call based on database schema + * Takes a table name, cleans off quoting and removes the schema name if unambiguous + * + * @param fSchema $schema The schema object for the database being inspected + * @param string $table The table name to be made cleaned + * @return string The cleaned table name + */ + static private function cleanTableName($schema, $table) + { + $table = str_replace('"', '', $table); + $tables = array_flip($schema->getTables()); + if (!isset($tables[$table])) { + $short_table = preg_replace('#^\w\.#', '', $table); + if (isset($tables[$short_table])) { + $table = $short_table; + } + } + return $table; + } + + + /** + * Creates a `FROM` clause from a join array * * @internal * - * @param string $table The table to store the value - * @param string $column The column to store the value in, may also be shorthand column name like `table.column` or `table=>related_table.column` or concatenated column names like `table.column||table.other_column` - * @param mixed $value The value to escape - * @param string $comparison_operator Optional: should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'` - * @return string The SQL-ready representation of the value + * @param fDatabase $db The database the query will be run on + * @param array $joins The joins to create the `FROM` clause out of + * @return string The from clause (does not include the word `FROM`) */ - static public function escapeBySchema($table, $column, $value, $comparison_operator=NULL) + static private function createFromClauseFromJoins($db, $joins) { - // handle concatenated column names - if (preg_match('#\|\|#', $column)) { - - if (is_object($value) && is_callable(array($value, '__toString'))) { - $value = $value->__toString(); - } elseif (is_object($value)) { - $value = (string) $value; - } - - $column_info = array( - 'not_null' => FALSE, - 'default' => NULL, - 'type' => 'varchar' - ); - - } else { - - // Handle shorthand column names like table.column and table=>related_table.column - if (preg_match('#(\w+)(?:\{\w+\})?\.(\w+)$#D', $column, $match)) { - $table = $match[1]; - $column = $match[2]; - } - - $column_info = fORMSchema::retrieve()->getColumnInfo($table, $column); + $sql = ''; + + foreach ($joins as $join) { + // Here we handle the first table in a join + if ($join['join_type'] == 'none') { + $sql .= $db->escape('%r', $join['table_name']); + if ($join['table_alias'] != $join['table_name']) { + $sql .= ' ' . $db->escape('%r', $join['table_alias']); + } - // Some of the tables being escaped for are linking tables that might break with classize() - if (is_object($value)) { - $class = fORM::classize($table); - $value = fORM::scalarize($class, $column, $value); + // Here we handle all other joins + } else { + $sql .= ' ' . strtoupper($join['join_type']) . ' ' . $db->escape('%r', $join['table_name']); + if ($join['table_alias'] != $join['table_name']) { + $sql .= ' ' . $db->escape('%r', $join['table_alias']); + } + if (!empty($join['on_clause_fields'])) { + $sql .= ' ON ' . $db->escape('%r', $join['on_clause_fields'][0]) . ' = ' . $db->escape('%r', $join['on_clause_fields'][1]); + } } - - } - - if ($comparison_operator !== NULL) { - $comparison_operator = strtr($comparison_operator, array('!' => '<>', '!=' => '<>')); } - $valid_comparison_operators = array('=', '!=', '!', '<>', '<=', '<', '>=', '>', 'IN', 'NOT IN'); - if ($comparison_operator !== NULL && !in_array(strtoupper($comparison_operator), $valid_comparison_operators)) { + return $sql; + } + + + /** + * Creates join information for the table shortcut provided + * + * @internal + * + * @param fSchema $schema The schema object for the tables/joins + * @param string $table The primary table + * @param string $table_alias The primary table alias + * @param string $related_table The related table + * @param string $route The route to the related table + * @param array &$joins The names of the joins that have been created + * @param array &$used_aliases The aliases that have been used + * @return string The name of the significant join created + */ + static private function createJoin($schema, $table, $table_alias, $related_table, $route, &$joins, &$used_aliases) + { + $routes = fORMSchema::getRoutes($schema, $table, $related_table); + + if (!isset($routes[$route])) { throw new fProgrammerException( - 'The comparison operator specified, %1$s, is invalid. Must be one of: %2$s.', - $comparison_operator, - join(', ', $valid_comparison_operators) + 'An invalid route, %1$s, was specified for the relationship from %2$s to %3$s', + $route, + $table, + $related_table ); } - $co = (is_null($comparison_operator)) ? '' : ' ' . strtoupper($comparison_operator) . ' '; - - if ($column_info['not_null'] && $value === NULL && $column_info['default'] !== NULL) { - $value = $column_info['default']; + if (isset($joins[$table . '_' . $related_table . '{' . $route . '}'])) { + return $table . '_' . $related_table . '{' . $route . '}'; } - if (is_null($value)) { - $prepared_value = 'NULL'; + // If the route uses a join table + if (isset($routes[$route]['join_table'])) { + $join = array( + 'join_type' => 'LEFT JOIN', + 'table_name' => $routes[$route]['join_table'], + 'table_alias' => self::createNewAlias($routes[$route]['join_table'], $used_aliases), + 'on_clause_fields' => array() + ); + + $join2 = array( + 'join_type' => 'LEFT JOIN', + 'table_name' => $related_table, + 'table_alias' => self::createNewAlias($related_table, $used_aliases), + 'on_clause_fields' => array() + ); + + if ($table != $related_table) { + $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column']; + $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column']; + $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column']; + $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column']; + } else { + $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column']; + $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_related_column']; + $join2['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['join_column']; + $join2['on_clause_fields'][] = $join2['table_alias'] . '.' . $routes[$route]['related_column']; + } + + $joins[$table . '_' . $related_table . '{' . $route . '}_join'] = $join; + $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join2; + + // If the route is a direct join } else { - $prepared_value = self::retrieve()->escape($column_info['type'], $value); - } + + $join = array( + 'join_type' => 'LEFT JOIN', + 'table_name' => $related_table, + 'table_alias' => self::createNewAlias($related_table, $used_aliases), + 'on_clause_fields' => array() + ); + + $join['on_clause_fields'][] = $table_alias . '.' . $routes[$route]['column']; + $join['on_clause_fields'][] = $join['table_alias'] . '.' . $routes[$route]['related_column']; + + $joins[$table . '_' . $related_table . '{' . $route . '}'] = $join; - if ($prepared_value == 'NULL') { - if ($co) { - if (in_array(trim($co), array('=', 'IN'))) { - $co = ' IS '; - } elseif (in_array(trim($co), array('<>', 'NOT IN'))) { - $co = ' IS NOT '; - } - } } - return $co . $prepared_value; + return $table . '_' . $related_table . '{' . $route . '}'; } /** - * Escapes a value for a DB call based on variable type - * + * Creates a new table alias + * * @internal - * - * @param mixed $value The value to escape - * @param string $comparison_operator Optional: should be `'='`, `'!='`, `'!'`, `'<>'`, `'<'`, `'<='`, `'>'`, `'>='`, `'IN'`, `'NOT IN'` - * @return string The SQL-ready representation of the value + * + * @param string $table The table to create an alias for + * @param array &$used_aliases The aliases that have been used + * @return string The alias to use for the table */ - static public function escapeByType($value, $comparison_operator=NULL) + static private function createNewAlias($table, &$used_aliases) { - if ($comparison_operator !== NULL) { - $comparison_operator = strtr($comparison_operator, array('!' => '<>', '!=' => '<>')); - } - - $valid_comparison_operators = array('=', '<>', '<=', '<', '>=', '>', 'IN', 'NOT IN'); - if ($comparison_operator !== NULL && !in_array(strtoupper($comparison_operator), $valid_comparison_operators)) { - throw new fProgrammerException( - 'The comparison operator specified, %1$s, is invalid. Must be one of: %2$s.', - $comparison_operator, - join(', ', $valid_comparison_operators) - ); + if (!in_array($table, $used_aliases)) { + $used_aliases[] = $table; + return $table; } - $co = (is_null($comparison_operator)) ? '' : ' ' . strtoupper($comparison_operator) . ' '; + // This will strip any schema name off the beginning + $table = preg_replace('#^\w+\.#', '', $table); - if (is_int($value)) { - $prepared_value = self::retrieve()->escape('integer', $value); - } elseif (is_float($value)) { - $prepared_value = self::retrieve()->escape('float', $value); - } elseif (is_bool($value)) { - $prepared_value = self::retrieve()->escape('boolean', $value); - } elseif (is_null($value)) { - if ($co) { - if (in_array(trim($co), array('=', 'IN'))) { - $co = ' IS '; - } elseif (in_array(trim($co), array('<>', 'NOT IN'))) { - $co = ' IS NOT '; - } - } - $prepared_value = 'NULL'; - } else { - $prepared_value = self::retrieve()->escape('string', $value); + $i = 1; + while(in_array($table . $i, $used_aliases)) { + $i++; } + $used_aliases[] = $table . $i; + return $table . $i; + } + + + /** + * Gets the table and column name from a shorthand column name + * + * @param fSchema $schema The schema for the database + * @param string $table The table to use when no table is specified in the shorthand + * @param string $column The shorthand column definition - see fRecordSet::build() for possible syntaxes + * @return array The $table and $column, suitable for use with fSchema + */ + static private function getTableAndColumn($schema, $table, $column) + { + // Handle shorthand column names like table.column and table=>related_table.column + if (preg_match('#((?:"?\w+"?\.)?"?\w+)(?:\{[\w.]+\})?"?\."?(\w+)"?$#D', $column, $match)) { + $table = $match[1]; + $column = $match[2]; + } + $table = self::cleanTableName($schema, $table); + $column = str_replace('"', '', $column); - return $co . $prepared_value; + return array($table, $column); } @@ -858,34 +817,37 @@ static public function escapeByType($value, $comparison_operator=NULL) * * @internal * - * @param string $table The main table to be queried - * @param string $sql The SQL to insert the `FROM` clause into - * @return string The SQL `FROM` clause + * @param fDatabase $db The database the query is to be executed on + * @param fSchema $schema The schema for the database + * @param array $params The parameters for the fDatabase::query() call + * @param string $table The main table to be queried + * @return array The params with the SQL `FROM` and `GROUP BY` clauses injected */ - static public function insertFromAndGroupByClauses($table, $sql) + static public function injectFromAndGroupByClauses($db, $schema, $params, $table) { + $table = self::cleanTableName($schema, $table); $joins = array(); - if (strpos($sql, ':from_clause') === FALSE) { + if (strpos($params[0], ':from_clause') === FALSE) { throw new fProgrammerException( - "No %1\$s placeholder was found in:%2\$s", + 'No %1$s placeholder was found in:%2$s', ':from_clause', - "\n" . $sql + "\n" . $params[0] ); } - if (strpos($sql, ':group_by_clause') === FALSE && !preg_match('#group\s+by#i', $sql)) { + if (strpos($params[0], ':group_by_clause') === FALSE && !preg_match('#group\s+by#i', $params[0])) { throw new fProgrammerException( - "No %1\$s placeholder was found in:%2\$s", + 'No %1$s placeholder was found in:%2$s', ':group_by_clause', - "\n" . $sql + "\n" . $params[0] ); } - $has_group_by_placeholder = (strpos($sql, ':group_by_clause') !== FALSE) ? TRUE : FALSE; + $has_group_by_placeholder = (strpos($params[0], ':group_by_clause') !== FALSE) ? TRUE : FALSE; // Separate the SQL from quoted values - preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\])*')|(?:[^']+)#", $sql, $matches); + preg_match_all("#(?:'(?:''|\\\\'|\\\\[^']|[^'\\\\])*')|(?:[^']+)#", $params[0], $matches); $table_alias = $table; @@ -905,48 +867,65 @@ static public function insertFromAndGroupByClauses($table, $sql) foreach ($matches[0] as $match) { if ($match[0] != "'") { - preg_match_all('#\b((?:(\w+)(?:\{(\w+)\})?=>)?(\w+)(?:\{(\w+)\})?)\.\w+\b#m', $match, $table_matches, PREG_SET_ORDER); + // This removes quotes from around . in the {route} specified of a shorthand column name + $match = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $match); + + //fCore::expose($match); + preg_match_all('#(?)((?:"?((?:\w+"?\."?)?\w+)(?:\{([\w.]+)\})?"?=>)?("?(?:\w+"?\."?)?\w+)(?:\{([\w.]+)\})?"?)\."?\w+"?(?=[^\w".{])#m', $match, $table_matches, PREG_SET_ORDER); foreach ($table_matches as $table_match) { if (!isset($table_match[5])) { $table_match[5] = NULL; } + if (!empty($table_match[2])) { + $table_match[2] = self::cleanTableName($schema, $table_match[2]); + } + $table_match[4] = self::cleanTableName($schema, $table_match[4]); + + if ($db->getType() == 'oracle') { + foreach (array(2, 3, 4, 5) as $subpattern) { + if (isset($table_match[$subpattern])) { + $table_match[$subpattern] = strtolower($table_match[$subpattern]); + } + } + } + // This is a related table that is going to join to a once-removed table if (!empty($table_match[2])) { $related_table = $table_match[2]; - $route = fORMSchema::getRouteName($table, $related_table, $table_match[3]); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $table_match[3]); $join_name = $table . '_' . $related_table . '{' . $route . '}'; - self::createJoin($table, $table_alias, $related_table, $route, $joins, $used_aliases); + self::createJoin($schema, $table, $table_alias, $related_table, $route, $joins, $used_aliases); $once_removed_table = $table_match[4]; - $route = fORMSchema::getRouteName($related_table, $once_removed_table, $table_match[5]); + $route = fORMSchema::getRouteName($schema, $related_table, $once_removed_table, $table_match[5]); - $join_name = self::createJoin($related_table, $joins[$join_name]['table_alias'], $once_removed_table, $route, $joins, $used_aliases); + $join_name = self::createJoin($schema, $related_table, $joins[$join_name]['table_alias'], $once_removed_table, $route, $joins, $used_aliases); - $table_map[$table_match[1]] = $joins[$join_name]['table_alias']; + $table_map[$table_match[1]] = $db->escape('%r', $joins[$join_name]['table_alias']); // This is a related table - } elseif (($table_match[4] != $table || fORMSchema::getRoutes($table, $table_match[4])) && $table_match[1] != $table) { + } elseif (($table_match[4] != $table || fORMSchema::getRoutes($schema, $table, $table_match[4])) && $table_match[1] != $table) { $related_table = $table_match[4]; - $route = fORMSchema::getRouteName($table, $related_table, $table_match[5]); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $table_match[5]); // If the related table is the current table and it is a one-to-many we don't want to join if ($table_match[4] == $table) { - $one_to_many_routes = fORMSchema::getRoutes($table, $related_table, 'one-to-many'); + $one_to_many_routes = fORMSchema::getRoutes($schema, $table, $related_table, 'one-to-many'); if (isset($one_to_many_routes[$route])) { - $table_map[$table_match[1]] = $table_alias; + $table_map[$table_match[1]] = $db->escape('%r', $table_alias); continue; } } - $join_name = self::createJoin($table, $table_alias, $related_table, $route, $joins, $used_aliases); + $join_name = self::createJoin($schema, $table, $table_alias, $related_table, $route, $joins, $used_aliases); - $table_map[$table_match[1]] = $joins[$join_name]['table_alias']; + $table_map[$table_match[1]] = $db->escape('%r', $joins[$join_name]['table_alias']); } } } @@ -966,8 +945,8 @@ static public function insertFromAndGroupByClauses($table, $sql) $main_table = preg_replace('#_' . $join['table_name'] . '{\w+}$#iD', '', $name); $second_table = $join['table_name']; - $route = preg_replace('#^[^{]+{(\w+)}$#D', '\1', $name); - $routes = fORMSchema::getRoutes($main_table, $second_table, '*-to-many'); + $route = preg_replace('#^[^{]+{([\w.]+)}$#D', '\1', $name); + $routes = fORMSchema::getRoutes($schema, $main_table, $second_table, '*-to-many'); if (isset($routes[$route])) { $joined_to_many = TRUE; break; @@ -975,19 +954,18 @@ static public function insertFromAndGroupByClauses($table, $sql) } $found_order_by = FALSE; - $from_clause = self::createFromClauseFromJoins($joins); + $from_clause = self::createFromClauseFromJoins($db, $joins); // If we are joining on a *-to-many relationship we need to group by the // columns in the main table to prevent duplicate entries if ($joined_to_many) { - $column_info = fORMSchema::retrieve()->getColumnInfo($table); - $group_by_clause = ' GROUP BY '; + $column_info = $schema->getColumnInfo($table); $columns = array(); foreach ($column_info as $column => $info) { $columns[] = $table . '.' . $column; } - $group_by_columns = join(', ', $columns) . ' '; - $group_by_clause .= $group_by_columns; + $group_by_columns = $db->escape('%r ', $columns); + $group_by_clause = ' GROUP BY ' . $group_by_columns; } else { $group_by_clause = ' '; $group_by_columns = ''; @@ -995,32 +973,37 @@ static public function insertFromAndGroupByClauses($table, $sql) // Put the SQL back together $new_sql = ''; + + $preg_table_pattern = preg_quote($table, '#') . '\.|' . preg_quote('"' . $table . '"', '#') . '\.'; foreach ($matches[0] as $match) { $temp_sql = $match; // Get rid of the => notation and the :from_clause placeholder if ($match[0] !== "'") { + // This removes quotes from around . in the {route} specified of a shorthand column name + $temp_sql = preg_replace('#(\{\w+)"\."(\w+\})#', '\1.\2', $match); + foreach ($table_map as $arrow_table => $alias) { $temp_sql = str_replace($arrow_table, $alias, $temp_sql); } // In the ORDER BY clause we need to wrap columns in if ($found_order_by && $joined_to_many) { - $temp_sql = preg_replace('#(? '\\\\', '$' => '\\$')), $temp_sql); + $temp_sql = preg_replace('#\s:group_by_clause(\s|$)#', strtr($group_by_clause, array('\\' => '\\\\', '$' => '\\$')), $temp_sql); } elseif ($group_by_columns) { $temp_sql = preg_replace('#(\sGROUP\s+BY\s((?!HAVING|ORDER\s+BY).)*)\s#i', '\1, ' . strtr($group_by_columns, array('\\' => '\\\\', '$' => '\\$')), $temp_sql); } @@ -1028,8 +1011,40 @@ static public function insertFromAndGroupByClauses($table, $sql) $new_sql .= $temp_sql; } - - return $new_sql; + + $params[0] = $new_sql; + + return $params; + } + + + /** + * Makes a condition for a SQL statement out of fDatabase::escape() placeholders + * + * @internal + * + * @param fSchema $schema The schema object for the database the query will be executed on + * @param string $table The table to create the condition for + * @param string $column The column to make the condition for + * @param string $comparison_operator The comparison operator for the condition + * @param mixed $value The value for the condition, which allows the $comparison_operator to be tweaked for NULL values + * @return string A SQL condition using fDatabase::escape() placeholders + */ + static public function makeCondition($schema, $table, $column, $comparison_operator, $value) + { + list($table, $column) = self::getTableAndColumn($schema, $table, $column); + + $co = strtr($comparison_operator, array('!' => '<>', '!=' => '<>')); + + if ($value === NULL) { + if (in_array(trim($co), array('=', 'IN'))) { + $co = 'IS'; + } elseif (in_array(trim($co), array('<>', 'NOT IN'))) { + $co = 'IS NOT'; + } + } + + return '%r ' . $co . ' ' . $schema->getColumnInfo($table, $column, 'placeholder'); } @@ -1082,30 +1097,6 @@ static public function parseSearchTerms($terms, $ignore_stop_words=FALSE) } - /** - * Removed aggregate function calls from where conditions array and puts them in a having conditions array - * - * @internal - * - * @param array &$where_conditions The where conditions to look through for aggregate functions - * @return array The conditions to be put in a `HAVING` clause - */ - static public function splitHavingConditions(&$where_conditions) - { - $having_conditions = array(); - - foreach ($where_conditions as $column => $value) - { - if (preg_match('#^(count\(|max\(|avg\(|min\(|sum\()#i', $column)) { - $having_conditions[$column] = $value; - unset($where_conditions[$column]); - } - } - - return $having_conditions; - } - - /** * Resets the configuration of the class * @@ -1137,6 +1128,30 @@ static public function retrieve() } + /** + * Removed aggregate function calls from where conditions array and puts them in a having conditions array + * + * @internal + * + * @param array &$where_conditions The where conditions to look through for aggregate functions + * @return array The conditions to be put in a `HAVING` clause + */ + static public function splitHavingConditions(&$where_conditions) + { + $having_conditions = array(); + + foreach ($where_conditions as $column => $value) + { + if (preg_match('#^(count\(|max\(|avg\(|min\(|sum\()#i', $column)) { + $having_conditions[$column] = $value; + unset($where_conditions[$column]); + } + } + + return $having_conditions; + } + + /** * Forces use as a static class * diff --git a/classes/fORMOrdering.php b/classes/fORMOrdering.php index bc41339b..ae7337e0 100644 --- a/classes/fORMOrdering.php +++ b/classes/fORMOrdering.php @@ -10,7 +10,8 @@ * @package Flourish * @link http://flourishlib.com/fORMOrdering * - * @version 1.0.0b11 + * @version 1.0.0b12 + * @changes 1.0.0b12 Changed SQL statements to use value placeholders, identifier escaping and schema support [wb, 2009-10-22] * @changes 1.0.0b11 Fixed another bug with deleting records in the middle of a set, added support for reordering multiple records at once [dc-imarc, 2009-07-17] * @changes 1.0.0b10 Fixed a bug with deleting multiple in-memory records in the same set [dc-imarc, 2009-07-15] * @changes 1.0.0b9 Fixed a bug with using fORM::registerInspectCallback() [wb, 2009-07-15] @@ -80,8 +81,9 @@ static public function configureOrderingColumn($class, $column) { $class = fORM::getClass($class); $table = fORM::tablize($class); - $data_type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); - $unique_keys = fORMSchema::retrieve()->getKeys($table, 'unique'); + $schema = fORMSchema::retrieve(); + $data_type = $schema->getColumnInfo($table, $column, 'type'); + $unique_keys = $schema->getKeys($table, 'unique'); if ($data_type != 'integer') { throw new fProgrammerException( @@ -123,42 +125,59 @@ static public function configureOrderingColumn($class, $column) /** - * Creates a `WHERE` clause for the //old// multi-column set a record was part of + * Add params for a `WHERE` clause for the //old// multi-column set a record was part of * - * @param string $table The table the `WHERE` clause is for - * @param array $other_columns The other columns in the multi-column unique constraint - * @param array &$values The record's current values - * @param array &$old_values The record's old values - * @return string An SQL `WHERE` clause for the //old// other columns in a multi-column `UNIQUE` constraint + * @param fSchema $schema The schema of the database the query will be executed on + * @param array $params The params for the fDatabase::query() call + * @param string $table The table the `WHERE` clause is for + * @param array $other_columns The other columns in the multi-column unique constraint + * @param array &$values The record's current values + * @param array &$old_values The record's old values + * @return array The updated params for fDatabase::query() */ - static private function createOldOtherFieldsWhereClause($table, $other_columns, &$values, &$old_values) + static private function addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, &$values, &$old_values) { $conditions = array(); foreach ($other_columns as $other_column) { - $other_value = fActiveRecord::retrieveOld($old_values, $other_column, $values[$other_column]); - $conditions[] = $table . "." . $other_column . fORMDatabase::escapeBySchema($table, $other_column, $other_value, '='); + $other_value = fActiveRecord::retrieveOld($old_values, $other_column, $values[$other_column]); + + $params[] = $table . '.' . $other_column; + $params[] = $other_value; + + $conditions[] = fORMDatabase::makeCondition($schema, $table, $other_column, '=', $other_value); } - return join(' AND ', $conditions); + $params[0] .= join(', ', $conditions); + + return $params; } /** - * Creates a `WHERE` clause to ensure a database call is only selecting from rows that are part of the same set when an ordering field is in multi-column `UNIQUE` constraint. + * Adds params for a `WHERE` clause to ensure a database call is only selecting from rows that are part of the same set when an ordering field is in multi-column `UNIQUE` constraint. * - * @param string $table The table the `WHERE` clause is for - * @param array $other_columns The other columns in the multi-column unique constraint - * @param array &$values The values to match with - * @return string An SQL `WHERE` clause for the other columns in a multi-column `UNIQUE` constraint + * @param fSchema $schema The schema of the database the query will be executed on + * @param array $params The parameters for the fDatabase::query() call + * @param string $table The table the `WHERE` clause is for + * @param array $other_columns The other columns in the multi-column unique constraint + * @param array &$values The values to match with + * @return array The updated params for fDatabase::query() */ - static private function createOtherFieldsWhereClause($table, $other_columns, &$values) + static private function addOtherFieldsWhereParams($schema, $params, $table, $other_columns, &$values) { $conditions = array(); foreach ($other_columns as $other_column) { - $conditions[] = $other_column . fORMDatabase::escapeBySchema($table, $other_column, $values[$other_column], '='); + $value = $values[$other_column]; + + $params[] = $table . '.' . $other_column; + $params[] = $value; + + $conditions[] = fORMDatabase::makeCondition($schema, $table, $other_column, '=', $value); } - return join(' AND ', $conditions); + $params[0] .= join(', ', $conditions); + + return $params; } @@ -179,6 +198,9 @@ static public function delete($object, &$values, &$old_values, &$related_records $class = get_class($object); $table = fORM::tablize($class); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $column = self::$ordering_columns[$class]['column']; $other_columns = self::$ordering_columns[$class]['other_columns']; @@ -186,26 +208,42 @@ static public function delete($object, &$values, &$old_values, &$related_records $old_value = fActiveRecord::retrieveOld($old_values, $column, $current_value); // Figure out the range we are dealing with - $sql = "SELECT max(" . $column . ") FROM " . $table; + $params = array("SELECT MAX(%r) FROM %r", $column, $table); if ($other_columns) { - $sql .= " WHERE " . self::createOtherFieldsWhereClause($table, $other_columns, $values); + $params[0] .= " WHERE "; + $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } - $current_max_value = (integer) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); + $current_max_value = (integer) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); $shift_down = $current_max_value + 10; $shift_up = $current_max_value + 9; - $sql = "SELECT " . $table . "." . $column . " FROM " . $table . " LEFT JOIN " . $table . " AS t2 ON " . $table . "." . $column . " = t2." . $column . " + 1"; + $params = array( + "SELECT %r FROM %r LEFT JOIN %r t2 ON %r = t2.%r + 1", + $table . '.' . $column, + $table, + $table, + $table . '.' . $column, + $column + ); + foreach ($other_columns as $other_column) { - $sql .= " AND " . $table . "." . $other_column . " = t2." . $other_column; - } - $sql .= " WHERE t2." . $column . " IS NULL AND " . $table . "." . $column . " != 1"; + $params[0] .= " AND %r = t2.%r"; + $params[] = $table . '.' . $other_column; + $params[] = $other_column; + } + + $params[0] .= " WHERE t2.%r IS NULL AND %r != 1"; + $params[] = $column; + $params[] = $table . '.' . $column; + if ($other_columns) { - $sql .= " AND " . self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); + $params[0] .= " AND "; + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); } - $res = fORMDatabase::retrieve()->translatedQuery($sql); + $res = call_user_func_array($db->translatedQuery, $params); if (!$res->countReturnedRows()) { return; @@ -214,22 +252,37 @@ static public function delete($object, &$values, &$old_values, &$related_records $old_value = $res->fetchScalar() - 1; // Close the gap for all records after this one in the set - $sql = "UPDATE " . $table . " SET " . $column . ' = ' . $column . ' - ' . $shift_down . ' '; - $sql .= 'WHERE ' . $column . ' > ' . $old_value; + $params = array( + 'UPDATE %r SET %r = %r - %i WHERE %r > %i', + $table, + $column, + $column, + $shift_down, + $column, + $old_value + ); if ($other_columns) { - $sql .= " AND " . self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); + $params[0] .= " AND "; + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); } - fORMDatabase::retrieve()->translatedQuery($sql); + call_user_func_array($db->translatedQuery, $params); // Close the gap for all records after this one in the set - $sql = "UPDATE " . $table . " SET " . $column . ' = ' . $column . ' + ' . $shift_up . ' '; - $sql .= 'WHERE ' . $column . ' < 0'; + $params = array( + 'UPDATE %r SET %r = %r + %i WHERE %r < 0', + $table, + $column, + $column, + $shift_up, + $column + ); if ($other_columns) { - $sql .= " AND " . self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); + $params[0] .= " AND "; + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); } - fORMDatabase::retrieve()->translatedQuery($sql); + call_user_func_array($db->translatedQuery, $params); } @@ -254,18 +307,22 @@ static public function inspect($object, &$values, &$old_values, &$related_record $class = get_class($object); $table = fORM::tablize($class); - $info = fORMSchema::retrieve()->getColumnInfo($table, $column); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + + $info = $schema->getColumnInfo($table, $column); $element = (isset($parameters[0])) ? $parameters[0] : NULL; $column = self::$ordering_columns[$class]['column']; $other_columns = self::$ordering_columns[$class]['other_columns']; // Retrieve the current max ordering index from the database - $sql = "SELECT max(" . $column . ") FROM " . $table; + $params = array("SELECT MAX(%r) FROM %r", $column, $table); if ($other_columns) { - $sql .= " WHERE " . self::createOtherFieldsWhereClause($table, $other_columns, $values); + $params[0] .= " WHERE "; + $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } - $max_value = (integer) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); + $max_value = (integer) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); // If this is a new record, or in a new set, we need one more space in the ordering index if (self::isInNewSet($column, $other_columns, $values, $old_values)) { @@ -378,6 +435,9 @@ static public function reorder($object, &$values, &$old_values, &$related_record $class = get_class($object); $table = fORM::tablize($class); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $column = self::$ordering_columns[$class]['column']; $other_columns = self::$ordering_columns[$class]['other_columns']; @@ -385,19 +445,19 @@ static public function reorder($object, &$values, &$old_values, &$related_record if (!$object->exists()) { $old_value = fActiveRecord::retrieveOld($old_values, $column); } else { - $old_value = fORMDatabase::retrieve()->translatedQuery( - "SELECT " . $column . " FROM " . $table . " WHERE " . - fORMDatabase::createPrimaryKeyWhereClause($table, $table, $values, $old_values) - )->fetchScalar(); + $params = array("SELECT %r FROM %r WHERE ", $column, $table); + $params = fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $values, $old_values); + $old_value = call_user_func_array($db->translatedQuery, $params)->fetchScalar(); } // Figure out the range we are dealing with - $sql = "SELECT max(" . $column . ") FROM " . $table; + $params = array("SELECT MAX(%r) FROM %r", $column, $table); if ($other_columns) { - $sql .= " WHERE " . self::createOtherFieldsWhereClause($table, $other_columns, $values); + $params[0] .= ' WHERE '; + $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } - $current_max_value = (integer) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); + $current_max_value = (integer) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); $new_max_value = $current_max_value; if ($new_set = self::isInNewSet($column, $other_columns, $values, $old_values)) { @@ -448,9 +508,9 @@ static public function reorder($object, &$values, &$old_values, &$related_record // If the object already exists in the database, grab the ordering value // right now in case some other object reordered it since it was loaded if ($object->exists()) { - $sql = "SELECT " . $column . " FROM " . $table . " WHERE "; - $sql .= fORMDatabase::createPrimaryKeyWhereClause($table, $table, $values, $old_values); - $db_value = (integer) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); + $params = array("SELECT %r FROM %r WHERE ", $column, $table); + $params = fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $values, $old_values); + $db_value = (integer) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); } @@ -459,57 +519,91 @@ static public function reorder($object, &$values, &$old_values, &$related_record if (!$new_set || ($new_set && $current_value != $new_max_value)) { $shift_down = $new_max_value + 10; + // To prevent issues with the unique constraint, we move everything below 0 + $params = array( + "UPDATE %r SET %r = %r - %i WHERE ", + $table, + $column, + $column, + $shift_down + ); + $conditions = array(); + // If we are moving into the middle of a new set we just push everything up one value if ($new_set) { - $shift_up = $new_max_value + 11; - $down_condition = $column . " >= " . $current_value; - + $shift_up = $new_max_value + 11; + $conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '>=', $current_value); + $params[] = $table . '.' . $column; + $params[] = $current_value; + // If we are moving a value down in a set, we push values in the difference zone up one } elseif ($current_value < $db_value) { - $shift_up = $new_max_value + 11; - $down_condition = $column . " < " . $db_value . " AND " . $column . " >= " . $current_value; + $shift_up = $new_max_value + 11; + $conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '<', $db_value); + $params[] = $table . '.' . $column; + $params[] = $db_value; + $conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '>=', $current_value); + $params[] = $table . '.' . $column; + $params[] = $current_value; // If we are moving a value up in a set, we push values in the difference zone down one } else { - $shift_up = $new_max_value + 9; - $down_condition = $column . " > " . $db_value . " AND " . $column . " <= " . $current_value; + $shift_up = $new_max_value + 9; + $conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '>', $db_value); + $params[] = $table . '.' . $column; + $params[] = $db_value; + $conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '<=', $current_value); + $params[] = $table . '.' . $column; + $params[] = $current_value; } - // To prevent issues with the unique constraint, we move everything below 0 - $sql = "UPDATE " . $table . " SET " . $column . " = " . $column . " - " . $shift_down; - $sql .= " WHERE " . $down_condition; + $params[0] .= join(' AND ', $conditions); if ($other_columns) { - $sql .= " AND " . self::createOtherFieldsWhereClause($table, $other_columns, $values); + $params[0] .= " AND "; + $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } - fORMDatabase::retrieve()->translatedQuery($sql); + call_user_func_array($db->translatedQuery, $params); if ($object->exists()) { // Put the actual record we are changing in limbo to be updated when the actual update happens - $sql = "UPDATE " . $table . " SET " . $column . " = 0"; - $sql .= " WHERE " . $column . " = " . $db_value; + $params = array( + "UPDATE %r SET %r = 0 WHERE %r = %i", + $table, + $column, + $column, + $db_value + ); if ($other_columns) { - $sql .= " AND " . self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); + $params[0] .= " AND "; + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); } - fORMDatabase::retrieve()->translatedQuery($sql); + call_user_func_array($db->translatedQuery, $params); } // Anything below zero needs to be moved back up into its new position - $sql = "UPDATE " . $table . " SET " . $column . " = " . $column . " + " . $shift_up; - $sql .= " WHERE " . $column . " < 0"; + $params = array( + "UPDATE %r SET %r = %r + %i WHERE %r < 0", + $table, + $column, + $column, + $shift_up, + $column + ); if ($other_columns) { - $sql .= " AND " . self::createOtherFieldsWhereClause($table, $other_columns, $values); + $params[0] .= " AND "; + $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } - fORMDatabase::retrieve()->translatedQuery($sql); + call_user_func_array($db->translatedQuery, $params); } // If there was an old set, we need to close the gap if ($object->exists() && $new_set) { - $sql = "SELECT max(" . $column . ") FROM " . $table . " WHERE "; - $sql .= self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); + $params = array("SELECT MAX(%r) FROM %r WHERE ", $column, $table); + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); - $old_set_max = (integer) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); + $old_set_max = (integer) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); // We only need to close the gap if the record was not at the end if ($db_value < $old_set_max) { @@ -517,25 +611,45 @@ static public function reorder($object, &$values, &$old_values, &$related_record $shift_up = $old_set_max + 9; // To prevent issues with the unique constraint, we move everything below 0 and then back up above - $sql = "UPDATE " . $table . " SET " . $column . ' = ' . $column . ' - ' . $shift_down . " WHERE "; - $sql .= self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); - $sql .= " AND " . $column . " > " . $db_value; - fORMDatabase::retrieve()->translatedQuery($sql); + + $params = array( + "UPDATE %r SET %r = %r - %i WHERE %r > %i AND ", + $table, + $column, + $column, + $shift_down, + $column, + $db_value + ); + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); + call_user_func_array($db->translatedQuery, $params); if ($current_value == $new_max_value) { // Put the actual record we are changing in limbo to be updated when the actual update happens - $sql = "UPDATE " . $table . " SET " . $column . " = 0"; - $sql .= " WHERE " . $column . " = " . $db_value; + $params = array( + "UPDATE %r SET %r = 0 WHERE %r = %i", + $table, + $column, + $column, + $db_value + ); if ($other_columns) { - $sql .= " AND " . self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); + $params[0] .= " AND "; + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); } - fORMDatabase::retrieve()->translatedQuery($sql); + call_user_func_array($db->translatedQuery, $params); } - $sql = "UPDATE " . $table . " SET " . $column . ' = ' . $column . ' + ' . $shift_up . " WHERE "; - $sql .= self::createOldOtherFieldsWhereClause($table, $other_columns, $values, $old_values); - $sql .= " AND " . $column . " < 0"; - fORMDatabase::retrieve()->translatedQuery($sql); + $params = array( + "UPDATE %r SET %r = %r + %i WHERE %r < 0 AND ", + $table, + $column, + $column, + $shift_up, + $column + ); + $params = self::addOldOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values, $old_values); + call_user_func_array($db->translatedQuery, $params); } } } @@ -572,18 +686,22 @@ static public function validate($object, &$values, &$old_values, &$related_recor $class = get_class($object); $table = fORM::tablize($class); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $column = self::$ordering_columns[$class]['column']; $other_columns = self::$ordering_columns[$class]['other_columns']; $current_value = $values[$column]; $old_value = fActiveRecord::retrieveOld($old_values, $column); - $sql = "SELECT max(" . $column . ") FROM " . $table; + $params = array("SELECT MAX(%r) FROM %r", $column, $table); if ($other_columns) { - $sql .= " WHERE " . self::createOtherFieldsWhereClause($table, $other_columns, $values); + $params[0] .= " WHERE "; + $params = self::addOtherFieldsWhereParams($schema, $params, $table, $other_columns, $values); } - $current_max_value = (integer) fORMDatabase::retrieve()->translatedQuery($sql)->fetchScalar(); + $current_max_value = (integer) call_user_func_array($db->translatedQuery, $params)->fetchScalar(); $new_max_value = $current_max_value; if ($new_set = self::isInNewSet($column, $other_columns, $values, $old_values)) { diff --git a/classes/fORMRelated.php b/classes/fORMRelated.php index 37b647ee..f9279c47 100644 --- a/classes/fORMRelated.php +++ b/classes/fORMRelated.php @@ -12,7 +12,8 @@ * @package Flourish * @link http://flourishlib.com/fORMRelated * - * @version 1.0.0b18 + * @version 1.0.0b19 + * @changes 1.0.0b19 Internal Backwards Compatibility Break - Added the `$class` parameter to ::storeManyToMany() - also fixed ::countRecords() to work across all databases, changed SQL statements to use value placeholders, identifier escaping and support schemas [wb, 2009-10-22] * @changes 1.0.0b18 Fixed a bug in ::countRecords() that would occur when multiple routes existed to the table being counted [wb, 2009-10-05] * @changes 1.0.0b17 Updated code for new fRecordSet API [wb, 2009-09-16] * @changes 1.0.0b16 Fixed a bug with ::createRecord() not creating non-existent record when the related value is NULL [wb, 2009-08-25] @@ -101,8 +102,9 @@ static public function associateRecord($class, &$related_records, $related_class $record = new $related_class($record); } + $schema = fORMSchema::retrieve(); $records = fRecordSet::buildFromArray($related_class, array($record)); - $route = fORMSchema::getRouteName($table, $related_table, $route, 'one-to-one'); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, 'one-to-one'); self::setRecordSet($class, $related_records, $related_class, $records, $route); self::flagForAssociation($class, $related_records, $related_class, $route); @@ -142,7 +144,8 @@ static public function associateRecords($class, &$related_records, $related_clas $primary_keys = TRUE; } - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); if ($primary_keys) { self::setPrimaryKeys($class, $related_records, $related_class, $records_to_associate, $route); @@ -170,14 +173,15 @@ static public function buildRecords($class, &$values, &$related_records, $relate $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); // If we already have the sequence, we can stop here if (isset($related_records[$related_table][$route]['record_set'])) { return $related_records[$related_table][$route]['record_set']; } - $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-many'); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many'); // Determine how we are going to build the sequence if ($values[$relationship['column']] === NULL) { @@ -242,43 +246,61 @@ static private function compose($message) */ static public function countRecords($class, &$values, &$related_records, $related_class, $route=NULL) { + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); // If we already have the sequence, we can stop here if (isset($related_records[$related_table][$route]['count'])) { return $related_records[$related_table][$route]['count']; } - $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-many'); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many'); // Determine how we are going to build the sequence if ($values[$relationship['column']] === NULL) { $count = 0; } else { - $column = $table . '.' . $relationship['column']; - $value = fORMDatabase::escapeBySchema($table, $relationship['column'], $values[$relationship['column']], '='); - - $primary_keys = fORMSchema::retrieve()->getKeys($related_table, 'primary'); - - if ($route) { - $related_table .= '{' . $route . '}'; - } + $column = $relationship['column']; + $value = $values[$column]; - $primary_keys = fORMDatabase::addTableToValues($related_table, $primary_keys); - $primary_keys = join(', ', $primary_keys); + $pk_columns = $schema->getKeys($related_table, 'primary'); - $sql = "SELECT count(" . $primary_keys . ") AS __flourish_count "; - $sql .= "FROM :from_clause "; - $sql .= "WHERE " . $column . $value; - $sql .= ' :group_by_clause '; - $sql .= 'ORDER BY ' . $column . ' ASC'; + // One-to-many relationships require joins + if (!isset($relationship['join_table'])) { + $table_with_route = $table . '{' . $relationship['related_column'] . '}'; + + $params = array("SELECT count(*) AS flourish__count FROM :from_clause WHERE "); + + $params[0] .= str_replace( + '%r', + $db->escape('%r', $table_with_route . '.' . $column), + fORMDatabase::makeCondition($schema, $table, $column, '=', $value) + ); + $params[] = $value; + + $params[0] .= ' :group_by_clause'; + + $params = fORMDatabase::injectFromAndGroupByClauses($db, $schema, $params, $related_table); - $sql = fORMDatabase::insertFromAndGroupByClauses($table, $sql); + // Many-to-many relationships allow counting just from the join table + } else { + + $params = array($db->escape( + "SELECT count(*) FROM %r WHERE %r = ", + $relationship['join_table'], + $relationship['join_column'] + )); + + $params[0] .= $schema->getColumnInfo($table, $column, 'placeholder'); + $params[] = $value; + } - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $result = call_user_func_array($db->translatedQuery, $params); $count = ($result->valid()) ? (int) $result->fetchScalar() : 0; } @@ -303,10 +325,11 @@ static public function countRecords($class, &$values, &$related_records, $relate */ static public function createRecord($class, $values, &$related_records, $related_class, $route=NULL) { + $schema = fORMSchema::retrieve(); $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-one'); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-one'); $route = $relationship['column']; // Determine if the relationship is one-to-one @@ -315,7 +338,7 @@ static public function createRecord($class, $values, &$related_records, $related } else { $one_to_one = FALSE; - $one_to_one_relationships = fORMSchema::getRoutes($table, $related_table, 'one-to-one'); + $one_to_one_relationships = fORMSchema::getRoutes($schema, $table, $related_table, 'one-to-one'); foreach ($one_to_one_relationships as $one_to_one_relationship) { if ($relationship['column'] == $one_to_one_relationship['column']) { $one_to_one = TRUE; @@ -337,7 +360,7 @@ static public function createRecord($class, $values, &$related_records, $related // If the value is NULL, don't pass it to the constructor because an fNotFoundException will be thrown if ($values[$relationship['column']] !== NULL) { - $records = array(new $related_class(array($relationship['column'] => $values[$relationship['column']]))); + $records = array(new $related_class(array($relationship['related_column'] => $values[$relationship['column']]))); } else { $records = array(); } @@ -375,13 +398,14 @@ static public function determineFirstPKColumn($class, $related_class, $route) $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $pk_columns = fORMSchema::retrieve()->getKeys($related_table, 'primary'); + $schema = fORMSchema::retrieve(); + $pk_columns = $schema->getKeys($related_table, 'primary'); // If there is a multi-fiend primary key we want to populate based on any field BUT the foreign key to the current class if (sizeof($pk_columns) > 1) { $first_pk_column = NULL; - $relationships = fORMSchema::getRoutes($related_table, $table, '*-to-one'); + $relationships = fORMSchema::getRoutes($schema, $related_table, $table, '*-to-one'); foreach ($pk_columns as $pk_column) { foreach ($relationships as $relationship) { if ($pk_column == $relationship['column']) { @@ -417,12 +441,14 @@ static public function determineFirstPKColumn($class, $related_class, $route) static public function determineRequestFilter($class, $related_class, $route) { $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); + $related_table = fORM::tablize($related_class); - $relationship = fORMSchema::getRoute($table, $related_table, $route); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); $route_name = fORMSchema::getRouteNameFromRelationship('one-to-many', $relationship); - $primary_keys = fORMSchema::retrieve()->getKeys($related_table, 'primary'); + $primary_keys = $schema->getKeys($related_table, 'primary'); $first_pk_column = $primary_keys[0]; $filter_table = $related_table; @@ -455,11 +481,8 @@ static public function flagForAssociation($class, &$related_records, $related_cl $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - try { - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); - } catch (fProgrammerException $e) { - $route = fORMSchema::getRouteName($table, $related_table, $route, 'one-to-one'); - } + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '!many-to-one'); if (!isset($related_records[$related_table][$route]['record_set']) && !isset($related_records[$related_table][$route]['primary_keys'])) { throw new fProgrammerException( @@ -489,7 +512,8 @@ static public function getOrderBys($class, $related_class, $route) $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route); if (!isset(self::$order_bys[$table][$related_table]) || !isset(self::$order_bys[$table][$related_table][$route])) { return array(); @@ -516,7 +540,10 @@ static public function getPrimaryKeys($class, &$values, &$related_records, $rela $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); if (!isset($related_records[$related_table])) { $related_records[$related_table] = array(); @@ -532,48 +559,60 @@ static public function getPrimaryKeys($class, &$values, &$related_records, $rela // If we don't have a record set yet we want to use a single SQL query to just get the primary keys } else { - $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-many'); - $related_pk_columns = fORMSchema::retrieve()->getKeys($related_table, 'primary'); - $column_info = fORMSchema::retrieve()->getColumnInfo($related_table); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many'); + $related_pk_columns = $schema->getKeys($related_table, 'primary'); + $column_info = $schema->getColumnInfo($related_table); + $column = $relationship['column']; + + $new_related_pk_columns = array(); + foreach ($related_pk_columns as $related_pk_column) { + $new_related_pk_columns[] = $related_table . '.' . $related_pk_column; + } + $related_pk_columns = $new_related_pk_columns; + + if (isset($relationship['join_table'])) { + $table_with_route = $table . '{' . $relationship['join_table'] . '}'; + } else { + $table_with_route = $table . '{' . $relationship['related_column'] . '}'; + } $column = $relationship['column']; $related_column = $relationship['related_column']; - if (!isset($relationship['join_table'])) { - - $result = fORMDatabase::retrieve()->translatedQuery( - "SELECT " . join(', ', $related_pk_columns) . " FROM " . $related_table . - " WHERE " . $related_column . fORMDatabase::escapeBySchema($table, $relationship['column'], $values[$column], '=') - ); - - } else { + $params = array( + $db->escape( + "SELECT %r FROM :from_clause WHERE %r = ", + $related_pk_columns, + $table_with_route . '.' . $column + ), + ); + $params[0] .= $schema->getColumnInfo($table, $column, 'placeholder'); + $params[] = $values[$column]; - // Add the related table to each pk column to ensure we don't get ambiguity SQL errors - foreach ($related_pk_columns as &$related_pk_column) { - $related_pk_column = $related_table . "." . $related_pk_column; - } - - $result = fORMDatabase::retrieve()->translatedQuery( - "SELECT " . join(', ', $related_pk_columns) . " FROM " . $related_table . " INNER JOIN " . $relationship['join_table'] . - " ON " . $related_table . "." . $related_column . " = " . $relationship['join_table'] . "." . $relationship['join_related_column'] . - " WHERE " . $relationship['join_table'] . "." . $relationship['join_column'] . fORMDatabase::escapeBySchema($table, $relationship['column'], $values[$column], '=') - ); - + $params[0] .= " :group_by_clause "; + + if ($order_bys = self::getOrderBys($class, $related_class, $route)) { + $params[0] .= " ORDER BY "; + $params = fORMDatabase::addOrderByClause($db, $schema, $params, $related_table, $order_bys); } + $params = fORMDatabase::injectFromAndGroupByClauses($db, $schema, $params, $related_table); + + $result = call_user_func_array($db->translatedQuery, $params); + $primary_keys = array(); foreach ($result as $row) { if (sizeof($row) > 1) { $primary_key = array(); foreach ($row as $column => $value) { - $value = fORMDatabase::retrieve()->unescape($column_info[$column]['type'], $value); + $value = $db->unescape($column_info[$column]['type'], $value); $primary_key[$column] = $value; } $primary_keys[] = $primary_key; } else { $column = key($row); - $primary_keys[] = fORMDatabase::retrieve()->unescape($column_info[$column]['type'], $row[$column]); + $primary_keys[] = $db->unescape($column_info[$column]['type'], $row[$column]); } } @@ -605,7 +644,8 @@ static public function getRelatedRecordName($class, $related_class, $route=NULL) $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route); if (!isset(self::$related_record_names[$table]) || !isset(self::$related_record_names[$table][$related_class]) || @@ -633,8 +673,9 @@ static public function linkRecords($class, &$related_records, $related_class, $r $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route_name = fORMSchema::getRouteName($table, $related_table, $route, 'many-to-many'); - $relationship = fORMSchema::getRoute($table, $related_table, $route, 'many-to-many'); + $schema = fORMSchema::retrieve(); + $route_name = fORMSchema::getRouteName($schema, $table, $related_table, $route, 'many-to-many'); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, 'many-to-many'); $field_table = $relationship['related_table']; $field_column = '::' . $relationship['related_column']; @@ -702,7 +743,8 @@ static public function overrideRelatedRecordName($class, $related_class, $record $related_class = fORM::getClass($related_class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); if (!isset(self::$related_record_names[$table])) { self::$related_record_names[$table] = array(); @@ -731,7 +773,8 @@ static public function populateRecords($class, &$related_records, $related_class { $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $pk_columns = fORMSchema::retrieve()->getKeys($related_table, 'primary'); + $schema = fORMSchema::retrieve(); + $pk_columns = $schema->getKeys($related_table, 'primary'); $first_pk_column = self::determineFirstPKColumn($class, $related_class, $route); @@ -785,12 +828,13 @@ static public function populateRecords($class, &$related_records, $related_class */ static public function reflect($class, &$signatures, $include_doc_comments) { - $table = fORM::tablize($class); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); - $one_to_one_relationships = fORMSchema::retrieve()->getRelationships($table, 'one-to-one'); - $one_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'one-to-many'); - $many_to_one_relationships = fORMSchema::retrieve()->getRelationships($table, 'many-to-one'); - $many_to_many_relationships = fORMSchema::retrieve()->getRelationships($table, 'many-to-many'); + $one_to_one_relationships = $schema->getRelationships($table, 'one-to-one'); + $one_to_many_relationships = $schema->getRelationships($table, 'one-to-many'); + $many_to_one_relationships = $schema->getRelationships($table, 'many-to-one'); + $many_to_many_relationships = $schema->getRelationships($table, 'many-to-many'); $to_one_relationships = array_merge($one_to_one_relationships, $many_to_one_relationships); $to_many_relationships = array_merge($one_to_many_relationships, $many_to_many_relationships); @@ -804,7 +848,7 @@ static public function reflect($class, &$signatures, $include_doc_comments) continue; } - $routes = fORMSchema::getRoutes($table, $relationship['related_table'], '*-to-one'); + $routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], '*-to-one'); $route_names = array(); foreach ($routes as $route) { @@ -843,7 +887,7 @@ static public function reflect($class, &$signatures, $include_doc_comments) continue; } - $routes = fORMSchema::getRoutes($table, $relationship['related_table'], 'one-to-one'); + $routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], 'one-to-one'); $route_names = array(); foreach ($routes as $route) { @@ -903,7 +947,7 @@ static public function reflect($class, &$signatures, $include_doc_comments) continue; } - $routes = fORMSchema::getRoutes($table, $relationship['related_table'], '*-to-many'); + $routes = fORMSchema::getRoutes($schema, $table, $relationship['related_table'], '*-to-many'); $route_names = array(); $many_to_many_route_names = array(); @@ -1073,7 +1117,8 @@ static public function setOrderBys($class, $related_class, $order_bys, $route=NU $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); if (!isset(self::$order_bys[$table])) { self::$order_bys[$table] = array(); @@ -1105,7 +1150,8 @@ static public function setCount($class, &$related_records, $related_class, $coun $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); // Cache the results for subsequent calls if (!isset($related_records[$related_table])) { @@ -1142,7 +1188,8 @@ static public function setPrimaryKeys($class, &$related_records, $related_class, $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); if (!isset($related_records[$related_table])) { $related_records[$related_table] = array(); @@ -1175,11 +1222,8 @@ static public function setRecordSet($class, &$related_records, $related_class, f $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - try { - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); - } catch (fProgrammerException $e) { - $route = fORMSchema::getRouteName($table, $related_table, $route, 'one-to-one'); - } + $schema = fORMSchema::retrieve(); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '!many-to-one'); if (!isset($related_records[$related_table])) { $related_records[$related_table] = array(); @@ -1207,7 +1251,8 @@ static public function setRecordSet($class, &$related_records, $related_class, f */ static public function store($class, &$values, &$related_records) { - $table = fORM::tablize($class); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); foreach ($related_records as $related_table => $relationships) { foreach ($relationships as $route => $related_info) { @@ -1215,9 +1260,9 @@ static public function store($class, &$values, &$related_records) continue; } - $relationship = fORMSchema::getRoute($table, $related_table, $route); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); if (isset($relationship['join_table'])) { - fORMRelated::storeManyToMany($values, $relationship, $related_info); + fORMRelated::storeManyToMany($class, $values, $relationship, $related_info); } else { fORMRelated::storeOneToStar($class, $values, $related_records, fORM::classize($related_table), $route); } @@ -1231,30 +1276,35 @@ static public function store($class, &$values, &$related_records) * * @internal * - * @param array &$values The current values for the main record being stored - * @param array $relationship The information about the relationship between this object and the records in the record set - * @param array $related_info An array containing the keys `'record_set'`, `'count'`, `'primary_keys'` and `'associate'` + * @param string $class The class the relationship is being stored for + * @param array &$values The current values for the main record being stored + * @param array $relationship The information about the relationship between this object and the records in the record set + * @param array $related_info An array containing the keys `'record_set'`, `'count'`, `'primary_keys'` and `'associate'` * @return void */ - static public function storeManyToMany(&$values, $relationship, $related_info) + static public function storeManyToMany($class, &$values, $relationship, $related_info) { + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $column_value = $values[$relationship['column']]; // First, we remove all existing relationships between the two tables $join_table = $relationship['join_table']; $join_column = $relationship['join_column']; - $join_column_value = fORMDatabase::escapeBySchema($join_table, $join_column, $column_value); - - $delete_sql = 'DELETE FROM ' . $join_table; - $delete_sql .= ' WHERE ' . $join_column . ' = ' . $join_column_value; - - fORMDatabase::retrieve()->translatedQuery($delete_sql); + $params = array( + "DELETE FROM %r WHERE " . fORMDatabase::makeCondition($schema, $join_table, $join_column, '=', $column_value), + $join_table, + $join_column, + $column_value + ); + call_user_func_array($db->translatedQuery, $params); // Then we add back the ones in the record set $join_related_column = $relationship['join_related_column']; - $related_pk_columns = fORMSchema::retrieve()->getKeys($relationship['related_table'], 'primary'); + $related_pk_columns = $schema->getKeys($relationship['related_table'], 'primary'); $related_column_values = array(); @@ -1280,13 +1330,19 @@ static public function storeManyToMany(&$values, $relationship, $related_info) // Ensure we aren't storing duplicates $related_column_values = array_unique($related_column_values); + $join_column_placeholder = $schema->getColumnInfo($join_table, $join_column, 'placeholder'); + $related_column_placeholder = $schema->getColumnInfo($join_table, $join_related_column, 'placeholder'); + foreach ($related_column_values as $related_column_value) { - $related_column_value = fORMDatabase::escapeBySchema($join_table, $join_related_column, $related_column_value); - - $insert_sql = 'INSERT INTO ' . $join_table . ' (' . $join_column . ', ' . $join_related_column . ') '; - $insert_sql .= 'VALUES (' . $join_column_value . ', ' . $related_column_value . ')'; - - fORMDatabase::retrieve()->translatedQuery($insert_sql); + $params = array( + "INSERT INTO %r (%r, %r) VALUES (" . $join_column_placeholder . ", " . $related_column_placeholder . ")", + $join_table, + $join_column, + $join_related_column, + $column_value, + $related_column_value + ); + call_user_func_array($db->translatedQuery, $params); } } @@ -1309,7 +1365,8 @@ static public function storeOneToStar($class, &$values, &$related_records, $rela $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); - $relationship = fORMSchema::getRoute($table, $related_table, $route); + $schema = fORMSchema::retrieve(); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); $column_value = $values[$relationship['column']]; if (!empty($related_records[$related_table][$route]['record_set'])) { @@ -1367,7 +1424,8 @@ static public function storeOneToStar($class, &$values, &$related_records, $rela */ static public function validate($class, &$values, &$related_records) { - $table = fORM::tablize($class); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); $validation_messages = array(); @@ -1379,7 +1437,7 @@ static public function validate($class, &$values, &$related_records) } $related_class = fORM::classize($related_table); - $relationship = fORMSchema::getRoute($table, $related_table, $route); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route); if (isset($relationship['join_table'])) { $related_messages = self::validateManyToMany($class, $related_class, $route, $related_info); @@ -1407,6 +1465,7 @@ static public function validate($class, &$values, &$related_records) */ static private function validateOneToStar($class, &$values, &$related_records, $related_class, $route) { + $schema = fORMSchema::retrieve(); $table = fORM::tablize($class); $related_table = fORM::tablize($related_class); @@ -1419,7 +1478,7 @@ static private function validateOneToStar($class, &$values, &$related_records, $ $messages = array(); - $one_to_one = fORMSchema::isOneToOne($table, $related_table, $route); + $one_to_one = fORMSchema::isOneToOne($schema, $table, $related_table, $route); if ($one_to_one) { $records = array(self::createRecord($class, $values, $related_records, $related_class, $route)); diff --git a/classes/fORMSchema.php b/classes/fORMSchema.php index a0c378d9..50d3d0ae 100644 --- a/classes/fORMSchema.php +++ b/classes/fORMSchema.php @@ -9,7 +9,8 @@ * @package Flourish * @link http://flourishlib.com/fORMSchema * - * @version 1.0.0b5 + * @version 1.0.0b6 + * @changes 1.0.0b6 Internal Backwards Compatibility Break - Added the `$schema` parameter to the beginning of ::getRoute(), ::getRouteName(), ::getRoutes() and ::isOneToOne() - added '!many-to-one' relationship type handling [wb, 2009-10-22] * @changes 1.0.0b5 Fixed some error messaging to not include {empty_string} in some situations [wb, 2009-07-31] * @changes 1.0.0b4 Added ::isOneToOne() [wb, 2009-07-21] * @changes 1.0.0b3 Added routes caching for performance [wb, 2009-06-15] @@ -64,13 +65,14 @@ static public function attach($schema) * * @internal * - * @param string $table The main table we are searching on behalf of - * @param string $related_table The related table we are searching under - * @param string $route The route to get info about - * @param string $relationship_type The relationship type: `NULL`, `'*-to-many'`, `'*-to-one'`, `'one-to-one'`, `'one-to-meny'`, `'many-to-one'`, `'many-to-many'` + * @param fSchema $schema The schema object to get the route from + * @param string $table The main table we are searching on behalf of + * @param string $related_table The related table we are searching under + * @param string $route The route to get info about + * @param string $relationship_type The relationship type: `NULL`, `'*-to-many'`, `'*-to-one'`, `'one-to-one'`, `'one-to-meny'`, `'many-to-one'`, `'many-to-many'` * @return void */ - static public function getRoute($table, $related_table, $route, $relationship_type=NULL) + static public function getRoute($schema, $table, $related_table, $route, $relationship_type=NULL) { $valid_relationship_types = array( NULL, @@ -91,10 +93,10 @@ static public function getRoute($table, $related_table, $route, $relationship_ty } if ($route === NULL) { - $route = self::getRouteName($table, $related_table, $route, $relationship_type); + $route = self::getRouteName($schema, $table, $related_table, $route, $relationship_type); } - $routes = self::getRoutes($table, $related_table, $relationship_type); + $routes = self::getRoutes($schema, $table, $related_table, $relationship_type); if (!isset($routes[$route])) { $relationship_type .= ($relationship_type) ? ' ' : ''; @@ -116,18 +118,20 @@ static public function getRoute($table, $related_table, $route, $relationship_ty * * @internal * - * @param string $table The main table we are searching on behalf of - * @param string $related_table The related table we are trying to find the routes for - * @param string $route The route that was preselected, will be verified if present - * @param string $relationship_type The relationship type: `NULL`, `'*-to-many'`, `'*-to-one'`, `'one-to-one'`, `'one-to-meny'`, `'many-to-one'`, `'many-to-many'` + * @param fSchema $schema The schema object to get the route name from + * @param string $table The main table we are searching on behalf of + * @param string $related_table The related table we are trying to find the routes for + * @param string $route The route that was preselected, will be verified if present + * @param string $relationship_type The relationship type: `NULL`, `'*-to-many'`, `'*-to-one'`, `'!many-to-one'`, `'one-to-one'`, `'one-to-meny'`, `'many-to-one'`, `'many-to-many'` * @return string The only route from the main table to the related table */ - static public function getRouteName($table, $related_table, $route=NULL, $relationship_type=NULL) + static public function getRouteName($schema, $table, $related_table, $route=NULL, $relationship_type=NULL) { $valid_relationship_types = array( NULL, '*-to-many', '*-to-one', + '!many-to-one', 'many-to-many', 'many-to-one', 'one-to-many', @@ -142,10 +146,19 @@ static public function getRouteName($table, $related_table, $route=NULL, $relati ); } - $routes = self::getRoutes($table, $related_table, $relationship_type); + $routes = self::getRoutes($schema, $table, $related_table, $relationship_type); - if (!empty($route) && isset($routes[$route])) { - return $route; + if (!empty($route)) { + if (isset($routes[$route])) { + return $route; + } + throw new fProgrammerException( + 'The route specified, %1$s, is not a valid route between %2$s and %3$s. Must be one of: %4$s.', + $route, + $table, + $related_table, + join(', ', array_keys($routes)) + ); } $keys = array_keys($routes); @@ -208,12 +221,13 @@ static public function getRouteNameFromRelationship($type, $relationship) * * @internal * - * @param string $table The main table we are searching on behalf of - * @param string $related_table The related table we are trying to find the routes for - * @param string $relationship_type The relationship type: `NULL`, `'*-to-many'`, `'*-to-one'`, `'one-to-one'`, `'one-to-meny'`, `'many-to-one'`, `'many-to-many'` + * @param fSchema $schema The schema object to get the routes for + * @param string $table The main table we are searching on behalf of + * @param string $related_table The related table we are trying to find the routes for + * @param string $relationship_type The relationship type: `NULL`, `'*-to-many'`, `'*-to-one'`, `'!many-to-one'`, `'one-to-one'`, `'one-to-meny'`, `'many-to-one'`, `'many-to-many'` * @return array All of the routes from the main table to the related table */ - static public function getRoutes($table, $related_table, $relationship_type=NULL) + static public function getRoutes($schema, $table, $related_table, $relationship_type=NULL) { $key = $table . '::' . $related_table . '::' . $relationship_type; if (isset(self::$cache['getRoutes'][$key])) { @@ -224,6 +238,7 @@ static public function getRoutes($table, $related_table, $relationship_type=NULL NULL, '*-to-many', '*-to-one', + '!many-to-one', 'many-to-many', 'many-to-one', 'one-to-many', @@ -238,7 +253,7 @@ static public function getRoutes($table, $related_table, $relationship_type=NULL ); } - $all_relationships = self::retrieve()->getRelationships($table); + $all_relationships = $schema->getRelationships($table); $routes = array(); @@ -246,9 +261,14 @@ static public function getRoutes($table, $related_table, $relationship_type=NULL // Filter the relationships by the relationship type if ($relationship_type !== NULL) { - $match = strpos($type, str_replace('*', '', $relationship_type)) !== FALSE; - if (!$match) { - continue; + if ($relationship_type == '!many-to-one') { + if ($type == 'many-to-one') { + continue; + } + } else { + if (strpos($type, str_replace('*', '', $relationship_type)) === FALSE) { + continue; + } } } @@ -276,14 +296,15 @@ static public function getRoutes($table, $related_table, $relationship_type=NULL * * @internal * - * @param string $table The main table we are searching on behalf of - * @param string $related_table The related table we are trying to find the routes for - * @param string $route The route between the two tables + * @param fSchema $schema The schema object the tables are from + * @param string $table The main table we are searching on behalf of + * @param string $related_table The related table we are trying to find the routes for + * @param string $route The route between the two tables * @return boolean If the table is in a one-to-one relationship with the related table over the route specified */ - static public function isOneToOne($table, $related_table, $route=NULL) + static public function isOneToOne($schema, $table, $related_table, $route=NULL) { - $relationships = self::getRoutes($table, $related_table, 'one-to-one', $route); + $relationships = self::getRoutes($schema, $table, $related_table, 'one-to-one', $route); if ($route === NULL && sizeof($relationships) > 1) { throw new fProgrammerException( diff --git a/classes/fORMValidation.php b/classes/fORMValidation.php index d0f82629..58cfb5d7 100644 --- a/classes/fORMValidation.php +++ b/classes/fORMValidation.php @@ -10,7 +10,8 @@ * @package Flourish * @link http://flourishlib.com/fORMValidation * - * @version 1.0.0b18 + * @version 1.0.0b19 + * @changes 1.0.0b19 Changed SQL statements to use value placeholders, identifier escaping and schema support [wb, 2009-10-22] * @changes 1.0.0b18 Fixed ::checkOnlyOneRule() and ::checkOneOrMoreRule() to consider blank strings as NULL [wb, 2009-08-21] * @changes 1.0.0b17 Added @internal methods ::removeStringReplacement() and ::removeRegexReplacement() [wb, 2009-07-29] * @changes 1.0.0b16 Backwards Compatibility Break - renamed ::addConditionalValidationRule() to ::addConditionalRule(), ::addManyToManyValidationRule() to ::addManyToManyRule(), ::addOneOrMoreValidationRule() to ::addOneOrMoreRule(), ::addOneToManyValidationRule() to ::addOneToManyRule(), ::addOnlyOneValidationRule() to ::addOnlyOneRule(), ::addValidValuesValidationRule() to ::addValidValuesRule() [wb, 2009-07-13] @@ -174,6 +175,7 @@ static public function addManyToManyRule($class, $related_class, $route=NULL) } $route = fORMSchema::getRouteName( + fORMSchema::retrieve(), fORM::tablize($class), fORM::tablize($related_class), $route, @@ -229,6 +231,7 @@ static public function addOneToManyRule($class, $related_class, $route=NULL) } $route = fORMSchema::getRouteName( + fORMSchema::retrieve(), fORM::tablize($class), fORM::tablize($related_class), $route, @@ -367,7 +370,8 @@ static private function checkAgainstSchema($object, $column, &$values, &$old_val $class = get_class($object); $table = fORM::tablize($class); - $column_info = fORMSchema::retrieve()->getColumnInfo($table, $column); + $schema = fORMSchema::retrieve(); + $column_info = $schema->getColumnInfo($table, $column); // Make sure a value is provided for required columns if ($values[$column] === NULL && $column_info['not_null'] && $column_info['default'] === NULL && $column_info['auto_increment'] === FALSE) { return self::compose( @@ -463,7 +467,8 @@ static private function checkConditionalRule($class, &$values, $main_columns, $c static private function checkDataType($class, $column, $value) { $table = fORM::tablize($class); - $column_info = fORMSchema::retrieve()->getColumnInfo($table, $column); + $schema = fORMSchema::retrieve(); + $column_info = $schema->getColumnInfo($table, $column); if ($value !== NULL) { switch ($column_info['type']) { @@ -537,20 +542,26 @@ static private function checkForeignKeyConstraints($class, $column, &$values) return; } + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); - $foreign_keys = fORMSchema::retrieve()->getKeys($table, 'foreign'); + $foreign_keys = $schema->getKeys($table, 'foreign'); foreach ($foreign_keys AS $foreign_key) { if ($foreign_key['column'] == $column) { try { - $sql = "SELECT " . $foreign_key['foreign_column']; - $sql .= " FROM " . $foreign_key['foreign_table']; - $sql .= " WHERE "; - $sql .= $column . fORMDatabase::escapeBySchema($table, $column, $values[$column], '='); - $sql = str_replace('WHERE ' . $column, 'WHERE ' . $foreign_key['foreign_column'], $sql); - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $params = array( + "SELECT %r FROM %r WHERE " . fORMDatabase::makeCondition($schema, $table, $column, '=', $values[$column]), + $foreign_key['foreign_column'], + $foreign_key['foreign_table'], + $foreign_key['foreign_column'], + $values[$column] + ); + $result = call_user_func_array($db->translatedQuery, $params); $result->tossIfNoRows(); + } catch (fNoRowsException $e) { return self::compose( '%sThe value specified is invalid', @@ -646,13 +657,16 @@ static private function checkPrimaryKeys($object, &$values, &$old_values) $class = get_class($object); $table = fORM::tablize($class); - $primary_keys = fORMSchema::retrieve()->getKeys($table, 'primary'); - $columns = array(); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + + $pk_columns = $schema->getKeys($table, 'primary'); + $columns = array(); $found_value = FALSE; - foreach ($primary_keys as $primary_key) { - $columns[] = fORM::getColumnName($class, $primary_key); - if ($values[$primary_key]) { + foreach ($pk_columns as $pk_column) { + $columns[] = fORM::getColumnName($class, $pk_column); + if ($values[$pk_column]) { $found_value = TRUE; } } @@ -662,13 +676,13 @@ static private function checkPrimaryKeys($object, &$values, &$old_values) } $different = FALSE; - foreach ($primary_keys as $primary_key) { - if (!fActiveRecord::hasOld($old_values, $primary_key)) { + foreach ($pk_columns as $pk_column) { + if (!fActiveRecord::hasOld($old_values, $pk_column)) { continue; } - $old_value = fActiveRecord::retrieveOld($old_values, $primary_key); - $value = $values[$primary_key]; - if (self::isCaseInsensitive($class, $primary_key) && self::stringlike($value) && self::stringlike($old_value)) { + $old_value = fActiveRecord::retrieveOld($old_values, $pk_column); + $value = $values[$pk_column]; + if (self::isCaseInsensitive($class, $pk_column) && self::stringlike($value) && self::stringlike($old_value)) { if (fUTF8::lower($value) != fUTF8::lower($old_value)) { $different = TRUE; } @@ -682,18 +696,30 @@ static private function checkPrimaryKeys($object, &$values, &$old_values) } try { - $sql = "SELECT " . join(', ', $primary_keys) . " FROM " . $table . " WHERE "; + $params = array( + "SELECT %r FROM %r WHERE ", + $pk_columns, + $table + ); + $conditions = array(); - foreach ($primary_keys as $primary_key) { - if (self::isCaseInsensitive($class, $primary_key) && self::stringlike($values[$primary_key])) { - $conditions[] = 'LOWER(' . $primary_key . ')' . fORMDatabase::escapeBySchema($table, $primary_key, fUTF8::lower($values[$primary_key]), '='); + foreach ($pk_columns as $pk_column) { + $value = $values[$pk_column]; + if (self::isCaseInsensitive($class, $pk_column) && self::stringlike($value)) { + $condition = fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value); + $conditions[] = str_replace('%r', 'LOWER(%r)', $condition); + $params[] = $pk_column; + $params[] = fUTF8::lower($value); + } else { - $conditions[] = $primary_key . fORMDatabase::escapeBySchema($table, $primary_key, $values[$primary_key], '='); + $conditions[] = fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value); + $params[] = $pk_column; + $params[] = $value; } } - $sql .= join(' AND ', $conditions); + $params[0] .= join(' AND ', $conditions); - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $result = call_user_func_array($db->translatedQuery, $params); $result->tossIfNoRows(); return self::compose( @@ -753,10 +779,13 @@ static private function checkUniqueConstraints($object, &$values, &$old_values) $class = get_class($object); $table = fORM::tablize($class); - $key_info = fORMSchema::retrieve()->getKeys($table); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); - $primary_keys = $key_info['primary']; - $unique_keys = $key_info['unique']; + $key_info = $schema->getKeys($table); + + $pk_columns = $key_info['primary']; + $unique_keys = $key_info['unique']; foreach ($unique_keys AS $unique_columns) { settype($unique_columns, 'array'); @@ -772,27 +801,41 @@ static private function checkUniqueConstraints($object, &$values, &$old_values) continue; } - $sql = "SELECT " . join(', ', $key_info['primary']) . " FROM " . $table . " WHERE "; - $first = TRUE; + $params = array( + "SELECT %r FROM %r WHERE ", + $key_info['primary'], + $table + ); + + $conditions = array(); foreach ($unique_columns as $unique_column) { - if ($first) { $first = FALSE; } else { $sql .= " AND "; } $value = $values[$unique_column]; if (self::isCaseInsensitive($class, $unique_column) && self::stringlike($value)) { - $sql .= 'LOWER(' . $unique_column . ')' . fORMDatabase::escapeBySchema($table, $unique_column, fUTF8::lower($value), '='); + $condition = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value); + $conditions[] = str_replace('%r', 'LOWER(%r)', $condition); + $params[] = $table . '.' . $unique_column; + $params[] = fUTF8::lower($value); + } else { - $sql .= $unique_column . fORMDatabase::escapeBySchema($table, $unique_column, $value, '='); + $conditions[] = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value); + $params[] = $table . '.' . $unique_column; + $params[] = $value; } } + $params[0] .= join(' AND ', $conditions); + if ($object->exists()) { - foreach ($primary_keys as $primary_key) { - $value = fActiveRecord::retrieveOld($old_values, $primary_key, $values[$primary_key]); - $sql .= ' AND ' . $primary_key . fORMDatabase::escapeBySchema($table, $primary_key, $value, '<>'); + foreach ($pk_columns as $pk_column) { + $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]); + $params[0] .= ' AND ' . fORMDatabase::makeCondition($schema, $table, $pk_column, '<>', $value); + $params[] = $table . '.' . $pk_column; + $params[] = $value; } } try { - $result = fORMDatabase::retrieve()->translatedQuery($sql); + $result = call_user_func_array($db->translatedQuery, $params); $result->tossIfNoRows(); // If an exception was not throw, we have existing values @@ -1104,10 +1147,11 @@ static public function reset() */ static public function setColumnCaseInsensitive($class, $column) { - $class = fORM::getClass($class); - $table = fORM::tablize($class); + $class = fORM::getClass($class); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); - $type = fORMSchema::retrieve()->getColumnInfo($table, $column, 'type'); + $type = $schema->getColumnInfo($table, $column, 'type'); $valid_types = array('varchar', 'char', 'text'); if (!in_array($type, $valid_types)) { throw new fProgrammerException( @@ -1191,8 +1235,9 @@ static private function stringlike($value) */ static public function validate($object, $values, $old_values) { - $class = get_class($object); - $table = fORM::tablize($class); + $class = get_class($object); + $table = fORM::tablize($class); + $schema = fORMSchema::retrieve(); self::initializeRuleArrays($class); @@ -1211,7 +1256,7 @@ static public function validate($object, $values, $old_values) $message = self::checkPrimaryKeys($object, $values, $old_values); if ($message) { $validation_messages[] = $message; } - $column_info = fORMSchema::retrieve()->getColumnInfo($table); + $column_info = $schema->getColumnInfo($table); foreach ($column_info as $column => $info) { $message = self::checkAgainstSchema($object, $column, $values, $old_values); if ($message) { $validation_messages[] = $message; } diff --git a/classes/fRecordSet.php b/classes/fRecordSet.php index 6191ae0a..5188fd98 100644 --- a/classes/fRecordSet.php +++ b/classes/fRecordSet.php @@ -9,7 +9,8 @@ * @package Flourish * @link http://flourishlib.com/fRecordSet * - * @version 1.0.0b27 + * @version 1.0.0b28 + * @changes 1.0.0b28 Fixed ::prebuild() and ::precount() to work across all databases, changed SQL statements to use value placeholders, identifier escaping and schema support [wb, 2009-10-22] * @changes 1.0.0b27 Changed fRecordSet::build() to fix bad $page numbers instead of throwing an fProgrammerException [wb, 2009-10-05] * @changes 1.0.0b26 Updated the documentation for ::build() and ::filter() to reflect new functionality [wb, 2009-09-21] * @changes 1.0.0b25 Fixed ::map() to work with string-style static method callbacks in PHP 5.1 [wb, 2009-09-18] @@ -166,46 +167,60 @@ static public function build($class, $where_conditions=array(), $order_bys=array // Ensure that the class has been configured fActiveRecord::forceConfigure($class); - $table = fORM::tablize($class); + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $table = fORM::tablize($class); - $sql = "SELECT " . $table . ".* FROM :from_clause"; + $params = array($db->escape("SELECT %r.* FROM :from_clause", $table)); if ($where_conditions) { $having_conditions = fORMDatabase::splitHavingConditions($where_conditions); + } else { + $having_conditions = NULL; + } - $sql .= ' WHERE ' . fORMDatabase::createWhereClause($table, $where_conditions); + if ($where_conditions) { + $params[0] .= ' WHERE '; + $params = fORMDatabase::addWhereClause($db, $schema, $params, $table, $where_conditions); } - $sql .= ' :group_by_clause '; + $params[0] .= ' :group_by_clause '; - if ($where_conditions && $having_conditions) { - $sql .= ' HAVING ' . fORMDatabase::createHavingClause($having_conditions); + if ($having_conditions) { + $params[0] .= ' HAVING '; + $params = fORMDatabase::addHavingClause($db, $schema, $params, $table, $having_conditions); } - if ($order_bys) { - $sql .= ' ORDER BY ' . fORMDatabase::createOrderByClause($table, $order_bys); - // If no ordering is specified, order by the primary key - } elseif ($primary_keys = fORMSchema::retrieve()->getKeys($table, 'primary')) { - $expressions = array(); - foreach ($primary_keys as $primary_key) { - $expressions[] = $table . '.' . $primary_key . ' ASC'; + if (!$order_bys) { + $order_bys = array(); + foreach ($schema->getKeys($table, 'primary') as $pk_column) { + $order_bys[$table . '.' . $pk_column] = 'ASC'; } - $sql .= ' ORDER BY ' . join(', ', $expressions); } - $sql = fORMDatabase::insertFromAndGroupByClauses($table, $sql); + $params[0] .= ' ORDER BY '; + $params = fORMDatabase::addOrderByClause($db, $schema, $params, $table, $order_bys); + + $params = fORMDatabase::injectFromAndGroupByClauses($db, $schema, $params, $table); // Add the limit clause and create a query to get the non-limited total $non_limited_count_sql = NULL; if ($limit !== NULL) { - $primary_key_fields = fORMSchema::retrieve()->getKeys($table, 'primary'); - $primary_key_fields = fORMDatabase::addTableToValues($table, $primary_key_fields); + $pk_columns = array(); + foreach ($schema->getKeys($table, 'primary') as $pk_column) { + $pk_columns[] = $table . '.' . $pk_column; + } - $non_limited_count_sql = str_replace('SELECT ' . $table . '.*', 'SELECT ' . join(', ', $primary_key_fields), $sql); - $non_limited_count_sql = 'SELECT count(*) FROM (' . $non_limited_count_sql . ') AS sq'; + $non_limited_count_sql = str_replace( + $db->escape('SELECT %r.*', $table), + $db->escape('SELECT %r', $pk_columns), + $params[0] + ); + $non_limited_count_sql = preg_replace('#\s+ORDER BY.*$#', '', $non_limited_count_sql); + $non_limited_count_sql = $db->escape('SELECT count(*) FROM (' . $non_limited_count_sql . ') subquery', array_slice($params, 1)); - $sql .= ' LIMIT ' . $limit; + $params[0] .= ' LIMIT ' . $limit; if ($page !== NULL) { @@ -213,11 +228,11 @@ static public function build($class, $where_conditions=array(), $order_bys=array $page = 1; } - $sql .= ' OFFSET ' . (($page-1) * $limit); + $params[0] .= ' OFFSET ' . (($page-1) * $limit); } } - return new fRecordSet($class, fORMDatabase::retrieve()->translatedQuery($sql), $non_limited_count_sql); + return new fRecordSet($class, call_user_func_array($db->translatedQuery, $params), $non_limited_count_sql); } @@ -294,16 +309,18 @@ static public function buildFromSQL($class, $sql, $non_limited_count_sql=NULL) { self::validateClass($class); - if (!preg_match('#^\s*SELECT\s*(DISTINCT|ALL)?\s*(\w+\.)?\*\s*FROM#i', $sql)) { + if (!preg_match('#^\s*SELECT\s*(DISTINCT|ALL)?\s*(("?\w+"?\.)?"?\w+"?\.)?\*\s*FROM#i', $sql)) { throw new fProgrammerException( 'The SQL statement specified, %s, does not appear to be in the form SELECT * FROM table', $sql ); } + $db = fORMDatabase::retrieve(); + return new fRecordSet( $class, - fORMDatabase::retrieve()->translatedQuery($sql), + $db->translatedQuery($sql), $non_limited_count_sql ); } @@ -492,6 +509,123 @@ public function __get($method) } + /** + * Adds an `ORDER BY` clause to the SQL for the primary keys of this record set + * + * @param fDatabase $db The database the query will be executed on + * @param fSchema $schema The schema for the database + * @param array $params The parameters for the fDatabase::query() call + * @param string $related_class The related class to add the order bys for + * @param string $route The route to this table from another table + * @return array The params with the `ORDER BY` clause added + */ + private function addOrderByParams($db, $schema, $params, $related_class, $route=NULL) + { + $table = fORM::tablize($this->class); + $table_with_route = ($route) ? $table . '{' . $route . '}' : $table; + + $pk_columns = $schema->getKeys($table, 'primary'); + $first_pk_column = $pk_columns[0]; + + $escaped_pk_columns = array(); + foreach ($pk_columns as $pk_column) { + $escaped_pk_columns[$pk_column] = $db->escape('%r', $table_with_route . '.' . $pk_column); + } + + $sql = ''; + $number = 0; + foreach ($this->getPrimaryKeys() as $primary_key) { + $sql .= 'WHEN '; + + if (is_array($primary_key)) { + $conditions = array(); + foreach ($pk_columns as $pk_column) { + $conditions[] = str_replace( + '%r', + $escaped_pk_columns[$pk_column], + fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $primary_key[$pk_column]) + ); + $params[] = $primary_key[$pk_column]; + } + $sql .= join(' AND ', $conditions); + + } else { + $sql .= str_replace( + '%r', + $escaped_pk_columns[$first_pk_column], + fORMDatabase::makeCondition($schema, $table, $first_pk_column, '=', $primary_key) + ); + $params[] = $primary_key; + } + + $sql .= ' THEN ' . $number . ' '; + + $number++; + } + + $params[0] .= 'CASE ' . $sql . 'END ASC'; + + if ($related_order_bys = fORMRelated::getOrderBys($this->class, $related_class, $route)) { + $params[0] .= ', '; + $params = fORMDatabase::addOrderByClause($db, $schema, $params, fORM::tablize($related_class), $related_order_bys); + } + + return $params; + } + + + /** + * Adds `WHERE` params to the SQL for the primary keys of this record set + * + * @param fDatabase $db The database the query will be executed on + * @param fSchema $schema The schema for the database + * @param array $params The parameters for the fDatabase::query() call + * @param string $route The route to this table from another table + * @return array The params with the `WHERE` clause added + */ + private function addWhereParams($db, $schema, $params, $route=NULL) + { + $table = fORM::tablize($this->class); + $table_with_route = ($route) ? $table . '{' . $route . '}' : $table; + + $pk_columns = $schema->getKeys($table, 'primary'); + + // We have a multi-field primary key, making things kinda ugly + if (sizeof($pk_columns) > 1) { + + $escape_pk_columns = array(); + foreach ($pk_columns as $pk_column) { + $escaped_pk_columns[$pk_column] = $db->escape('%r', $table_with_route . '.' . $pk_column); + } + + $conditions = array(); + + foreach ($this->getPrimaryKeys() as $primary_key) { + $sub_conditions = array(); + foreach ($pk_columns as $pk_column) { + $sub_conditions[] = str_replace( + '%r', + $escaped_pk_columns[$pk_column], + fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $primary_key[$pk_column]) + ); + $params[] = $primary_key[$pk_column]; + } + $conditions[] = join(' AND ', $sub_conditions); + } + $params[0] .= '(' . join(') OR (', $conditions) . ')'; + + // We have a single primary key field, making things nice and easy + } else { + $first_pk_column = $pk_columns[0]; + $params[0] .= $db->escape('%r IN ', $table_with_route . '.' . $first_pk_column); + $params[0] .= '(' . $schema->getColumnInfo($table, $first_pk_column, 'placeholder') . ')'; + $params[] = $this->getPrimaryKeys(); + } + + return $params; + } + + /** * Calls a specific method on each object, returning an fRecordSet of the results * @@ -593,89 +727,6 @@ public function call($method) } - /** - * Creates an `ORDER BY` clause for the primary keys of this record set - * - * @param string $route The route to this table from another table - * @return string The `ORDER BY` clause - */ - private function constructOrderByClause($route=NULL) - { - $table = fORM::tablize($this->class); - $table_with_route = ($route) ? $table . '{' . $route . '}' : $table; - - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); - $first_pk_column = $pk_columns[0]; - - $sql = ''; - - $number = 0; - foreach ($this->getPrimaryKeys() as $primary_key) { - $sql .= 'WHEN '; - - if (is_array($primary_key)) { - $conditions = array(); - foreach ($pk_columns as $pk_column) { - $conditions[] = $table_with_route . '.' . $pk_column . fORMDatabase::escapeBySchema($table, $pk_column, $primary_key[$pk_column], '='); - } - $sql .= join(' AND ', $conditions); - } else { - $sql .= $table_with_route . '.' . $first_pk_column . fORMDatabase::escapeBySchema($table, $first_pk_column, $primary_key, '='); - } - - $sql .= ' THEN ' . $number . ' '; - - $number++; - } - - return 'CASE ' . $sql . 'END ASC'; - } - - - /** - * Creates a `WHERE` clause for the primary keys of this record set - * - * @param string $route The route to this table from another table - * @return string The `WHERE` clause - */ - private function constructWhereClause($route=NULL) - { - $table = fORM::tablize($this->class); - $table_with_route = ($route) ? $table . '{' . $route . '}' : $table; - - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); - - $sql = ''; - - // We have a multi-field primary key, making things kinda ugly - if (sizeof($pk_columns) > 1) { - - $conditions = array(); - - foreach ($this->getPrimaryKeys() as $primary_key) { - $sub_conditions = array(); - foreach ($pk_columns as $pk_column) { - $sub_conditions[] = $table_with_route . '.' . $pk_column . fORMDatabase::escapeBySchema($table, $pk_column, $primary_key[$pk_column], '='); - } - $conditions[] = join(' AND ', $sub_conditions); - } - $sql .= '(' . join(') OR (', $conditions) . ')'; - - // We have a single primary key field, making things nice and easy - } else { - $first_pk_column = $pk_columns[0]; - - $values = array(); - foreach ($this->getPrimaryKeys() as $primary_key) { - $values[] = fORMDatabase::escapeBySchema($table, $first_pk_column, $primary_key); - } - $sql .= $table_with_route . '.' . $first_pk_column . ' IN (' . join(', ', $values) . ')'; - } - - return $sql; - } - - /** * Checks if the record set contains the record specified * @@ -724,7 +775,8 @@ public function count($ignore_limit=FALSE) if (!is_numeric($this->non_limited_count)) { try { - $this->non_limited_count = fORMDatabase::retrieve()->translatedQuery($this->non_limited_count)->fetchScalar(); + $db = fORMDatabase::retrieve(); + $this->non_limited_count = $db->translatedQuery($this->non_limited_count)->fetchScalar(); } catch (fExpectedException $e) { $this->non_limited_count = $this->count(); } @@ -958,7 +1010,8 @@ public function getPrimaryKeys() $this->validateSingleClass('get primary key'); $table = fORM::tablize($this->class); - $pk_columns = fORMSchema::retrieve()->getKeys($table, 'primary'); + $schema = fORMSchema::retrieve(); + $pk_columns = $schema->getKeys($table, 'primary'); $first_pk_column = $pk_columns[0]; $primary_keys = array(); @@ -1205,44 +1258,43 @@ private function prebuild($related_class, $route=NULL) return $this; } + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $related_table = fORM::tablize($related_class); $table = fORM::tablize($this->class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); - $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-many'); + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many'); $table_with_route = ($route) ? $table . '{' . $route . '}' : $table; // Build the query out - $where_sql = $this->constructWhereClause($route); - - $order_by_sql = $this->constructOrderByClause($route); - if ($related_order_bys = fORMRelated::getOrderBys($this->class, $related_class, $route)) { - $order_by_sql .= ', ' . fORMDatabase::createOrderByClause($related_table, $related_order_bys); - } - - $new_sql = 'SELECT ' . $related_table . '.*'; + $params = array($db->escape('SELECT %r.*', $related_table)); // If we are going through a join table we need the related primary key for matching if (isset($relationship['join_table'])) { - $new_sql .= ", " . $table_with_route . '.' . $relationship['column']; + $params[0] .= $db->escape(", %r", $table_with_route . '.' . $relationship['column']); } - $new_sql .= ' FROM :from_clause '; - $new_sql .= ' WHERE ' . $where_sql; - $new_sql .= ' :group_by_clause '; - $new_sql .= ' ORDER BY ' . $order_by_sql; - - $new_sql = fORMDatabase::insertFromAndGroupByClauses($related_table, $new_sql); + $params[0] .= ' FROM :from_clause WHERE '; + $params = $this->addWhereParams($db, $schema, $params, $route); + $params[0] .= ' :group_by_clause ORDER BY '; + $params = $this->addOrderByParams($db, $schema, $params, $related_class, $route); + + $params = fORMDatabase::injectFromAndGroupByClauses($db, $schema, $params, $related_table); // Add the joining column to the group by - if (strpos($new_sql, 'GROUP BY') !== FALSE) { - $new_sql = str_replace(' ORDER BY', ', ' . $table . '.' . $relationship['column'] . ' ORDER BY', $new_sql); - } - + if (strpos($params[0], 'GROUP BY') !== FALSE) { + $params[0] = str_replace( + ' ORDER BY', + $db->escape(', %r ORDER BY', $table . '.' . $relationship['column']), + $params[0] + ); + } // Run the query and inject the results into the records - $result = fORMDatabase::retrieve()->translatedQuery($new_sql); + $result = call_user_func_array($db->translatedQuery, $params); $total_records = sizeof($this->records); for ($i=0; $i < $total_records; $i++) { @@ -1262,7 +1314,7 @@ private function prebuild($related_class, $route=NULL) // If it is a straight join, keep track of the value by the related column value } else { - $method = 'get' . fGrammar::camelize($relationship['related_column'], TRUE); + $method = 'get' . fGrammar::camelize($relationship['column'], TRUE); $keys[$relationship['related_column']] = $record->$method(); } @@ -1316,38 +1368,44 @@ private function precount($related_class, $route=NULL) return $this; } + $db = fORMDatabase::retrieve(); + $schema = fORMSchema::retrieve(); + $related_table = fORM::tablize($related_class); $table = fORM::tablize($this->class); - $route = fORMSchema::getRouteName($table, $related_table, $route, '*-to-many'); - $relationship = fORMSchema::getRoute($table, $related_table, $route, '*-to-many'); - - $table_with_route = ($route) ? $table . '{' . $route . '}' : $table; + $route = fORMSchema::getRouteName($schema, $table, $related_table, $route, '*-to-many'); + $relationship = fORMSchema::getRoute($schema, $table, $related_table, $route, '*-to-many'); // Build the query out - $where_sql = $this->constructWhereClause($route); - $order_by_sql = $this->constructOrderByClause($route); + $table_and_column = $table . '.' . $relationship['column']; - $related_table_keys = fORMSchema::retrieve()->getKeys($related_table, 'primary'); - $related_table_keys = fORMDatabase::addTableToValues($related_table, $related_table_keys); - $related_table_keys = join(', ', $related_table_keys); + if (isset($relationship['join_table'])) { + $table_to_join = $relationship['join_table']; + $column_to_join = $relationship['join_table'] . '.' . $relationship['join_column']; + + } else { + $table_to_join = $related_table; + $column_to_join = $related_table . '.' . $relationship['related_column']; + } - $column = $table_with_route . '.' . $relationship['column']; + $params = array($db->escape( + "SELECT count(*) AS flourish__count, %r AS flourish__column FROM %r INNER JOIN %r ON %r = %r WHERE ", + $table_and_column, + $table, + $table_to_join, + $table_and_column, + $column_to_join + )); + $params = $this->addWhereParams($db, $schema, $params); + $params[0] .= $db->escape(' GROUP BY %r', $table_and_column); - $new_sql = 'SELECT count(' . $related_table_keys . ') AS __flourish_count, ' . $column . ' AS __flourish_column '; - $new_sql .= ' FROM :from_clause '; - $new_sql .= ' WHERE ' . $where_sql; - $new_sql .= ' GROUP BY ' . $column; - $new_sql .= ' ORDER BY ' . $column . ' ASC'; - - $new_sql = fORMDatabase::insertFromAndGroupByClauses($related_table, $new_sql); - // Run the query and inject the results into the records - $result = fORMDatabase::retrieve()->translatedQuery($new_sql); + $result = call_user_func_array($db->translatedQuery, $params); $counts = array(); foreach ($result as $row) { - $counts[$row['__flourish_column']] = (int) $row['__flourish_count']; + $counts[$row['flourish__column']] = (int) $row['flourish__count']; } unset($result); @@ -1387,6 +1445,7 @@ private function precreate($related_class, $route=NULL) } $relationship = fORMSchema::getRoute( + fORMSchema::retrieve(), fORM::tablize($this->class), fORM::tablize($related_class), $route, @@ -1648,4 +1707,4 @@ private function validateSingleClass($operation) * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. - */ \ No newline at end of file + */ diff --git a/classes/fSQLTranslation.php b/classes/fSQLTranslation.php index a5a95cb3..c9bddb78 100644 --- a/classes/fSQLTranslation.php +++ b/classes/fSQLTranslation.php @@ -9,7 +9,8 @@ * @package Flourish * @link http://flourishlib.com/fSQLTranslation * - * @version 1.0.0b11 + * @version 1.0.0b12 + * @changes 1.0.0b12 Backwards Compatibility Break - Removed date translation functionality, changed the signature of ::translate(), updated to support quoted identifiers, added support for PostgreSQL, MSSQL and Oracle schemas [wb, 2009-10-22] * @changes 1.0.0b11 Fixed a bug with translating MSSQL national columns over an ODBC connection [wb, 2009-09-18] * @changes 1.0.0b10 Changed last bug fix to support PHP 5.1.6 [wb, 2009-09-18] * @changes 1.0.0b9 Fixed another bug with parsing table aliases for MSSQL national columns [wb, 2009-09-18] @@ -64,7 +65,7 @@ static private function parseTableAliases($sql) $aliases = array(); // Turn comma joins into cross joins - if (preg_match('#^(?:\w+(?:\s+(?:as\s+)?(?:\w+))?)(?:\s*,\s*(?:\w+(?:\s+(?:as\s+)?(?:\w+))?))*$#isD', $sql)) { + if (preg_match('#^(?:"?\w+"?(?:\s+(?:as\s+)?(?:"?\w+"?))?)(?:\s*,\s*(?:"?\w+"?(?:\s+(?:as\s+)?(?:"?\w+"?))?))*$#isD', $sql)) { $sql = str_replace(',', ' CROSS JOIN ', $sql); } @@ -72,13 +73,16 @@ static private function parseTableAliases($sql) foreach ($tables as $table) { // This grabs the table name and alias (if there is one) - preg_match('#^\s*([\w.]+|\(((?:[^()]+|\((?2)\))*)\))(?:\s+(?:as\s+)?((?!ON|USING)[\w.]+))?\s*(?:(?:ON|USING)\s+(.*))?\s*$#im', $table, $parts); + preg_match('#^\s*(["\w.]+|\(((?:[^()]+|\((?2)\))*)\))(?:\s+(?:as\s+)?((?!ON|USING)["\w.]+))?\s*(?:(?:ON|USING)\s+(.*))?\s*$#im', $table, $parts); $table_name = $parts[1]; $table_alias = (!empty($parts[3])) ? $parts[3] : $parts[1]; + $table_name = str_replace('"', '', $table_name); + $table_alias = str_replace('"', '', $table_alias); + $aliases[$table_alias] = $table_name; - } + } return $aliases; } @@ -266,28 +270,28 @@ private function createSQLiteForeignKeyTriggerOnDelete(&$extra_statements, $refe switch (strtolower($delete_clause)) { case 'no action': case 'restrict': - $extra_statements[] = "CREATE TRIGGER fkd_res_" . $referencing_table . "_" . $referencing_column . " - BEFORE DELETE ON " . $referenced_table . " + $extra_statements[] = 'CREATE TRIGGER fkd_res_' . $referencing_table . '_' . $referencing_column . ' + BEFORE DELETE ON "' . $referenced_table . '" FOR EACH ROW BEGIN - SELECT RAISE(ROLLBACK, 'delete on table \"" . $referenced_table . "\" can not be executed because it would violate the foreign key constraint on column \"" . $referencing_column . "\" of table \"" . $referencing_table . "\"') - WHERE (SELECT " . $referencing_column . " FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_table . ") IS NOT NULL; - END"; + SELECT RAISE(ROLLBACK, \'delete on table "' . $referenced_table . '" can not be executed because it would violate the foreign key constraint on column "' . $referencing_column . '" of table "' . $referencing_table . '"\') + WHERE (SELECT "' . $referencing_column . '" FROM "' . $referencing_table . '" WHERE "' . $referencing_column . '" = OLD."' . $referenced_table . '") IS NOT NULL; + END'; break; case 'set null': - $extra_statements[] = "CREATE TRIGGER fkd_nul_" . $referencing_table . "_" . $referencing_column . " - BEFORE DELETE ON " . $referenced_table . " + $extra_statements[] = 'CREATE TRIGGER fkd_nul_' . $referencing_table . '_' . $referencing_column . ' + BEFORE DELETE ON "' . $referenced_table . '" FOR EACH ROW BEGIN - UPDATE " . $referencing_table . " SET " . $referencing_column . " = NULL WHERE " . $referencing_column . " = OLD." . $referenced_column . "; - END"; + UPDATE "' . $referencing_table . '" SET "' . $referencing_column . '" = NULL WHERE "' . $referencing_column . '" = OLD."' . $referenced_column . '"; + END'; break; case 'cascade': - $extra_statements[] = "CREATE TRIGGER fkd_cas_" . $referencing_table . "_" . $referencing_column . " - BEFORE DELETE ON " . $referenced_table . " + $extra_statements[] = 'CREATE TRIGGER fkd_cas_' . $referencing_table . '_' . $referencing_column . ' + BEFORE DELETE ON "' . $referenced_table . '" FOR EACH ROW BEGIN - DELETE FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_column . "; - END"; + DELETE FROM "' . $referencing_table . '" WHERE "' . $referencing_column . '" = OLD."' . $referenced_column . '"; + END'; break; } } @@ -309,28 +313,28 @@ private function createSQLiteForeignKeyTriggerOnUpdate(&$extra_statements, $refe switch (strtolower($update_clause)) { case 'no action': case 'restrict': - $extra_statements[] = "\nCREATE TRIGGER fku_res_" . $referencing_table . "_" . $referencing_column . " - BEFORE UPDATE ON " . $referenced_table . " + $extra_statements[] = 'CREATE TRIGGER fku_res_' . $referencing_table . '_' . $referencing_column . ' + BEFORE UPDATE ON "' . $referenced_table . '" FOR EACH ROW BEGIN - SELECT RAISE(ROLLBACK, 'update on table \"" . $referenced_table . "\" can not be executed because it would violate the foreign key constraint on column \"" . $referencing_column . "\" of table \"" . $referencing_table . "\"') - WHERE (SELECT " . $referencing_column . " FROM " . $referencing_table . " WHERE " . $referencing_column . " = OLD." . $referenced_column . ") IS NOT NULL; - END"; + SELECT RAISE(ROLLBACK, \'update on table "' . $referenced_table . '" can not be executed because it would violate the foreign key constraint on column "' . $referencing_column . '" of table "' . $referencing_table . '"\') + WHERE (SELECT "' . $referencing_column . '" FROM "' . $referencing_table . '" WHERE "' . $referencing_column . '" = OLD."' . $referenced_column . '") IS NOT NULL; + END'; break; case 'set null': - $extra_statements[] = "\nCREATE TRIGGER fku_nul_" . $referencing_table . "_" . $referencing_column . " - BEFORE UPDATE ON " . $referenced_table . " + $extra_statements[] = 'CREATE TRIGGER fku_nul_' . $referencing_table . '_' . $referencing_column . ' + BEFORE UPDATE ON "' . $referenced_table . '" FOR EACH ROW BEGIN - UPDATE " . $referencing_table . " SET " . $referencing_column . " = NULL WHERE OLD." . $referenced_column . " <> NEW." . $referenced_column . " AND " . $referencing_column . " = OLD." . $referenced_column . "; - END"; + UPDATE "' . $referencing_table . '" SET "' . $referencing_column . '" = NULL WHERE OLD."' . $referenced_column . '" <> NEW."' . $referenced_column . '" AND "' . $referencing_column . '" = OLD."' . $referenced_column . '"; + END'; break; case 'cascade': - $extra_statements[] = "\nCREATE TRIGGER fku_cas_" . $referencing_table . "_" . $referencing_column . " - BEFORE UPDATE ON " . $referenced_table . " + $extra_statements[] = 'CREATE TRIGGER fku_cas_' . $referencing_table . '_' . $referencing_column . ' + BEFORE UPDATE ON "' . $referenced_table . '" FOR EACH ROW BEGIN - UPDATE " . $referencing_table . " SET " . $referencing_column . " = NEW." . $referenced_column . " WHERE OLD." . $referenced_column . " <> NEW." . $referenced_column . " AND " . $referencing_column . " = OLD." . $referenced_column . "; - END"; + UPDATE "' . $referencing_table . '" SET "' . $referencing_column . '" = NEW."' . $referenced_column . '" WHERE OLD."' . $referenced_column . '" <> NEW."' . $referenced_column . '" AND "' . $referencing_column . '" = OLD."' . $referenced_column . '"; + END'; break; } } @@ -350,30 +354,30 @@ private function createSQLiteForeignKeyTriggerOnUpdate(&$extra_statements, $refe private function createSQLiteForeignKeyTriggerValidInsertUpdate(&$extra_statements, $referencing_table, $referencing_column, $referenced_table, $referenced_column, $referencing_not_null) { // Verify key on inserts - $sql = "\nCREATE TRIGGER fki_ver_" . $referencing_table . "_" . $referencing_column . " - BEFORE INSERT ON " . $referencing_table . " + $sql = 'CREATE TRIGGER fki_ver_' . $referencing_table . '_' . $referencing_column . ' + BEFORE INSERT ON "' . $referencing_table . '" FOR EACH ROW BEGIN - SELECT RAISE(ROLLBACK, 'insert on table \"" . $referencing_table . "\" violates foreign key constraint on column \"" . $referencing_column . "\"') - WHERE "; + SELECT RAISE(ROLLBACK, \'insert on table "' . $referencing_table . '" violates foreign key constraint on column "' . $referencing_column . '"\') + WHERE '; if (!$referencing_not_null) { - $sql .= "NEW." . $referencing_column . " IS NOT NULL AND "; + $sql .= 'NEW."' . $referencing_column . '" IS NOT NULL AND '; } - $sql .= " (SELECT " . $referenced_column . " FROM " . $referenced_table . " WHERE " . $referenced_column . " = NEW." . $referencing_column . ") IS NULL; - END"; + $sql .= ' (SELECT "' . $referenced_column . '" FROM "' . $referenced_table . '" WHERE "' . $referenced_column . '" = NEW."' . $referencing_column . '") IS NULL; + END'; $extra_statements[] = $sql; // Verify key on updates - $sql = "\nCREATE TRIGGER fku_ver_" . $referencing_table . "_" . $referencing_column . " - BEFORE UPDATE ON " . $referencing_table . " + $sql = 'CREATE TRIGGER fku_ver_' . $referencing_table . '_' . $referencing_column . ' + BEFORE UPDATE ON "' . $referencing_table . '" FOR EACH ROW BEGIN - SELECT RAISE(ROLLBACK, 'update on table \"" . $referencing_table . "\" violates foreign key constraint on column \"" . $referencing_column . "\"') - WHERE "; + SELECT RAISE(ROLLBACK, \'update on table "' . $referencing_table . '" violates foreign key constraint on column "' . $referencing_column . '"\') + WHERE '; if (!$referencing_not_null) { - $sql .= "NEW." . $referencing_column . " IS NOT NULL AND "; + $sql .= 'NEW."' . $referencing_column . '" IS NOT NULL AND '; } - $sql .= " (SELECT " . $referenced_column . " FROM " . $referenced_table . " WHERE " . $referenced_column . " = NEW." . $referencing_column . ") IS NULL; - END"; + $sql .= ' (SELECT "' . $referenced_column . '" FROM "' . $referenced_table . '" WHERE "' . $referenced_column . '" = NEW."' . $referencing_column . '") IS NULL; + END'; $extra_statements[] = $sql; } @@ -467,9 +471,10 @@ private function fixMSSQLNationalColumns($sql) if (!isset($this->schema_info['national_columns'])) { $result = $this->database->query( "SELECT - c.table_name AS 'table', - c.column_name AS 'column', - c.data_type AS 'type' + c.table_schema AS \"schema\", + c.table_name AS \"table\", + c.column_name AS \"column\", + c.data_type AS \"type\" FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE @@ -489,9 +494,13 @@ private function fixMSSQLNationalColumns($sql) if (!isset($national_columns[$row['table']])) { $national_columns[$row['table']] = array(); $national_types[$row['table']] = array(); + $national_columns[$row['schema'] . '.' . $row['table']] = array(); + $national_types[$row['schema'] . '.' . $row['table']] = array(); } $national_columns[$row['table']][] = $row['column']; $national_types[$row['table']][$row['column']] = $row['type']; + $national_columns[$row['schema'] . '.' . $row['table']][] = $row['column']; + $national_types[$row['schema'] . '.' . $row['table']][$row['column']] = $row['type']; } $this->schema_info['national_columns'] = $national_columns; @@ -529,7 +538,8 @@ private function fixMSSQLNationalColumns($sql) continue; } - if (preg_match('#(\w+)\.\*#i', $selection, $match)) { + if (preg_match('#(("?\w+"?\.)"?\w+"?)\.\*#i', $selection, $match)) { + $match[1] = str_replace('"', '', $match[1]); $table = $table_aliases[$match[1]]; if (empty($national_columns[$table])) { continue; @@ -550,12 +560,17 @@ private function fixMSSQLNationalColumns($sql) $to_fix[$table] = array_merge($to_fix[$table], $national_columns[$table]); } - } elseif (preg_match('#^(?:(\w+)\.(\w+)|((?:min|max|trim|rtrim|ltrim|substring|replace)\((\w+)\.(\w+).*?\)))(?:\s+as\s+(\w+))?$#iD', $selection, $match)) { + } elseif (preg_match('#^(?:((?:"?\w+"?\.)?"?\w+"?)\.("?\w+"?)|((?:min|max|trim|rtrim|ltrim|substring|replace)\(((?:"?\w+"?\.)"?\w+"?)\.("?\w+"?).*?\)))(?:\s+as\s+("?\w+"?))?$#iD', $selection, $match)) { $table = $match[1] . ((isset($match[4])) ? $match[4] : ''); - $table = $table_aliases[$table]; $column = $match[2] . ((isset($match[5])) ? $match[5] : '');; + // Unquote identifiers + $table = str_replace('"', '', $table); + $column = str_replace('"', '', $column); + + $table = $table_aliases[$table]; + if (empty($national_columns[$table]) || !in_array($column, $national_columns[$table])) { continue; } @@ -566,7 +581,7 @@ private function fixMSSQLNationalColumns($sql) // Handle column aliasing if (!empty($match[6])) { - $column = array('column' => $column, 'alias' => $match[6]); + $column = array('column' => $column, 'alias' => str_replace('"', '', $match[6])); } if (!empty($match[3])) { @@ -579,8 +594,12 @@ private function fixMSSQLNationalColumns($sql) $to_fix[$table] = array_merge($to_fix[$table], array($column)); // Match unqualified column names - } elseif (preg_match('#^(?:(\w+)|((?:min|max|trim|rtrim|ltrim|substring|replace)\((\w+).*?\)))(?:\s+as\s+(\w+))?$#iD', $selection, $match)) { + } elseif (preg_match('#^(?:("?\w+"?)|((?:min|max|trim|rtrim|ltrim|substring|replace)\(("?\w+"?).*?\)))(?:\s+as\s+("?\w+"?))?$#iD', $selection, $match)) { $column = $match[1] . ((isset($match[3])) ? $match[3] : ''); + + // Unquote the identifiers + $column = str_replace('"', '', $column); + foreach ($table_aliases as $alias => $table) { if (empty($national_columns[$table])) { continue; @@ -594,7 +613,7 @@ private function fixMSSQLNationalColumns($sql) // Handle column aliasing if (!empty($match[4])) { - $column = array('column' => $column, 'alias' => $match[4]); + $column = array('column' => $column, 'alias' => str_replace('"', '', $match[4])); } if (!empty($match[2])) { @@ -623,12 +642,12 @@ private function fixMSSQLNationalColumns($sql) if (isset($column['expression'])) { $expression = $column['expression']; } else { - $expression = $alias . '.' . $column['column']; + $expression = '"' . $alias . '"."' . $column['column'] . '"'; } $column = $column['column']; } else { $as = ' AS fmssqln__' . $column; - $expression = $alias . '.' . $column; + $expression = '"' . $alias . '"."' . $column . '"'; } if ($national_types[$table][$column] == 'ntext') { $cast = 'CAST(' . $expression . ' AS IMAGE)'; @@ -648,30 +667,22 @@ private function fixMSSQLNationalColumns($sql) /** - * Fixes pulling unicode data out of national data type MSSQL columns + * Fixes empty string comparisons in Oracle * - * @param string $sql The SQL to fix - * @param array &$strings The strings from the SQL + * @param string $sql The SQL to fix * @return string The fixed SQL */ - private function fixOracleEmptyStrings($sql, &$strings) + private function fixOracleEmptyStrings($sql) { - if (preg_match('#^(UPDATE\s+(?:\w+\.)?\w+\s+)(SET((?:(?:(?!\bwhere\b|\breturning\b)[^()])+|\(((?:[^()]+|\((?3)\))*)\))*))(.*)$#i', $sql, $set_match)) { + if (preg_match('#^(UPDATE\s+(?:(?:"?\w+"?\.)?"?\w+"?\.)?"?\w+"?\s+)(SET((?:(?:(?!\bwhere\b|\breturning\b)[^()])+|\(((?:[^()]+|\((?3)\))*)\))*))(.*)$#i', $sql, $set_match)) { $sql = $set_match[1] . ':set_clause ' . $set_match[5]; $set_clause = $set_match[2]; } else { $set_clause = FALSE; } - foreach ($strings as $number => &$string) { - if ($string == "''") { - $sql = preg_replace('#(\s)=(?=\s+:string_' . $number . '\b)#', '\1IS', $sql, 1, $count_equal); - $sql = preg_replace('#(\s)(!=|<>)(?=\s+:string_' . $number . '\b)#', '\1IS NOT', $sql, 1, $count_not_equal); - if ($count_equal || $count_not_equal) { - $string = 'NULL'; - } - } - } + $sql = preg_replace('#(?<=[\sa-z"])=\s*\'\'(?=[^\']|$)#', 'IS NULL', $sql); + $sql = preg_replace('#(?<=[\sa-z"])(!=|<>)\s*\'\'(?=[^\']|$)#', 'IS NOT NULL', $sql); if ($set_clause) { $sql = preg_replace('#:set_clause\b#', strtr($set_clause, array('\\' => '\\\\', '$' => '\\$')), $sql, 1); @@ -709,10 +720,9 @@ private function makeCachePrefix() * @internal * * @param array $statements The SQL statements to translate - * @param array $strings The strings to interpolate back into the SQL statements * @return array The translated SQL statements all ready for execution. Statements that have been translated will have string key of the original SQL, all other will have a numeric key. */ - public function translate($statements, $strings) + public function translate($statements) { $output = array(); @@ -732,25 +742,12 @@ public function translate($statements, $strings) // Oracle has this nasty habit of silently translating empty strings to null if ($this->database->getType() == 'oracle') { - $new_sql = $this->fixOracleEmptyStrings($new_sql, $strings[$number]); + $new_sql = $this->fixOracleEmptyStrings($new_sql); } - // Unescape literal semicolons in the queries - $sql = preg_replace('#(?translateCreateTableStatements($new_sql, $extra_statements); - // Put the strings back into the SQL - foreach ($strings[$number] as $index => $string) { - $string = strtr($string, array('\\' => '\\\\', '$' => '\\$')); - $sql = preg_replace('#:string_' . $index . '\b#', $string, $sql, 1); - $new_sql = preg_replace('#:string_' . $index . '\b#', $string, $new_sql, 1); - } - - $new_sql = $this->translateDateFunctions($new_sql); - if ($sql != $new_sql || $extra_statements) { fCore::debug( self::compose( @@ -822,15 +819,15 @@ private function translateBasicSyntax($sql) '#\bcot\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(1/TAN(\1))', '#\bdegrees\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(\1 * 57.295779513083)', '#\bradians\(\s*((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*\)#i' => '(\1 * 0.017453292519943)', - '#(?:\b|^)((?>[^()%\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*%\s*((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'MOD(\1, \3)', - '#(?:\b|^)((?>[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s+LIKE\s+((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'LOWER(\1) LIKE LOWER(\3)' + '#(?:\b|^)((?>[^()%\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s*%(?![lbdfristp]\b)\s*((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'MOD(\1, \3)', + '#(?[^()\s]+|\(((?:[^()]+|\((?2)\))*)\))+)\s+(NOT\s+)?LIKE\s+((?>[^()\s]+|\(((?:[^()]+|\((?4)\))*)\))+)(?:\b|$)#i' => 'LOWER(\1) \3LIKE LOWER(\4)' ); } elseif ($this->database->getType() == 'postgresql') { $regex = array( - '#\b([\w.]+)\s+(not\s+)?like\b#i' => 'CAST(\1 AS VARCHAR) \2ILIKE', - '#\blike\b#i' => 'ILIKE' + '#(? 'CAST(\1 AS VARCHAR) \2ILIKE', + '#\blike\b#i' => 'ILIKE' ); @@ -870,8 +867,8 @@ private function translateBasicSyntax($sql) */ private function translateCreateTableStatements($sql, &$extra_statements) { - if (!preg_match('#^\s*CREATE\s+TABLE\s+(\w+)#i', $sql, $table_matches) ) { - return $sql; + if (!preg_match('#^\s*CREATE\s+TABLE\s+(["`\[]?\w+["`\]]?)#i', $sql, $table_matches) ) { + return $sql; } $table = $table_matches[1]; @@ -907,7 +904,7 @@ private function translateCreateTableStatements($sql, &$extra_statements) $sql = preg_replace(array_keys($regex), array_values($regex), $sql); // Make sure MySQL uses InnoDB tables, translate check constraints to enums and fix column-level foreign key definitions - preg_match_all('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\(\d+\))?(?:\s+unsigned|\s+zerofill|\s+character\s+set\s+[^ ]+|\s+collate\s+[^ ]+|\s+NULL|\s+NOT\s+NULL|(\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|\s+UNIQUE|\s+PRIMARY\s+KEY|(\s+CHECK\s*\(\w+\s+IN\s+(\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\))\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(,|\s*(?=\)))#mi', $sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?<=,|\()\s*(["`]?\w+["`]?)\s+(?:[a-z]+)(?:\(\d+\))?(?:\s+unsigned|\s+zerofill|\s+character\s+set\s+[^ ]+|\s+collate\s+[^ ]+|\s+NULL|\s+NOT\s+NULL|(\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|\s+UNIQUE|\s+PRIMARY\s+KEY|(\s+CHECK\s*\(\w+\s+IN\s+(\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\))\)))*(\s+REFERENCES\s+["`]?\w+["`]?\s*\(\s*["`]?\w+["`]?\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(,|\s*(?=\)))#mi', $sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { // MySQL has the enum data type, so we switch check constraints to that @@ -943,10 +940,10 @@ private function translateCreateTableStatements($sql, &$extra_statements) $sql = preg_replace(array_keys($regex), array_values($regex), $sql); // Create sequences and triggers for Oracle - if (stripos($sql, 'autoincrement') !== FALSE && preg_match('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?.*?\bAUTOINCREMENT\b[^,\)]*(?:,|\s*(?=\)))#mi', $sql, $matches)) { + if (stripos($sql, 'autoincrement') !== FALSE && preg_match('#(?<=,|\()\s*("?\w+"?)\s+(?:[a-z]+)(?:\((?:\d+)\))?.*?\bAUTOINCREMENT\b[^,\)]*(?:,|\s*(?=\)))#mi', $sql, $matches)) { $column = $matches[1]; - $table_column = substr($table . '_' . $column, 0, 26); + $table_column = substr(str_replace('"' , '', $table) . '_' . str_replace('"', '', $column), 0, 26); $sequence_name = $table_column . '_seq'; $trigger_name = $table_column . '_trg'; @@ -992,10 +989,17 @@ private function translateCreateTableStatements($sql, &$extra_statements) // Create foreign key triggers for SQLite if (stripos($sql, 'REFERENCES') !== FALSE) { - preg_match_all('#(?:(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?(?:(\s+NOT\s+NULL)|(?:\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(?:\s+UNIQUE)|(?:\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(?:\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?)?\s*(?:,|\s*(?=\)))|(?<=,|\()\s*FOREIGN\s+KEY\s*(?:(\w+)|\((\w+)\))\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?\s*(?:,|\s*(?=\))))#mi', $sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?:(?<=,|\()\s*(["`\[]?\w+["`\]]?)\s+(?:[a-z]+)(?:\((?:\d+)\))?(?:(\s+NOT\s+NULL)|(?:\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(?:\s+UNIQUE)|(?:\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(?:\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+(["`\[]?\w+["`\]]?)\s*\(\s*(["`\[]?\w+["`\]]?)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?)?\s*(?:,|\s*(?=\)))|(?<=,|\()\s*FOREIGN\s+KEY\s*(?:(["`\[]?\w+["`\]]?)|\((["`\[]?\w+["`\]]?)\))\s+REFERENCES\s+(["`\[]?\w+["`\]]?)\s*\(\s*(["`\[]?\w+["`\]]?)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL)))?\s*(?:,|\s*(?=\))))#mi', $sql, $matches, PREG_SET_ORDER); $not_null_columns = array(); foreach ($matches as $match) { + $fields_to_unquote = array(1, 4, 5, 9, 10, 11); + foreach ($fields_to_unquote as $field) { + if (isset($match[$field])) { + $match[$field] = str_replace(array('[', '"', '`', ']'), '', $match[$field]); + } + } + // Find all of the not null columns if (!empty($match[2])) { $not_null_columns[] = $match[1]; @@ -1050,126 +1054,6 @@ private function translateCreateTableStatements($sql, &$extra_statements) } - /** - * Translates custom date/time functions to the current database - * - * @param string $sql The SQL to translate - * @return string The translated SQL - */ - private function translateDateFunctions($sql) - { - // Handle diff_seconds() - // diff_seconds() accepts two parameters, the two dates to get the difference between - // Regex matches: - // 1 - The first date - // 2 - The second date - preg_match_all("#diff_seconds\(((?>(?:[^()',]+|'[^']+')|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*((?>(?:[^()',]+|'[^']+')|\((?2)(?:,(?2))?\)|\(\))+)\)#ims", $sql, $diff_matches, PREG_SET_ORDER); - foreach ($diff_matches as $match) { - - // SQLite - if ($this->database->getType() == 'sqlite') { - $sql = str_replace($match[0], "round((julianday(" . $match[2] . ") - julianday('1970-01-01 00:00:00')) * 86400) - round((julianday(" . $match[1] . ") - julianday('1970-01-01 00:00:00')) * 86400)", $sql); - - // PostgreSQL - } elseif ($this->database->getType() == 'postgresql') { - $sql = str_replace($match[0], "extract(EPOCH FROM age(" . $match[2] . ", " . $match[1] . "))", $sql); - - // Oracle - } elseif ($this->database->getType() == 'oracle') { - if (substr($match[1], 0, 1) == "'") { - $match[1] = 'CAST(' . $match[1] . " AS TIMESTAMP)"; - } - if (substr($match[2], 0, 1) == "'") { - $match[2] = 'CAST(' . $match[2] . " AS TIMESTAMP)"; - } - $sql = str_replace($match[0], "((TO_NUMBER(TO_CHAR(" . $match[2] . ", 'J')) - TO_NUMBER(TO_CHAR(" . $match[1] . ", 'J'))) * 86400) + (TO_NUMBER(TO_CHAR(" . $match[2] . ", 'SSSSS')) - TO_NUMBER(TO_CHAR(" . $match[1] . ", 'SSSSS')))", $sql); - - // MySQL - } elseif ($this->database->getType() == 'mysql') { - $sql = str_replace($match[0], "(UNIX_TIMESTAMP(" . $match[2] . ") - UNIX_TIMESTAMP(" . $match[1] . "))", $sql); - - // MSSQL - } elseif ($this->database->getType() == 'mssql') { - $sql = str_replace($match[0], "DATEDIFF(second, " . $match[1] . ", " . $match[2] . ")", $sql); - } - } - - // Handle add_interval() - // add_interval() accepts two parameters, the date to modify and the interval to add - // Regex matches: - // 1 - The first parameter - // 2 - The second parameter - preg_match_all("#add_interval\(((?>(?:[^()',]+|'[^']+')|\((?1)(?:,(?1))?\)|\(\))+)\s*,\s*'([^']+)'\s*\)#i", $sql, $add_matches, PREG_SET_ORDER); - foreach ($add_matches as $match) { - - // SQLite - if ($this->database->getType() == 'sqlite') { - - // Regex matches: - // 0 - The adjustment in the form: +/- number units - preg_match_all("#(?:\\+|\\-)\\d+\\s+(?:year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches); - $strings = "'" . join("', '", $individual_matches[0]) . "'"; - $sql = str_replace($match[0], "datetime(" . $match[1] . ", " . $strings . ")", $sql); - - // PostgreSQL - } elseif ($this->database->getType() == 'postgresql') { - if (substr($match[1], 0, 1) == "'") { - if (preg_match('#^\'\d{4}-\d{2}-\d{2}\'$#', $match[1])) { - $match[1] = 'DATE ' . $match[1]; - } elseif (preg_match('#^\'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\'$#', $match[1])) { - $match[1] = 'TIMESTAMP ' . $match[1]; - } elseif (preg_match('#^\'\d{2}:\d{2}:\d{2}\'$#', $match[1])) { - $match[1] = 'TIME ' . $match[1]; - } - } - $sql = str_replace($match[0], "(" . $match[1] . " + INTERVAL '" . $match[2] . "')", $sql); - - // MySQL and Oracle - } elseif ($this->database->getType() == 'mysql' || $this->database->getType() == 'oracle') { - - // Regex matches: - // 1 - The sign, +/- - // 2 - The number - // 3 - The units, hour, minute, etc - preg_match_all("#(\\+|\\-)(\\d+)\\s+(year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches, PREG_SET_ORDER); - $intervals_string = ''; - foreach ($individual_matches as $individual_match) { - $intervals_string .= ' ' . $individual_match[1] . " INTERVAL '" . $individual_match[2] . "' " . strtoupper($individual_match[3]); - } - - if ($this->database->getType() == 'oracle' && substr($match[1], 0, 1) == "'") { - if (preg_match('#^\'\d{4}-\d{2}-\d{2}\'$#', $match[1])) { - $match[1] = 'CAST(' . $match[1] . ' AS DATE)'; - } elseif (preg_match('#^\'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\'$#', $match[1])) { - $match[1] = 'CAST(' . $match[1] . ' AS TIMESTAMP)'; - } - } - - $sql = str_replace($match[0], "(" . $match[1] . $intervals_string . ")", $sql); - - // MSSQL - } elseif ($this->database->getType() == 'mssql') { - - // Regex matches: - // 1 - The sign, +/- - // 2 - The number - // 3 - The units, hour, minute, etc - preg_match_all("#(\\+|\\-)(\\d+)\\s+(year|month|day|hour|minute|second)(?:s)?#i", $match[2], $individual_matches, PREG_SET_ORDER); - $date_add_string = ''; - $stack = 0; - foreach ($individual_matches as $individual_match) { - $stack++; - $date_add_string .= 'DATEADD(' . $individual_match[3] . ', ' . $individual_match[1] . $individual_match[2] . ', '; - } - $sql = str_replace($match[0], $date_add_string . $match[1] . str_pad('', $stack, ')'), $sql); - - } - } - - return $sql; - } - - /** * Translates `LIMIT x OFFSET x` to `ROW_NUMBER() OVER (ORDER BY)` syntax * diff --git a/classes/fSchema.php b/classes/fSchema.php index 94654246..f66f43c3 100644 --- a/classes/fSchema.php +++ b/classes/fSchema.php @@ -9,7 +9,8 @@ * @package Flourish * @link http://flourishlib.com/fSchema * - * @version 1.0.0b25 + * @version 1.0.0b26 + * @changes 1.0.0b26 Added the placeholder element to the output of ::getColumnInfo(), added support for PostgreSQL, MSSQL and Oracle "schemas", added support for parsing quoted SQLite identifiers [wb, 2009-10-22] * @changes 1.0.0b25 One-to-one relationships utilizing the primary key as a foreign key are now properly detected [wb, 2009-09-22] * @changes 1.0.0b24 Fixed MSSQL support to work with ODBC database connections [wb, 2009-09-18] * @changes 1.0.0b23 Fixed a bug where one-to-one relationships were being listed as many-to-one [wb, 2009-07-21] @@ -294,7 +295,7 @@ private function fetchKeys() $keys = $this->fetchSQLiteKeys(); break; } - + $this->keys = $keys; if ($this->cache) { $this->cache->set($this->makeCachePrefix() . 'keys', $this->keys); @@ -305,29 +306,18 @@ private function fetchKeys() /** * Gets the column info from a MSSQL database * - * The returned array is in the format: - * - * {{{ - * array( - * (string) {column name} => array( - * 'type' => (string) {data type}, - * 'not_null' => (boolean) {if value can't be null}, - * 'default' => (mixed) {the default value-may contain special string CURRENT_TIMESTAMP}, - * 'valid_values' => (array) {the valid values for a char/varchar field}, - * 'max_length' => (integer) {the maximum length in a char/varchar field}, - * 'decimal_places' => (integer) {the number of decimal places for a decimal/numeric/money/smallmoney field}, - * 'auto_increment' => (boolean) {if the integer primary key column is an identity column} - * ), ... - * ) - * }}} - * * @param string $table The table to fetch the column info for - * @return array The column info for the table specified - see method description for details + * @return array The column info for the table specified - see ::getColumnInfo() for details */ private function fetchMSSQLColumnInfo($table) { $column_info = array(); + $schema = 'dbo'; + if (strpos($table, '.') !== FALSE) { + list ($schema, $table) = explode('.', $table); + } + $data_type_mapping = array( 'bit' => 'boolean', 'tinyint' => 'integer', @@ -377,9 +367,11 @@ private function fetchMSSQLColumnInfo($table) INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS ccu ON c.column_name = ccu.column_name AND c.table_name = ccu.table_name AND c.table_catalog = ccu.table_catalog LEFT JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS cc ON ccu.constraint_name = cc.constraint_name AND ccu.constraint_catalog = cc.constraint_catalog WHERE - c.table_name = '" . $table . "' AND + c.table_name = %s AND + c.table_schema = %s AND c.table_catalog = DB_NAME()"; - $result = $this->database->query($sql); + + $result = $this->database->query($sql, $table, $schema); foreach ($result as $row) { @@ -457,31 +449,7 @@ private function fetchMSSQLColumnInfo($table) /** * Fetches the key info for an MSSQL database * - * The structure of the returned array is: - * - * {{{ - * array( - * 'primary' => array( - * {column name}, ... - * ), - * 'unique' => array( - * array( - * {column name}, ... - * ), ... - * ), - * 'foreign' => array( - * array( - * 'column' => {column name}, - * 'foreign_table' => {foreign table name}, - * 'foreign_column' => {foreign column name}, - * 'on_delete' => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}, - * 'on_update' => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'} - * ), ... - * ) - * ) - * }}} - * - * @return array The key info arrays for every table in the database - see method description for details + * @return array The key info arrays for every table in the database - see ::getKeys() for details */ private function fetchMSSQLKeys() { @@ -496,7 +464,8 @@ private function fetchMSSQLKeys() } $sql = "SELECT - c.table_name AS 'table', + c.table_schema AS \"schema\", + c.table_name AS \"table\", kcu.constraint_name AS constraint_name, CASE c.constraint_type WHEN 'PRIMARY KEY' THEN 'primary' @@ -504,6 +473,7 @@ private function fetchMSSQLKeys() WHEN 'UNIQUE' THEN 'unique' END AS 'type', kcu.column_name AS 'column', + ccu.table_schema AS foreign_schema, ccu.table_name AS foreign_table, ccu.column_name AS foreign_column, REPLACE(LOWER(rc.delete_rule), ' ', '_') AS on_delete, @@ -517,6 +487,7 @@ private function fetchMSSQLKeys() c.constraint_catalog = DB_NAME() AND c.table_name != 'sysdiagrams' ORDER BY + LOWER(c.table_schema), LOWER(c.table_name), c.constraint_type, LOWER(kcu.constraint_name), @@ -548,6 +519,9 @@ private function fetchMSSQLKeys() $temp['column'] = $row['column']; $temp['foreign_table'] = $row['foreign_table']; + if ($row['foreign_schema'] != 'dbo') { + $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table']; + } $temp['foreign_column'] = $row['foreign_column']; $temp['on_delete'] = 'no_action'; $temp['on_update'] = 'no_action'; @@ -563,6 +537,9 @@ private function fetchMSSQLKeys() } $last_table = $row['table']; + if ($row['schema'] != 'dbo') { + $last_table = $row['schema'] . '.' . $last_table; + } $last_name = $row['constraint_name']; $last_type = $row['type']; @@ -589,24 +566,8 @@ private function fetchMSSQLKeys() /** * Gets the column info from a MySQL database * - * The returned array is in the format: - * - * {{{ - * array( - * (string) {column name} => array( - * 'type' => (string) {data type}, - * 'not_null' => (boolean) {if value can't be null}, - * 'default' => (mixed) {the default value-may contain special string CURRENT_TIMESTAMP}, - * 'valid_values' => (array) {the valid values for a char/varchar field}, - * 'max_length' => (integer) {the maximum length in a char/varchar field}, - * 'decimal_places' => (integer) {the number of decimal places for a decimal field}, - * 'auto_increment' => (boolean) {if the integer primary key column is auto_increment} - * ), ... - * ) - * }}} - * * @param string $table The table to fetch the column info for - * @return array The column info for the table specified - see method description for details + * @return array The column info for the table specified - see ::getColumnInfo() for details */ private function fetchMySQLColumnInfo($table) { @@ -726,31 +687,7 @@ private function fetchMySQLColumnInfo($table) /** * Fetches the keys for a MySQL database * - * The structure of the returned array is: - * - * {{{ - * array( - * 'primary' => array( - * {column name}, ... - * ), - * 'unique' => array( - * array( - * {column name}, ... - * ), ... - * ), - * 'foreign' => array( - * array( - * 'column' => {column name}, - * 'foreign_table' => {foreign table name}, - * 'foreign_column' => {foreign column name}, - * 'on_delete' => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}, - * 'on_update' => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'} - * ), ... - * ) - * ) - * }}} - * - * @return array The keys arrays for every table in the database - see method description for details + * @return array The keys arrays for every table in the database - see ::getKeys() for details */ private function fetchMySQLKeys() { @@ -804,29 +741,18 @@ private function fetchMySQLKeys() /** * Gets the column info from an Oracle database * - * The returned array is in the format: - * - * {{{ - * array( - * (string) {column name} => array( - * 'type' => (string) {data type}, - * 'not_null' => (boolean) {if value can't be null}, - * 'default' => (mixed) {the default value-may contain special strings CURRENT_TIMESTAMP, CURRENT_TIME or CURRENT_DATE}, - * 'valid_values' => (array) {the valid values for a char/varchar field}, - * 'max_length' => (integer) {the maximum length in a char/varchar field}, - * 'decimal_places' => (integer) {the number of decimal places for a decimal field}, - * 'auto_increment' => (boolean) {if the integer primary key column is auto_increment} - * ), ... - * ) - * }}} - * * @param string $table The table to fetch the column info for - * @return array The column info for the table specified - see method description for details + * @return array The column info for the table specified - see ::getColumnInfo() for details */ private function fetchOracleColumnInfo($table) { $table = strtoupper($table); + $schema = strtoupper($this->database->getUsername()); + if (strpos($table, '.') !== FALSE) { + list ($schema, $table) = explode('.', $table); + } + $column_info = array(); $data_type_mapping = array( @@ -848,64 +774,68 @@ private function fetchOracleColumnInfo($table) ); $sql = "SELECT - LOWER(UTC.COLUMN_NAME) COLUMN_NAME, + LOWER(ATC.COLUMN_NAME) COLUMN_NAME, CASE WHEN - UTC.DATA_TYPE = 'NUMBER' AND - UTC.DATA_PRECISION IS NULL AND - UTC.DATA_SCALE = 0 + ATC.DATA_TYPE = 'NUMBER' AND + ATC.DATA_PRECISION IS NULL AND + ATC.DATA_SCALE = 0 THEN 'integer' WHEN - UTC.DATA_TYPE = 'NUMBER' AND - UTC.DATA_PRECISION = 1 AND - UTC.DATA_SCALE = 0 + ATC.DATA_TYPE = 'NUMBER' AND + ATC.DATA_PRECISION = 1 AND + ATC.DATA_SCALE = 0 THEN 'boolean' WHEN - UTC.DATA_TYPE = 'NUMBER' AND - UTC.DATA_PRECISION IS NOT NULL AND - UTC.DATA_SCALE != 0 AND - UTC.DATA_SCALE IS NOT NULL + ATC.DATA_TYPE = 'NUMBER' AND + ATC.DATA_PRECISION IS NOT NULL AND + ATC.DATA_SCALE != 0 AND + ATC.DATA_SCALE IS NOT NULL THEN 'float' ELSE - LOWER(UTC.DATA_TYPE) + LOWER(ATC.DATA_TYPE) END DATA_TYPE, CASE WHEN - UTC.CHAR_LENGTH <> 0 + ATC.CHAR_LENGTH <> 0 THEN - UTC.CHAR_LENGTH + ATC.CHAR_LENGTH WHEN - UTC.DATA_TYPE = 'NUMBER' AND - UTC.DATA_PRECISION != 1 AND - UTC.DATA_SCALE != 0 AND - UTC.DATA_PRECISION IS NOT NULL + ATC.DATA_TYPE = 'NUMBER' AND + ATC.DATA_PRECISION != 1 AND + ATC.DATA_SCALE != 0 AND + ATC.DATA_PRECISION IS NOT NULL THEN - UTC.DATA_SCALE + ATC.DATA_SCALE ELSE NULL END LENGTH, - UTC.NULLABLE, - UTC.DATA_DEFAULT, - UC.SEARCH_CONDITION CHECK_CONSTRAINT + ATC.NULLABLE, + ATC.DATA_DEFAULT, + AC.SEARCH_CONDITION CHECK_CONSTRAINT FROM - USER_TAB_COLUMNS UTC LEFT JOIN - USER_CONS_COLUMNS UCC ON - UTC.COLUMN_NAME = UCC.COLUMN_NAME AND - UTC.TABLE_NAME = UCC.TABLE_NAME AND - UCC.POSITION IS NULL LEFT JOIN - USER_CONSTRAINTS UC ON - UC.CONSTRAINT_NAME = UCC.CONSTRAINT_NAME AND - UC.CONSTRAINT_TYPE = 'C' AND - UC.STATUS = 'ENABLED' + ALL_TAB_COLUMNS ATC LEFT JOIN + ALL_CONS_COLUMNS ACC ON + ATC.OWNER = ACC.OWNER AND + ATC.COLUMN_NAME = ACC.COLUMN_NAME AND + ATC.TABLE_NAME = ACC.TABLE_NAME AND + ACC.POSITION IS NULL LEFT JOIN + ALL_CONSTRAINTS AC ON + AC.OWNER = ACC.OWNER AND + AC.CONSTRAINT_NAME = ACC.CONSTRAINT_NAME AND + AC.CONSTRAINT_TYPE = 'C' AND + AC.STATUS = 'ENABLED' WHERE - UTC.TABLE_NAME = %s + ATC.TABLE_NAME = %s AND + ATC.OWNER = %s ORDER BY - UTC.TABLE_NAME ASC, - UTC.COLUMN_ID ASC"; - $result = $this->database->query($sql, $table); + ATC.TABLE_NAME ASC, + ATC.COLUMN_ID ASC"; + + $result = $this->database->query($sql, $table, $schema); foreach ($result as $row) { @@ -984,15 +914,16 @@ private function fetchOracleColumnInfo($table) $sql = "SELECT TRIGGER_BODY FROM - USER_TRIGGERS + ALL_TRIGGERS WHERE TRIGGERING_EVENT = 'INSERT' AND STATUS = 'ENABLED' AND TRIGGER_NAME NOT LIKE 'BIN\$%' AND - TABLE_NAME = %s"; + TABLE_NAME = %s AND + OWNER = %s"; - foreach ($this->database->query($sql, $table) as $row) { - if (preg_match('#SELECT\s+(\w+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) { + foreach ($this->database->query($sql, $table, $schema) as $row) { + if (preg_match('#SELECT\s+(["\w.]+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) { $column = strtolower($matches[2]); $column_info[$column]['auto_increment'] = TRUE; } @@ -1005,36 +936,14 @@ private function fetchOracleColumnInfo($table) /** * Fetches the key info for an Oracle database * - * The structure of the returned array is: - * - * {{{ - * array( - * 'primary' => array( - * {column name}, ... - * ), - * 'unique' => array( - * array( - * {column name}, ... - * ), ... - * ), - * 'foreign' => array( - * array( - * 'column' => {column name}, - * 'foreign_table' => {foreign table name}, - * 'foreign_column' => {foreign column name}, - * 'on_delete' => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}, - * 'on_update' => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'} - * ), ... - * ) - * ) - * }}} - * - * @return array The keys arrays for every table in the database - see method description for details + * @return array The keys arrays for every table in the database - see ::getKeys() for details */ private function fetchOracleKeys() { $keys = array(); + $default_schema = strtolower($this->database->getUsername()); + $tables = $this->getTables(); foreach ($tables as $table) { $keys[$table] = array(); @@ -1043,34 +952,50 @@ private function fetchOracleKeys() $keys[$table]['foreign'] = array(); } + $params = array(); + $sql = "SELECT - LOWER(UC.TABLE_NAME) \"TABLE\", - UC.CONSTRAINT_NAME CONSTRAINT_NAME, - CASE UC.CONSTRAINT_TYPE + LOWER(AC.OWNER) \"SCHEMA\", + LOWER(AC.TABLE_NAME) \"TABLE\", + AC.CONSTRAINT_NAME CONSTRAINT_NAME, + CASE AC.CONSTRAINT_TYPE WHEN 'P' THEN 'primary' WHEN 'R' THEN 'foreign' WHEN 'U' THEN 'unique' END TYPE, - LOWER(UCC.COLUMN_NAME) \"COLUMN\", + LOWER(ACC.COLUMN_NAME) \"COLUMN\", + LOWER(FKC.OWNER) FOREIGN_SCHEMA, LOWER(FKC.TABLE_NAME) FOREIGN_TABLE, LOWER(FKC.COLUMN_NAME) FOREIGN_COLUMN, - CASE WHEN FKC.TABLE_NAME IS NOT NULL THEN REPLACE(LOWER(UC.DELETE_RULE), ' ', '_') ELSE NULL END ON_DELETE + CASE WHEN FKC.TABLE_NAME IS NOT NULL THEN REPLACE(LOWER(AC.DELETE_RULE), ' ', '_') ELSE NULL END ON_DELETE FROM - USER_CONSTRAINTS UC INNER JOIN - USER_CONS_COLUMNS UCC ON UC.CONSTRAINT_NAME = UCC.CONSTRAINT_NAME LEFT JOIN - USER_CONSTRAINTS FK ON UC.R_CONSTRAINT_NAME = FK.CONSTRAINT_NAME LEFT JOIN - USER_CONS_COLUMNS FKC ON FK.CONSTRAINT_NAME = FKC.CONSTRAINT_NAME + ALL_CONSTRAINTS AC INNER JOIN + ALL_CONS_COLUMNS ACC ON AC.CONSTRAINT_NAME = ACC.CONSTRAINT_NAME AND AC.OWNER = ACC.OWNER LEFT JOIN + ALL_CONSTRAINTS FK ON AC.R_CONSTRAINT_NAME = FK.CONSTRAINT_NAME AND AC.OWNER = FK.OWNER LEFT JOIN + ALL_CONS_COLUMNS FKC ON FK.CONSTRAINT_NAME = FKC.CONSTRAINT_NAME AND FK.OWNER = FKC.OWNER WHERE - UC.CONSTRAINT_TYPE IN ('U', 'P', 'R') AND - UC.STATUS = 'ENABLED' AND - SUBSTR(UC.TABLE_NAME, 1, 4) <> 'BIN\$' - ORDER BY - UC.TABLE_NAME ASC, - UC.CONSTRAINT_TYPE ASC, - UC.CONSTRAINT_NAME ASC, - UCC.POSITION ASC"; + AC.CONSTRAINT_TYPE IN ('U', 'P', 'R') AND "; - $result = $this->database->query($sql); + $conditions = array(); + foreach ($tables as $table) { + if (strpos($table, '.') === FALSE) { + $table = $default_schema . '.' . $table; + } + list ($schema, $table) = explode('.', strtoupper($table)); + $conditions[] = "AC.OWNER = %s AND AC.TABLE_NAME = %s"; + $params[] = $schema; + $params[] = $table; + } + $sql .= '((' . join(') OR( ', $conditions) . '))'; + + $sql .= " ORDER BY + AC.OWNER ASC, + AC.TABLE_NAME ASC, + AC.CONSTRAINT_TYPE ASC, + AC.CONSTRAINT_NAME ASC, + ACC.POSITION ASC"; + + $result = $this->database->query($sql, $params); $last_name = ''; $last_table = ''; @@ -1092,6 +1017,9 @@ private function fetchOracleKeys() $temp['column'] = $row['column']; $temp['foreign_table'] = $row['foreign_table']; + if ($row['foreign_schema'] != $default_schema) { + $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table']; + } $temp['foreign_column'] = $row['foreign_column']; $temp['on_delete'] = 'no_action'; $temp['on_update'] = 'no_action'; @@ -1105,6 +1033,9 @@ private function fetchOracleKeys() } $last_table = $row['table']; + if ($row['schema'] != $default_schema) { + $last_table = $row['schema'] . '.' . $last_table; + } $last_name = $row['constraint_name']; $last_type = $row['type']; @@ -1128,29 +1059,18 @@ private function fetchOracleKeys() /** * Gets the column info from a PostgreSQL database * - * The returned array is in the format: - * - * {{{ - * array( - * (string) {column name} => array( - * 'type' => (string) {data type}, - * 'not_null' => (boolean) {if value can't be null}, - * 'default' => (mixed) {the default value-may contain special strings CURRENT_TIMESTAMP, CURRENT_TIME or CURRENT_DATE}, - * 'valid_values' => (array) {the valid values for a char/varchar field}, - * 'max_length' => (integer) {the maximum length in a char/varchar field}, - * 'decimal_places' => (integer) {the number of decimal places for a decimal field}, - * 'auto_increment' => (boolean) {if the integer primary key column is auto_increment} - * ), ... - * ) - * }}} - * * @param string $table The table to fetch the column info for - * @return array The column info for the table specified - see method description for details + * @return array The column info for the table specified - see ::getColumnInfo() for details */ private function fetchPostgreSQLColumnInfo($table) { $column_info = array(); + $schema = 'public'; + if (strpos($table, '.') !== FALSE) { + list ($schema, $table) = explode('.', $table); + } + $data_type_mapping = array( 'boolean' => 'boolean', 'smallint' => 'integer', @@ -1183,6 +1103,7 @@ private function fetchPostgreSQLColumnInfo($table) FROM pg_attribute LEFT JOIN pg_class ON pg_attribute.attrelid = pg_class.oid LEFT JOIN + pg_namespace ON pg_class.relnamespace = pg_namespace.oid LEFT JOIN pg_type ON pg_type.oid = pg_attribute.atttypid LEFT JOIN pg_constraint ON pg_constraint.conrelid = pg_class.oid AND pg_attribute.attnum = ANY (pg_constraint.conkey) AND @@ -1192,11 +1113,12 @@ private function fetchPostgreSQLColumnInfo($table) WHERE NOT pg_attribute.attisdropped AND pg_class.relname = %s AND + pg_namespace.nspname = %s AND pg_type.typname NOT IN ('oid', 'cid', 'xid', 'cid', 'xid', 'tid') ORDER BY pg_attribute.attnum, pg_constraint.contype"; - $result = $this->database->query($sql, $table); + $result = $this->database->query($sql, $table, $schema); foreach ($result as $row) { @@ -1272,31 +1194,7 @@ private function fetchPostgreSQLColumnInfo($table) /** * Fetches the key info for a PostgreSQL database * - * The structure of the returned array is: - * - * {{{ - * array( - * 'primary' => array( - * {column name}, ... - * ), - * 'unique' => array( - * array( - * {column name}, ... - * ), ... - * ), - * 'foreign' => array( - * array( - * 'column' => {column name}, - * 'foreign_table' => {foreign table name}, - * 'foreign_column' => {foreign column name}, - * 'on_delete' => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}, - * 'on_update' => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'} - * ), ... - * ) - * ) - * }}} - * - * @return array The keys arrays for every table in the database - see method description for details + * @return array The keys arrays for every table in the database - see ::getKeys() for details */ private function fetchPostgreSQLKeys() { @@ -1312,7 +1210,8 @@ private function fetchPostgreSQLKeys() $sql = "( SELECT - t.relname AS table, + s.nspname AS \"schema\", + t.relname AS \"table\", con.conname AS constraint_name, CASE con.contype WHEN 'f' THEN 'foreign' @@ -1320,6 +1219,7 @@ private function fetchPostgreSQLKeys() WHEN 'u' THEN 'unique' END AS type, col.attname AS column, + fs.nspname AS foreign_schema, ft.relname AS foreign_table, fc.attname AS foreign_column, CASE con.confdeltype @@ -1340,9 +1240,11 @@ private function fetchPostgreSQLKeys() FROM pg_attribute AS col INNER JOIN pg_class AS t ON col.attrelid = t.oid INNER JOIN + pg_namespace AS s ON t.relnamespace = s.oid INNER JOIN pg_constraint AS con ON (col.attnum = ANY (con.conkey) AND con.conrelid = t.oid) LEFT JOIN pg_class AS ft ON con.confrelid = ft.oid LEFT JOIN + pg_namespace AS fs ON ft.relnamespace = fs.oid LEFT JOIN pg_attribute AS fc ON (fc.attnum = ANY (con.confkey) AND ft.oid = fc.attrelid) WHERE @@ -1352,10 +1254,12 @@ private function fetchPostgreSQLKeys() con.contype = 'u') ) UNION ( SELECT - t.relname AS table, + n.nspname AS \"schema\", + t.relname AS \"table\", ic.relname AS constraint_name, 'unique' AS type, col.attname AS column, + NULL AS foreign_schema, NULL AS foreign_table, NULL AS foreign_column, NULL AS on_delete, @@ -1373,7 +1277,7 @@ private function fetchPostgreSQLKeys() indisunique = TRUE AND indisprimary = FALSE AND con.oid IS NULL - ) ORDER BY 1, 3, 2, 9"; + ) ORDER BY 1, 2, 4, 3, 11"; $result = $this->database->query($sql); @@ -1397,6 +1301,9 @@ private function fetchPostgreSQLKeys() $temp['column'] = $row['column']; $temp['foreign_table'] = $row['foreign_table']; + if ($row['foreign_schema'] != 'public') { + $temp['foreign_table'] = $row['foreign_schema'] . '.' . $temp['foreign_table']; + } $temp['foreign_column'] = $row['foreign_column']; $temp['on_delete'] = 'no_action'; $temp['on_update'] = 'no_action'; @@ -1414,6 +1321,9 @@ private function fetchPostgreSQLKeys() } $last_table = $row['table']; + if ($row['schema'] != 'public') { + $last_table = $row['schema'] . '.' . $last_table; + } $last_name = $row['constraint_name']; $last_type = $row['type']; @@ -1437,24 +1347,8 @@ private function fetchPostgreSQLKeys() /** * Gets the column info from a SQLite database * - * The returned array is in the format: - * - * {{{ - * array( - * (string) {column name} => array( - * 'type' => (string) {data type}, - * 'not_null' => (boolean) {if value can't be null}, - * 'default' => (mixed) {the default value-may contain special strings CURRENT_TIMESTAMP, CURRENT_TIME or CURRENT_DATE}, - * 'valid_values' => (array) {the valid values for a char/varchar field}, - * 'max_length' => (integer) {the maximum length in a char/varchar field}, - * 'decimal_places' => (integer) {the number of decimal places for a decimal field}, - * 'auto_increment' => (boolean) {if the integer primary key column is auto_increment} - * ), ... - * ) - * }}} - * * @param string $table The table to fetch the column info for - * @return array The column info for the table specified - see method description for details + * @return array The column info for the table specified - see ::getColumnInfo() for details */ private function fetchSQLiteColumnInfo($table) { @@ -1490,7 +1384,7 @@ private function fetchSQLiteColumnInfo($table) return array(); } - preg_match_all('#(?<=,|\()\s*(?:`|"|\[)?(\w+)(?:`|"|\])?\s+([a-z]+)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?(?:(\s+NOT\s+NULL)|(?:\s+NULL)|(?:\s+DEFAULT\s+([^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+\w+\s*\(\s*\w+\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?<=,|\()\s*(?:`|"|\[)?(\w+)(?:`|"|\])?\s+([a-z]+)(?:\(\s*(\d+)(?:\s*,\s*(\d+))?\s*\))?(?:(\s+NOT\s+NULL)|(?:\s+NULL)|(?:\s+DEFAULT\s+([^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+["`\[]?\w+["`\]]?\s*\(\s*["`\[]?\w+["`\]]?\s*\)\s*(?:\s+(?:ON\s+DELETE|ON\s+UPDATE)\s+(?:CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $info = array(); @@ -1543,31 +1437,7 @@ private function fetchSQLiteColumnInfo($table) /** * Fetches the key info for an SQLite database * - * The structure of the returned array is: - * - * {{{ - * array( - * 'primary' => array( - * {column name}, ... - * ), - * 'unique' => array( - * array( - * {column name}, ... - * ), ... - * ), - * 'foreign' => array( - * array( - * 'column' => {column name}, - * 'foreign_table' => {foreign table name}, - * 'foreign_column' => {foreign column name}, - * 'on_delete' => {the ON DELETE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'}, - * 'on_update' => {the ON UPDATE action: 'no_action', 'restrict', 'cascade', 'set_null', or 'set_default'} - * ), ... - * ) - * ) - * }}} - * - * @return array The keys arrays for every table in the database - see method description for details + * @return array The keys arrays for every table in the database - see ::getKeys() for details */ private function fetchSQLiteKeys() { @@ -1585,7 +1455,7 @@ private function fetchSQLiteKeys() $create_sql = $row['sql']; // Get column level key definitions - preg_match_all('#(?<=,|\()\s*(\w+)\s+(?:[a-z]+)(?:\((?:\d+)\))?(?:(?:\s+NOT\s+NULL)|(?:\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(?:\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))|(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?<=,|\()\s*["`\[]?(\w+)["`\]]?\s+(?:[a-z]+)(?:\((?:\d+)\))?(?:(?:\s+NOT\s+NULL)|(?:\s+DEFAULT\s+(?:[^, \']*|\'(?:\'\'|[^\']+)*\'))|(\s+UNIQUE)|(\s+PRIMARY\s+KEY(?:\s+AUTOINCREMENT)?)|(?:\s+CHECK\s*\(\w+\s+IN\s+\(\s*(?:(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\s*,\s*)*\s*(?:[^, \']+|\'(?:\'\'|[^\']+)*\')\)\)))*(\s+REFERENCES\s+["`\[]?(\w+)["`\]]?\s*\(\s*["`\[]?(\w+)["`\]]?\s*\)\s*(?:(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))|(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT))))*(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?)?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { if (!empty($match[2])) { @@ -1613,14 +1483,17 @@ private function fetchSQLiteKeys() } // Get table level primary key definitions - preg_match_all('#(?<=,|\()\s*PRIMARY\s+KEY\s*\(\s*((?:\s*\w+\s*,\s*)*\w+)\s*\)\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?<=,|\()\s*PRIMARY\s+KEY\s*\(\s*((?:\s*["`\[]?\w+["`\]]?\s*,\s*)*["`\[]?\w+["`\]]?)\s*\)\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { - $keys[$table]['primary'] = preg_split('#\s*,\s*#', $match[1]); + $columns = preg_split('#\s*,\s*#', $match[1]); + foreach ($columns as $column) { + $keys[$table]['primary'][] = str_replace(array('[', '"', '`', ']'), '', $column); + } } // Get table level foreign key definitions - preg_match_all('#(?<=,|\()\s*FOREIGN\s+KEY\s*(?:(\w+)|\((\w+)\))\s+REFERENCES\s+(\w+)\s*\(\s*(\w+)\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))?(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?<=,|\()\s*FOREIGN\s+KEY\s*(?:["`\[]?(\w+)["`\]]?|\(\s*["`\[]?(\w+)["`\]]?\s*\))\s+REFERENCES\s+["`\[]?(\w+)["`\]]?\s*\(\s*["`\[]?(\w+)["`\]]?\s*\)\s*(?:\s+(?:ON\s+DELETE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))?(?:\s+(?:ON\s+UPDATE\s+(CASCADE|NO\s+ACTION|RESTRICT|SET\s+NULL|SET\s+DEFAULT)))?(?:\s+(?:DEFERRABLE|NOT\s+DEFERRABLE))?\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { if (empty($match[1])) { $match[1] = $match[2]; } @@ -1639,10 +1512,15 @@ private function fetchSQLiteKeys() } // Get table level unique key definitions - preg_match_all('#(?<=,|\()\s*UNIQUE\s*\(\s*((?:\s*\w+\s*,\s*)*\w+)\s*\)\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); + preg_match_all('#(?<=,|\()\s*UNIQUE\s*\(\s*((?:\s*["`\[]?\w+["`\]]?\s*,\s*)*["`\[]?\w+["`\]]?)\s*\)\s*(?:,|\s*(?=\)))#mi', $create_sql, $matches, PREG_SET_ORDER); foreach ($matches as $match) { - $keys[$table]['unique'][] = preg_split('#\s*,\s*#', $match[1]); + $columns = preg_split('#\s*,\s*#', $match[1]); + $key = array(); + foreach ($columns as $column) { + $key[] = str_replace(array('[', '"', '`', ']'), '', $column); + } + $keys[$table]['unique'][] = $key; } } @@ -1777,6 +1655,7 @@ private function findRelationships() * array( * (string) {column name} => array( * 'type' => (string) {data type}, + * 'placeholder' => (string) {fDatabase::escape() placeholder for this data type}, * 'not_null' => (boolean) {if value can't be null}, * 'default' => (mixed) {the default value}, * 'valid_values' => (array) {the valid values for a varchar field}, @@ -1792,6 +1671,7 @@ private function findRelationships() * {{{ * array( * 'type' => (string) {data type}, + * 'placeholder' => (string) {fDatabase::escape() placeholder for this data type}, * 'not_null' => (boolean) {if value can't be null}, * 'default' => (mixed) {the default value-may contain special strings CURRENT_TIMESTAMP, CURRENT_TIME or CURRENT_DATE}, * 'valid_values' => (array) {the valid values for a varchar field}, @@ -1818,7 +1698,7 @@ private function findRelationships() * * @param string $table The table to get the column info for * @param string $column The column to get the info for - * @param string $element The element to return: `'type'`, `'not_null'`, `'default'`, `'valid_values'`, `'max_length'`, `'decimal_places'`, `'auto_increment'` + * @param string $element The element to return: `'type'`, `'placeholder'`, `'not_null'`, `'default'`, `'valid_values'`, `'max_length'`, `'decimal_places'`, `'auto_increment'` * @return mixed The column info for the table/column/element specified - see method description for format */ public function getColumnInfo($table, $column=NULL, $element=NULL) @@ -2101,7 +1981,8 @@ public function getTables() switch ($this->database->getType()) { case 'mssql': $sql = "SELECT - TABLE_NAME + TABLE_SCHEMA AS \"schema\", + TABLE_NAME AS \"table\" FROM INFORMATION_SCHEMA.TABLES WHERE @@ -2116,18 +1997,26 @@ public function getTables() case 'oracle': $sql = "SELECT - LOWER(TABLE_NAME) + LOWER(OWNER) AS \"SCHEMA\", + LOWER(TABLE_NAME) AS \"TABLE\" FROM - USER_TABLES + ALL_TABLES WHERE - SUBSTR(TABLE_NAME, 1, 4) <> 'BIN\$' + OWNER IN (SELECT + username + FROM + dba_users + WHERE + default_tablespace NOT IN ('SYSTEM', 'SYSAUX')) AND + DROPPED = 'NO' ORDER BY TABLE_NAME ASC"; break; case 'postgresql': $sql = "SELECT - tablename + schemaname AS \"schema\", + tablename as \"table\" FROM pg_tables WHERE @@ -2153,11 +2042,36 @@ public function getTables() $this->tables = array(); - foreach ($result as $row) { - $keys = array_keys($row); - $this->tables[] = $row[$keys[0]]; + // For databases with schemas we only include the schema + // name if there are conflicting table names + if (!in_array($this->database->getType(), array('mysql', 'sqlite'))) { + + $default_schema_map = array( + 'mssql' => 'dbo', + 'oracle' => strtolower($this->database->getUsername()), + 'postgresql' => 'public' + ); + + $default_schema = $default_schema_map[$this->database->getType()]; + + foreach ($result as $row) { + if ($row['schema'] == $default_schema) { + $this->tables[] = $row['table']; + } else { + $this->tables[] = $row['schema'] . '.' . $row['table']; + } + } + + // SQLite and MySQL don't support schemas + } else { + foreach ($result as $row) { + $keys = array_keys($row); + $this->tables[] = $row[$keys[0]]; + } } + sort($this->tables); + if ($this->cache) { $this->cache->set($this->makeCachePrefix() . 'tables', $this->tables); } @@ -2260,6 +2174,25 @@ private function mergeColumnInfo() if (empty($info['type'])) { throw new fProgrammerException('The data type for the column %1$s is empty', $column); } + + if (empty($this->merged_column_info[$table][$column]['placeholder'])) { + $this->merged_column_info[$table][$column]['placeholder'] = strtr( + $info['type'], + array( + 'blob' => '%l', + 'boolean' => '%b', + 'date' => '%d', + 'float' => '%f', + 'integer' => '%i', + 'char' => '%s', + 'text' => '%s', + 'varchar' => '%s', + 'time' => '%t', + 'timestamp' => '%p' + ) + ); + } + foreach ($optional_elements as $element) { if (!isset($this->merged_column_info[$table][$column][$element])) { $this->merged_column_info[$table][$column][$element] = ($element == 'auto_increment') ? FALSE : NULL; @@ -2311,6 +2244,7 @@ private function mergeKeys() * associative array containing one or more of the following keys. Please * see ::getColumnInfo() for a description of each. * - `'type'` + * - `'placeholder'` * - `'not_null'` * - `'default'` * - `'valid_values'`