Another thing to keep in mind is that for a large DB rewriting a table, even in the background, is actually physically expensive. FDB is a huge distributed btree, and btrees have a lot of write amplification. Rewriting the rows in order will reduce that amplification considerably (because FDB naturally batches disk updates due to the in-memory MVCC design) but even then rewriting every key is naturally going to wear out your SSDs, which is not free. On the cloud they will charge you iops instead, so same problem.
After some thought I think this design would work:
- Store a schema somewhere in the keyspace, perhaps in its own tenant
- Store the ecto migration “version” under a key in the schema (transactionally) - I assume an Ecto adapter has access to this value?
- Store a list of fields in the schema, and give each one a unique (monotonic would be fine) integer id
To create a field: add a new field
key with a new (monotonic) integer id. It is easy to allocate ids transactionally because migrations are rare so there is no contention. You can store the next_id
in the schema.
To rename a field: simply point the new name to the id previously used by the old name, and then delete the old name.
To delete a field: well, delete the field! The id will remain “allocated” because they are monotonic.
So, to run a migration, we perform the above operations and then update the “schema version” to whatever Ecto provides, similar to how it works with Postgres. There should be no problem submitting this as a single transaction to FDB, as it won’t be large.
Naturally, you can then store rows with integer keys instead of string keys, where the integers are the above ids. You could even encode them as varints for even more space savings since there are only as many ids as fields have existed (i.e. not many).
Finally, for correctness you would have to read the “schema version” in every transaction to ensure it matches what the client expects. This would create a bottleneck for a large DB, but you could use the \xFF/metadataVersion
key for cache invalidation (increment it on every migration). That key was specifically designed to do exactly this.
You can cache the %{field => id}
and vice-versa mappings in an ets table (or persistent_term, perhaps, would be better) along with the Ecto version and the metadataVersion.