// // The ISchema interface // public VxSchemaErrors Put(VxSchema schema, VxSchemaChecksums sums, VxPutOpts opts) { log.print("Put\n"); bool no_retry = (opts & VxPutOpts.NoRetry) != 0; int old_err_count = -1; IEnumerable<string> keys = schema.Keys; VxSchemaErrors errs = new VxSchemaErrors(); // Sometimes we'll get schema elements in the wrong order, so retry // until the number of errors stops decreasing. while (errs.Count != old_err_count) { log.print("Calling Put on {0} entries\n", old_err_count == -1 ? schema.Count : errs.Count); old_err_count = errs.Count; errs.Clear(); List<string> tables = new List<string>(); List<string> nontables = new List<string>(); foreach (string key in keys) { if (schema[key].type == "Table") tables.Add(key); else nontables.Add(key); } errs.Add(PutSchemaTables(tables, schema, sums, opts)); foreach (string key in nontables) { log.print("Calling PutSchema on {0}\n", key); VxSchemaError e = PutSchemaElement(schema[key], opts); if (e != null) errs.Add(key, e); } // If we only had one schema element, retrying it isn't going to // fix anything. We retry to fix ordering problems. if (no_retry || errs.Count == 0 || schema.Count == 1) break; log.print("Got {0} errors, old_errs={1}, retrying\n", errs.Count, old_err_count); keys = errs.Keys.ToList(); } return errs; }
private VxSchemaErrors PutSchemaTable(VxSchemaTable curtable, VxSchemaTable newtable, VxPutOpts opts) { bool destructive = (opts & VxPutOpts.Destructive) != 0; string tabname = newtable.name; string key = newtable.key; var diff = VxSchemaTable.GetDiff(curtable, newtable); var coladd = new List<VxSchemaTableElement>(); var coldel = new List<VxSchemaTableElement>(); var colchanged = new List<VxSchemaTableElement>(); var otheradd = new List<VxSchemaTableElement>(); var otherdel = new List<VxSchemaTableElement>(); foreach (var kvp in diff) { VxSchemaTableElement elem = kvp.Key; VxDiffType difftype = kvp.Value; if (elem.elemtype == "primary-key" || elem.elemtype == "index") { if (difftype == VxDiffType.Add) otheradd.Add(elem); else if (difftype == VxDiffType.Remove) otherdel.Add(elem); else if (difftype == VxDiffType.Change) { // We don't want to bother trying to change indexes or // primary keys; it's easier to just delete and re-add // them. otherdel.Add(curtable[elem.GetElemKey()]); otheradd.Add(elem); } } else { if (difftype == VxDiffType.Add) coladd.Add(elem); else if (difftype == VxDiffType.Remove) coldel.Add(elem); else if (difftype == VxDiffType.Change) colchanged.Add(elem); } } var errs = new VxSchemaErrors(); // Might as well check this sooner rather than later. if (!destructive && coldel.Count > 0) { List<string> colstrs = new List<string>(); foreach (var elem in coldel) colstrs.Add(elem.GetParam("name")); // Sorting this is mostly unnecessary, except it makes life a lot // nicer in the unit tests. colstrs.Sort(); string errmsg = wv.fmt("Refusing to drop columns ([{0}]) " + "when the destructive option is not set.", colstrs.join("], [")); errs.Add(key, new VxSchemaError(key, errmsg, -1)); goto done; } // Perform any needed column changes. // Note: we call dbi.execute directly, instead of DbiExec, as we're // running SQL we generated ourselves so we shouldn't blame any // errors on the client's SQL. We'll catch the DbExceptions and // turn them into VxSchemaErrors. var deleted_indexes = new List<VxSchemaTableElement>(); var added_columns = new List<VxSchemaTableElement>(); bool transaction_started = false; bool transaction_resolved = false; try { // Delete any to-delete indexes first, to get them out of the way. // Indexes are easy to deal with, they don't cause data loss. // Note: we can't do this inside the transaction, MSSQL doesn't // let you change columns that used to be covered by the dropped // indexes. Instead we'll drop the indexes outside the // transaction, and restore them by hand if there's an error. foreach (var elem in otherdel) { log.print("Dropping {0}\n", elem.ToString()); string idxname = elem.GetParam("name"); // Use the default primary key name if none was specified. if (elem.elemtype == "primary-key" && idxname.e()) idxname = curtable.GetDefaultPKName(); var err = DropSchemaElement("Index/" + tabname + "/" + idxname); if (err != null) { errs.Add(key, err); goto done; } deleted_indexes.Add(elem); } // If an ALTER TABLE query fails inside a transaction, the // transaction is automatically rolled back, even if you start // an inner transaction first. This makes error handling // annoying. So before we start the real transaction, try to make // the column changes in a test transaction that we'll always roll // back to see if they'd fail. var ErrWhenAltering = new Dictionary<string, VxSchemaError>(); foreach (var elem in colchanged) { VxSchemaError err = null; log.print("Doing a trial run of modifying {0}\n", elem.GetElemKey()); dbi.execute("BEGIN TRANSACTION coltest"); try { // Try to change the column the easy way, without dropping // or adding anything and without any expected errors. var change_errs = ApplyChangedColumn(newtable, curtable[elem.GetElemKey()], elem, null, VxPutOpts.None); if (change_errs.Count > 0) err = change_errs[newtable.key][0]; } catch (SqlException e) { // OK, the easy way doesn't work. Remember the error for // when we do it for real. log.print("Caught exception in trial run: {0} ({1})\n", e.Message, e.Number); err = new VxSchemaError(key, e); } log.print("Rolling back, err='{0}'\n", err == null ? "" : err.ToString()); DbiExecRollback("coltest"); ErrWhenAltering.Add(elem.GetElemKey(), err); } log.print("About to begin real transaction\n"); // Add new columns before deleting old ones; MSSQL won't let a // table have no data columns in it, even temporarily. // Do this outside the transaction since failures here will // automatically cause a rollback, even if we handle them. // It's easy enough for us to roll back by hand if needed. foreach (var elem in coladd) { log.print("Adding {0}\n", elem.ToString()); string add_format = "ALTER TABLE [{0}] ADD {1}\n"; string query = wv.fmt(add_format, tabname, newtable.ColumnToSql(elem, true)); try { dbi.execute(query); } catch (SqlException e) { // Error 4901: adding a column on a non-empty table failed // due to neither having a default nor being nullable. // Don't try anything special in destructive mode, just // fail and nuke the table. if (!destructive && e.Number == 4901) { log.print("Couldn't add a new non-nullable column " + "without a default. Making column nullable.\n"); var nullable = GetNullableColumn(elem); string nullquery = wv.fmt(add_format, tabname, newtable.ColumnToSql(nullable, true)); log.print("Executing {0}", nullquery); dbi.execute(nullquery); } else throw; } added_columns.Add(elem); } transaction_started = true; dbi.execute("BEGIN TRANSACTION TableUpdate"); foreach (var elem in coldel) { log.print("Dropping {0}\n", elem.ToString()); DropTableColumn(newtable, elem); } foreach (var elem in colchanged) { var expected_err = ErrWhenAltering[elem.GetElemKey()]; var change_errs = ApplyChangedColumn(newtable, curtable[elem.GetElemKey()], elem, expected_err, opts); if (change_errs != null && change_errs.Count > 0) { errs.Add(change_errs); goto done; } } // Now that all the columns are finalized, add in any new indices. foreach (var elem in otheradd) { log.print("Adding {0}\n", elem.ToString()); VxSchemaError err = PutSchemaTableIndex(key, curtable, elem); if (err != null) { errs.Add(key, err); goto done; } } log.print("All changes made, committing transaction.\n"); dbi.execute("COMMIT TRANSACTION TableUpdate"); transaction_resolved = true; } catch (SqlException e) { var err = new VxSchemaError(key, e); log.print("Caught exception: {0}\n", err.ToString()); errs.Add(key, err); } finally { if (transaction_started && !transaction_resolved) { log.print("Transaction failed, rolling back.\n"); if (transaction_started) DbiExecRollback("TableUpdate"); foreach (var elem in added_columns) { log.print("Restoring {0}\n", elem.ToString()); try { DropTableColumn(newtable, elem); } catch (SqlException e) { log.print("Caught error clearing column: {0}\n", e.Message); } } foreach (var elem in deleted_indexes) { log.print("Restoring index {0}\n", elem.ToString()); var err = PutSchemaTableIndex(key, curtable, elem); if (err != null) errs.Add(key, err); } } } // Check for null entries in columns that are supposed to be non-null if (errs.Count == 0) { foreach (var elem in newtable) { string nullity = elem.GetParam("null"); if (elem.elemtype == "column" && nullity.ne() && nullity != "1") { string colname = elem.GetParam("name"); string query = wv.fmt("SELECT count(*) FROM [{0}] " + "WHERE [{1}] IS NULL", tabname, colname); int num_nulls = -1; try { num_nulls = dbi.select_one(query); } catch (SqlException e) { string errmsg = wv.fmt( "Couldn't figure out if '{0}' has nulls: {1}", colname, e.Message); log.print(errmsg + "\n"); errs.Add(key, new VxSchemaError( key, errmsg, -1, WvLog.L.Warning)); } if (num_nulls > 0) { string errmsg = wv.fmt("Column '{0}' was requested " + "to be non-null but has {1} null elements.", colname, num_nulls); log.print(errmsg + "\n"); errs.Add(key, new VxSchemaError( key, errmsg, -1, WvLog.L.Warning)); } } } } done: return errs; }
private VxSchemaErrors PutSchemaTables(List<string> tables, VxSchema newschema, VxSchemaChecksums newsums, VxPutOpts opts) { VxSchema curschema = Get(tables); VxSchemaErrors errs = new VxSchemaErrors(); foreach (string key in tables) { log.print("Putting table {0}\n", key); string curtype = curschema.ContainsKey(key) ? curschema[key].type : "Table"; string newtype = newschema.ContainsKey(key) ? newschema[key].type : "Table"; if (newtype != "Table" || curtype != "Table") throw new ArgumentException("PutSchemaTables called on " + "non-table element '" + key + "'."); // Check for the easy cases, an all-new table or table deletion if (!curschema.ContainsKey(key)) { // New table, let PutSchemaElement handle it like before. VxSchemaError e = PutSchemaElement(newschema[key], opts); if (e != null) errs.Add(key, e); continue; } if (!newschema.ContainsKey(key)) { // Deleted table, let DropSchemaElement deal with it. VxSchemaError e = DropSchemaElement(key); if (e != null) errs.Add(key, e); continue; } // An existing table has been modified. VxSchemaTable newtable; VxSchemaTable curtable; if (newschema[key] is VxSchemaTable) newtable = (VxSchemaTable)newschema[key]; else newtable = new VxSchemaTable(newschema[key]); if (curschema[key] is VxSchemaTable) curtable = (VxSchemaTable)curschema[key]; else curtable = new VxSchemaTable(curschema[key]); VxSchemaErrors put_table_errs = null; put_table_errs = PutSchemaTable(curtable, newtable, opts); // If anything goes wrong updating a table in destructive mode, // drop and re-add it. We want to be sure the schema is updated // exactly. bool destructive = (opts & VxPutOpts.Destructive) != 0; if (destructive && put_table_errs.Count > 0) { put_table_errs = null; log.print("Couldn't cleanly modify table '{0}'. Dropping " + "and re-adding it.\n", newtable.name); VxSchemaError e = PutSchemaElement(newschema[key], opts); if (e != null) errs.Add(key, e); } if (put_table_errs != null && put_table_errs.Count > 0) errs.Add(put_table_errs); } return errs; }
private VxSchemaErrors ApplyChangedColumn(VxSchemaTable table, VxSchemaTableElement oldelem, VxSchemaTableElement newelem, VxSchemaError expected_err, VxPutOpts opts) { VxSchemaErrors errs = new VxSchemaErrors(); log.print("Altering {0}\n", newelem.ToString()); bool destructive = (opts & VxPutOpts.Destructive) != 0; string colname = newelem.GetParam("name"); // Remove any old default constraint; even if it doesn't change, it // can get in the way of modifying the column. We'll add it again // later if needed. if (oldelem.HasDefault()) { string defquery = wv.fmt("ALTER TABLE [{0}] DROP CONSTRAINT {1}", table.name, table.GetDefaultDefaultName(colname)); log.print("Executing {0}\n", defquery); dbi.execute(defquery); } bool did_default_constraint = false; // Don't try to alter the table if we know it won't work. if (expected_err == null) { string query = wv.fmt("ALTER TABLE [{0}] ALTER COLUMN {1}", table.name, table.ColumnToSql(newelem, false)); log.print("Executing {0}\n", query); dbi.execute(query); } else { // Some table attributes can't be changed by ALTER TABLE, // such as changing identity values, or data type changes that // would truncate data. If the client has set the Destructive // flag though, we can try to drop and re-add the column. if (destructive) { log.print("Alter column would fail, dropping and adding.\n"); log.print("Expected error message: {0} ({1})\n", expected_err.msg, expected_err.errnum); string delquery = wv.fmt("ALTER TABLE [{0}] " + "DROP COLUMN [{1}]", table.name, colname); // We need to include the default value here (the second // parameter to ColumnToSql), otherwise adding a column to a // table with data in it might not work. string addquery = wv.fmt("ALTER TABLE [{0}] ADD {1}", table.name, table.ColumnToSql(newelem, true)); log.print("Executing {0}\n", delquery); dbi.execute(delquery); log.print("Executing {0}\n", addquery); dbi.execute(addquery); did_default_constraint = true; } else { // Error 515: Can't modify a column because it contains nulls // and the column requires non-nulls. if (expected_err.errnum == 515) { log.print("Couldn't modify column due to null " + "restriction. Making column nullable.\n"); var nullable = GetNullableColumn(newelem); string query = wv.fmt("ALTER TABLE [{0}] ALTER COLUMN {1}", table.name, table.ColumnToSql(nullable, false)); log.print("Executing {0}\n", query); dbi.execute(query); } else { log.print("Can't alter table and destructive flag " + "not set. Giving up.\n"); string key = table.key; string errmsg = wv.fmt("Refusing to drop and re-add " + "column [{0}] when the destructive option " + "is not set. Error when altering was: '{1}'", colname, expected_err.msg); errs.Add(key, new VxSchemaError(key, errmsg, -1)); } } } // No errors so far, let's try to add the new default values if we // didn't do it already. // FIXME: Check for actual errors, don't care about warnings. if (errs.Count == 0 && newelem.HasDefault() && !did_default_constraint) { string defquery = wv.fmt("ALTER TABLE [{0}] ADD CONSTRAINT {1} " + "DEFAULT {2} FOR {3}", table.name, table.GetDefaultDefaultName(colname), newelem.GetParam("default"), colname); log.print("Executing {0}\n", defquery); dbi.execute(defquery); } if (errs.Count != 0) log.print("Altering column had errors: " + errs.ToString()); return errs; }
// Deletes the named objects in the database. public VxSchemaErrors DropSchema(params string[] keys) { VxSchemaErrors errs = new VxSchemaErrors(); foreach (string key in keys) { VxSchemaError e = DropSchemaElement(key); if (e != null) errs.Add(key, e); } return errs; }