Plugin baselines skip tables that already exist
This commit is contained in:
@@ -104,7 +104,12 @@ def create_plugin_tables(plugin_name: str):
|
||||
this is safe on an existing database that has the table from the pre-cutover
|
||||
core baseline as well as on a fresh install (ADR-014 Phase 2).
|
||||
|
||||
Called from each plugin's 0001 baseline.py upgrade().
|
||||
NOT actually called from anywhere: every post-cutover baseline emits
|
||||
explicit Alembic ops instead, because this helper's per-plugin MetaData
|
||||
cannot resolve a foreign key to a CORE table at CreateTable-compile time.
|
||||
Kept because it is still the reference for what idempotent means here, and
|
||||
because create_table_if_missing below is the same rule in a form the
|
||||
baselines can use. See that function for what went wrong without it.
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
@@ -120,6 +125,67 @@ def create_plugin_tables(plugin_name: str):
|
||||
op.execute(str(CreateTable(table).compile(dialect=bind.dialect)))
|
||||
|
||||
|
||||
def create_table_if_missing(table_name: str, *columns, **kwargs) -> bool:
|
||||
"""op.create_table, skipped when the table is already there.
|
||||
|
||||
WHY EVERY POST-CUTOVER BASELINE NEEDS THIS. A baseline is the sole creator
|
||||
of its plugin's tables, so it was written as a plain op.create_table - which
|
||||
is correct only if the table's existence and the plugin's Alembic stamp can
|
||||
never disagree. They can, and routinely do:
|
||||
|
||||
* `flask db-utils create-all` runs db.create_all(), which builds EVERY
|
||||
table registered on the metadata - plugin tables included - and stamps
|
||||
NOTHING. `flask plugin upgrade-all` then runs the baseline against
|
||||
tables that already exist.
|
||||
* A run that creates the table and then fails before its stamp commits
|
||||
leaves the same split, and re-running is the obvious thing to try.
|
||||
|
||||
Reported as `flask plugin upgrade-all` failing the tools plugin with
|
||||
"table securesendsecrets already exists". Nothing caught it because the
|
||||
migration test drops the post-cutover tables db.create_all() made before
|
||||
running upgrade-all, which arranges the one state where an unguarded create
|
||||
works and never the state a real database is in.
|
||||
|
||||
Returns True when it created the table, so a caller can skip the indexes
|
||||
that belong to it.
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
|
||||
if inspect(op.get_bind()).has_table(table_name):
|
||||
logger.info('%s already exists, skipping create', table_name)
|
||||
return False
|
||||
op.create_table(table_name, *columns, **kwargs)
|
||||
return True
|
||||
|
||||
|
||||
def create_index_if_missing(index_name: str, table_name: str, columns, **kwargs):
|
||||
"""op.create_index, skipped when an equivalent index already exists.
|
||||
|
||||
Matched on the COLUMN LIST rather than the name, because the two creators
|
||||
do not agree on names. db.create_all() takes them from SQLAlchemy's default
|
||||
convention (`ix_securesendsecrets_token`) while a baseline spells them the
|
||||
way this repo's naming rules require (`ixsecuresendsecretstoken`). Matching
|
||||
on the name would call the index missing and add a second one over the same
|
||||
column - MySQL permits that, with only a warning, so the duplicate would
|
||||
accumulate silently on every affected site.
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
|
||||
wanted = [str(column) for column in columns]
|
||||
inspector = inspect(op.get_bind())
|
||||
if not inspector.has_table(table_name):
|
||||
op.create_index(index_name, table_name, columns, **kwargs)
|
||||
return
|
||||
for existing in inspector.get_indexes(table_name):
|
||||
if list(existing.get('column_names') or []) == wanted:
|
||||
logger.info('index over %s(%s) already exists as %s, skipping',
|
||||
table_name, ', '.join(wanted), existing.get('name'))
|
||||
return
|
||||
op.create_index(index_name, table_name, columns, **kwargs)
|
||||
|
||||
|
||||
def drop_plugin_tables(plugin_name: str):
|
||||
"""Mirror of create_plugin_tables for downgrade(). Drops in reverse FK
|
||||
order."""
|
||||
|
||||
Reference in New Issue
Block a user