Repository navigation
Conversation
0bf2152 to
cdf3811
Compare
|
Given something like a rolling deploy, does this do anything to prevent writes to the table while we're undergoing this process? |
| fn {reason, blocks} -> " - #{reason} (#{blocked_when(blocks)})" end | ||
| ) | ||
|
|
||
| warn(""" |
There was a problem hiding this comment.
wonder if we should error instead, and then have them pass like an explicit --no-rebuild if they want to skip it, and we just ignore it then and don't even try to generate migrations that we know won't work.
There was a problem hiding this comment.
I would say eventually yes, but initially i didn't want to force it on users since it's quite a big change, and users that do want that behaviour can easily flip the config at the repo level, to not have to think about it
It also depends on whether you want to support multiple ecto_sql versions. Since older ones lack the required callbacks they cannot have this default rebuild behaviour.
There was a problem hiding this comment.
Right but at the moment don't the migratiosn that emit alter statements just straight up fail?
There was a problem hiding this comment.
True, in most cases it fails with ecto_sqlite3 and ecto_libsql runs some of them, I listed every case in the updated description
Also reading again I think I misunderstood the first comment 😅
Is this what you meant?
- the task should raise when rebuild_tables isn't set and a change needs a rebuild
- --no-rebuild should leave failing changes out of the migration
- --rebuild-tables is what this PR does
|
Had some comments, in general this looks good but we can't merge it until we could reasonably deploy it unless you can do something to make it support both old and new versions of ecto_sql when it releases. |
cdf3811 to
8fe9f0d
Compare
I actually didn't know it was possible to have a rolling deploy with sqlite. On a single host, it seems possible to have overlapping containers sharing one db. The current PR on its own doesn't do anything to prevent writes during the migration, the guarantee comes from sqlite itself and its single writer model. So if the migration transaction acquires the write lock, concurrent writes either wait or fail. EDIT: added a guard to ensure the migration runs inside a transaction f652cd3 |
Yes of course, I am fine with that! I thought it could be useful to already open the PR even if it's not merged anytime soon I didn't think about supporting multiple ecto_sql versions, I explored this in 104a56a and 39b2fa0 does that look reasonable? Currently the CI would only run against a single ecto_sql version, the rebuild table part is not covered unless we change the workflow. So up to you, maybe waiting for the ecto_sql release is simpler |
55aa54b to
f652cd3
Compare
f652cd3 to
77d071a
Compare
Closes #239.
Adds opt-in table rebuilds for changes sqlite can't make in place (--rebuild-tables, or rebuild_tables: true in the repo config).
The generator then writes a rebuild_table migration (create, copy, drop, rename, as in the sqlite docs) instead of a failing statement.
By default, generated migrations are unchanged and only a hint says what could not be done.
It needs before_transaction/1 and after_transaction/1 from elixir-ecto/ecto_sql#758 (merged, not released yet).
I suggest reading by commit, but it's still quite large. The last 3 could be separate if you prefer.
Manual testing
The steps below were drafted with AI help. I ran them myself against a fresh Ash project.
Setup
(The
ecto_sqlandectolines are only needed until elixir-ecto/ecto_sql#758 is released; Mix only honorsoverridein your ownmix.exs, not the one in this branch. Amin_sqlite_versionwarning is printed when compiling; it comes from upstream.)1. Make an attribute optional. In
lib/support_desk/support/ticket.ex, replace thesubjectattribute with:Prints (in yellow) what SQLite can't do in place, then a
modify :subject, :text, null: truemigration that would fail.The migration is a
rebuild_table :ticketswith a comment saying why,subjecthas noNOT NULLany more, and both tickets are still there. (It also listsmodify :status ... default: "open": a plaindefault:becomes the column default.)2. Add a required attribute without a default. In the same file, add above the
statusattribute:mix ash.codegen add_priority --rebuild-tables && mix ash.migrateThe migration has a
REVIEW:note, andmix ash.migratestops withNOT NULL constraint failed: tickets_rebuild.priority, changing nothing. Give the existing rows a value, as the note says:Both rows have
priority = 1.3. Change a type. Change
priorityto a string:typeofis nowtext. The migration comment warns that a value that doesn't convert would be copied as it is.4. Drop a foreign key. First give a ticket a representative, while the foreign key still exists:
sqlite3 support_desk_dev.db "insert into representatives (id, name) values ('r', 'Ada'); update tickets set representative_id = 'r' where id = 'a'"In
lib/support_desk/support/ticket.ex, delete this block:and add this attribute above
status:The migration comment says
dropping the foreign key tickets_representative_id_fkey. The query showsa|r,b|and0: the rows keep their value and the table has no foreign key any more. Without--rebuild-tables, the generated migration raises "SQLite does not support dropping foreign key constraints".5. Roll back.
mix ash_sqlite.rollback -n 1 sqlite3 support_desk_dev.db "select id, representative_id from tickets; select \"table\" from pragma_foreign_key_list('tickets')"The foreign key to
representativesis back (representatives) and the rows are still there (a|r,b|).Rebuild cases
Behaviour on ash_sqlite main, and with
rebuild_tablesAI drafted, checked manually
rebuild_tables(this branch)upfails:ecto_sqlite3raisesALTER COLUMN not supported by SQLite3on themodifythat ash_sqlite generated.upanddownrun without error.upanddownrun and the rows are kept.updrops NOT NULL anddownrestores it.upfails:ecto_sqlite3raisesALTER COLUMN not supported by SQLite3on themodifythat ash_sqlite generated.upanddownrun without error. A row with NULL in the column keeps the NULL while the column is now NOT NULL, with no error.upanddownrun and the rows are kept.upmakes the column NOT NULL anddownmakes it optional again. If a row has NULL in the column,upfails in SQLite itself (NOT NULL constraint failed) and nothing is changed. The migration has aREVIEWcomment that says so, with acopy:example.upfails:ecto_sqlite3raisesALTER COLUMN not supported by SQLite3on themodifythat ash_sqlite generated.upanddownrun without error. The declared type changes, but existing values are not converted (a text'1'stays text in a BIGINT column).upanddownrun and the rows are kept. Existing values are converted (a text'1'becomes the integer1), anddownconverts them back to text.upfails:ecto_sqlite3raisesALTER COLUMN not supported by SQLite3on themodifythat ash_sqlite generated.upanddownrun without error.upanddownrun and the rows are kept.upadds the default anddownremoves it. Existing rows with NULL stay NULL.upfails:ecto_sqlite3raisesSQLite3 does not support ALTER TABLE DROP CONSTRAINTon thedrop constraintthat ash_sqlite generated. The migration also only marks the changed column as primary key, so the key would not be composite even if it ran.upfails:ecto_libsqlraisesLibSQL/SQLite does not support ALTER TABLE DROP CONSTRAINTon thedrop constraintthat ash_sqlite generated. The migration also only marks the changed column as primary key, so the key would not be composite even if it ran.upanddownrun and the rows are kept.upmakes the primary key(id, code)anddownmakes itidagain.on_deletechanged, or made deferrableupfails: the migration ash_sqlite generated containsraise "SQLite does not support dropping foreign key constraints…".upfails: the migration ash_sqlite generated containsraise "SQLite does not support dropping foreign key constraints…".upanddownrun and the rows are kept.upchangesON DELETEor removes the foreign key, anddownrestores it. A change todeferrablealone is also rebuilt, and the table stays the same (see the trade-offs).upruns.downfails: the migration ash_sqlite generated containsraise "SQLite does not support dropping foreign key constraints…".upruns.downfails: the migration ash_sqlite generated containsraise "SQLite does not support dropping foreign key constraints…".upanddownrun and the rows are kept.upadds the column with its foreign key, anddownrebuilds the table without the column.upfails in SQLite itself,Cannot add a NOT NULL column with default value NULL. Empty table:upanddownrun.upfails in SQLite itself,Cannot add a NOT NULL column with default value NULL. Empty table:upruns, anddownfails because of a bug inecto_libsql(no function clause matching in Ecto.Adapters.LibSql.Connection.execute_ddl/1), which it hits for any added column.upfails in SQLite itself (NOT NULL constraint failed) because there is no value to copy, and nothing is changed. The migration has aREVIEWcomment that says how to give the rows a value (copy:or a default). Empty table, on both:upanddownrun, includingdownonecto_libsql, which a plaindrop columnrollback does not.upanddownrun. The migration ash_sqlite generated only renames the column. The change is not in it.upanddownrun. The migration ash_sqlite generated only renames the column. The change is not in it.upanddownrun and the rows are kept.uprenames the column and applies the change, anddownrenames it back and restores the old definition.ecto_sqlite3,upanddownrun and the rows are kept: the table becomes STRICT, or stops being. Onecto_libsql,upfails withno function clause matching in Keyword.get/3Tradeoffs
Contributor checklist
Leave anything that you believe does not apply unchecked.