dbconform 0.2.4__tar.gz → 0.2.6__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.4/src/dbconform.egg-info → dbconform-0.2.6}/PKG-INFO +17 -1
  2. {dbconform-0.2.4 → dbconform-0.2.6}/README.md +15 -0
  3. {dbconform-0.2.4 → dbconform-0.2.6}/pyproject.toml +3 -2
  4. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/adapters/model_schema.py +69 -3
  5. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/plan/builder.py +10 -1
  6. {dbconform-0.2.4 → dbconform-0.2.6/src/dbconform.egg-info}/PKG-INFO +17 -1
  7. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform.egg-info/requires.txt +1 -0
  8. {dbconform-0.2.4 → dbconform-0.2.6}/LICENSE +0 -0
  9. {dbconform-0.2.4 → dbconform-0.2.6}/setup.cfg +0 -0
  10. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/__init__.py +0 -0
  11. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/adapters/__init__.py +0 -0
  12. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/adapters/sa_to_neutral.py +0 -0
  13. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/cli.py +0 -0
  14. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/compare/__init__.py +0 -0
  15. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/compare/db_schema.py +0 -0
  16. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/compare/diff.py +0 -0
  17. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/conform.py +0 -0
  18. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/errors.py +0 -0
  19. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/internal/__init__.py +0 -0
  20. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/internal/objects.py +0 -0
  21. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/internal/types.py +0 -0
  22. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/plan/__init__.py +0 -0
  23. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/plan/steps.py +0 -0
  24. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/schema/__init__.py +0 -0
  25. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/schema/db_schema.py +0 -0
  26. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/schema/diff.py +0 -0
  27. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/schema/model_schema.py +0 -0
  28. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/schema/objects.py +0 -0
  29. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/schema/sa_to_neutral.py +0 -0
  30. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/sql_dialect/__init__.py +0 -0
  31. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/sql_dialect/base.py +0 -0
  32. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/sql_dialect/postgresql.py +0 -0
  33. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/sql_dialect/sqlite.py +0 -0
  34. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform/sql_dialect/sqlite_rebuild.py +0 -0
  35. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform.egg-info/SOURCES.txt +0 -0
  36. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform.egg-info/dependency_links.txt +0 -0
  37. {dbconform-0.2.4 → dbconform-0.2.6}/src/dbconform.egg-info/entry_points.txt +0 -0
  38. {dbconform-0.2.4 → dbconform-0.2.6}/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.4
3
+ Version: 0.2.6
4
4
  Summary: Synchronize database schema to models — document-driven project.
5
5
  Author: Brian L. Pond
6
6
  License: MIT
@@ -24,6 +24,7 @@ Requires-Dist: pytest; extra == "dev"
24
24
  Requires-Dist: pytest-asyncio; extra == "dev"
25
25
  Requires-Dist: ruff; extra == "dev"
26
26
  Requires-Dist: sqlmodel; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
27
28
  Requires-Dist: typer>=0.9; extra == "dev"
28
29
  Provides-Extra: postgres
29
30
  Requires-Dist: psycopg[binary]>=3; extra == "postgres"
@@ -144,6 +145,21 @@ else:
144
145
  print(result.sql()) # Full DDL script
145
146
  ```
146
147
 
148
+ **View a human-readable summary:**
149
+
150
+ ```python
151
+ result.print_summary()
152
+ # Output:
153
+ # ConformPlan: 2 steps, 0 extra tables, 1 skipped steps
154
+ # Steps:
155
+ # - Add column price to product
156
+ # - Create table cart
157
+ # Skipped steps:
158
+ # - Drop column legacy_field from product (reason: Column drop blocked: allow_drop_extra_columns=False)
159
+ ```
160
+
161
+ The summary shows planned steps, extra tables (in DB but not in models), and skipped steps (drift that requires opt-in to fix).
162
+
147
163
  ### 3. Apply changes
148
164
 
149
165
  ```python
@@ -113,6 +113,21 @@ else:
113
113
  print(result.sql()) # Full DDL script
114
114
  ```
115
115
 
116
+ **View a human-readable summary:**
117
+
118
+ ```python
119
+ result.print_summary()
120
+ # Output:
121
+ # ConformPlan: 2 steps, 0 extra tables, 1 skipped steps
122
+ # Steps:
123
+ # - Add column price to product
124
+ # - Create table cart
125
+ # Skipped steps:
126
+ # - Drop column legacy_field from product (reason: Column drop blocked: allow_drop_extra_columns=False)
127
+ ```
128
+
129
+ The summary shows planned steps, extra tables (in DB but not in models), and skipped steps (drift that requires opt-in to fix).
130
+
116
131
  ### 3. Apply changes
117
132
 
118
133
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbconform"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "Synchronize database schema to models — document-driven project."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -36,6 +36,7 @@ dev = [
36
36
  "pytest-asyncio",
37
37
  "ruff",
38
38
  "sqlmodel",
39
+ "twine",
39
40
  "typer>=0.9",
40
41
  ]
41
42
  postgres = [
@@ -61,7 +62,7 @@ where = ["src"]
61
62
  dbconform = "dbconform.cli:main"
62
63
 
63
64
  [tool.commitizen]
64
- version = "0.2.4"
65
+ version = "0.2.6"
65
66
  version_scheme = "semver"
66
67
  commit = true
67
68
  tag = true
@@ -9,16 +9,25 @@ definitions and build a ModelSchema (name -> TableDef). See docs/requirements/01
9
9
  model.__table__ and its columns/constraints/indexes; we never assign to or modify
10
10
  the caller's Table or column objects. ModelSchema stores only internal TableDef
11
11
  instances, not references to the original tables.
12
+
13
+ **Column defaults:** How Python and server defaults become DDL strings (including the
14
+ PostgreSQL date-literal pitfall) is documented in docs/technical/05-model-column-defaults.md.
12
15
  """
13
16
 
14
17
  from __future__ import annotations
15
18
 
19
+ import math
16
20
  from collections.abc import Sequence
21
+ from datetime import date, datetime, time
22
+ from decimal import Decimal
23
+ from enum import Enum
17
24
  from typing import Any, Protocol
25
+ from uuid import UUID
18
26
 
19
27
  from sqlalchemy import Table
20
28
  from sqlalchemy.engine import Dialect
21
29
  from sqlalchemy.schema import CheckConstraint, ForeignKeyConstraint, UniqueConstraint
30
+ from sqlalchemy.sql.elements import ClauseElement
22
31
 
23
32
  from dbconform.adapters.sa_to_neutral import sa_column_to_neutral_type
24
33
  from dbconform.internal.objects import (
@@ -69,15 +78,72 @@ def _check_expression_str(sqltext: Any) -> str:
69
78
  return str(sqltext)
70
79
 
71
80
 
72
- def _default_expr(column: Any, _dialect: Dialect) -> str | None:
73
- """Return server default expression as string, or None."""
81
+ def _python_scalar_to_sql_literal(value: Any) -> str | None:
82
+ """
83
+ Map a Python scalar to a SQL DEFAULT fragment (dialect-agnostic literals).
84
+
85
+ Used when SQLAlchemy's column ``default`` carries a Python value (e.g. SQLModel
86
+ ``Field(default=date(...))``). Must not use ``str(value)`` alone: bare dates
87
+ would emit ``1970-01-01``, which PostgreSQL parses as integer subtraction, not
88
+ a DATE literal. See docs/technical/05-model-column-defaults.md.
89
+
90
+ Traceability: docs/requirements/01-functional.md (Schema parity: column defaults).
91
+
92
+ Returns:
93
+ A string safe to place after ``DEFAULT `` in DDL, or None if no static
94
+ literal can be produced (unknown types, non-finite float).
95
+ """
96
+ if isinstance(value, datetime):
97
+ inner = value.isoformat(sep=" ").replace("'", "''")
98
+ return f"'{inner}'"
99
+ if isinstance(value, date):
100
+ return f"'{value.isoformat()}'"
101
+ if isinstance(value, time):
102
+ return f"'{value.isoformat()}'"
103
+ if isinstance(value, bool):
104
+ return "TRUE" if value else "FALSE"
105
+ if isinstance(value, int):
106
+ return str(value)
107
+ if isinstance(value, float):
108
+ if not math.isfinite(value):
109
+ return None
110
+ return str(value)
111
+ if isinstance(value, Decimal):
112
+ return str(value)
113
+ if isinstance(value, str):
114
+ return "'" + value.replace("'", "''") + "'"
115
+ if isinstance(value, Enum):
116
+ return _python_scalar_to_sql_literal(value.value)
117
+ if isinstance(value, UUID):
118
+ return "'" + str(value).replace("'", "''") + "'"
119
+ return None
120
+
121
+
122
+ def _default_expr(column: Any, _dialect: Dialect | None) -> str | None:
123
+ """
124
+ Return a column default as a SQL expression string for DDL, or None.
125
+
126
+ Prefer ``server_default``; otherwise use ``default`` (Python-side). Callable
127
+ ``.arg`` (e.g. ``default_factory``) yields None—no static DDL.
128
+
129
+ For ``.arg`` that is a SQLAlchemy :class:`~sqlalchemy.sql.elements.ClauseElement`
130
+ (typical for ``server_default=text(...)`` and many reflected defaults),
131
+ ``str(.arg)`` is used so quoting matches SQLAlchemy's rendering.
132
+
133
+ For other ``.arg`` values, :func:`_python_scalar_to_sql_literal` produces quoted
134
+ literals. See docs/technical/05-model-column-defaults.md (PostgreSQL date bug).
135
+
136
+ Traceability: docs/requirements/01-functional.md (Schema parity: columns, defaults).
137
+ """
74
138
  default = getattr(column, "server_default", None) or getattr(column, "default", None)
75
139
  if default is None:
76
140
  return None
77
141
  if hasattr(default, "arg") and default.arg is not None:
78
142
  if callable(default.arg):
79
143
  return None # Python-side default; no DDL expression
80
- return str(default.arg)
144
+ if isinstance(default.arg, ClauseElement):
145
+ return str(default.arg)
146
+ return _python_scalar_to_sql_literal(default.arg)
81
147
  if hasattr(default, "text") and default.text is not None:
82
148
  return default.text
83
149
  return None
@@ -213,11 +213,20 @@ class ConformPlanBuilder:
213
213
  if drop_sql:
214
214
  steps.append(
215
215
  AlterTableStep(
216
- description=f"Drop column {col.name} from {name}",
216
+ description=f"Drop column `{col.name}` from `{name}`",
217
217
  sql=drop_sql,
218
218
  table_name=name,
219
219
  )
220
220
  )
221
+ else:
222
+ for col in table_diff.removed_columns:
223
+ skipped_steps.append(
224
+ SkippedStep(
225
+ description=f"Drop column `{col.name}` from `{name}`",
226
+ reason="Column drop blocked: allow_drop_extra_columns=False",
227
+ table_name=name,
228
+ )
229
+ )
221
230
  for col in table_diff.added_columns:
222
231
  sql = self.dialect.add_column_sql(name, col)
223
232
  steps.append(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbconform
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Synchronize database schema to models — document-driven project.
5
5
  Author: Brian L. Pond
6
6
  License: MIT
@@ -24,6 +24,7 @@ Requires-Dist: pytest; extra == "dev"
24
24
  Requires-Dist: pytest-asyncio; extra == "dev"
25
25
  Requires-Dist: ruff; extra == "dev"
26
26
  Requires-Dist: sqlmodel; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
27
28
  Requires-Dist: typer>=0.9; extra == "dev"
28
29
  Provides-Extra: postgres
29
30
  Requires-Dist: psycopg[binary]>=3; extra == "postgres"
@@ -144,6 +145,21 @@ else:
144
145
  print(result.sql()) # Full DDL script
145
146
  ```
146
147
 
148
+ **View a human-readable summary:**
149
+
150
+ ```python
151
+ result.print_summary()
152
+ # Output:
153
+ # ConformPlan: 2 steps, 0 extra tables, 1 skipped steps
154
+ # Steps:
155
+ # - Add column price to product
156
+ # - Create table cart
157
+ # Skipped steps:
158
+ # - Drop column legacy_field from product (reason: Column drop blocked: allow_drop_extra_columns=False)
159
+ ```
160
+
161
+ The summary shows planned steps, extra tables (in DB but not in models), and skipped steps (drift that requires opt-in to fix).
162
+
147
163
  ### 3. Apply changes
148
164
 
149
165
  ```python
@@ -11,6 +11,7 @@ pytest
11
11
  pytest-asyncio
12
12
  ruff
13
13
  sqlmodel
14
+ twine
14
15
  typer>=0.9
15
16
 
16
17
  [postgres]
File without changes
File without changes