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.
Files changed (38) hide show
  1. {dbconform-0.2.0/src/dbconform.egg-info → dbconform-0.2.2}/PKG-INFO +1 -1
  2. {dbconform-0.2.0 → dbconform-0.2.2}/pyproject.toml +2 -2
  3. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/cli.py +12 -32
  4. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/postgresql.py +47 -5
  5. {dbconform-0.2.0 → dbconform-0.2.2/src/dbconform.egg-info}/PKG-INFO +1 -1
  6. {dbconform-0.2.0 → dbconform-0.2.2}/LICENSE +0 -0
  7. {dbconform-0.2.0 → dbconform-0.2.2}/README.md +0 -0
  8. {dbconform-0.2.0 → dbconform-0.2.2}/setup.cfg +0 -0
  9. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/__init__.py +0 -0
  10. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/adapters/__init__.py +0 -0
  11. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/adapters/model_schema.py +0 -0
  12. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/adapters/sa_to_neutral.py +0 -0
  13. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/compare/__init__.py +0 -0
  14. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/compare/db_schema.py +0 -0
  15. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/compare/diff.py +0 -0
  16. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/conform.py +0 -0
  17. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/errors.py +0 -0
  18. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/internal/__init__.py +0 -0
  19. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/internal/objects.py +0 -0
  20. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/internal/types.py +0 -0
  21. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/plan/__init__.py +0 -0
  22. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/plan/builder.py +0 -0
  23. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/plan/steps.py +0 -0
  24. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/__init__.py +0 -0
  25. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/db_schema.py +0 -0
  26. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/diff.py +0 -0
  27. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/model_schema.py +0 -0
  28. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/objects.py +0 -0
  29. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/schema/sa_to_neutral.py +0 -0
  30. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/__init__.py +0 -0
  31. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/base.py +0 -0
  32. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/sqlite.py +0 -0
  33. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform/sql_dialect/sqlite_rebuild.py +0 -0
  34. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/SOURCES.txt +0 -0
  35. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/dependency_links.txt +0 -0
  36. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/entry_points.txt +0 -0
  37. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/requires.txt +0 -0
  38. {dbconform-0.2.0 → dbconform-0.2.2}/src/dbconform.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbconform
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Synchronize database schema to models — document-driven project.
5
5
  Author: Brian L. Pond
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbconform"
7
- version = "0.2.0"
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.0"
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("To enable the CLI, install dbconform with optional development packages:\n\tpip install dbconform[dev]", # noqa: E501
21
- file=sys.stderr)
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, _ = _try_connect_postgres(POSTGRES_URL, timeout=3)
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 = _try_connect_postgres(POSTGRES_URL, timeout=3)
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 = _try_connect_postgres(POSTGRES_URL, timeout=3)
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 = _try_connect_postgres(POSTGRES_URL, timeout=3)
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 SERIAL/sequence columns compare equal to model.
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
- 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.
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=table_def.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())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbconform
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Synchronize database schema to models — document-driven project.
5
5
  Author: Brian L. Pond
6
6
  License: MIT
File without changes
File without changes
File without changes