Skip to content

Weasel 9.29.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 02 Sep 11:45
· 106 commits to master since this release
fbc2c44

Clears 9.28's known upgrade break, makes the SQLite table rebuild safe against a database that has foreign keys, and adds two capabilities. Minor rather than patch: two of these add API, and two change what an existing migration does.

If you stayed on 9.27

You can upgrade straight to 9.29. The 9.28 break — a numeric or decimal SQLite column making the first migration throw on every AutoCreate except All — is fixed. No pass through 9.28, no run with AutoCreate.All.

Read this before upgrading

Two changes can turn a migration that used to appear to succeed into one that throws. Both are deliberate, and in both cases it was not succeeding before.

A SQLite rebuild that would leave a dangling foreign key reference is now refused and rolled back, with an InvalidOperationException naming the table. Previously it committed the damage. The check only runs when foreign key enforcement was on to begin with — a database deliberately running with foreign_keys OFF is allowed to hold dangling rows and is not second-guessed.

A custom IMigrationLogger that declines to rethrow used to let a failed rebuild reach COMMIT half-applied. It is still handed the failure through OnFailure, but the migration no longer continues past it.

What changed

#542 — the guard in front of a rebuild no longer refuses it. Closes #538. SchemaMigration.AssertPatchingIsValid threw for SchemaPatchDifference.Invalid on every AutoCreate except All, without asking whether the delta could actually apply the change. Both apply paths below it do ask, and answer a rebuildable delta by rebuilding it and copying the data rather than dropping and recreating. The gate was stricter than the thing it gated. CreateOrUpdate now permits a rebuild; CreateOnly still refuses one, and needs no special case to do it — the delta is still Invalid, so the migration's Difference is still not Create.

This reverses a decision #477 shipped on purpose, that a rebuild always needs AutoCreate.All because a rebuild which also drops a column takes that column's data with it. That reasoning did not survive contact with the code: AssertPatchingIsValid could only express it by refusing every rebuildable delta, including the great majority that drop nothing, while SQLite's ordinary Update path was already emitting ALTER TABLE … DROP COLUMN under CreateOrUpdate. The strict rule only refused the same loss when the column happened to sit in a key.

#539 — the SQLite table rebuild is atomic. SQLite cannot ALTER most of a table, so a change is applied by rebuilding it: create, copy, drop, rename. Those four statements ran with no transaction, each one autocommitting. Rebuilding a table that another table's foreign key referenced broke the database — the DROP failed on its implicit delete, after the replacement had been created, filled and committed. The orphan _new table survived, CREATE TABLE IF NOT EXISTS no-opped on it at the next start, and every run after that failed differently until someone dropped it by hand. Foreign keys are on by default in Microsoft.Data.Sqlite and in all three SqlitePragmaSettings presets, so this was the ordinary case. The rebuild now runs the way SQLite documents it: enforcement suspended, the whole thing in one transaction, foreign_key_check before the commit. Two smaller fixes ride along — an AUTOINCREMENT table came out of a rebuild with a lower sqlite_sequence high-water mark than it went in with and reissued an id it had already handed out, and a view over the rebuilt table failed the rename outright.

#540 — mutually referencing tables can be created from scratch. A table's foreign keys are written into its own create statement, so a key pointing at a table the migration has not created yet references nothing. Two tables referencing each other could never be created — neither could go first, the apply threw on the first ALTER, and every later delta was abandoned including the create of the table the key was waiting for, so re-running reproduced the same failure and never recovered. A migration now holds back exactly the keys that would fail, those whose referenced table is created by a later delta, and applies them once every delta has run. Keys whose target already exists, or is created earlier in the same migration, stay where they were, so a schema that never had the problem generates byte-for-byte identical DDL. Only SQL Server and PostgreSQL defer; SQLite writes its foreign keys inline and never had the problem. Note that rolling such a migration back is not symmetrical — on SQL Server neither table of a cycle can be dropped while the other's key points at it, and that is a follow-up.

#543 — a full text index over a tsvector you built yourself. Closes #541; PostgreSQL. FullTextIndexDefinition wrapped whatever it was given in to_tsvector, so DocumentConfig was always text to be converted and there was no way to hand it an expression that was already a tsvector. That put per-member weighting out of reach, because setweight labels a vector — weighting works by converting each member separately and concatenating the vectors, not the text. TsVectorExpression is consumed without the wrapping, via the ForTsVector factory. Leave it unset and the definition behaves exactly as it always has, down to the byte, which matters because a changed index expression makes Weasel drop and recreate the index. Read IndexedTsVector to find what is actually indexed: the DDL comes from that property and nothing else, so a consumer building the query-side filter should read it from there too — a ts_rank computed over a different vector than the one @@ filtered on is silently wrong rather than merely slow. This unblocks the setweight half of JasperFx/marten#5298.

#545 — SQLite delete-all no longer clears the wrong schema's table. GenerateDeleteAllSql built every statement from the table name alone and threw the schema away, so each went out unqualified and SQLite resolved it by search order — which puts temp first. A temp table silently took the delete meant for the main one, and sqlite_sequence is per-database for the same reason.

Known issue

#546: on SQLite, resetting identity against a schema that has no AUTOINCREMENT table fails with no such table: <schema>.sqlite_sequence, because that schema has no sqlite_sequence. The single-schema form of this predates 9.29; the multi-schema form is new with #545 and is the price of that fix being correct. Calling GenerateDeleteAllSql yourself you can pass resetIdentity: false; going through DatabaseCleaner you cannot.

Full upgrade guide: https://weasel.jasperfx.net/release-9-29