public static function build_upgrade_slonik_replica_set($old_db_doc, $new_db_doc, $old_replica_set, $new_replica_set, $slonik_file_prefix, $origin_header = '') { dbsteward::notice("Building slonik upgrade replica set ID " . $new_replica_set['id']); $timestamp = date('r'); // output preamble file standalone for tool chains that use the preamble to do additional slonik commands $slony_preamble_file = $slonik_file_prefix . '_preamble.slonik'; pgsql8::build_slonik_preamble($new_db_doc, $new_replica_set, $slony_preamble_file); $slony_stage1_file = $slonik_file_prefix . '_stage1.slonik'; pgsql8::build_slonik_preamble($new_db_doc, $new_replica_set, $slony_stage1_file); $slony_stage1_fp = fopen($slony_stage1_file, 'a'); if ($slony_stage1_fp === FALSE) { throw new exception("failed to open upgrade slony stage 1 output file " . $slony_stage1_file); } $slony_stage1_ofs = new output_file_segmenter($slony_stage1_file, 1, $slony_stage1_fp, $slony_stage1_file); $slony_stage1_ofs->set_comment_line_prefix("#"); // keep slonik file comment lines consistent $slony_stage1_ofs->write("# DBSteward slony stage 1 upgrade generated " . $timestamp . "\n"); $slony_stage1_ofs->write($origin_header . "\n"); $slony_stage1_ofs->write("ECHO 'DBSteward slony upgrade replica set " . $new_replica_set['id'] . " stage 1 generated " . date('r') . "';\n\n"); $slony_stage3_file = $slonik_file_prefix . '_stage3.slonik'; pgsql8::build_slonik_preamble($new_db_doc, $new_replica_set, $slony_stage3_file); $slony_stage3_fp = fopen($slony_stage3_file, 'a'); if ($slony_stage3_fp === FALSE) { throw new exception("failed to open upgrade slony stage 3 output file " . $slony_stage3_file . ' for output'); } $slony_stage3_ofs = new output_file_segmenter($slony_stage3_file, 1, $slony_stage3_fp, $slony_stage3_file); $slony_stage3_ofs->set_comment_line_prefix("#"); // keep slonik file comment lines consistent $slony_stage3_ofs->write("# DBSteward slony stage 3 upgrade generated " . $timestamp . "\n"); $slony_stage3_ofs->write($origin_header . "\n"); $slony_stage3_ofs->write("ECHO 'DBSteward slony upgrade replica set " . $new_replica_set['id'] . " stage 3 generated " . date('r') . "';\n\n"); // slony replication configuration changes // SLONY STAGE 1 // unsubscribe to abandoned tables/sequences foreach ($old_db_doc->schema as $old_schema) { // look for the schema in the new definition $new_schema = dbx::get_schema($new_db_doc, $old_schema['name']); // slony replicated tables that are no longer present foreach ($old_schema->table as $old_table) { if (!pgsql8::slony_replica_set_contains_table($old_db_doc, $old_replica_set, $old_schema, $old_table)) { // this old table is not contained in the old replica set being processed continue; } $new_table = NULL; if ($new_schema) { $new_table = dbx::get_table($new_schema, $old_table['name']); } if ($new_schema === NULL || $new_table === NULL) { // schema or table no longer exists // drop sequence subscriptions owned by the table foreach ($old_table->column as $old_column) { // is a replicated type? if (preg_match(pgsql8::PATTERN_REPLICATED_COLUMN, $old_column['type']) > 0 && isset($old_column['slonyId']) && strcasecmp('IGNORE_REQUIRED', $old_column['slonyId']) != 0) { $slony_stage1_ofs->write("# replicated table column " . $old_schema['name'] . '.' . $old_table['name'] . '.' . $old_column['name'] . " slonyId " . $old_table['slonyId'] . " no longer defined, dropping\n"); $slony_stage1_ofs->write(sprintf(slony1_slonik::script_drop_sequence, dbsteward::string_cast($old_replica_set['originNodeId']), dbsteward::string_cast($old_column['slonyId'])) . "\n\n"); } } if (isset($old_table['slonyId']) && strcasecmp('IGNORE_REQUIRED', $old_table['slonyId']) != 0) { // drop table subscription to the table $slony_stage1_ofs->write("# replicated table " . $old_schema['name'] . '.' . $old_table['name'] . " slonyId " . $old_table['slonyId'] . " no longer defined, dropping\n"); $slony_stage1_ofs->write(sprintf(slony1_slonik::script_drop_table, dbsteward::string_cast($old_replica_set['originNodeId']), dbsteward::string_cast($old_table['slonyId'])) . "\n\n"); } } if ($new_table !== NULL) { // table exists, look for replicated columns that have been abandoned or are no longer replicated types foreach ($old_table->column as $old_column) { // it was previously a replicated column type? if (preg_match(pgsql8::PATTERN_REPLICATED_COLUMN, $old_column['type']) > 0) { $nodes = $new_table->xpath("column[@name='" . dbsteward::string_cast($old_column['name']) . "']"); $new_column = NULL; if (count($nodes) == 1) { $new_column = $nodes[0]; if (preg_match(pgsql8::PATTERN_REPLICATED_COLUMN, $new_column['type']) == 0) { // not replicated type anymore $new_column = NULL; } } if ($new_column === NULL && strcasecmp('IGNORE_REQUIRED', $old_column['slonyId']) != 0) { $slony_stage1_ofs->write(sprintf(slony1_slonik::script_drop_sequence, dbsteward::string_cast($old_replica_set['originNodeId']), dbsteward::string_cast($old_column['slonyId'])) . "\n\n"); } } } } } // slony replicated stand-alone sequences that are no longer present foreach ($old_schema->sequence as $old_sequence) { if (!static::slony_replica_set_contains_sequence($old_db_doc, $old_replica_set, $old_schema, $old_sequence)) { // this old sequence is not contained in the old replica set being processed continue; } $new_sequence = NULL; if ($new_schema !== NULL) { $new_sequence = dbx::get_sequence($new_schema, $old_sequence['name']); } if (($new_schema === NULL || $new_sequence === NULL) && strcasecmp('IGNORE_REQUIRED', $old_sequence['slonyId']) != 0) { // schema or sequence no longer exists, drop the sequence subscription $slony_stage1_ofs->write(sprintf(slony1_slonik::script_drop_sequence, dbsteward::string_cast($old_replica_set['originNodeId']), dbsteward::string_cast($old_sequence['slonyId'])) . "\n\n"); } } } $upgrade_set_created = FALSE; // SLONY STAGE 3 // new table replication foreach ($new_db_doc->schema as $new_schema) { // look for the schema in the old definition $old_schema = dbx::get_schema($old_db_doc, $new_schema['name']); // new tables that were not previously present // new replicated columns that were not previously present foreach ($new_schema->table as $new_table) { // is this table replicated in this replica set? if (pgsql8::slony_replica_set_contains_table($new_db_doc, $new_replica_set, $new_schema, $new_table)) { if (!is_numeric(dbsteward::string_cast($new_table['slonyId']))) { throw new exception('table ' . $new_table['name'] . " slonyId " . $new_table['slonyId'] . " is not numeric"); } if (in_array(dbsteward::string_cast($new_table['slonyId']), self::$table_slony_ids)) { throw new exception("table " . $new_table['name'] . " slonyId " . $new_table['slonyId'] . " already in table_slony_ids -- duplicates not allowed"); } self::$table_slony_ids[] = dbsteward::string_cast($new_table['slonyId']); $old_table = NULL; if ($old_schema) { $old_table = dbx::get_table($old_schema, $new_table['name']); if ($old_table && isset($old_table['slonyId']) && strcasecmp('IGNORE_REQUIRED', $old_table['slonyId']) !== 0 && strcasecmp('IGNORE_REQUIRED', $new_table['slonyId']) !== 0 && (string) $new_table['slonyId'] != (string) $old_table['slonyId']) { throw new Exception("table slonyId {$new_table['slonyId']} in new does not match slonyId {$old_table['slonyId']} in old"); } } if (($old_schema === NULL || $old_table === NULL) && strcasecmp('IGNORE_REQUIRED', $new_table['slonyId']) != 0) { // if it has not been declared, create the upgrade set to be merged if (!$upgrade_set_created) { self::create_slonik_upgrade_set($slony_stage3_ofs, $new_db_doc, $new_replica_set); $upgrade_set_created = TRUE; } // schema or table did not exist before, add it $slony_stage3_ofs->write(sprintf(slony1_slonik::script_add_table, dbsteward::string_cast($new_replica_set['upgradeSetId']), dbsteward::string_cast($new_replica_set['originNodeId']), dbsteward::string_cast($new_table['slonyId']), $new_schema['name'] . '.' . $new_table['name'], $new_schema['name'] . '.' . $new_table['name'] . ' table replication') . "\n\n"); } } // add table owned sequence subscriptions for any not already present foreach ($new_table->column as $new_column) { // is this column sequence replicated in this replica set? if (pgsql8::slony_replica_set_contains_table_column_serial_sequence($new_db_doc, $new_replica_set, $new_schema, $new_table, $new_column)) { self::check_duplicate_sequence_slony_id((string) $new_column['name'], 'column', (string) $new_column['slonyId']); self::set_sequence_slony_ids($new_column, $new_db_doc); // resolve $old_table on our own -- the table itself may not be replicated $old_table = NULL; if ($old_schema) { $old_table = dbx::get_table($old_schema, $new_table['name']); if ($old_table && isset($old_table['slonyId']) && strcasecmp('IGNORE_REQUIRED', $old_table['slonyId']) !== 0 && strcasecmp('IGNORE_REQUIRED', $new_table['slonyId']) !== 0 && (string) $new_table['slonyId'] != (string) $old_table['slonyId']) { throw new Exception("table slonyId {$new_table['slonyId']} in new does not match slonyId {$old_table['slonyId']} in old"); } } // schema/table/column not present before $old_column = NULL; if ($old_table !== NULL) { $nodes = $old_table->xpath("column[@name='" . dbsteward::string_cast($new_column['name']) . "']"); if (count($nodes) == 1) { // column is in new schema $old_column = $nodes[0]; } } if ($old_column && isset($old_column['slonyId']) && strcasecmp('IGNORE_REQUIRED', $old_column['slonyId']) !== 0 && strcasecmp('IGNORE_REQUIRED', $new_column['slonyId']) !== 0 && (string) $new_column['slonyId'] != (string) $old_column['slonyId']) { throw new Exception("column sequence slonyId {$new_column['slonyId']} in new does not match slonyId {$old_column['slonyId']} in old"); } if (($old_schema === NULL || $old_table === NULL || $old_column === NULL) && strcasecmp('IGNORE_REQUIRED', $new_column['slonyId']) != 0) { // if it has not been declared, create the upgrade set to be merged if (!$upgrade_set_created) { self::create_slonik_upgrade_set($slony_stage3_ofs, $new_db_doc, $new_replica_set); $upgrade_set_created = TRUE; } $col_sequence = pgsql8::identifier_name($new_schema['name'], $new_table['name'], $new_column['name'], '_seq'); $slony_stage3_ofs->write(sprintf(slony1_slonik::script_add_sequence, dbsteward::string_cast($new_replica_set['upgradeSetId']), dbsteward::string_cast($new_replica_set['originNodeId']), dbsteward::string_cast($new_column['slonyId']), $new_schema['name'] . '.' . $col_sequence, $new_schema['name'] . '.' . $col_sequence . ' serial sequence column replication') . "\n\n"); } // also check to make sure that additions of slonyIds in new XML // where the column did exist before also generates slonik changes if ($old_schema !== NULL && $old_table !== NULL && $old_column !== NULL && strcasecmp('IGNORE_REQUIRED', $new_column['slonyId']) !== 0 && !isset($old_column['slonyId'])) { if (!$upgrade_set_created) { self::create_slonik_upgrade_set($slony_stage3_ofs, $new_db_doc, $new_replica_set); $upgrade_set_created = TRUE; } $col_sequence = pgsql8::identifier_name($new_schema['name'], $new_table['name'], $new_column['name'], '_seq'); $slony_stage3_ofs->write(sprintf(slony1_slonik::script_add_sequence, dbsteward::string_cast($new_replica_set['upgradeSetId']), dbsteward::string_cast($new_replica_set['originNodeId']), dbsteward::string_cast($new_column['slonyId']), $new_schema['name'] . '.' . $col_sequence, $new_schema['name'] . '.' . $col_sequence . ' serial sequence column replication') . "\n\n"); } } } } // new stand alone sequences not owned by tables that were not previously present foreach ($new_schema->sequence as $new_sequence) { // is this sequence replicated in this replica set? if (pgsql8::slony_replica_set_contains_sequence($new_db_doc, $new_replica_set, $new_schema, $new_sequence)) { self::check_duplicate_sequence_slony_id((string) $new_sequence['name'], 'sequence', (string) $new_sequence['slonyId']); self::set_sequence_slony_ids($new_sequence, $new_db_doc); } $old_sequence = NULL; if ($old_schema) { $old_sequence = dbx::get_sequence($old_schema, $new_sequence['name']); } if ($old_sequence && isset($old_sequence['slonyId']) && strcasecmp('IGNORE_REQUIRED', $old_sequence['slonyId']) !== 0 && strcasecmp('IGNORE_REQUIRED', $new_sequence['slonyId']) !== 0 && (string) $new_sequence['slonyId'] != (string) $old_sequence['slonyId']) { throw new Exception("sequence slonyId {$new_sequence['slonyId']} in new does not match slonyId {$old_sequence['slonyId']} in old"); } if (($old_schema === NULL || $old_sequence === NULL) && strcasecmp('IGNORE_REQUIRED', $new_sequence['slonyId']) != 0) { // if it has not been declared, create the upgrade set to be merged if (!$upgrade_set_created) { self::create_slonik_upgrade_set($slony_stage3_ofs, $new_db_doc, $new_replica_set); $upgrade_set_created = TRUE; } // sequence did not previously exist, add it $slony_stage3_ofs->write(sprintf(slony1_slonik::script_add_sequence, dbsteward::string_cast($new_replica_set['upgradeSetId']), dbsteward::string_cast($new_replica_set['originNodeId']), dbsteward::string_cast($new_sequence['slonyId']), $new_schema['name'] . '.' . $new_sequence['name'], $new_schema['name'] . '.' . $new_sequence['name'] . ' sequence replication') . "\n\n"); } } } // if we created an upgrade set, subscribe and merge it if ($upgrade_set_created) { $slony_stage3_ofs->write("ECHO 'Waiting for merge set creation';\n"); $slony_stage3_ofs->write(sprintf(slony1_slonik::script_node_sync_wait, $new_replica_set['originNodeId'], $new_replica_set['originNodeId'], $new_replica_set['originNodeId']) . "\n\n"); // foreach ($new_replica_set->slonyReplicaSetNode as $replica_node) { // subscribe replicaNode to its provider node providerId $slony_stage3_ofs->write("ECHO 'Subscribing replicaNode " . $replica_node['id'] . " to providerNodeId " . $replica_node['providerNodeId'] . " set ID " . $new_replica_set['upgradeSetId'] . "';\n"); $slony_stage3_ofs->write(sprintf(slony1_slonik::script_subscribe_set, $new_replica_set['upgradeSetId'], $replica_node['providerNodeId'], $replica_node['id']) . "\n\n"); // do a sync and wait for it on the subscribing node $slony_stage3_ofs->write("ECHO 'Waiting for replicaNode " . $replica_node['id'] . " subscription to providerNodeId " . $replica_node['providerNodeId'] . " set ID " . $new_replica_set['upgradeSetId'] . "';\n"); $slony_stage3_ofs->write(sprintf(slony1_slonik::script_node_sync_wait, $new_replica_set['originNodeId'], $new_replica_set['originNodeId'], $replica_node['id']) . "\n\n"); } // now we can merge the upgrade set to the main $slony_stage3_ofs->write("ECHO 'Merging replicationUpgradeSet " . $new_replica_set['upgradeSetId'] . " to set " . $new_replica_set['id'] . "';\n"); $slony_stage3_ofs->write(sprintf(slony1_slonik::script_merge_set, $new_replica_set['id'], $new_replica_set['upgradeSetId'], $new_replica_set['originNodeId']) . "\n\n"); } // execute post-slony shaping SQL DDL / DCL commands at the end of stage 1 and 3 .slonik files $sql_stage1_file = $slonik_file_prefix . '_stage1_schema1.sql'; // TODO: need to collect sql_stage1_file names from diff_doc() if there is // more than one as a result of many changes between definition files $slony_stage1_ofs->write("ECHO 'DBSteward upgrade replica set " . $new_replica_set['id'] . " stage 1 SQL EXECUTE SCRIPT';\n"); $slony_stage1_ofs->write("EXECUTE SCRIPT (\n FILENAME = '" . basename($sql_stage1_file) . "',\n EVENT NODE = " . $new_replica_set['originNodeId'] . "\n);\n\n"); $sql_stage3_file = $slonik_file_prefix . '_stage3_schema1.sql'; $slony_stage3_ofs->write("ECHO 'DBSteward upgrade replica set " . $new_replica_set['id'] . " stage 3 SQL EXECUTE SCRIPT';\n"); $slony_stage3_ofs->write("EXECUTE SCRIPT (\n FILENAME = '" . basename($sql_stage3_file) . "',\n EVENT NODE = " . $new_replica_set['originNodeId'] . "\n);\n\n"); }