dbconform 0.2.0__tar.gz → 0.2.2__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.
- {dbconform-0.2.0/src/dbconform.egg-info → dbconform-0.2.2}/PKG-INFO +1 -1
- {dbconform-0.2.0 → dbconform-0.2.2}/pyproject.toml +2 -2
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/cli.py +12 -32
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/postgresql.py +47 -5
- {dbconform-0.2.0 → dbconform-0.2.2/src/dbconform.egg-info}/PKG-INFO +1 -1
- {dbconform-0.2.0 → dbconform-0.2.2}/LICENSE +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/README.md +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/setup.cfg +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/adapters/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/adapters/model_schema.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/adapters/sa_to_neutral.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/compare/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/compare/db_schema.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/compare/diff.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/conform.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/errors.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/internal/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/internal/objects.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/internal/types.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/plan/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/plan/builder.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/plan/steps.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/db_schema.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/diff.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/model_schema.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/objects.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/sa_to_neutral.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/__init__.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/base.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/sqlite.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/sqlite_rebuild.py +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/SOURCES.txt +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/dependency_links.txt +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/entry_points.txt +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/requires.txt +0 -0
- {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dbconform"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Synchronize database schema to models — document-driven project."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -61,7 +61,7 @@ where = ["src"]
|
|
|
61
61
|
dbconform = "dbconform.cli:main"
|
|
62
62
|
|
|
63
63
|
[tool.commitizen]
|
|
64
|
-
version = "0.2.
|
|
64
|
+
version = "0.2.2"
|
|
65
65
|
version_scheme = "semver"
|
|
66
66
|
commit = true
|
|
67
67
|
tag = true
|
|
@@ -17,41 +17,21 @@ from pathlib import Path
|
|
|
17
17
|
try:
|
|
18
18
|
import typer
|
|
19
19
|
except ImportError:
|
|
20
|
-
print(
|
|
21
|
-
|
|
20
|
+
print(
|
|
21
|
+
"To enable the CLI, install dbconform with optional development packages:\n\tpip install dbconform[dev]", # noqa: E501
|
|
22
|
+
file=sys.stderr,
|
|
23
|
+
)
|
|
22
24
|
sys.exit(1)
|
|
23
25
|
|
|
24
26
|
|
|
27
|
+
from dbconform.sql_dialect.postgresql import try_connect_to_postgres
|
|
28
|
+
|
|
29
|
+
|
|
25
30
|
# Container constants (match tests/docker-compose.yml image)
|
|
26
31
|
POSTGRES_IMAGE = "postgres:16-alpine"
|
|
27
32
|
CONTAINER_NAME = "dbconform-postgres"
|
|
28
33
|
POSTGRES_PORT = 15432
|
|
29
34
|
POSTGRES_URL = f"postgresql://postgres:postgres@127.0.0.1:{POSTGRES_PORT}/postgres"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _try_connect_postgres(url: str, timeout: float = 5.0) -> tuple[bool, str | None]:
|
|
33
|
-
"""
|
|
34
|
-
Try to connect to Postgres at url (postgresql:// or postgresql+psycopg://),
|
|
35
|
-
run SELECT 1 to validate auth and database. Returns (True, None) on success;
|
|
36
|
-
(False, error_message) on failure. Requires psycopg; returns (False, 'psycopg not installed')
|
|
37
|
-
if missing.
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
import psycopg
|
|
41
|
-
except ImportError:
|
|
42
|
-
return (False, "psycopg not installed")
|
|
43
|
-
conninfo = url.replace("postgresql+psycopg://", "postgresql://", 1)
|
|
44
|
-
try:
|
|
45
|
-
with psycopg.connect(conninfo, connect_timeout=timeout) as conn, conn.cursor() as cur:
|
|
46
|
-
cur.execute("SELECT 1")
|
|
47
|
-
cur.fetchone()
|
|
48
|
-
return (True, None)
|
|
49
|
-
except psycopg.OperationalError as e:
|
|
50
|
-
return (False, str(e).strip())
|
|
51
|
-
except Exception as e:
|
|
52
|
-
return (False, str(e).strip())
|
|
53
|
-
|
|
54
|
-
|
|
55
35
|
def _get_container_runtime_path_or_none() -> str | None:
|
|
56
36
|
"""Return docker or podman path if available, else None. Does not echo or exit."""
|
|
57
37
|
cmd = os.environ.get("DBCONFORM_CONTAINER_CMD", "").strip()
|
|
@@ -122,7 +102,7 @@ def show_version() -> None:
|
|
|
122
102
|
"dbconform package version is unknown (distribution not installed).",
|
|
123
103
|
err=True,
|
|
124
104
|
)
|
|
125
|
-
raise typer.Exit(1)
|
|
105
|
+
raise typer.Exit(1) # noqa: B904
|
|
126
106
|
typer.echo(v)
|
|
127
107
|
|
|
128
108
|
|
|
@@ -205,7 +185,7 @@ def _ensure_postgres_container_up(container_cmd: str) -> bool:
|
|
|
205
185
|
Cleans up partially-created containers on failure.
|
|
206
186
|
"""
|
|
207
187
|
if _container_running(container_cmd):
|
|
208
|
-
ok, _ =
|
|
188
|
+
ok, _ = try_connect_to_postgres(POSTGRES_URL, timeout=3)
|
|
209
189
|
return ok
|
|
210
190
|
if _container_exists(container_cmd):
|
|
211
191
|
_run_subprocess([container_cmd, "start", CONTAINER_NAME], timeout=10)
|
|
@@ -230,7 +210,7 @@ def _ensure_postgres_container_up(container_cmd: str) -> bool:
|
|
|
230
210
|
return False
|
|
231
211
|
deadline = time.monotonic() + 30
|
|
232
212
|
while time.monotonic() < deadline:
|
|
233
|
-
ok, err_msg =
|
|
213
|
+
ok, err_msg = try_connect_to_postgres(POSTGRES_URL, timeout=3)
|
|
234
214
|
if ok:
|
|
235
215
|
return True
|
|
236
216
|
if err_msg and "password authentication failed" in err_msg:
|
|
@@ -299,7 +279,7 @@ def postgres_up() -> None:
|
|
|
299
279
|
typer.echo(f"Start failed: {err}", err=True)
|
|
300
280
|
raise typer.Exit(1)
|
|
301
281
|
# Wait for Postgres to accept TCP connections with our password (up to 30s)
|
|
302
|
-
ok, err_msg =
|
|
282
|
+
ok, err_msg = try_connect_to_postgres(POSTGRES_URL, timeout=3)
|
|
303
283
|
if err_msg == "psycopg not installed":
|
|
304
284
|
typer.echo(f"Set: DBCONFORM_TEST_POSTGRES_URL={POSTGRES_URL}")
|
|
305
285
|
typer.echo(
|
|
@@ -308,7 +288,7 @@ def postgres_up() -> None:
|
|
|
308
288
|
return
|
|
309
289
|
deadline = time.monotonic() + 30
|
|
310
290
|
while time.monotonic() < deadline:
|
|
311
|
-
ok, err_msg =
|
|
291
|
+
ok, err_msg = try_connect_to_postgres(POSTGRES_URL, timeout=3)
|
|
312
292
|
if ok:
|
|
313
293
|
typer.echo(f"Set: DBCONFORM_TEST_POSTGRES_URL={POSTGRES_URL}")
|
|
314
294
|
typer.echo("Connection verified (SELECT 1).")
|
|
@@ -223,13 +223,18 @@ class PostgreSQLDialect(Dialect):
|
|
|
223
223
|
|
|
224
224
|
def normalize_reflected_table(self, table_def: TableDef) -> TableDef:
|
|
225
225
|
"""
|
|
226
|
-
Normalize reflected table so
|
|
226
|
+
Normalize reflected table so it compares equal to model-side internal schema.
|
|
227
227
|
|
|
228
|
-
Columns with a sequence default (nextval) and integer-like type are rewritten
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
- Columns with a sequence default (nextval) and integer-like type are rewritten
|
|
229
|
+
to default=None, autoincrement=True, data_type_name=INTEGER|BIGINT so they match
|
|
230
|
+
the model's representation and no spurious ALTER steps are emitted.
|
|
231
|
+
- Implicit single-column UNIQUE constraints (e.g. habitat.name created from
|
|
232
|
+
column unique=True) are given auto-generated names like habitat_name_key
|
|
233
|
+
in PostgreSQL. Model-side UniqueDef for such constraints has name=None,
|
|
234
|
+
so we strip the auto-generated name here to avoid drop/add churn on recompare.
|
|
231
235
|
"""
|
|
232
236
|
INTEGER_LIKE = ("SERIAL", "INTEGER", "BIGSERIAL", "BIGINT", "INT8")
|
|
237
|
+
|
|
233
238
|
new_columns: list[ColumnDef] = []
|
|
234
239
|
for col in table_def.columns:
|
|
235
240
|
if (
|
|
@@ -266,13 +271,50 @@ class PostgreSQLDialect(Dialect):
|
|
|
266
271
|
autoincrement=False,
|
|
267
272
|
)
|
|
268
273
|
)
|
|
274
|
+
|
|
275
|
+
# Normalize unique constraint names for implicit single-column uniques.
|
|
276
|
+
new_uniques: list[UniqueDef] = []
|
|
277
|
+
table_name = table_def.name.name
|
|
278
|
+
for u in table_def.unique_constraints:
|
|
279
|
+
name = u.name
|
|
280
|
+
if name and len(u.column_names) == 1:
|
|
281
|
+
col = u.column_names[0]
|
|
282
|
+
auto_name = f"{table_name}_{col}_key"
|
|
283
|
+
if name == auto_name:
|
|
284
|
+
# Match model-side representation where name is None for column unique=True.
|
|
285
|
+
name = None
|
|
286
|
+
new_uniques.append(UniqueDef(name=name, column_names=u.column_names))
|
|
287
|
+
|
|
269
288
|
return TableDef(
|
|
270
289
|
name=table_def.name,
|
|
271
290
|
columns=tuple(new_columns),
|
|
272
291
|
primary_key=table_def.primary_key,
|
|
273
|
-
unique_constraints=
|
|
292
|
+
unique_constraints=tuple(new_uniques),
|
|
274
293
|
foreign_keys=table_def.foreign_keys,
|
|
275
294
|
check_constraints=table_def.check_constraints,
|
|
276
295
|
indexes=table_def.indexes,
|
|
277
296
|
comment=table_def.comment,
|
|
278
297
|
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def try_connect_to_postgres(url: str, timeout: float = 5.0) -> tuple[bool, str | None]:
|
|
301
|
+
"""
|
|
302
|
+
Try to connect to Postgres at url (postgresql:// or postgresql+psycopg://),
|
|
303
|
+
run SELECT 1 to validate auth and database. Returns (True, None) on success;
|
|
304
|
+
(False, error_message) on failure. Requires psycopg; returns (False, 'psycopg not installed')
|
|
305
|
+
if missing.
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
import psycopg
|
|
309
|
+
except ImportError:
|
|
310
|
+
return (False, "psycopg not installed")
|
|
311
|
+
conninfo = url.replace("postgresql+psycopg://", "postgresql://", 1)
|
|
312
|
+
try:
|
|
313
|
+
with psycopg.connect(conninfo, connect_timeout=timeout) as conn, conn.cursor() as cur:
|
|
314
|
+
cur.execute("SELECT 1")
|
|
315
|
+
cur.fetchone()
|
|
316
|
+
return (True, None)
|
|
317
|
+
except psycopg.OperationalError as e:
|
|
318
|
+
return (False, str(e).strip())
|
|
319
|
+
except Exception as e:
|
|
320
|
+
return (False, str(e).strip())
|
|
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
|