Repository navigation
Add idempotent seed-data command #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
b225fb3
Add idempotent seed-data command
Subhransu-De 93e74ca
Lock seeded tables while moving sequences
Subhransu-De bf67368
Seed explicit IDs into always-generated identity columns
Subhransu-De 24b73fa
Seed rows one at a time and respect sequence direction
Subhransu-De d95c5dc
Batch seed inserts and restart sequences transactionally
Subhransu-De 85de8d0
Keep fixture order and sequence state on seed reruns
Subhransu-De 8339d6f
Restart sequences on their increment progression
Subhransu-De 4f1c65b
Require a conflict key per fixture row and respect sequence bounds
Subhransu-De bf48606
Ignore partial indexes as seed keys and keep restarts in range
Subhransu-De 8459dea
Reserve explicit fixture IDs and reject sequence exhaustion
Subhransu-De bb050d7
Narrow seeding to primary-key fixtures
Subhransu-De bad4f95
Seed only fixture tables in the default schema
Subhransu-De 662555e
Only advance identity sequences when seeding
Subhransu-De File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| from collections.abc import Mapping, Sequence | ||
| from itertools import groupby | ||
|
|
||
| from sqlalchemy import ( | ||
| Connection, | ||
| MetaData, | ||
| Table, | ||
| cast, | ||
| column, | ||
| func, | ||
| select, | ||
| table, | ||
| text, | ||
| ) | ||
| from sqlalchemy.dialects.postgresql import REGCLASS, insert | ||
|
|
||
| type Row = Mapping[str, object] | ||
| type Fixtures = Mapping[str, Sequence[Row]] | ||
|
|
||
| _PG_CLASS = table("pg_class", column("oid"), column("relname"), column("relnamespace")) | ||
| _PG_NAMESPACE = table("pg_namespace", column("oid"), column("nspname")) | ||
|
|
||
|
|
||
| def seed(connection: Connection, fixtures: Fixtures) -> dict[str, int]: | ||
| metadata = MetaData() | ||
| metadata.reflect(bind=connection, only=list(fixtures)) | ||
| inserted: dict[str, int] = {} | ||
| for seeded_table in metadata.sorted_tables: | ||
| rows = fixtures.get(seeded_table.name) | ||
| if seeded_table.schema is not None or not rows: | ||
| continue | ||
| _require_primary_keys(seeded_table, rows) | ||
| _lock_against_writers(connection, seeded_table) | ||
| inserted[seeded_table.name] = _insert_rows(connection, seeded_table, rows) | ||
| _restart_identity_sequences(connection, seeded_table) | ||
| return inserted | ||
|
|
||
|
|
||
| def _require_primary_keys(seeded_table: Table, rows: Sequence[Row]) -> None: | ||
| key = [column.name for column in seeded_table.primary_key.columns] | ||
| if not key or any(row.get(name) is None for row in rows for name in key): | ||
| msg = f"{seeded_table.name} fixture rows must set the primary key {key}" | ||
| raise ValueError(msg) | ||
|
|
||
|
|
||
| def _lock_against_writers(connection: Connection, seeded_table: Table) -> None: | ||
| table_name = connection.dialect.identifier_preparer.format_table(seeded_table) | ||
| connection.execute(text(f"LOCK TABLE {table_name} IN SHARE ROW EXCLUSIVE MODE")) | ||
|
|
||
|
|
||
| def _insert_rows( | ||
| connection: Connection, seeded_table: Table, rows: Sequence[Row] | ||
| ) -> int: | ||
| key = list(seeded_table.primary_key.columns) | ||
| statement = ( | ||
| insert(seeded_table).on_conflict_do_nothing(index_elements=key).returning(*key) | ||
| ) | ||
| return sum( | ||
| len(connection.execute(statement, list(batch)).all()) | ||
|
Subhransu-De marked this conversation as resolved.
|
||
| for _, batch in groupby(rows, key=frozenset) | ||
| ) | ||
|
|
||
|
|
||
| def _restart_identity_sequences(connection: Connection, seeded_table: Table) -> None: | ||
| table_name = connection.dialect.identifier_preparer.format_table(seeded_table) | ||
| for key_column in seeded_table.primary_key.columns: | ||
| sequence = connection.scalar( | ||
| select(func.pg_get_serial_sequence(table_name, key_column.name)) | ||
| ) | ||
| if sequence is None: | ||
| continue | ||
| highest = connection.execute(select(func.max(key_column))).scalar_one() | ||
| if highest >= _next_value(connection, sequence): | ||
|
Subhransu-De marked this conversation as resolved.
|
||
| connection.execute( | ||
| text(f"ALTER SEQUENCE {sequence} RESTART WITH {int(highest) + 1}") | ||
| ) | ||
|
|
||
|
|
||
| def _next_value(connection: Connection, sequence: str) -> int: | ||
| schema, name = connection.execute( | ||
| select(_PG_NAMESPACE.c.nspname, _PG_CLASS.c.relname) | ||
| .join_from( | ||
| _PG_CLASS, _PG_NAMESPACE, _PG_CLASS.c.relnamespace == _PG_NAMESPACE.c.oid | ||
| ) | ||
| .where(_PG_CLASS.c.oid == cast(sequence, REGCLASS)) | ||
| ).one() | ||
| state = table(name, column("last_value"), column("is_called"), schema=schema) | ||
| last_value, is_called = connection.execute( | ||
| select(state.c.last_value, state.c.is_called) | ||
| ).one() | ||
| return last_value + 1 if is_called else last_value | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import logging | ||
| import os | ||
|
|
||
| from sqlalchemy import create_engine, pool | ||
|
|
||
| from seed import seed | ||
| from seed.fixtures import FIXTURES | ||
|
|
||
| logger = logging.getLogger("seed") | ||
|
|
||
|
|
||
| def main() -> None: | ||
| logging.basicConfig(level=logging.INFO, format="%(message)s") | ||
| database_url = os.environ.get("DATABASE_URL") | ||
| if not database_url: | ||
| msg = "DATABASE_URL must be set to seed the database" | ||
| raise RuntimeError(msg) | ||
| engine = create_engine(database_url, poolclass=pool.NullPool) | ||
| try: | ||
| with engine.begin() as connection: | ||
| inserted = seed(connection, FIXTURES) | ||
| finally: | ||
| engine.dispose() | ||
| for table, count in inserted.items(): | ||
| logger.info("%s: %d new rows", table, count) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| from uuid import UUID | ||
|
|
||
| from seed import Fixtures | ||
|
|
||
| FIXTURES: Fixtures = { | ||
| "entities": [ | ||
| { | ||
| "id": UUID("6f1c2a52-8d3b-4c1e-9a4f-0b7e5d2c1a01"), | ||
| "name": "Sample entity", | ||
| "description": "Reference data loaded by make seed.", | ||
| }, | ||
| { | ||
| "id": UUID("6f1c2a52-8d3b-4c1e-9a4f-0b7e5d2c1a02"), | ||
| "name": "Second sample entity", | ||
| "description": None, | ||
| }, | ||
| ], | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.