From e972129a35851018db6b5fe43bcd2822ddc6b4d4 Mon Sep 17 00:00:00 2001 From: Austin Hyde Date: Wed, 1 Jul 2015 14:48:33 -0400 Subject: [PATCH 1/4] Add foreignKey definition to DTD --- lib/DBSteward/dbsteward.dtd | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/DBSteward/dbsteward.dtd b/lib/DBSteward/dbsteward.dtd index 3f0e552..f8c11a1 100755 --- a/lib/DBSteward/dbsteward.dtd +++ b/lib/DBSteward/dbsteward.dtd @@ -68,7 +68,7 @@ - + @@ -141,6 +141,14 @@ + + + + + + + + From 862f4fcaabec4ab5192a02ba74aa1d13d8e6fdf5 Mon Sep 17 00:00:00 2001 From: Austin Hyde Date: Sat, 11 Jul 2015 14:19:29 -0400 Subject: [PATCH 2/4] Move all references to dbx::get_table_constraint/s to format_constraint --- lib/DBSteward/dbx.php | 165 ------------------ .../sql_format/mssql10/mssql10_constraint.php | 12 ++ .../mssql10/mssql10_diff_tables.php | 18 +- .../sql_format/pgsql8/pgsql8_diff_tables.php | 14 +- .../pgsql8/pgsql8_parser_alter_table.php | 6 +- .../sql_format/sql99/sql99_diff_tables.php | 2 +- tests/OptionalForeignSchemaTest.php | 12 +- tests/pgsql8/QuotedNamesTest.php | 4 +- 8 files changed, 40 insertions(+), 193 deletions(-) create mode 100644 lib/DBSteward/sql_format/mssql10/mssql10_constraint.php diff --git a/lib/DBSteward/dbx.php b/lib/DBSteward/dbx.php index 128b4cf..ce44f9b 100755 --- a/lib/DBSteward/dbx.php +++ b/lib/DBSteward/dbx.php @@ -220,177 +220,12 @@ public static function &get_table_rows(&$node_table, $create_if_not_exist = FALS return $node_rows; } - /** - * @NOTE: because this gets the defintion from the composite list returned by get_table_constraints - * the constraint is not returned by reference as it is not modifiable like other get functions in this class - * when saving changes to constraints, need to lookup the child where they would come from explicitly - */ - public static function get_table_constraint($db_doc, $node_schema, $node_table, $name) { - $constraints = self::get_table_constraints($db_doc, $node_schema, $node_table, 'all'); - $return_constraint = NULL; - foreach ($constraints AS $constraint) { - if (strcasecmp($constraint['name'], $name) == 0) { - if ($return_constraint == NULL) { - $return_constraint = $constraint; - } - else { - var_dump($constraints); - throw new exception("more than one table " . $node_schema['name'] . '.' . $node_table['name'] . " constraint called " . $name . " found"); - } - } - } - return $return_constraint; - } - public function &create_table_constraint(&$node_table, $name) { $node_constraint = $node_table->addChild('constraint'); $node_constraint->addAttribute('name', $name); return $node_constraint; } - /** - * return collection of arrays representing all of the constraints on a table - * this is more than just the discret children of a table element - * this is also primary key, inline column foreign keys, and inline column unique constraints - * everything comparing the constraints of a table should be calling this - */ - public static function get_table_constraints($db_doc, $node_schema, $node_table, $type = 'all') { - if ( !is_object($node_table) ) { - var_dump($node_table); - throw new exception("node_table is not an object, check trace for bad table pointer"); - } - switch ($type) { - case 'all': - case 'primaryKey': - case 'constraint': - case 'foreignKey': - break; - default: - throw new exception("unknown type " . $type . " encountered"); - } - $constraints = array(); - if ($type == 'all' || $type == 'primaryKey') { - if (isset($node_table['primaryKey'])) { - if (isset($node_table['primaryKeyName']) - && strlen($node_table['primaryKeyName']) > 0) { - $primary_key_name = dbsteward::string_cast($node_table['primaryKeyName']); - } - else { - $primary_key_name = pgsql8_index::index_name($node_table['name'], NULL, 'pkey'); - } - - // quoted column name processing for primary key definitions - $primary_key_columns = preg_split("/[\,\s]+/", $node_table['primaryKey'], -1, PREG_SPLIT_NO_EMPTY); - $primary_key_list = ''; - foreach ($primary_key_columns AS $primary_key_column) { - $primary_key_list .= pgsql8::get_quoted_column_name($primary_key_column) . ', '; - } - $primary_key_list = substr($primary_key_list, 0, -2); - - $constraints[] = array( - 'name' => $primary_key_name, - 'schema_name' => (string)$node_schema['name'], - 'table_name' => (string)$node_table['name'], - 'type' => 'PRIMARY KEY', - 'definition' => '(' . $primary_key_list . ')' - ); - } - } - if ($type == 'all' - || $type == 'constraint' - || $type == 'foreignKey' ) { - $nodes = $node_table->xpath("constraint"); - foreach ($nodes AS $node_constraint) { - // sanity check node definition constraint types - switch ((string)$node_constraint['type']) { - case 'CHECK': - case 'FOREIGN KEY': - case 'PRIMARY KEY': - case 'UNIQUE': - break; - default: - throw new exception('unknown constraint type ' . $node_constraint['type'] . ' encountered'); - break; - } - - if ( $type == 'foreignKey' && strcasecmp($node_constraint['type'], 'FOREIGN KEY') != 0 ) { - // requested type is foreignKey yet node type is not FOREIGN KEY, continue on - continue; - } - - $constraints[] = array( - 'name' => (string)$node_constraint['name'], - 'schema_name' => (string)$node_schema['name'], - 'table_name' => (string)$node_table['name'], - 'type' => (string)$node_constraint['type'], - 'definition' => (string)$node_constraint['definition'] - ); - } - } - - if ($type == 'all' - || $type == 'constraint' - || $type == 'foreignKey' ) { - foreach ($node_table->column AS $column) { - // add column foreign key constraints to the list - if (isset($column['foreignSchema']) || isset($column['foreignTable'])) { - if (strlen($column['foreignTable']) == 0) { - throw new exception("Invalid foreignTable for " . dbsteward::string_cast($node_schema['name']) . "." . dbsteward::string_cast($node_table['name']) . "." . dbsteward::string_cast($column['name'])); - } - if (isset($column['type']) - || strlen($column['type']) > 0) { - throw new exception("Foreign-Keyed columns should not specify a type for " . dbsteward::string_cast($node_schema['name']) . "." . dbsteward::string_cast($node_table['name']) . "." . dbsteward::string_cast($column['name'])); - } - - $foreign = array(); - dbx::foreign_key($db_doc, $node_schema, $node_table, $column, $foreign); - if (isset($column['foreignKeyName']) - && strlen($column['foreignKeyName']) > 0) { - // explicitly name the foreign key if specified in the node - $foreign['name'] = (string)$column['foreignKeyName']; - } - - $column_fkey_constraint = array( - 'name' => (string)$foreign['name'], - 'schema_name' => (string)$node_schema['name'], - 'table_name' => (string)$node_table['name'], - 'type' => 'FOREIGN KEY', - 'definition' => '(' . dbsteward::quote_column_name($column['name']) . ') REFERENCES ' . $foreign['references'], - 'foreign_key_data' => $foreign - ); - - if (isset($column['foreignOnDelete']) && strlen($column['foreignOnDelete'])) { - $column_fkey_constraint['foreignOnDelete'] = (string)$column['foreignOnDelete']; - } - if (isset($column['foreignOnUpdate']) && strlen($column['foreignOnUpdate'])) { - $column_fkey_constraint['foreignOnUpdate'] = (string)$column['foreignOnUpdate']; - } - - $constraints[] = $column_fkey_constraint; - } - } - } - - if ($type == 'all' - || $type == 'constraint' - || $type == 'check' ) { - foreach ($node_table->column AS $column) { - // add column check constraints to the list - if ( isset($column['check']) ) { - $column_check_constraint = array( - 'name' => $column['name'] . '_check', - 'schema_name' => (string)$node_schema['name'], - 'table_name' => (string)$node_table['name'], - 'type' => 'CHECK', - 'definition' => $column['check'] - ); - $constraints[] = $column_check_constraint; - } - } - } - return $constraints; - } - /** * return the constraints of other tables that refer to the table specified * diff --git a/lib/DBSteward/sql_format/mssql10/mssql10_constraint.php b/lib/DBSteward/sql_format/mssql10/mssql10_constraint.php new file mode 100644 index 0000000..7ac83e1 --- /dev/null +++ b/lib/DBSteward/sql_format/mssql10/mssql10_constraint.php @@ -0,0 +1,12 @@ + + */ + +class mssql10_constraint extends sql99_constraint { +} \ No newline at end of file diff --git a/lib/DBSteward/sql_format/mssql10/mssql10_diff_tables.php b/lib/DBSteward/sql_format/mssql10/mssql10_diff_tables.php index 004d79e..977faa6 100755 --- a/lib/DBSteward/sql_format/mssql10/mssql10_diff_tables.php +++ b/lib/DBSteward/sql_format/mssql10/mssql10_diff_tables.php @@ -337,7 +337,7 @@ private static function add_modify_table_columns(&$commands, $old_table, $new_sc } // before altering column, remove any constraint that would stop us from doing so - foreach(dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, 'constraint') as $constraint) { + foreach(mssql10_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, 'constraint') as $constraint) { if (preg_match('/' . $new_column['name'] . '[\s,=)]/', $constraint['definition']) > 0) { $commands[] = array( 'stage' => '3', @@ -352,7 +352,7 @@ private static function add_modify_table_columns(&$commands, $old_table, $new_sc ); // add the constraint back on - foreach(dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, 'constraint') as $constraint) { + foreach(mssql10_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, 'constraint') as $constraint) { if (preg_match('/' . $new_column['name'] . '[\s,=\)]/', $constraint['definition']) > 0) { $commands[] = array( 'stage' => '3', @@ -977,7 +977,7 @@ public static function diff_constraints_table($ofs, $old_schema, $old_table, $ne else { // if it is a renamed table, remove all constraints and recreate with new table name conventions if ( mssql10_diff_tables::is_renamed_table($new_schema, $new_table) ) { - foreach(dbx::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { + foreach(mssql10_constraint::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { // rewrite the constraint definer to refer to the new table name // so the constraint by the old name, but part of the new table // will be referenced properly in the drop statement @@ -986,7 +986,7 @@ public static function diff_constraints_table($ofs, $old_schema, $old_table, $ne } // add all defined constraints back to the new table - foreach(dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { + foreach(mssql10_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { $ofs->write(mssql10_table::get_constraint_sql($constraint) . "\n"); } return; @@ -1019,8 +1019,8 @@ protected static function get_drop_constraints($old_schema, $old_table, $new_sch if ($old_table->getName() != 'table') { throw new exception("Unexpected element type: " . $old_table->getName() . " panicing"); } - foreach (dbx::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { - $new_constraint = dbx::get_table_constraint(dbsteward::$new_database, $new_schema, $new_table, $constraint['name']); + foreach (mssql10_constraint::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { + $new_constraint = mssql10_constraint::get_table_constraint(dbsteward::$new_database, $new_schema, $new_table, $constraint['name']); if (!mssql10_table::contains_constraint(dbsteward::$new_database, $new_schema, $new_table, $constraint['name']) || !mssql10_table::constraint_equals($new_constraint, $constraint)) { @@ -1046,13 +1046,13 @@ protected static function get_new_constraints($old_schema, $old_table, $new_sche if ($new_table != NULL) { if ($old_table == NULL) { - foreach (dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { + foreach (mssql10_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { $list[] = $constraint; } } else { - foreach (dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { - $old_constraint = dbx::get_table_constraint(dbsteward::$old_database, $old_schema, $old_table, $constraint['name']); + foreach (mssql10_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { + $old_constraint = mssql10_constraint::get_table_constraint(dbsteward::$old_database, $old_schema, $old_table, $constraint['name']); if (!mssql10_table::contains_constraint(dbsteward::$old_database, $old_schema, $old_table, $constraint['name']) || !mssql10_table::constraint_equals($old_constraint, $constraint)) { diff --git a/lib/DBSteward/sql_format/pgsql8/pgsql8_diff_tables.php b/lib/DBSteward/sql_format/pgsql8/pgsql8_diff_tables.php index 0545dfb..fd4aba7 100755 --- a/lib/DBSteward/sql_format/pgsql8/pgsql8_diff_tables.php +++ b/lib/DBSteward/sql_format/pgsql8/pgsql8_diff_tables.php @@ -1107,7 +1107,7 @@ public static function diff_constraints_table($ofs, $old_schema, $old_table, $ne // if it is a renamed table if ( pgsql8_diff_tables::is_renamed_table($new_schema, $new_table) ) { // remove all constraints and recreate with new table name conventions - foreach(dbx::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { + foreach(pgsql8_constraint::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { // rewrite the constraint definer to refer to the new table name // so the constraint by the old name, but part of the new table // will be referenced properly in the drop statement @@ -1117,7 +1117,7 @@ public static function diff_constraints_table($ofs, $old_schema, $old_table, $ne } // add all still-define constraints back and any new ones to the table - foreach(dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { + foreach(pgsql8_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { $ofs->write(pgsql8_table::get_constraint_sql($constraint) . "\n"); } // this gets any new constraints as well. @@ -1153,8 +1153,8 @@ private static function get_drop_constraints($old_schema, $old_table, $new_schem if ( $old_table->getName() != 'table' ) { throw new exception("Unexpected element type: " . $old_table->getName() . " panicing"); } - foreach(dbx::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { - $new_constraint = dbx::get_table_constraint(dbsteward::$new_database, $new_schema, $new_table, $constraint['name']); + foreach(pgsql8_constraint::get_table_constraints(dbsteward::$old_database, $old_schema, $old_table, $type) as $constraint) { + $new_constraint = pgsql8_constraint::get_table_constraint(dbsteward::$new_database, $new_schema, $new_table, $constraint['name']); if ( !pgsql8_table::contains_constraint(dbsteward::$new_database, $new_schema, $new_table, $constraint['name']) || !pgsql8_table::constraint_equals($new_constraint, $constraint) @@ -1182,12 +1182,12 @@ private static function get_new_constraints($old_schema, $old_table, $new_schema if ($new_table != null) { if ($old_table == null) { - foreach(dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { + foreach(pgsql8_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { $list[] = $constraint; } } else { - foreach(dbx::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { - $old_constraint = dbx::get_table_constraint(dbsteward::$old_database, $old_schema, $old_table, $constraint['name']); + foreach(pgsql8_constraint::get_table_constraints(dbsteward::$new_database, $new_schema, $new_table, $type) as $constraint) { + $old_constraint = pgsql8_constraint::get_table_constraint(dbsteward::$old_database, $old_schema, $old_table, $constraint['name']); if ( !pgsql8_table::contains_constraint(dbsteward::$old_database, $old_schema, $old_table, $constraint['name']) || !pgsql8_table::constraint_equals($old_constraint, $constraint) diff --git a/lib/DBSteward/sql_format/pgsql8/pgsql8_parser_alter_table.php b/lib/DBSteward/sql_format/pgsql8/pgsql8_parser_alter_table.php index c936740..b8cff7a 100644 --- a/lib/DBSteward/sql_format/pgsql8/pgsql8_parser_alter_table.php +++ b/lib/DBSteward/sql_format/pgsql8/pgsql8_parser_alter_table.php @@ -130,7 +130,7 @@ private static function parse_rows(&$db_doc, &$node_schema, &$node_table, $comma if (preg_match(self::PATTERN_ADD_CONSTRAINT_FOREIGN_KEY, $subCommand, $matches) > 0) { $column_name = trim($matches[3]); $constraint_name = trim($matches[1]); - $node_constraint = &dbx::get_table_constraint($db_doc, $node_table, $constraint_name, true); + $node_constraint = pgsql8_constraint::get_table_constraint($db_doc, $node_table, $constraint_name, true); dbx::set_attribute($node_constraint, 'definition', trim($matches[2])); $subCommand = ""; } @@ -138,7 +138,7 @@ private static function parse_rows(&$db_doc, &$node_schema, &$node_table, $comma if (preg_match(self::PATTERN_ADD_CONSTRAINT, $subCommand, $matches) > 0) { $constraint_name = trim($matches[1]); - $node_constraint = &dbx::get_table_constraint($db_doc, $node_table, $constraint_name, true); + $node_constraint = pgsql8_constraint::get_table_constraint($db_doc, $node_table, $constraint_name, true); dbx::set_attribute($node_constraint, 'definition', trim($matches[2])); $subCommand = ""; } @@ -157,7 +157,7 @@ private static function parse_rows(&$db_doc, &$node_schema, &$node_table, $comma if (preg_match(self::PATTERN_ADD_FOREIGN_KEY, $subCommand, $matches) > 0) { $column_name = trim($matches[2]); $constraint_name = pgsql8::identifier_name($node_schema['name'], $node_table['name'], $column_name, '_fkey'); - $node_constraint = &dbx::get_table_constraint($db_doc, $node_table, $constraint_name, true); + $node_constraint = pgsql8_constraint::get_table_constraint($db_doc, $node_table, $constraint_name, true); dbx::set_attribute($node_constraint, 'definition', trim($matches[1])); $subCommand = ""; } diff --git a/lib/DBSteward/sql_format/sql99/sql99_diff_tables.php b/lib/DBSteward/sql_format/sql99/sql99_diff_tables.php index e03cfbe..f66a121 100755 --- a/lib/DBSteward/sql_format/sql99/sql99_diff_tables.php +++ b/lib/DBSteward/sql_format/sql99/sql99_diff_tables.php @@ -69,7 +69,7 @@ public static function is_renamed_table($schema, $table) { * return boolean */ public static function constrains_against_renamed_table($db_doc, $schema, $table) { - foreach(dbx::get_table_constraints($db_doc, $schema, $table, 'constraint') as $constraint) { + foreach(format_constraint::get_table_constraints($db_doc, $schema, $table, 'constraint') as $constraint) { if ( pgsql8_table::constraint_depends_on_renamed_table($db_doc, $constraint) ) { dbsteward::info("NOTICE: " . $schema['name'] . "." . $table['name'] . " constrains against a renamed table with constraint " . $constraint['name']); return TRUE; diff --git a/tests/OptionalForeignSchemaTest.php b/tests/OptionalForeignSchemaTest.php index c635a53..6c73e56 100644 --- a/tests/OptionalForeignSchemaTest.php +++ b/tests/OptionalForeignSchemaTest.php @@ -80,16 +80,16 @@ private function _testConstraintFKLookup() { } /** @group pgsql8 */ - public function testDbxGetTableConstraintsPgsql8() { + public function testFormatGetTableConstraintsPgsql8() { dbsteward::set_sql_format('pgsql8'); - $this->_testDbxGetTableConstraints(); + $this->_testFormatGetTableConstraints(); } /** @group mysql5 */ - public function testDbxGetTableConstraintsMysql5() { + public function testFormatGetTableConstraintsMysql5() { dbsteward::set_sql_format('mysql5'); - $this->_testDbxGetTableConstraints(); + $this->_testFormatGetTableConstraints(); } - private function _testDbxGetTableConstraints() { + private function _testFormatGetTableConstraints() { $doc = simplexml_load_string($this->xml); $schema = $doc->schema; $table1 = $schema->table[0]; @@ -98,7 +98,7 @@ private function _testDbxGetTableConstraints() { $table2 = $schema->table[1]; $table2_t2id = $table2->column[0]; - $constraints = dbx::get_table_constraints($doc, $schema, $table1, 'foreignKey'); + $constraints = format_constraint::get_table_constraints($doc, $schema, $table1, 'foreignKey'); $this->assertCount(1, $constraints); $foreign = $constraints[0]['foreign_key_data']; diff --git a/tests/pgsql8/QuotedNamesTest.php b/tests/pgsql8/QuotedNamesTest.php index ee21c55..7df10d5 100755 --- a/tests/pgsql8/QuotedNamesTest.php +++ b/tests/pgsql8/QuotedNamesTest.php @@ -71,10 +71,10 @@ public function testQuoteFKeyTable() { XML; $db_doc = simplexml_load_string($xml); - $constraints = dbx::get_table_constraints($db_doc, $db_doc->schema, $db_doc->schema->table[1], 'foreignKey'); + $constraints = pgsql8_constraint::get_table_constraints($db_doc, $db_doc->schema, $db_doc->schema->table[1], 'foreignKey'); $sql = trim(pgsql8_table::get_constraint_sql_change_statement($constraints[0])); - $expected = 'ADD CONSTRAINT "test2_col1_fkey" FOREIGN KEY ("col1") REFERENCES "test"."test1"("col1")'; + $expected = 'ADD CONSTRAINT "test2_col1_fkey" FOREIGN KEY ("col1") REFERENCES "test"."test1" ("col1")'; $this->assertEquals($expected, $sql); } From 78b4279b9ed4b37c99c8745bf006fa97975721e8 Mon Sep 17 00:00:00 2001 From: Austin Hyde Date: Sat, 11 Jul 2015 17:50:16 -0400 Subject: [PATCH 3/4] Consider foreignKey elements when finding table constraints --- .../sql_format/sql99/sql99_constraint.php | 133 ++++++++++-- tests/general/GetTableConstraintsTest.php | 195 ++++++++++++++++++ 2 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 tests/general/GetTableConstraintsTest.php diff --git a/lib/DBSteward/sql_format/sql99/sql99_constraint.php b/lib/DBSteward/sql_format/sql99/sql99_constraint.php index 722e097..a31dea4 100755 --- a/lib/DBSteward/sql_format/sql99/sql99_constraint.php +++ b/lib/DBSteward/sql_format/sql99/sql99_constraint.php @@ -33,51 +33,126 @@ public static function foreign_key_lookup($db_doc, $node_schema, $node_table, $c $foreignColumn = $column['name']; } + $foreign['column'] = self::resolve_foreign_column($db_doc, + $node_schema, $node_table, (string)$column['name'], + $foreign['schema'], $foreign['table'], $foreignColumn, $visited); $table = $foreign['table']; $schema = $foreign['schema']; + $foreign['name'] = format_index::index_name($node_table['name'], $column['name'], 'fkey'); + $foreign['references'] = static::get_foreign_key_reference_sql($foreign); + + return $foreign; + } + + public static function foreign_key_lookup_compound($db_doc, $node_schema, $node_table, $node_fkey) { + $lschema_name = (string)$node_schema['name']; + $ltable_name = (string)$node_table['name']; + + $lcol_names = (string)$node_fkey['columns']; + $lcol_names = preg_split('/[\s,]+/', $lcol_names, -1, PREG_SPLIT_NO_EMPTY); + if (empty($lcol_names)) { + throw new Exception("Columns list on foreignKey on table $lschema_name.$ltable_name is empty or missing"); + } + + // fall back to local column names of foreign columns aren't defined + $fcol_names = (string)$node_fkey['foreignColumns']; + if (strlen($fcol_names)) { + $fcol_names = preg_split('/[\s,]+/', $fcol_names, -1, PREG_SPLIT_NO_EMPTY); + } else { + $fcol_names = $lcol_names; + } + + $index_name = strlen($node_fkey['indexName']) ? (string)$node_fkey['indexName'] : format_index::index_name($ltable_name, $lcol_names[0], 'fkey'); + + if (($f=count($fcol_names)) !== ($l=count($lcol_names))) { + throw new Exception("Column mismatch on foreignKey $lschema_name.$ltable_name.$index_name: $l local columns, $f foreign columns"); + } + + // fall back to current schema name if not explicit + $fschema_name = strlen($node_fkey['foreignSchema']) ? (string)$node_fkey['foreignSchema'] : (string)$node_schema['name']; + $fschema = dbx::get_schema($db_doc, $fschema_name); + if (!$fschema) { + throw new Exception("Failed to find foreign schema '$fschema_name' for foreignKey $lschema_name.$ltable_name.$index_name"); + } + + $ftable_name = (string)$node_fkey['foreignTable']; + if (!strlen($ftable_name)) { + throw new Exception("foreignTable attribute is required on foreignKey $lschema_name.$ltable_name.$index_name"); + } + $ftable = dbx::get_table($fschema, $ftable_name); + if (!$ftable) { + throw new Exception("Failed to find foreign table '$ftable_name' for foreignKey $lschema_name.$ltable_name.$index_name"); + } + + $fcols = array(); + foreach ($fcol_names as $i => $fcol_name) { + $fcols[] = self::resolve_foreign_column($db_doc, + $node_schema, $node_table, $lcol_names[$i], + $fschema, $ftable, $fcol_name); + } + + $quoted_fcols = implode(', ', array_map('format::get_quoted_column_name', $fcol_names)); + + return array( + 'schema' => $fschema, + 'table' => $ftable, + 'column' => $fcols, + 'name' => $index_name, + 'references' => format::get_fully_qualified_table_name($fschema['name'], $ftable['name']) . " ($quoted_fcols)" + ); + } + + /** + * Attepts to find a column on a foreign table. + * Walks up table inheritance chains. + * If the foreign column is itself a foreign key, resolves the type of that column before returning. + */ + private static function resolve_foreign_column($db_doc, + $local_schema, $local_table, $local_colname, + $foreign_schema, $foreign_table, $foreign_colname, $visited=array()) { + + // walk up the foreign table inheritance chain to find the foreign column definition + $fschema = $foreign_schema; + $ftable = $foreign_table; do { - $col = dbx::get_table_column($table, $foreignColumn); - if ($table['inheritsSchema']) { - $schema = dbx::get_schema($db_doc, $table['inheritsSchema']); + $foreign_column = dbx::get_table_column($foreign_table, $foreign_colname); + if ($ftable['inheritsSchema']) { + $fschema = dbx::get_schema($db_doc, $ftable['inheritsSchema']); } - if ($table['inheritsTable']) { - $table = dbx::get_table($schema, $table['inheritsTable']); + if ($ftable['inheritsTable']) { + $ftable = dbx::get_table($fschema, $ftable['inheritsTable']); } else { - $table = null; + $ftable = null; } - } while (!$col && !!$schema && !!$table); + } while (!$foreign_column && !!$fschema && !!$ftable); - $foreign['column'] = $col; - if ( ! $foreign['column'] ) { - var_dump($foreign['column']); - throw new Exception("Failed to find foreign column '{$foreignColumn}' for {$node_schema['name']}.{$node_table['name']}.{$column['name']}"); + if (!$foreign_column) { + // column wasn't found in any referenced tables + throw new Exception("Local column {$local_schema['name']}.{$local_table['name']}.$local_colname references unknown column {$foreign_schema['name']}.{$foreign_table['name']}.$foreign_colname"); } // column type is missing, and resolved foreign is also a foreign key? // recurse and find the cascading foreign key - if ( empty($foreign['column']['type']) && !empty($foreign['column']['foreignColumn']) ) { + if ( empty($foreign_column['type']) && !empty($foreign_column['foreignTable']) ) { // make sure we don't visit the same column twice - $foreign_col = format::get_fully_qualified_column_name($foreign['schema']['name'], $foreign['table']['name'], $foreign['column']['name']); + $foreign_col = format::get_fully_qualified_column_name($foreign_schema['name'], $foreign_table['name'], $foreign_column['name']); if ( in_array($foreign_col, $visited) ) { - $local = format::get_fully_qualified_column_name($node_schema['name'], $node_table['name'], $column['name']); + $local = format::get_fully_qualified_column_name($local_schema['name'], $local_table['name'], $local_colname); throw new Exception("Foreign key cyclic dependency detected! Local column $local pointing to foreign column $foreign_col"); } $visited[] = $foreign_col; - $nested_fkey = self::foreign_key_lookup($db_doc, $foreign['schema'], $foreign['table'], $foreign['column'], $visited); + $nested_fkey = self::foreign_key_lookup($db_doc, $foreign_schema, $foreign_table, $foreign_column, $visited); // make a separate clone of the column element because we are specifying the type only for foreign key type referencing - $foreign['column'] = new SimpleXMLElement($foreign['column']->asXML()); - $foreign['column']['type'] = $nested_fkey['column']['type']; + $foreign_column = new SimpleXMLElement($foreign_column->asXML()); + $foreign_column['type'] = (string)$nested_fkey['column']['type']; } - $foreign['name'] = format_index::index_name($node_table['name'], $column['name'], 'fkey'); - $foreign['references'] = static::get_foreign_key_reference_sql($foreign); - - return $foreign; + return $foreign_column; } public static function get_foreign_key_reference_sql($foreign) { @@ -178,6 +253,22 @@ public static function get_table_constraints($db_doc, $node_schema, $node_table, } } + // look for explicit elements + foreach ($node_table->foreignKey as $node_fkey) { + $foreign = self::foreign_key_lookup_compound($db_doc, $node_schema, $node_table, $node_fkey); + $local_cols = preg_split('/[\s,]+/', $node_fkey['columns'], -1, PREG_SPLIT_NO_EMPTY); + $quoted_cols = implode(', ', array_map('format::get_quoted_column_name', $local_cols)); + + $constraints[] = array( + 'name' => (string)$node_fkey['constraintName'], + 'schema_name' => (string)$node_schema['name'], + 'table_name' => (string)$node_table['name'], + 'type' => 'FOREIGN KEY', + 'definition' => "($quoted_cols) REFERENCES {$foreign['references']}", + 'foreign_key_data' => $foreign + ); + } + // look for constraints in columns: foreign key and unique foreach ($node_table->column AS $column) { if ( isset($column['foreignTable']) ) { diff --git a/tests/general/GetTableConstraintsTest.php b/tests/general/GetTableConstraintsTest.php new file mode 100644 index 0000000..694bd45 --- /dev/null +++ b/tests/general/GetTableConstraintsTest.php @@ -0,0 +1,195 @@ + + */ + +use DBSteward\Definition\XML\XMLDatabaseDefinition; + +class GetTableConstraintsTest extends PHPUnit_Framework_TestCase { + public function setUp() { + dbsteward::$quote_all_names = true; + } + /** @group pgsql8 */ + public function testGetsExplicitForeignKeysPgsql8() { + dbsteward::set_sql_format('pgsql8'); + $this->_testGetsExplicitForeignKeys('("t1c1", "t1c2")', '"s2"."t2" ("t2c1", "t2c2")'); + } + /** @group mysql5 */ + public function testGetsExplicitForeignKeysMysql5() { + dbsteward::set_sql_format('mysql5'); + $this->_testGetsExplicitForeignKeys('(`t1c1`, `t1c2`)', '`t2` (`t2c1`, `t2c2`)'); + } + private function _testGetsExplicitForeignKeys($local, $foreign) { + $xml = << + + + + + +
+ + + + + +
+
+XML; + + $doc = $this->getDoc($xml); + $constraints = format_constraint::get_table_constraints($doc, $doc->schema[0], $doc->schema[0]->table[0], 'foreignKey'); + + $this->assertCount(1, $constraints); + + $c = $constraints[0]; + + $this->assertEquals('fkey1', $c['name']); + $this->assertEquals('s1', $c['schema_name']); + $this->assertEquals('t1', $c['table_name']); + $this->assertEquals('FOREIGN KEY', $c['type']); + $this->assertEquals("$local REFERENCES $foreign", $c['definition']); + $this->assertEquals(array( + 'schema' => $doc->schema[1], + 'table' => $doc->schema[1]->table[0], + 'column' => array( + $doc->schema[1]->table[0]->column[0], + $doc->schema[1]->table[0]->column[1] + ), + 'name' => 'fkey1_idx', + 'references' => $foreign + ), $c['foreign_key_data']); + } + + /** @group pgsql8 */ + public function testExplicitFKeyDefaultSchemaPgsql8() { + dbsteward::set_sql_format('pgsql8'); + $this->_testExplicitFKeyDefaultSchema('("t1c1", "t1c2")', '"s1"."t2" ("t2c1", "t2c2")'); + } + /** @group mysql5 */ + public function testExplicitFKeyDefaultSchemaMysql5() { + dbsteward::set_sql_format('mysql5'); + $this->_testExplicitFKeyDefaultSchema('(`t1c1`, `t1c2`)', '`t2` (`t2c1`, `t2c2`)'); + } + private function _testExplicitFKeyDefaultSchema($local, $foreign) { + $xml = << + + + + + +
+ + + +
+ +XML; + + $doc = $this->getDoc($xml); + $constraints = format_constraint::get_table_constraints($doc, $doc->schema[0], $doc->schema[0]->table[0], 'foreignKey'); + + $this->assertCount(1, $constraints); + + $c = $constraints[0]; + + $this->assertEquals('fkey1', $c['name']); + $this->assertEquals('s1', $c['schema_name']); + $this->assertEquals('t1', $c['table_name']); + $this->assertEquals('FOREIGN KEY', $c['type']); + $this->assertEquals("$local REFERENCES $foreign", $c['definition']); + $this->assertEquals(array( + 'schema' => $doc->schema[0], + 'table' => $doc->schema[0]->table[1], + 'column' => array( + $doc->schema[0]->table[1]->column[0], + $doc->schema[0]->table[1]->column[1] + ), + 'name' => 'fkey1_idx', + 'references' => $foreign + ), $c['foreign_key_data']); + } + + /** @group pgsql8 */ + public function testExplicitFKeyDefaultColumnsPgsql8() { + dbsteward::set_sql_format('pgsql8'); + $this->_testExplicitFKeyDefaultColumns('("t2c1", "t2c2")', '"s2"."t2" ("t2c1", "t2c2")'); + } + /** @group mysql5 */ + public function testExplicitFKeyDefaultColumnsMysql5() { + dbsteward::set_sql_format('mysql5'); + $this->_testExplicitFKeyDefaultColumns('(`t2c1`, `t2c2`)', '`t2` (`t2c1`, `t2c2`)'); + } + private function _testExplicitFKeyDefaultColumns($local, $foreign) { + $xml = << + + + + + +
+ + + + + +
+
+XML; + + $doc = $this->getDoc($xml); + $constraints = format_constraint::get_table_constraints($doc, $doc->schema[0], $doc->schema[0]->table[0], 'foreignKey'); + + $this->assertCount(1, $constraints); + + $c = $constraints[0]; + + $this->assertEquals('fkey1', $c['name']); + $this->assertEquals('s1', $c['schema_name']); + $this->assertEquals('t1', $c['table_name']); + $this->assertEquals('FOREIGN KEY', $c['type']); + $this->assertEquals("$local REFERENCES $foreign", $c['definition']); + $this->assertEquals(array( + 'schema' => $doc->schema[1], + 'table' => $doc->schema[1]->table[0], + 'column' => array( + $doc->schema[1]->table[0]->column[0], + $doc->schema[1]->table[0]->column[1] + ), + 'name' => 'fkey1_idx', + 'references' => $foreign + ), $c['foreign_key_data']); + } + + private function getDoc($schemaXml) { + $xml = << + $schemaXml + +XML; + + return simplexml_load_string($xml); + } +} \ No newline at end of file From e8d3d6bb878767e76ec9b7f6c98596bc38a123a0 Mon Sep 17 00:00:00 2001 From: Austin Hyde Date: Sat, 11 Jul 2015 18:05:46 -0400 Subject: [PATCH 4/4] Fix typo in inheritance traversal --- lib/DBSteward/sql_format/sql99/sql99_constraint.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/DBSteward/sql_format/sql99/sql99_constraint.php b/lib/DBSteward/sql_format/sql99/sql99_constraint.php index a31dea4..c3c8009 100755 --- a/lib/DBSteward/sql_format/sql99/sql99_constraint.php +++ b/lib/DBSteward/sql_format/sql99/sql99_constraint.php @@ -117,13 +117,13 @@ private static function resolve_foreign_column($db_doc, $fschema = $foreign_schema; $ftable = $foreign_table; do { - $foreign_column = dbx::get_table_column($foreign_table, $foreign_colname); + $foreign_column = dbx::get_table_column($ftable, $foreign_colname); if ($ftable['inheritsSchema']) { - $fschema = dbx::get_schema($db_doc, $ftable['inheritsSchema']); + $fschema = dbx::get_schema($db_doc, (string)$ftable['inheritsSchema']); } if ($ftable['inheritsTable']) { - $ftable = dbx::get_table($fschema, $ftable['inheritsTable']); + $ftable = dbx::get_table($fschema, (string)$ftable['inheritsTable']); } else { $ftable = null; }