pgdevkit 0.3.6__py3-none-any.whl → 0.4.0__py3-none-any.whl

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/areas.py ADDED
@@ -0,0 +1,64 @@
1
+ """Optional `-- area: NAME[, NAME...]` tag recognized in the leading comment
2
+ block of a migration file or a `database/` code file (blank lines and `--`
3
+ comments at the very top, stopping at the first real statement — like a file
4
+ header). A file may declare more than one area, either as a comma-separated
5
+ list on one line or across several `-- area:` lines (the areas union).
6
+
7
+ A file with no such directive is "untagged" and is treated as shared/common:
8
+ `only` filters always keep untagged files, and `exclude` filters never drop
9
+ them — only a file that explicitly declares an excluded area is dropped.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from pathlib import Path
16
+
17
+ _AREA_LINE = re.compile(r"^\s*--\s*area\s*:\s*(.+?)\s*$", re.IGNORECASE)
18
+
19
+
20
+ def parse_areas(content: str) -> frozenset[str]:
21
+ """Area names declared in `content`'s leading comment block."""
22
+ areas: set[str] = set()
23
+ for line in content.splitlines():
24
+ stripped = line.strip()
25
+ if not stripped:
26
+ continue
27
+ if not stripped.startswith("--"):
28
+ break
29
+ m = _AREA_LINE.match(stripped)
30
+ if m:
31
+ areas.update(a.strip() for a in m.group(1).split(",") if a.strip())
32
+ return frozenset(areas)
33
+
34
+
35
+ def file_areas(path: Path) -> frozenset[str]:
36
+ """Area names declared in the file at `path`."""
37
+ return parse_areas(path.read_text(encoding="utf-8"))
38
+
39
+
40
+ def area_allowed(
41
+ areas: frozenset[str],
42
+ *,
43
+ only: frozenset[str] | None = None,
44
+ exclude: frozenset[str] | None = None,
45
+ ) -> bool:
46
+ """Whether a file that declares `areas` passes an `only`/`exclude` filter."""
47
+ if exclude and areas & exclude:
48
+ return False
49
+ if only and areas and not (areas & only):
50
+ return False
51
+ return True
52
+
53
+
54
+ def filter_by_area(
55
+ paths: list[Path],
56
+ *,
57
+ only: frozenset[str] | None = None,
58
+ exclude: frozenset[str] | None = None,
59
+ ) -> list[Path]:
60
+ """`paths` restricted by an `only`/`exclude` area filter. Returns `paths`
61
+ unchanged (no file reads) when neither filter is set."""
62
+ if not only and not exclude:
63
+ return paths
64
+ return [p for p in paths if area_allowed(file_areas(p), only=only, exclude=exclude)]
pgdevkit/cli.py CHANGED
@@ -23,6 +23,17 @@ app = typer.Typer(name="pgdb", help="PostgreSQL database schema tools")
23
23
  console = Console()
24
24
  err_console = Console(stderr=True)
25
25
 
26
+ _AREA_OPTION = typer.Option(
27
+ [], "--area", help="Restrict to files declaring this area (repeatable); untagged files always stay in scope"
28
+ )
29
+ _EXCLUDE_AREA_OPTION = typer.Option(
30
+ [], "--exclude-area", help="Skip files declaring this area (repeatable); untagged files are never excluded"
31
+ )
32
+
33
+
34
+ def _as_area_set(values: list[str]) -> frozenset[str] | None:
35
+ return frozenset(values) if values else None
36
+
26
37
  testdb_app = typer.Typer(name="testdb", help="Manage the shared local Postgres test container")
27
38
  app.add_typer(testdb_app, name="testdb")
28
39
 
@@ -46,12 +57,20 @@ def compare(
46
57
  ),
47
58
  report_extra_db: bool = typer.Option(False, "--report-extra-db", help="Report objects in DB but not in scripts"),
48
59
  dialect: str = typer.Option("postgres", "--dialect", help="postgres (default) or mssql"),
60
+ area: list[str] = _AREA_OPTION,
61
+ exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
49
62
  scripts_dir: Path = typer.Argument(..., help="Directory containing SQL scripts"),
50
63
  ) -> None:
51
64
  """Compare SQL scripts to a live database and report differences."""
52
65
  if not scripts_dir.is_dir():
53
66
  err_console.print(f"[red]Error:[/red] {scripts_dir} is not a directory")
54
67
  raise typer.Exit(2)
68
+ if report_extra_db and (area or exclude_area):
69
+ console.print(
70
+ "[yellow]⚠[/yellow] --report-extra-db with --area/--exclude-area will report every DB object "
71
+ "outside the filtered area(s) as \"missing in scripts\", since the live database has no concept "
72
+ "of areas — only the scripts side is filtered."
73
+ )
55
74
 
56
75
  try:
57
76
  conninfo = build_conninfo(
@@ -71,7 +90,9 @@ def compare(
71
90
  raise typer.Exit(2)
72
91
 
73
92
  with console.status("Parsing SQL scripts..."):
74
- scripts_schema = parse_directory(scripts_dir, dialect=backend.dialect)
93
+ scripts_schema = parse_directory(
94
+ scripts_dir, dialect=backend.dialect, areas=_as_area_set(area), exclude_areas=_as_area_set(exclude_area)
95
+ )
75
96
 
76
97
  with console.status("Introspecting database..."):
77
98
  db_schema = backend.introspect(conninfo)
@@ -110,7 +131,11 @@ def fetch_missing(
110
131
  only: list[str] = typer.Option([], "--only", help="Only fetch schema.name (repeatable); default is everything"),
111
132
  ) -> None:
112
133
  """Find tables/views/functions that exist in the database but aren't
113
- tracked under scripts_dir, and reverse-engineer their DDL into new files."""
134
+ tracked under scripts_dir, and reverse-engineer their DDL into new files.
135
+
136
+ No --area/--exclude-area here (unlike compare/migrate): this diffs the
137
+ full live database against scripts, so filtering the scripts side by
138
+ area would misreport objects tracked under other areas as missing."""
114
139
  if not scripts_dir.is_dir():
115
140
  err_console.print(f"[red]Error:[/red] {scripts_dir} is not a directory")
116
141
  raise typer.Exit(2)
@@ -245,6 +270,8 @@ def migrate_check(
245
270
  help="schema.table recording applied migrations "
246
271
  "(default: tool.pgdevkit.migrations_table in pyproject.toml, else public.schema_migrations)",
247
272
  ),
273
+ area: list[str] = _AREA_OPTION,
274
+ exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
248
275
  ) -> None:
249
276
  """List which migration files under migrations_dir are applied vs. pending."""
250
277
  if not migrations_dir.is_dir():
@@ -253,7 +280,9 @@ def migrate_check(
253
280
 
254
281
  conninfo = build_conninfo(url, entra_user)
255
282
  tracking_table = tracking_table or migrate.default_tracking_table(migrations_dir)
256
- local_files = migrate.list_migration_files(migrations_dir)
283
+ local_files = migrate.list_migration_files(
284
+ migrations_dir, areas=_as_area_set(area), exclude_areas=_as_area_set(exclude_area)
285
+ )
257
286
  try:
258
287
  applied = migrate.applied_migrations(conninfo, tracking_table)
259
288
  except migrate.TrackingTableMissing:
@@ -292,6 +321,8 @@ def migrate_apply(
292
321
  ),
293
322
  ask: bool = typer.Option(False, "--ask", help="Show and confirm each migration before running it"),
294
323
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirm-target prompt"),
324
+ area: list[str] = _AREA_OPTION,
325
+ exclude_area: list[str] = _EXCLUDE_AREA_OPTION,
295
326
  ) -> None:
296
327
  """Apply pending migration files, in filename order, tracking each in tracking_table."""
297
328
  if not migrations_dir.is_dir():
@@ -300,6 +331,7 @@ def migrate_apply(
300
331
 
301
332
  conninfo = build_conninfo(url, entra_user)
302
333
  tracking_table = tracking_table or migrate.default_tracking_table(migrations_dir)
334
+ areas, exclude_areas = _as_area_set(area), _as_area_set(exclude_area)
303
335
  target_desc = url.rsplit("@", 1)[-1] if "@" in url else url
304
336
  if not yes:
305
337
  typer.confirm(f"About to run migrations against {target_desc}. Continue?", abort=True)
@@ -308,12 +340,14 @@ def migrate_apply(
308
340
  targets = [migrations_dir / file]
309
341
  else:
310
342
  try:
311
- targets = migrate.pending_migrations(migrations_dir, conninfo, tracking_table)
343
+ targets = migrate.pending_migrations(
344
+ migrations_dir, conninfo, tracking_table, areas=areas, exclude_areas=exclude_areas
345
+ )
312
346
  except migrate.TrackingTableMissing:
313
347
  err_console.print(
314
348
  f"[yellow]⚠[/yellow] {tracking_table} not found — treating every migration as pending"
315
349
  )
316
- targets = migrate.list_migration_files(migrations_dir)
350
+ targets = migrate.list_migration_files(migrations_dir, areas=areas, exclude_areas=exclude_areas)
317
351
 
318
352
  if not targets:
319
353
  console.print("No pending migrations.")
pgdevkit/fetch_missing.py CHANGED
@@ -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
@@ -50,7 +50,12 @@ def layer_folder_for(scripts_dir: Path, schema: str) -> Path:
50
50
 
51
51
  def find_missing_objects(scripts_dir: Path, conninfo: str) -> list[MissingObject]:
52
52
  """Tables, views, and functions that exist in the live database but
53
- aren't tracked as .sql files under scripts_dir."""
53
+ aren't tracked as .sql files under scripts_dir.
54
+
55
+ Deliberately not area-filterable: this diffs the *full* database against
56
+ scripts with report_extra_db=True, so narrowing the scripts side to one
57
+ area would make every object tracked only under a different area look
58
+ "missing" too — and --write would reconstruct a duplicate file for it."""
54
59
  scripts = parse_directory(scripts_dir)
55
60
  db = introspect_db(conninfo)
56
61
  diffs = compute_diff(scripts, db, report_extra_db=True)
pgdevkit/migrate.py CHANGED
@@ -18,6 +18,8 @@ import sqlglot
18
18
  from psycopg import errors as pg_errors
19
19
  from psycopg import sql as pg_sql
20
20
 
21
+ from .areas import filter_by_area
22
+
21
23
  _IDENTIFIER = r"[A-Za-z_][A-Za-z0-9_]*"
22
24
  _DEFAULT_TRACKING_TABLE = "public.schema_migrations"
23
25
 
@@ -274,8 +276,14 @@ def already_fully_applied(conninfo: str, path: Path) -> bool:
274
276
  return all(_target_exists(con, cast(tuple[str, ...], t)) for t in targets)
275
277
 
276
278
 
277
- def list_migration_files(migrations_dir: Path) -> list[Path]:
278
- return sorted(migrations_dir.glob("*.sql"))
279
+ def list_migration_files(
280
+ migrations_dir: Path,
281
+ *,
282
+ areas: frozenset[str] | None = None,
283
+ exclude_areas: frozenset[str] | None = None,
284
+ ) -> list[Path]:
285
+ files = sorted(migrations_dir.glob("*.sql"))
286
+ return filter_by_area(files, only=areas, exclude=exclude_areas)
279
287
 
280
288
 
281
289
  def applied_migrations(conninfo: str, tracking_table: str) -> dict[str, tuple[datetime, str]]:
@@ -291,9 +299,17 @@ def applied_migrations(conninfo: str, tracking_table: str) -> dict[str, tuple[da
291
299
  return {r[0]: (r[1], r[2]) for r in rows}
292
300
 
293
301
 
294
- def pending_migrations(migrations_dir: Path, conninfo: str, tracking_table: str) -> list[Path]:
302
+ def pending_migrations(
303
+ migrations_dir: Path,
304
+ conninfo: str,
305
+ tracking_table: str,
306
+ *,
307
+ areas: frozenset[str] | None = None,
308
+ exclude_areas: frozenset[str] | None = None,
309
+ ) -> list[Path]:
295
310
  applied = applied_migrations(conninfo, tracking_table)
296
- return [p for p in list_migration_files(migrations_dir) if p.name not in applied]
311
+ files = list_migration_files(migrations_dir, areas=areas, exclude_areas=exclude_areas)
312
+ return [p for p in files if p.name not in applied]
297
313
 
298
314
 
299
315
  def record_applied(conninfo: str, tracking_table: str, filename: str) -> bool:
pgdevkit/parser.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import re
4
5
  import logging
5
6
  from pathlib import Path
@@ -7,6 +8,7 @@ from pathlib import Path
7
8
  import sqlglot
8
9
  import sqlglot.expressions as exp
9
10
 
11
+ from .areas import area_allowed, parse_areas
10
12
  from .dialect import Dialect, POSTGRES, resolve_dialect
11
13
  from .models import (
12
14
  ColumnDef, ConstraintDef, CompositeTypeDef, DatabaseSchema,
@@ -15,6 +17,14 @@ from .models import (
15
17
 
16
18
  logger = logging.getLogger(__name__)
17
19
 
20
+ # Tooling/scratch dirs that can end up inside a database/ tree (e.g. git
21
+ # worktrees checked out under database/.worktree for isolated test DBs) but
22
+ # never hold real schema content.
23
+ IGNORED_DIR_NAMES = {
24
+ ".worktree", ".worktrees", ".claude", ".vscode", "node_modules",
25
+ "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
26
+ }
27
+
18
28
  # Regex to extract dollar-quoted body (Postgres-only construct)
19
29
  _DOLLAR_BODY = re.compile(r'\$(\w*)\$(.*?)\$\1\$', re.DOTALL | re.IGNORECASE)
20
30
 
@@ -31,16 +41,38 @@ _DO_COMPOSITE = re.compile(
31
41
  )
32
42
 
33
43
 
34
- def parse_directory(scripts_dir: Path, *, dialect: str | Dialect = "postgres") -> DatabaseSchema:
44
+ def _iter_sql_files(scripts_dir: Path):
45
+ for root, dirs, files in os.walk(scripts_dir):
46
+ dirs[:] = [d for d in dirs if d not in IGNORED_DIR_NAMES]
47
+ for name in files:
48
+ if name.endswith(".sql"):
49
+ yield Path(root) / name
50
+
51
+
52
+ def parse_directory(
53
+ scripts_dir: Path,
54
+ *,
55
+ dialect: str | Dialect = "postgres",
56
+ areas: frozenset[str] | None = None,
57
+ exclude_areas: frozenset[str] | None = None,
58
+ ) -> DatabaseSchema:
35
59
  resolved = resolve_dialect(dialect)
36
60
  db_schema = DatabaseSchema()
37
- for sql_file in sorted(scripts_dir.rglob("*.sql")):
38
- _parse_file(sql_file, db_schema, resolved)
61
+ for sql_file in sorted(_iter_sql_files(scripts_dir)):
62
+ # Read once and reuse for both the area check and parsing, rather than
63
+ # filtering the file list up front (which would need its own read).
64
+ content = sql_file.read_text(encoding="utf-8")
65
+ if (areas or exclude_areas) and not area_allowed(parse_areas(content), only=areas, exclude=exclude_areas):
66
+ continue
67
+ _parse_file(sql_file, db_schema, resolved, content=content)
39
68
  return db_schema
40
69
 
41
70
 
42
- def _parse_file(path: Path, db_schema: DatabaseSchema, dialect: Dialect = POSTGRES) -> None:
43
- content = path.read_text(encoding="utf-8")
71
+ def _parse_file(
72
+ path: Path, db_schema: DatabaseSchema, dialect: Dialect = POSTGRES, content: str | None = None
73
+ ) -> None:
74
+ if content is None:
75
+ content = path.read_text(encoding="utf-8")
44
76
  try:
45
77
  exprs = sqlglot.parse(content, dialect=dialect.sqlglot_name, error_level=sqlglot.ErrorLevel.WARN)
46
78
  except Exception as e:
pgdevkit/testdb/schema.py CHANGED
@@ -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, _, dbfiles in os.walk(database_dir):
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,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pgdevkit
3
- Version: 0.3.6
3
+ Version: 0.4.0
4
4
  Summary: A helper for developing with Postgres
5
5
  Requires-Python: >=3.14
6
6
  Requires-Dist: docker>=7.1.0
@@ -79,6 +79,62 @@ which parses/introspects/diffs like any other column type; see
79
79
  handled on the CRUD side (write-side serialization only, no auto-parsing on
80
80
  read — `mssql-python` doesn't distinguish `json` columns from `nvarchar`).
81
81
 
82
+ ### Area tagging and filtering
83
+
84
+ Any migration file or `database/` code file can declare one or more areas by
85
+ starting with a `-- area:` comment:
86
+
87
+ ```sql
88
+ -- area: billing
89
+ CREATE TABLE billing.invoices (id int primary key);
90
+ ```
91
+
92
+ A file can declare more than one area, either comma-separated on one line
93
+ (`-- area: billing, reporting`) or across several `-- area:` lines — the
94
+ declared areas union. The directive is only recognized in the file's leading
95
+ comment block (blank lines and `--` comments at the very top, stopping at the
96
+ first real statement); a `-- area:` comment later in the file doesn't count.
97
+ A file with no directive is untagged, and untagged files are treated as
98
+ shared/common.
99
+
100
+ `pgdb compare`, `pgdb migrate check`, and `pgdb migrate apply` all accept:
101
+
102
+ - `--area NAME` (repeatable) — restrict to files declaring one of the given
103
+ areas, **plus every untagged file** (untagged files always stay in scope).
104
+ - `--exclude-area NAME` (repeatable) — drop files declaring one of the given
105
+ areas; untagged files are never dropped by this.
106
+
107
+ Both can be combined; a file matching both an included and an excluded area
108
+ is excluded. Passing neither option applies no filtering (the default,
109
+ unchanged behavior).
110
+
111
+ ```bash
112
+ pgdb migrate apply path/to/database/_migration_scripts --url ... --area billing
113
+ pgdb compare path/to/database/ --url ... --exclude-area reporting
114
+ ```
115
+
116
+ `compare`'s default report (no `--report-extra-db`) only checks that the
117
+ filtered scripts exist correctly in the DB, so it composes safely with area
118
+ filtering. Passing `--report-extra-db` together with an area filter also
119
+ reports every DB object outside the filtered area(s) as "missing in
120
+ scripts" — since the live database has no concept of areas, only the
121
+ scripts side is filtered — so treat that combination's "missing in scripts"
122
+ results with that in mind (the CLI prints a warning when you combine them).
123
+
124
+ `pgdb fetch-missing` deliberately has **no** `--area`/`--exclude-area`: it
125
+ diffs the full database against scripts to find genuinely untracked
126
+ objects, so narrowing the scripts side by area would make every object
127
+ tracked only under a different area look "missing" too — and `--write`
128
+ would then reconstruct a duplicate file for something that already exists.
129
+
130
+ `pgdevkit.areas` exposes the same logic for scripting:
131
+ `parse_areas`/`file_areas` read a file's declared areas, and
132
+ `area_allowed`/`filter_by_area` apply the `only`/`exclude` semantics above.
133
+ `pgdevkit.migrate.list_migration_files`/`pending_migrations` and
134
+ `pgdevkit.parser.parse_directory` take the same `areas`/`exclude_areas`
135
+ keyword arguments (`pgdevkit.fetch_missing.find_missing_objects` doesn't,
136
+ for the reason above).
137
+
82
138
  ## `pgdb testdb`
83
139
 
84
140
  Manages a single shared, Podman-backed Postgres container for local tests
@@ -259,3 +315,29 @@ async with pool.connection() as con:
259
315
  widget = await pg_retrieve(con, Widget, {"id": 1})
260
316
  await pg_upsert(con, Widget(id=1, name="thing"), Widget)
261
317
  ```
318
+
319
+ ## Releasing
320
+
321
+ Bump `version` in `pyproject.toml` as part of your PR, same as any other
322
+ change. Once that PR merges to `main` and the `Python Test` workflow passes
323
+ for that commit, `.github/workflows/auto-release.yml` automatically tags it
324
+ `vX.Y.Z`, cuts a GitHub Release (skipping if that version was already
325
+ released, e.g. a merge that didn't touch the version), and dispatches
326
+ `python-publish.yml` to publish it to PyPI — no manual release step, and no
327
+ extra secret to configure. Two non-obvious GitHub Actions quirks shaped
328
+ this (see the comments at the top of `auto-release.yml` for the full
329
+ reasoning, since both were hit and confirmed the hard way):
330
+
331
+ - A release created with the default `GITHUB_TOKEN` does **not** trigger
332
+ other workflows' `release: published` listeners (an anti-recursion
333
+ safeguard) — `workflow_dispatch` is the documented exception, so
334
+ `auto-release.yml` dispatches `python-publish.yml` directly (`gh workflow
335
+ run`) instead of relying on the release to cascade into it.
336
+ - `python-publish.yml` deliberately stays a plain, directly-triggered
337
+ top-level workflow rather than something `auto-release.yml` calls via
338
+ `workflow_call`: PyPI's OIDC trusted publishing does not support
339
+ reusable/called workflows and silently rejects the token in that shape.
340
+
341
+ `workflow_dispatch` (or an actual GitHub UI release) on `python-publish.yml`
342
+ still works as a manual fallback if you ever need to re-publish a version
343
+ without going through `auto-release.yml`.
@@ -1,15 +1,16 @@
1
1
  pgdevkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pgdevkit/cli.py,sha256=xP4fZB0Dg3wivhb00DcP2w2Vnp8BRyaR2o1wD7jopFc,17574
2
+ pgdevkit/areas.py,sha256=iEmOObYxhZyDFl2iQVf1OQJN4JtZ-H3EL-qMfxYZdSQ,2203
3
+ pgdevkit/cli.py,sha256=gZt4BlEuw4hhMq0dyMRCe1a3-Ci23yUDg-6Sc_H-OSo,19193
3
4
  pgdevkit/connection.py,sha256=x49aXKsJfyOsDTlqURs9idQUWaD-RO-XdtAqsG3guBI,4000
4
5
  pgdevkit/dialect.py,sha256=PL5Y8QPZNnoHIvAPomczGQX7IjZwxMgKGuREX80SwFA,3105
5
6
  pgdevkit/diff.py,sha256=Y2TbjwpFHYik0wZDhoI--9EAlIfp4AUg8BUubkOXyUg,9295
6
- pgdevkit/fetch_missing.py,sha256=1-2NBDCQNtirywO9Ucm775UKSACfAzcnQlp-4Jh2weE,8136
7
+ pgdevkit/fetch_missing.py,sha256=vb8hXWBKMWgqkxHso5SuhX9l6rxNXfaybAEsajPLYow,8495
7
8
  pgdevkit/introspect.py,sha256=QYIlRuZk4lBHGxaW7zb_kFRIuNfi3AxH0Q2F_GKzlVI,7870
8
9
  pgdevkit/lakebase.py,sha256=0PABOk4p-2klLTY8QAUibQf9QeyNHKywUWEWGdkUY8k,3165
9
- pgdevkit/migrate.py,sha256=pU7xVssJiAHfFFU60DP2cwpjQ1Tf8Hscc_Q6wIqeSfM,13677
10
+ pgdevkit/migrate.py,sha256=_8U-TG4MRZQasdm0UticdbM4rhthoZ9QSF9L4NX3Xq8,14057
10
11
  pgdevkit/models.py,sha256=dAZ8f7pgDsxoLVrDWsM4h0qwJ0pOzXDdWMGsnjrW80E,2103
11
12
  pgdevkit/mssql_introspect.py,sha256=Fhv1N4zHYEDZ0PEy0OIpawUmoIFeoi-Lm97pmEyDmYY,12031
12
- pgdevkit/parser.py,sha256=X1P8RbogRhsWc-H9w3IPlDo2blKpI33cJTvDzOXze1Y,15367
13
+ pgdevkit/parser.py,sha256=RWtCDx98OU3T7fmEA7ZZN82Fqo6wlbv03O_BEKwZGQo,16561
13
14
  pgdevkit/backends/__init__.py,sha256=ys4VBglMImRtnudJBlTPVBNevDMRippuwAJOz0yRKiY,610
14
15
  pgdevkit/backends/base.py,sha256=0i0KLCYVWlpVFnUoq2t0LCRE_abyWPPIhw1bMxmW9KQ,1048
15
16
  pgdevkit/backends/mssql.py,sha256=pocBdoQ14ZnYLutXEOStVmJvmpclXGs3gaxHN0uJCMk,1068
@@ -30,7 +31,7 @@ pgdevkit/testdb/constants.py,sha256=xi8uIoEgN9LrRkr41j3mmWB9PqqU0r8aCUb2JJXBAs0,
30
31
  pgdevkit/testdb/container.py,sha256=kELTb6qaECAO9a5Bwvx1btv6i5QKRFAa9PX6auWymNo,2807
31
32
  pgdevkit/testdb/naming.py,sha256=Kyk5qEfxUkbggUG5bD7U7CJ_kAET332VPQNB-O5kvTY,1318
32
33
  pgdevkit/testdb/query.py,sha256=iYpy_EoYIq5USfwr6pNktmDjcGi_yQx0vHvjHZNGRr0,3854
33
- pgdevkit/testdb/schema.py,sha256=vw75HsiMvZPmMWaD5P2DeOaEKS86qksRXHDtOc8SjJE,11941
34
+ pgdevkit/testdb/schema.py,sha256=0N0Q_BQGh2kuAqa_j-DdbjYlTUOOZSEtaoUOApxQg_Y,12049
34
35
  pgdevkit/testdb/mssql/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
35
36
  pgdevkit/testdb/mssql/api.py,sha256=42RAuJeiZlK1WJaTIYoj6l8GEAbEyt6LgAoF3G2jxnw,7537
36
37
  pgdevkit/testdb/mssql/constants.py,sha256=7n_NcYuhabbtb18CqG_H_rRbI7Wb_qu_l4nKk3281vI,2847
@@ -39,7 +40,7 @@ pgdevkit/docs/database-layout.md,sha256=4GBwv4PA2cKHl2BxwLd-awLOe9he9ie4X6CpQWjI
39
40
  pgdevkit/skills/pgdevkit/SKILL.md,sha256=6uxe6BQwOLexGiBpf00VTFTHOiWJo5k2Qqzu172rb4I,12156
40
41
  pgdevkit/skills/pgdevkit/references/dynamic-sql.md,sha256=aTR1sx81moiTwaMaiOecaqk-9rT8y5Zxz7W1BoC3vK0,2774
41
42
  pgdevkit/skills/pgdevkit/references/temporal-tables.md,sha256=LwZnItvQWI7bB0oRHxIfUc0ynnq7N0V5Mnv-QHvFN3Q,1364
42
- pgdevkit-0.3.6.dist-info/METADATA,sha256=DxmlfQZ0M5gVVrHhHHz-748Eu6-leFPurFjplxOZyQU,11346
43
- pgdevkit-0.3.6.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
44
- pgdevkit-0.3.6.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
45
- pgdevkit-0.3.6.dist-info/RECORD,,
43
+ pgdevkit-0.4.0.dist-info/METADATA,sha256=KgPFnAsptfih47V8mmLRNJ43aKBq013EWMH2M3EgDhs,15524
44
+ pgdevkit-0.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
45
+ pgdevkit-0.4.0.dist-info/entry_points.txt,sha256=FK2P55lf0WaGlOZJIINOGgCt8WB7ep0OnPsgnrp80bg,42
46
+ pgdevkit-0.4.0.dist-info/RECORD,,