pgdevkit 0.3.4__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/PKG-INFO +1 -1
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/cli.py +30 -18
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/fetch_missing.py +2 -2
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/migrate.py +77 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/parser.py +10 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/schema.py +3 -1
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pyproject.toml +1 -1
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_migrate.py +59 -1
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/uv.lock +1 -1
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/.gitignore +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/.python-version +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/README.md +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/docs/database-layout.md +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/backends/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/backends/base.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/backends/mssql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/backends/postgres.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/complex_types.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/mssql_crud.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/db/mssql_sql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/dialect.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/models.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/mssql_introspect.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/_docker.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/api.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/constants.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/container.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/mssql/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/mssql/api.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/mssql/constants.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/mssql/container.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/skills/pgdevkit/SKILL.md +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/conftest.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/test_complex_types.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/test_mssql_crud_live.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/db/test_mssql_crud_sql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_compare.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_compare_mssql_live.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_connection.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_dialect.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_diff_mssql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_mssql_introspect.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/test_parser_mssql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_api_mssql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_api_mssql_live.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_config_mssql.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_container.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_mssql_constants.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_schema.py +0 -0
- {pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/test_schema_mssql.py +0 -0
|
@@ -371,24 +371,36 @@ def migrate_apply(
|
|
|
371
371
|
break
|
|
372
372
|
already_done = False
|
|
373
373
|
if ask:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
374
|
+
already_done = migrate.already_fully_applied(conninfo, path)
|
|
375
|
+
if already_done:
|
|
376
|
+
bar_write(f"Auto: {path.name} is already fully present in the database — marking as already done")
|
|
377
|
+
else:
|
|
378
|
+
# Hold the lock across the whole show-file+prompt step (not just each write) so
|
|
379
|
+
# the worker's bar_write() calls block until the question is answered instead
|
|
380
|
+
# of interleaving with the prompt or the migration text while it's being read.
|
|
381
|
+
with bar_lock:
|
|
382
|
+
bar.write(f"\n=== {path.name} ===")
|
|
383
|
+
bar.write(path.read_text(encoding="utf-8"))
|
|
384
|
+
answer = (
|
|
385
|
+
typer.prompt("[Y]es execute / [n]o skip / [a]lready done / [q]uit", default="y")
|
|
386
|
+
.strip()
|
|
387
|
+
.lower()
|
|
388
|
+
)
|
|
389
|
+
if answer in ("q", "quit"):
|
|
390
|
+
quit_requested = True
|
|
391
|
+
break
|
|
392
|
+
if answer in ("n", "no"):
|
|
393
|
+
bar_write(f"Skipped {path.name}")
|
|
394
|
+
outcomes.append((path.name, "skipped"))
|
|
395
|
+
bar_step()
|
|
396
|
+
continue
|
|
397
|
+
if answer in ("a", "already", "already done"):
|
|
398
|
+
already_done = True
|
|
399
|
+
elif answer not in ("", "y", "yes"):
|
|
400
|
+
bar_write(f"Skipped {path.name}")
|
|
401
|
+
outcomes.append((path.name, "skipped"))
|
|
402
|
+
bar_step()
|
|
403
|
+
continue
|
|
392
404
|
|
|
393
405
|
work_q.put((path, already_done))
|
|
394
406
|
|
|
@@ -9,7 +9,7 @@ import psycopg
|
|
|
9
9
|
|
|
10
10
|
from .diff import DiffKind, compute_diff
|
|
11
11
|
from .introspect import introspect_db
|
|
12
|
-
from .parser import parse_directory
|
|
12
|
+
from .parser import IGNORED_DIR_NAMES, parse_directory
|
|
13
13
|
|
|
14
14
|
_LAYER_PREFIX_RE = re.compile(r"^\d+_?")
|
|
15
15
|
|
|
@@ -41,7 +41,7 @@ def layer_folder_for(scripts_dir: Path, schema: str) -> Path:
|
|
|
41
41
|
if no existing folder matches."""
|
|
42
42
|
if scripts_dir.is_dir():
|
|
43
43
|
for entry in scripts_dir.iterdir():
|
|
44
|
-
if not entry.is_dir() or entry.name in ("migrations", "_migration_scripts"):
|
|
44
|
+
if not entry.is_dir() or entry.name in ("migrations", "_migration_scripts") or entry.name in IGNORED_DIR_NAMES:
|
|
45
45
|
continue
|
|
46
46
|
if _LAYER_PREFIX_RE.sub("", entry.name).lower() == schema.lower():
|
|
47
47
|
return entry
|
|
@@ -197,6 +197,83 @@ def _created_table_names(stmts: list[str]) -> list[str]:
|
|
|
197
197
|
return names
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
_ADD_COLUMN_RE = re.compile(
|
|
201
|
+
rf"ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?([\w.\"]+)\s+ADD\s+COLUMN\s+"
|
|
202
|
+
rf"(?:IF\s+NOT\s+EXISTS\s+)?\"?({_IDENTIFIER})\"?",
|
|
203
|
+
re.IGNORECASE,
|
|
204
|
+
)
|
|
205
|
+
_CREATE_RELATION_RE = re.compile(
|
|
206
|
+
r"CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?([\w.\"]+)"
|
|
207
|
+
r"|CREATE\s+SEQUENCE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w.\"]+)"
|
|
208
|
+
r"|CREATE\s+(?:MATERIALIZED\s+)?VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w.\"]+)",
|
|
209
|
+
re.IGNORECASE,
|
|
210
|
+
)
|
|
211
|
+
_CREATE_SCHEMA_RE = re.compile(r"CREATE\s+SCHEMA\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w\"]+)", re.IGNORECASE)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _idempotent_target(stmt: str) -> tuple[str, ...] | None:
|
|
215
|
+
"""Classify a single statement as one of the create-if-missing DDL shapes this module
|
|
216
|
+
can check for "already applied" with no ambiguity: ("relation", name) for a table,
|
|
217
|
+
index, sequence or view; ("schema", name); or ("column", table, column) for an ADD
|
|
218
|
+
COLUMN. None if the statement isn't one of these shapes — including any CREATE OR
|
|
219
|
+
REPLACE, which is never safe to treat as a no-op just because the object exists, since
|
|
220
|
+
the migration could be replacing it with different content."""
|
|
221
|
+
stripped = _strip_line_comments(stmt).strip()
|
|
222
|
+
if re.search(r"\bOR\s+REPLACE\b", stripped, re.IGNORECASE):
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
if re.match(r"CREATE\s+TABLE\b", stripped, re.IGNORECASE):
|
|
226
|
+
names = _created_table_names([stmt])
|
|
227
|
+
return ("relation", names[0]) if names else None
|
|
228
|
+
|
|
229
|
+
m = _CREATE_RELATION_RE.match(stripped)
|
|
230
|
+
if m:
|
|
231
|
+
name = next(g for g in m.groups() if g is not None)
|
|
232
|
+
return ("relation", name)
|
|
233
|
+
|
|
234
|
+
m = _CREATE_SCHEMA_RE.match(stripped)
|
|
235
|
+
if m:
|
|
236
|
+
return ("schema", m.group(1))
|
|
237
|
+
|
|
238
|
+
m = _ADD_COLUMN_RE.match(stripped)
|
|
239
|
+
if m:
|
|
240
|
+
return ("column", m.group(1), m.group(2))
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _target_exists(con: psycopg.Connection, target: tuple[str, ...]) -> bool:
|
|
246
|
+
kind = target[0]
|
|
247
|
+
if kind == "relation":
|
|
248
|
+
row = con.execute("select to_regclass(%s)", (target[1],)).fetchone()
|
|
249
|
+
elif kind == "schema":
|
|
250
|
+
row = con.execute("select to_regnamespace(%s)", (target[1],)).fetchone()
|
|
251
|
+
else: # column
|
|
252
|
+
row = con.execute(
|
|
253
|
+
"select 1 from pg_attribute where attrelid = to_regclass(%s) "
|
|
254
|
+
"and attname = %s and not attisdropped",
|
|
255
|
+
(target[1], target[2]),
|
|
256
|
+
).fetchone()
|
|
257
|
+
return bool(row and row[0])
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def already_fully_applied(conninfo: str, path: Path) -> bool:
|
|
261
|
+
"""Whether every statement in this migration is a recognized create-if-missing shape
|
|
262
|
+
(table/index/sequence/view/schema, or add-column) AND its target already exists in the
|
|
263
|
+
database — i.e. re-running the migration would do nothing. Used by `--ask` to
|
|
264
|
+
auto-answer "already done" without prompting, so migrations that are trivially no-ops
|
|
265
|
+
don't interrupt review. A single unrecognized or not-yet-applied statement means
|
|
266
|
+
False — this never guesses."""
|
|
267
|
+
stmts = _split_sql(path.read_text(encoding="utf-8"))
|
|
268
|
+
if not stmts:
|
|
269
|
+
return False
|
|
270
|
+
targets = [_idempotent_target(s) for s in stmts]
|
|
271
|
+
if any(t is None for t in targets):
|
|
272
|
+
return False
|
|
273
|
+
with psycopg.connect(conninfo) as con:
|
|
274
|
+
return all(_target_exists(con, cast(tuple[str, ...], t)) for t in targets)
|
|
275
|
+
|
|
276
|
+
|
|
200
277
|
def list_migration_files(migrations_dir: Path) -> list[Path]:
|
|
201
278
|
return sorted(migrations_dir.glob("*.sql"))
|
|
202
279
|
|
|
@@ -15,6 +15,14 @@ from .models import (
|
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
|
+
# Tooling/scratch dirs that can end up inside a database/ tree (e.g. git
|
|
19
|
+
# worktrees checked out under database/.worktree for isolated test DBs) but
|
|
20
|
+
# never hold real schema content.
|
|
21
|
+
IGNORED_DIR_NAMES = {
|
|
22
|
+
".worktree", ".worktrees", ".claude", ".vscode", "node_modules",
|
|
23
|
+
"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
# Regex to extract dollar-quoted body (Postgres-only construct)
|
|
19
27
|
_DOLLAR_BODY = re.compile(r'\$(\w*)\$(.*?)\$\1\$', re.DOTALL | re.IGNORECASE)
|
|
20
28
|
|
|
@@ -35,6 +43,8 @@ def parse_directory(scripts_dir: Path, *, dialect: str | Dialect = "postgres") -
|
|
|
35
43
|
resolved = resolve_dialect(dialect)
|
|
36
44
|
db_schema = DatabaseSchema()
|
|
37
45
|
for sql_file in sorted(scripts_dir.rglob("*.sql")):
|
|
46
|
+
if IGNORED_DIR_NAMES.intersection(sql_file.relative_to(scripts_dir).parts[:-1]):
|
|
47
|
+
continue
|
|
38
48
|
_parse_file(sql_file, db_schema, resolved)
|
|
39
49
|
return db_schema
|
|
40
50
|
|
|
@@ -15,6 +15,7 @@ from psycopg.sql import SQL, Identifier, Placeholder
|
|
|
15
15
|
|
|
16
16
|
from ..db.complex_types import ComplexHelper
|
|
17
17
|
from ..dialect import Dialect, POSTGRES
|
|
18
|
+
from ..parser import IGNORED_DIR_NAMES
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
logging.getLogger("sqlglot").setLevel(logging.ERROR)
|
|
@@ -121,7 +122,8 @@ def _get_sql_deps(sql: str, dialect: Dialect = POSTGRES) -> set[str]:
|
|
|
121
122
|
def _iter_sql_files(database_dir: Path, dialect: Dialect = POSTGRES):
|
|
122
123
|
"""Yield (Path, sql_content) pairs in dependency-safe execution order."""
|
|
123
124
|
files: list[Path] = []
|
|
124
|
-
for root,
|
|
125
|
+
for root, dirs, dbfiles in os.walk(database_dir):
|
|
126
|
+
dirs[:] = [d for d in dirs if d not in IGNORED_DIR_NAMES]
|
|
125
127
|
if "_migration_scripts" in root or "migrations" in root:
|
|
126
128
|
continue
|
|
127
129
|
for file in dbfiles:
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from pgdevkit.migrate import
|
|
3
|
+
from pgdevkit.migrate import (
|
|
4
|
+
_created_table_names,
|
|
5
|
+
_idempotent_target,
|
|
6
|
+
_split_sql,
|
|
7
|
+
_strip_line_comments,
|
|
8
|
+
default_tracking_table,
|
|
9
|
+
)
|
|
4
10
|
|
|
5
11
|
|
|
6
12
|
def test_created_table_names_ignores_create_table_mentioned_in_a_comment():
|
|
@@ -74,3 +80,55 @@ def test_default_tracking_table_searches_parent_directories(tmp_path):
|
|
|
74
80
|
|
|
75
81
|
def test_default_tracking_table_falls_back_with_no_pyproject_at_all(tmp_path):
|
|
76
82
|
assert default_tracking_table(tmp_path / "nonexistent") == "public.schema_migrations"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_idempotent_target_recognizes_create_table_if_not_exists():
|
|
86
|
+
assert _idempotent_target("CREATE TABLE IF NOT EXISTS app.widgets (id int)") == ("relation", "app.widgets")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_idempotent_target_recognizes_create_index():
|
|
90
|
+
assert _idempotent_target(
|
|
91
|
+
"CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS widgets_name_idx ON app.widgets (name)"
|
|
92
|
+
) == ("relation", "widgets_name_idx")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_idempotent_target_recognizes_create_sequence():
|
|
96
|
+
assert _idempotent_target("CREATE SEQUENCE IF NOT EXISTS app.widgets_seq") == ("relation", "app.widgets_seq")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_idempotent_target_recognizes_create_view():
|
|
100
|
+
assert _idempotent_target("CREATE VIEW IF NOT EXISTS app.widgets_v AS SELECT * FROM app.widgets") == (
|
|
101
|
+
"relation",
|
|
102
|
+
"app.widgets_v",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_idempotent_target_recognizes_create_schema():
|
|
107
|
+
assert _idempotent_target("CREATE SCHEMA IF NOT EXISTS app") == ("schema", "app")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_idempotent_target_recognizes_add_column():
|
|
111
|
+
assert _idempotent_target("ALTER TABLE app.widgets ADD COLUMN IF NOT EXISTS name text") == (
|
|
112
|
+
"column",
|
|
113
|
+
"app.widgets",
|
|
114
|
+
"name",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_idempotent_target_recognizes_add_column_without_if_not_exists_guard():
|
|
119
|
+
assert _idempotent_target("ALTER TABLE app.widgets ADD COLUMN name text") == (
|
|
120
|
+
"column",
|
|
121
|
+
"app.widgets",
|
|
122
|
+
"name",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_idempotent_target_ignores_create_or_replace():
|
|
127
|
+
# A view/function could be replaced with different content, so existence alone never
|
|
128
|
+
# means "already applied" for these.
|
|
129
|
+
assert _idempotent_target("CREATE OR REPLACE VIEW app.widgets_v AS SELECT 1") is None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_idempotent_target_ignores_unrecognized_statements():
|
|
133
|
+
assert _idempotent_target("ALTER TABLE app.widgets DROP COLUMN name") is None
|
|
134
|
+
assert _idempotent_target("INSERT INTO app.widgets (id) VALUES (1)") is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/widget.test_data.json
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
{pgdevkit-0.3.4 → pgdevkit-0.3.8}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|