alterdb 0.2.2__tar.gz → 0.2.4__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 (109) hide show
  1. {alterdb-0.2.2 → alterdb-0.2.4}/CHANGELOG.md +165 -0
  2. {alterdb-0.2.2 → alterdb-0.2.4}/PKG-INFO +159 -4
  3. {alterdb-0.2.2 → alterdb-0.2.4}/README.md +158 -3
  4. alterdb-0.2.4/docs/alter_logo.png +0 -0
  5. {alterdb-0.2.2 → alterdb-0.2.4}/pyproject.toml +1 -1
  6. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/server.py +91 -6
  7. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/static/canvas.js +15 -4
  8. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/cli.py +10 -1
  9. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/_surgical.py +37 -6
  10. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/mcp_server.py +135 -9
  11. alterdb-0.2.4/src/alter/query.py +612 -0
  12. alterdb-0.2.4/tests/test_query.py +462 -0
  13. {alterdb-0.2.2 → alterdb-0.2.4}/.env.example +0 -0
  14. {alterdb-0.2.2 → alterdb-0.2.4}/.gitignore +0 -0
  15. {alterdb-0.2.2 → alterdb-0.2.4}/.python-version +0 -0
  16. {alterdb-0.2.2 → alterdb-0.2.4}/LICENSE +0 -0
  17. {alterdb-0.2.2 → alterdb-0.2.4}/docker-compose.yml +0 -0
  18. {alterdb-0.2.2 → alterdb-0.2.4}/docs/Canvas.png +0 -0
  19. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/alembic/alembic.ini +0 -0
  20. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/alembic/env.py +0 -0
  21. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/alembic/script.py.mako +0 -0
  22. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/__init__.py +0 -0
  23. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/database.py +0 -0
  24. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/enums.py +0 -0
  25. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/main.py +0 -0
  26. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/models/parents.py +0 -0
  27. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/models/starter.py +0 -0
  28. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/pyproject.toml +0 -0
  29. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/tests/__init__.py +0 -0
  30. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/tests/test_integration.py +0 -0
  31. {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/tests/test_round_trip.py +0 -0
  32. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/__init__.py +0 -0
  33. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/__init__.py +0 -0
  34. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/static/index.html +0 -0
  35. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/static/style.css +0 -0
  36. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/data/demo_schema.alter +0 -0
  37. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/diff.py +0 -0
  38. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/diff_format.py +0 -0
  39. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/errors.py +0 -0
  40. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/__init__.py +0 -0
  41. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/alter_file.py +0 -0
  42. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/mermaid.py +0 -0
  43. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/sql.py +0 -0
  44. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/__init__.py +0 -0
  45. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/base.py +0 -0
  46. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/sqlalchemy.py +0 -0
  47. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/sqlmodel.py +0 -0
  48. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/__init__.py +0 -0
  49. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/alter_file.py +0 -0
  50. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/database.py +0 -0
  51. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/sql.py +0 -0
  52. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/layout.py +0 -0
  53. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/merge_driver.py +0 -0
  54. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/__init__.py +0 -0
  55. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/base.py +0 -0
  56. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/sqlalchemy.py +0 -0
  57. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/sqlmodel.py +0 -0
  58. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/schema.py +0 -0
  59. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/staging.py +0 -0
  60. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/auth.alter +0 -0
  61. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/cms.alter +0 -0
  62. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/ecommerce.alter +0 -0
  63. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/saas-base.alter +0 -0
  64. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/types.py +0 -0
  65. {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/validate.py +0 -0
  66. {alterdb-0.2.2 → alterdb-0.2.4}/src/alterdb/__init__.py +0 -0
  67. {alterdb-0.2.2 → alterdb-0.2.4}/templates/auth.alter +0 -0
  68. {alterdb-0.2.2 → alterdb-0.2.4}/templates/cms.alter +0 -0
  69. {alterdb-0.2.2 → alterdb-0.2.4}/templates/ecommerce.alter +0 -0
  70. {alterdb-0.2.2 → alterdb-0.2.4}/templates/saas-base.alter +0 -0
  71. {alterdb-0.2.2 → alterdb-0.2.4}/tests/__init__.py +0 -0
  72. {alterdb-0.2.2 → alterdb-0.2.4}/tests/conftest.py +0 -0
  73. {alterdb-0.2.2 → alterdb-0.2.4}/tests/fixtures/__init__.py +0 -0
  74. {alterdb-0.2.2 → alterdb-0.2.4}/tests/fixtures/sqlalchemy_models.py +0 -0
  75. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_alembic_wrapper.py +0 -0
  76. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug10_sa_column_type.py +0 -0
  77. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug17_apply_preserve.py +0 -0
  78. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug6_list_any_json.py +0 -0
  79. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug7_table_args.py +0 -0
  80. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug7_unreferenced_enums.py +0 -0
  81. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug8_surgical_preserve.py +0 -0
  82. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug_table_args_tuple.py +0 -0
  83. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bugs_v013.py +0 -0
  84. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_canvas_actions.py +0 -0
  85. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_canvas_cors.py +0 -0
  86. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_cli.py +0 -0
  87. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_diff.py +0 -0
  88. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_e2e.py +0 -0
  89. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_enum_routing.py +0 -0
  90. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_exporters.py +0 -0
  91. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_generator_sqlalchemy.py +0 -0
  92. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_generator_sqlmodel.py +0 -0
  93. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_importer_database.py +0 -0
  94. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_importers.py +0 -0
  95. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_layout.py +0 -0
  96. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_mcp_server.py +0 -0
  97. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_merge_driver.py +0 -0
  98. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_migration_sql.py +0 -0
  99. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_parser_sqlalchemy.py +0 -0
  100. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_parser_sqlmodel.py +0 -0
  101. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_round_trip.py +0 -0
  102. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_schema.py +0 -0
  103. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_smoke.py +0 -0
  104. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_staging.py +0 -0
  105. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_surgical.py +0 -0
  106. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_sync_from_code.py +0 -0
  107. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_types.py +0 -0
  108. {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_validate.py +0 -0
  109. {alterdb-0.2.2 → alterdb-0.2.4}/uv.lock +0 -0
@@ -2,6 +2,171 @@
2
2
 
3
3
  All notable changes to Alter are documented here.
4
4
 
5
+ ## [0.2.4] — 2026-03-17
6
+
7
+ ### Bug Fixes
8
+
9
+ #### Duplicate FK constraints in migration SQL (Bug C)
10
+
11
+ When a new table with a foreign key column was added on the canvas, `_migration_sql()`
12
+ emitted the FK constraint twice: once inline inside the `CREATE TABLE` statement (via
13
+ `_table_to_sql()`), and a second time as a standalone `ALTER TABLE … ADD CONSTRAINT`
14
+ from the `add_relation` change handler.
15
+
16
+ Fixed by collecting the names of all tables being created in the current diff into an
17
+ `added_tables` set. The `add_relation` handler now skips emitting a separate FK
18
+ statement for any table in that set, since the `CREATE TABLE` block already contains
19
+ the inline reference.
20
+
21
+ #### `alter canvas` crashes on fresh project with no `schema.alter` file (Bug D)
22
+
23
+ Running `alter canvas` in a project that had never been initialised (no `schema.alter`
24
+ file yet) caused `watchfiles.watch()` to raise an error because the target path did not
25
+ exist. The traceback surfaced as an unhandled exception in the file-watcher thread,
26
+ producing a noisy and confusing error message in the terminal.
27
+
28
+ The file watcher thread now blocks on a `threading.Event` (`_file_created`) instead of
29
+ starting immediately. The event is signalled the first time `schema.alter` is written
30
+ (on the first Commit or Apply to Code), at which point `watchfiles.watch()` is called
31
+ on a path that is guaranteed to exist. Before the file exists, the canvas still loads
32
+ and works fully — changes are simply held in memory until the first commit.
33
+
34
+ #### `apply_to_code` silently using stale committed schema (Bug E)
35
+
36
+ When the canvas "Apply to Code" button was clicked with uncommitted staged changes
37
+ present, `_apply_to_code_impl` read `staging.current_schema` (the last committed
38
+ state) rather than `staging.proposed_schema` (the in-memory working state). Staged
39
+ changes were silently discarded from the code write while they remained visible on the
40
+ canvas, causing the model files to diverge from what the canvas showed.
41
+
42
+ Two-part fix:
43
+
44
+ 1. **Canvas handler auto-commit** — `_handle_apply_to_code` in `canvas/server.py` now
45
+ calls `staging.commit()` before delegating to `_apply_to_code_impl` whenever there
46
+ are pending staged changes. This ensures the code write always reflects the current
47
+ canvas state.
48
+
49
+ 2. **MCP guard** — `apply_to_code()` in `mcp_server.py` now returns an early error
50
+ message if `staging.has_pending()` is true, asking the caller to invoke
51
+ `commit_changes()` first. This prevents the MCP path from silently applying a stale
52
+ snapshot while an in-progress edit session is underway.
53
+
54
+ #### `alter import` creates spurious `app/models.py` on new projects (Bug F)
55
+
56
+ `alter import schema.sql` was routing imported tables to
57
+ `metadata.sqlmodel_module`, which defaults to `"app/models.py"`. On projects that had
58
+ no `app/` directory, this caused `alter apply` to create `app/models.py` as a new
59
+ file even though the project had no such layout.
60
+
61
+ The import command now calls `_default_model_path(current_schema, project_root)` to
62
+ infer the correct output file, applying the same priority logic used everywhere else
63
+ in the codebase: most-common directory across existing tracked tables → `app/` if it
64
+ exists → `models.py` in the project root. No phantom files are created.
65
+
66
+ #### MCP server emits `PydanticJsonSchemaWarning` on startup (Bug G)
67
+
68
+ The `_UNSET = object()` sentinel — used as the default for `default`, `max_length`,
69
+ and `foreign_key` parameters in `modify_column` so that callers can pass explicit
70
+ `None` to clear a field — caused Pydantic to emit a `PydanticJsonSchemaWarning` when
71
+ building the JSON schema for the MCP tool at server startup. The warning was harmless
72
+ (the sentinel works correctly at call time) but noisy.
73
+
74
+ Fixed by wrapping the tool-registration loop in `_LazyMCP._init_real` with a
75
+ `warnings.catch_warnings()` context manager that suppresses
76
+ `"Default value.*is not JSON serializable"` messages. The suppression is scoped
77
+ entirely to the registration calls; all other Pydantic warnings remain unaffected.
78
+
79
+ ### Improvements
80
+
81
+ #### Column rename detection in migration SQL
82
+
83
+ Alter's diff engine is name-based and cannot distinguish a column rename from a
84
+ drop + add of the same type. When `_migration_sql()` detects this pattern — a
85
+ `drop_column` and an `add_column` on the same table whose dropped column and added
86
+ column share the same type — the generated SQL now includes a warning comment:
87
+
88
+ ```sql
89
+ -- WARNING: 'orders.note' is being dropped while 'notes' (same type) is being added.
90
+ -- If this is a rename, replace the ADD+DROP below with:
91
+ -- ALTER TABLE orders RENAME COLUMN note TO notes;
92
+ ```
93
+
94
+ This makes it easy to spot a likely rename and swap the destructive ADD+DROP for a
95
+ safe `RENAME COLUMN` before executing the migration.
96
+
97
+ #### Improved initial canvas layout
98
+
99
+ The ELK graph layout used for the first-open auto-arrange now produces cleaner ERD
100
+ diagrams. Changes:
101
+
102
+ - Direction changed from `RIGHT` to `DOWN` — tables flow top-to-bottom, which reads
103
+ more naturally as an entity-relationship diagram.
104
+ - Node spacing increased from 60 px to 120 px, edge-to-node layer spacing from 80 px
105
+ to 130 px — tables no longer overlap on medium-sized schemas.
106
+ - `BRANDES_KOEPF` node placement and `GREEDY` cycle-breaking strategies added for
107
+ more compact, symmetrical layouts.
108
+ - Grid fallback spacing increased (`290 → 400 px` column width, `310 → 420 px` row
109
+ height) for schemas that fall back to the simpler grid arrangement.
110
+
111
+ ## [0.2.3] — 2026-03-15
112
+
113
+ ### Bug Fixes
114
+
115
+ #### `alter mcp` crashes with a cryptic `ModuleNotFoundError` when `mcp < 1.2.0` (Bug A)
116
+
117
+ `alter mcp` calls `init_mcp()` which imports `FastMCP` from `mcp.server.fastmcp`. That
118
+ submodule was introduced in `mcp 1.2.0`; older installations raise a bare
119
+ `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` with no indication of how
120
+ to fix it.
121
+
122
+ `init_mcp()` now wraps the import in a `try/except ImportError` and raises an
123
+ `AlterError` with an actionable message:
124
+
125
+ ```
126
+ 'alter mcp' requires mcp>=1.2.0, but mcp==1.1.3 is installed.
127
+ Upgrade with: pip install 'mcp>=1.2.0'
128
+ ```
129
+
130
+ The error surfaces cleanly in the CLI (no "MCP server error:" prefix) because the CLI
131
+ already handles `AlterError` separately from generic exceptions.
132
+
133
+ A second guard wraps the cosmetic `_mcp_server.version` assignment in
134
+ `try/except AttributeError` so that future changes to `mcp` internals do not break
135
+ `alter mcp` startup.
136
+
137
+ #### `alter apply` spuriously rewrites `datetime.now(timezone.utc)` defaults on Python 3.11+ (Bug B)
138
+
139
+ When a model file contained a column with `default_factory=lambda: datetime.now(timezone.utc)`,
140
+ running `alter apply` on Python 3.11+ would rewrite the line even though nothing had
141
+ changed in the schema.
142
+
143
+ Root cause: `_parse_field_kwargs` normalises kwargs via `ast.unparse`, and the Python
144
+ `ast` module changed how it serialises zero-argument lambdas between versions —
145
+ Python ≤ 3.10 produces `"lambda :"` (with a space after `lambda`), while Python 3.11+
146
+ produces `"lambda:"` (no space). Because `ast.unparse` is applied to both the
147
+ existing code and the freshly generated schema line, the comparison reached different
148
+ sides of the `_DEFAULT_FACTORY_EQUIV` lookup depending on the Python version in use,
149
+ causing the surgical patcher to believe a change was needed when there was none.
150
+
151
+ Two-part fix in `generators/_surgical.py`:
152
+
153
+ 1. **`_norm_lambda_ws()` helper** — strips extraneous whitespace between `lambda` and
154
+ `:` for zero-argument lambdas (`re.sub(r"^lambda\s*:", "lambda:", s)`), making
155
+ lambda strings compare equal across Python versions.
156
+
157
+ 2. **Normalization applied consistently** — `_normalize_kw_for_eq` now calls
158
+ `_norm_lambda_ws` on `default_factory` values after the `_DEFAULT_FACTORY_EQUIV`
159
+ lookup, so both the existing-file side and the schema side are normalised before
160
+ comparison. The rebuild path in `_rebuild_field_line` applies the same
161
+ normalization when checking whether the existing value is canonically equivalent.
162
+
163
+ The `_DEFAULT_FACTORY_EQUIV` dict value for `utcnow` was also corrected from
164
+ `"lambda : datetime.now(timezone.utc)"` (with spurious space, matching old Python 3.10
165
+ `ast.unparse` output) to `"lambda: datetime.now(timezone.utc)"` (canonical form) — the
166
+ `_norm_lambda_ws` normalization then makes both forms match on all Python versions.
167
+
168
+ ---
169
+
5
170
  ## [0.2.2] — 2026-03-15
6
171
 
7
172
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alterdb
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Visual schema design for SQLModel and SQLAlchemy. Edit your database as a diagram, write it back as code.
5
5
  Project-URL: Homepage, https://github.com/chimi-labs/alter
6
6
  Project-URL: Repository, https://github.com/chimi-labs/alter
@@ -28,6 +28,10 @@ Provides-Extra: db
28
28
  Requires-Dist: psycopg2-binary>=2.9; extra == 'db'
29
29
  Description-Content-Type: text/markdown
30
30
 
31
+ <p align="center">
32
+ <img src="https://raw.githubusercontent.com/chimi-labs/alter/main/docs/alter_logo.png" alt="Alter" width="320" />
33
+ </p>
34
+
31
35
  # Alter
32
36
 
33
37
  > Comprehension first, design second.
@@ -79,7 +83,7 @@ uv add alterdb
79
83
  > This keeps Alter's dependencies completely separate from your project's virtual environment while
80
84
  > making the `alter` command available on your `PATH`.
81
85
 
82
- > **Live database introspection** (MCP `introspect_db` tool): requires `psycopg2-binary`, install with `pip install alterdb[db]`.
86
+ > **Live database features** (MCP `introspect_db`, `query_db`, `describe_table_data`, `explain_query` tools): requires `psycopg2-binary`, install with `pip install alterdb[db]`.
83
87
 
84
88
  ## Quick Start
85
89
 
@@ -160,6 +164,10 @@ alter apply --preview # see exactly what will change (unified diff)
160
164
  alter apply # write the changes to your model files
161
165
  ```
162
166
 
167
+ > **Apply to Code auto-commits:** clicking **Apply to Code** in the canvas automatically commits
168
+ > any pending staged changes to `schema.alter` before writing to your model files — so you never
169
+ > accidentally apply a stale snapshot.
170
+
163
171
  `alter apply` is surgical — it only touches what the schema says has changed:
164
172
 
165
173
  - **Additions** — new tables are appended as new classes; new columns are inserted after the last existing field.
@@ -244,6 +252,12 @@ Alter generates the SQL — you run it with whatever migration tool you already
244
252
  The **Migrations tab** in the canvas shows the pending SQL at any time. The `preview_migration`
245
253
  MCP tool returns the same SQL to AI assistants. Copy it into your migration manager of choice.
246
254
 
255
+ > **Column rename detection:** Alter's diff engine is name-based and cannot distinguish a column
256
+ > rename from a drop + add. When it detects a `DROP COLUMN` paired with an `ADD COLUMN` of the
257
+ > same type on the same table, the generated SQL includes a comment pointing out the potential
258
+ > rename and showing the equivalent `ALTER TABLE … RENAME COLUMN` statement. Review this comment
259
+ > before running the migration to avoid accidental data loss.
260
+
247
261
  ### With Alembic (one-time setup)
248
262
 
249
263
  ```bash
@@ -519,7 +533,17 @@ alter init # scan your ORM models → create schema.alter
519
533
 
520
534
  The MCP server requires `schema.alter` to exist before it can start.
521
535
 
522
- **Step 2 — Register the server in your editor.**
536
+ **Step 2 — Ensure `mcp>=1.2.0` is installed.**
537
+
538
+ `alter mcp` requires `mcp>=1.2.0` (the `FastMCP` API used internally was added in that release). If you see an error on startup, upgrade:
539
+
540
+ ```bash
541
+ pip install 'mcp>=1.2.0'
542
+ # or
543
+ uv add 'mcp>=1.2.0'
544
+ ```
545
+
546
+ **Step 3 — Register the server in your editor.**
523
547
 
524
548
  Most editors use the same JSON config. Add this to your MCP settings file:
525
549
 
@@ -551,7 +575,7 @@ claude mcp add alter -- uv run --directory /path/to/project alter mcp
551
575
  > **Why `uv run`?** It ensures the command runs inside your project's virtual environment,
552
576
  > picking up the correct `alterdb` version and dependencies — no manual `source .venv/bin/activate` needed.
553
577
 
554
- **Step 3 — Restart your editor** (or open a new session) so the MCP server connects. Verify by asking:
578
+ **Step 4 — Restart your editor** (or open a new session) so the MCP server connects. Verify by asking:
555
579
 
556
580
  > _"What tools do you have available from alter?"_
557
581
 
@@ -564,8 +588,14 @@ claude mcp add alter -- uv run --directory /path/to/project alter mcp
564
588
  - **Preview migration SQL** — see the DDL that needs to run for pending changes
565
589
  - **Undo/redo** any staged change
566
590
  - **Commit** approved changes to `schema.alter`
591
+ - **Apply to code** — write committed schema changes to your ORM model files
567
592
  - **Export** as SQL, Mermaid, or JSON
568
593
  - **Validate** the schema for errors
594
+ - **Query live data** — run read-only SQL against a real database and get results back (see below)
595
+
596
+ > **`apply_to_code` requires a prior commit:** if there are uncommitted staged changes when
597
+ > `apply_to_code()` is called, the tool returns a message asking you to call `commit_changes()`
598
+ > first. This prevents silently applying a stale snapshot while discarding your pending edits.
569
599
 
570
600
  ### Example prompts
571
601
 
@@ -597,6 +627,131 @@ Once connected, just talk to your assistant:
597
627
  The assistant stages changes, shows you a diff, and only commits to `schema.alter` with your
598
628
  approval — nothing is written to your model files until you also run `alter apply`.
599
629
 
630
+ ### Querying live data
631
+
632
+ Alter's MCP server can also run read-only SQL queries against a live PostgreSQL database,
633
+ so your AI assistant can answer questions about actual data — not just schema structure.
634
+
635
+ **Setup**
636
+
637
+ **1 — Install the database extra** (if you haven't already):
638
+
639
+ ```bash
640
+ pip install alterdb[db]
641
+ # or
642
+ uv add alterdb[db]
643
+ ```
644
+
645
+ This adds `psycopg2-binary`, which is required for all live database tools.
646
+
647
+ **2 — Set the `DATABASE_URL` environment variable** before starting your editor or MCP session:
648
+
649
+ ```bash
650
+ export DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
651
+ ```
652
+
653
+ Or add it to your editor's MCP config so it's always available:
654
+
655
+ ```json
656
+ {
657
+ "mcpServers": {
658
+ "alter": {
659
+ "command": "uv",
660
+ "args": ["run", "--directory", "/path/to/project", "alter", "mcp"],
661
+ "env": {
662
+ "DATABASE_URL": "postgresql://user:password@localhost:5432/mydb"
663
+ }
664
+ }
665
+ }
666
+ }
667
+ ```
668
+
669
+ That's it. No other configuration needed — the tools pick up `DATABASE_URL` automatically.
670
+
671
+ **The three data tools**
672
+
673
+ | Tool | What it does |
674
+ |------|-------------|
675
+ | `query_db` | Execute a SELECT query, return results as a table, JSON, or CSV |
676
+ | `describe_table_data` | Show row count, column types, relationships, and sample rows for a table |
677
+ | `explain_query` | Show PostgreSQL's query execution plan (without running the query) |
678
+
679
+ All queries run in a **read-only transaction** — INSERT, UPDATE, DELETE, and DDL are
680
+ blocked at the database level. Results are capped at 1,000 rows (default: 100).
681
+
682
+ **Example prompts**
683
+
684
+ _"How many users signed up in the last 30 days?"_
685
+
686
+ The assistant calls `read_schema` to see the `users` table has a `created_at` column,
687
+ then calls `query_db`:
688
+ ```
689
+ | count |
690
+ | 847 |
691
+
692
+ 1 row in 4ms
693
+ ```
694
+ → _"847 users signed up in the last 30 days."_
695
+
696
+ ---
697
+
698
+ _"Which plan do most of our paying customers use?"_
699
+
700
+ ```
701
+ | plan | count |
702
+ | pro | 1,204 |
703
+ | starter | 891 |
704
+ | team | 342 |
705
+
706
+ 3 rows in 12ms
707
+ ```
708
+
709
+ ---
710
+
711
+ _"Tell me about the orders table before I write a query against it"_
712
+
713
+ The assistant calls `describe_table_data("orders")`:
714
+ ```
715
+ Table: public.orders (24,871 rows)
716
+
717
+ Columns:
718
+ id: uuid NOT NULL DEFAULT gen_random_uuid()
719
+ user_id: uuid NOT NULL
720
+ status: varchar NOT NULL
721
+ total_cents: integer NOT NULL
722
+ created_at: timestamptz NOT NULL DEFAULT now()
723
+
724
+ References (outgoing):
725
+ orders.user_id → users.id (CASCADE)
726
+
727
+ Referenced by (incoming):
728
+ order_items.order_id → orders.id (CASCADE)
729
+
730
+ Sample data (5 rows):
731
+ id | user_id | status | total_cents | created_at
732
+ -------+---------+-----------+-------------+-----------
733
+ a1b2… | x9y0… | completed | 4999 | 2024-03-01…
734
+
735
+ ```
736
+
737
+ ---
738
+
739
+ _"Why is my query slow? EXPLAIN this: SELECT * FROM orders JOIN users ON orders.user_id = users.id WHERE orders.status = 'pending'"_
740
+
741
+ The assistant calls `explain_query` and returns the plan — no rows are fetched, no data
742
+ is affected.
743
+
744
+ ```
745
+ Hash Join (cost=18.50..1842.30 rows=312 width=156)
746
+ Hash Cond: (orders.user_id = users.id)
747
+ -> Seq Scan on orders (cost=0.00..1810.71 rows=312 ...)
748
+ Filter: ((status)::text = 'pending'::text)
749
+ -> Hash (cost=14.70..14.70 rows=304 ...)
750
+ -> Seq Scan on users (cost=0.00..14.70 rows=304 ...)
751
+ ```
752
+
753
+ → _"The sequential scan on `orders` is the bottleneck — adding an index on `orders.status` would speed this up significantly."_
754
+
600
755
  ## Supported ORMs
601
756
 
602
757
  - **SQLModel** — auto-detected from `from sqlmodel import ...`
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/chimi-labs/alter/main/docs/alter_logo.png" alt="Alter" width="320" />
3
+ </p>
4
+
1
5
  # Alter
2
6
 
3
7
  > Comprehension first, design second.
@@ -49,7 +53,7 @@ uv add alterdb
49
53
  > This keeps Alter's dependencies completely separate from your project's virtual environment while
50
54
  > making the `alter` command available on your `PATH`.
51
55
 
52
- > **Live database introspection** (MCP `introspect_db` tool): requires `psycopg2-binary`, install with `pip install alterdb[db]`.
56
+ > **Live database features** (MCP `introspect_db`, `query_db`, `describe_table_data`, `explain_query` tools): requires `psycopg2-binary`, install with `pip install alterdb[db]`.
53
57
 
54
58
  ## Quick Start
55
59
 
@@ -130,6 +134,10 @@ alter apply --preview # see exactly what will change (unified diff)
130
134
  alter apply # write the changes to your model files
131
135
  ```
132
136
 
137
+ > **Apply to Code auto-commits:** clicking **Apply to Code** in the canvas automatically commits
138
+ > any pending staged changes to `schema.alter` before writing to your model files — so you never
139
+ > accidentally apply a stale snapshot.
140
+
133
141
  `alter apply` is surgical — it only touches what the schema says has changed:
134
142
 
135
143
  - **Additions** — new tables are appended as new classes; new columns are inserted after the last existing field.
@@ -214,6 +222,12 @@ Alter generates the SQL — you run it with whatever migration tool you already
214
222
  The **Migrations tab** in the canvas shows the pending SQL at any time. The `preview_migration`
215
223
  MCP tool returns the same SQL to AI assistants. Copy it into your migration manager of choice.
216
224
 
225
+ > **Column rename detection:** Alter's diff engine is name-based and cannot distinguish a column
226
+ > rename from a drop + add. When it detects a `DROP COLUMN` paired with an `ADD COLUMN` of the
227
+ > same type on the same table, the generated SQL includes a comment pointing out the potential
228
+ > rename and showing the equivalent `ALTER TABLE … RENAME COLUMN` statement. Review this comment
229
+ > before running the migration to avoid accidental data loss.
230
+
217
231
  ### With Alembic (one-time setup)
218
232
 
219
233
  ```bash
@@ -489,7 +503,17 @@ alter init # scan your ORM models → create schema.alter
489
503
 
490
504
  The MCP server requires `schema.alter` to exist before it can start.
491
505
 
492
- **Step 2 — Register the server in your editor.**
506
+ **Step 2 — Ensure `mcp>=1.2.0` is installed.**
507
+
508
+ `alter mcp` requires `mcp>=1.2.0` (the `FastMCP` API used internally was added in that release). If you see an error on startup, upgrade:
509
+
510
+ ```bash
511
+ pip install 'mcp>=1.2.0'
512
+ # or
513
+ uv add 'mcp>=1.2.0'
514
+ ```
515
+
516
+ **Step 3 — Register the server in your editor.**
493
517
 
494
518
  Most editors use the same JSON config. Add this to your MCP settings file:
495
519
 
@@ -521,7 +545,7 @@ claude mcp add alter -- uv run --directory /path/to/project alter mcp
521
545
  > **Why `uv run`?** It ensures the command runs inside your project's virtual environment,
522
546
  > picking up the correct `alterdb` version and dependencies — no manual `source .venv/bin/activate` needed.
523
547
 
524
- **Step 3 — Restart your editor** (or open a new session) so the MCP server connects. Verify by asking:
548
+ **Step 4 — Restart your editor** (or open a new session) so the MCP server connects. Verify by asking:
525
549
 
526
550
  > _"What tools do you have available from alter?"_
527
551
 
@@ -534,8 +558,14 @@ claude mcp add alter -- uv run --directory /path/to/project alter mcp
534
558
  - **Preview migration SQL** — see the DDL that needs to run for pending changes
535
559
  - **Undo/redo** any staged change
536
560
  - **Commit** approved changes to `schema.alter`
561
+ - **Apply to code** — write committed schema changes to your ORM model files
537
562
  - **Export** as SQL, Mermaid, or JSON
538
563
  - **Validate** the schema for errors
564
+ - **Query live data** — run read-only SQL against a real database and get results back (see below)
565
+
566
+ > **`apply_to_code` requires a prior commit:** if there are uncommitted staged changes when
567
+ > `apply_to_code()` is called, the tool returns a message asking you to call `commit_changes()`
568
+ > first. This prevents silently applying a stale snapshot while discarding your pending edits.
539
569
 
540
570
  ### Example prompts
541
571
 
@@ -567,6 +597,131 @@ Once connected, just talk to your assistant:
567
597
  The assistant stages changes, shows you a diff, and only commits to `schema.alter` with your
568
598
  approval — nothing is written to your model files until you also run `alter apply`.
569
599
 
600
+ ### Querying live data
601
+
602
+ Alter's MCP server can also run read-only SQL queries against a live PostgreSQL database,
603
+ so your AI assistant can answer questions about actual data — not just schema structure.
604
+
605
+ **Setup**
606
+
607
+ **1 — Install the database extra** (if you haven't already):
608
+
609
+ ```bash
610
+ pip install alterdb[db]
611
+ # or
612
+ uv add alterdb[db]
613
+ ```
614
+
615
+ This adds `psycopg2-binary`, which is required for all live database tools.
616
+
617
+ **2 — Set the `DATABASE_URL` environment variable** before starting your editor or MCP session:
618
+
619
+ ```bash
620
+ export DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
621
+ ```
622
+
623
+ Or add it to your editor's MCP config so it's always available:
624
+
625
+ ```json
626
+ {
627
+ "mcpServers": {
628
+ "alter": {
629
+ "command": "uv",
630
+ "args": ["run", "--directory", "/path/to/project", "alter", "mcp"],
631
+ "env": {
632
+ "DATABASE_URL": "postgresql://user:password@localhost:5432/mydb"
633
+ }
634
+ }
635
+ }
636
+ }
637
+ ```
638
+
639
+ That's it. No other configuration needed — the tools pick up `DATABASE_URL` automatically.
640
+
641
+ **The three data tools**
642
+
643
+ | Tool | What it does |
644
+ |------|-------------|
645
+ | `query_db` | Execute a SELECT query, return results as a table, JSON, or CSV |
646
+ | `describe_table_data` | Show row count, column types, relationships, and sample rows for a table |
647
+ | `explain_query` | Show PostgreSQL's query execution plan (without running the query) |
648
+
649
+ All queries run in a **read-only transaction** — INSERT, UPDATE, DELETE, and DDL are
650
+ blocked at the database level. Results are capped at 1,000 rows (default: 100).
651
+
652
+ **Example prompts**
653
+
654
+ _"How many users signed up in the last 30 days?"_
655
+
656
+ The assistant calls `read_schema` to see the `users` table has a `created_at` column,
657
+ then calls `query_db`:
658
+ ```
659
+ | count |
660
+ | 847 |
661
+
662
+ 1 row in 4ms
663
+ ```
664
+ → _"847 users signed up in the last 30 days."_
665
+
666
+ ---
667
+
668
+ _"Which plan do most of our paying customers use?"_
669
+
670
+ ```
671
+ | plan | count |
672
+ | pro | 1,204 |
673
+ | starter | 891 |
674
+ | team | 342 |
675
+
676
+ 3 rows in 12ms
677
+ ```
678
+
679
+ ---
680
+
681
+ _"Tell me about the orders table before I write a query against it"_
682
+
683
+ The assistant calls `describe_table_data("orders")`:
684
+ ```
685
+ Table: public.orders (24,871 rows)
686
+
687
+ Columns:
688
+ id: uuid NOT NULL DEFAULT gen_random_uuid()
689
+ user_id: uuid NOT NULL
690
+ status: varchar NOT NULL
691
+ total_cents: integer NOT NULL
692
+ created_at: timestamptz NOT NULL DEFAULT now()
693
+
694
+ References (outgoing):
695
+ orders.user_id → users.id (CASCADE)
696
+
697
+ Referenced by (incoming):
698
+ order_items.order_id → orders.id (CASCADE)
699
+
700
+ Sample data (5 rows):
701
+ id | user_id | status | total_cents | created_at
702
+ -------+---------+-----------+-------------+-----------
703
+ a1b2… | x9y0… | completed | 4999 | 2024-03-01…
704
+
705
+ ```
706
+
707
+ ---
708
+
709
+ _"Why is my query slow? EXPLAIN this: SELECT * FROM orders JOIN users ON orders.user_id = users.id WHERE orders.status = 'pending'"_
710
+
711
+ The assistant calls `explain_query` and returns the plan — no rows are fetched, no data
712
+ is affected.
713
+
714
+ ```
715
+ Hash Join (cost=18.50..1842.30 rows=312 width=156)
716
+ Hash Cond: (orders.user_id = users.id)
717
+ -> Seq Scan on orders (cost=0.00..1810.71 rows=312 ...)
718
+ Filter: ((status)::text = 'pending'::text)
719
+ -> Hash (cost=14.70..14.70 rows=304 ...)
720
+ -> Seq Scan on users (cost=0.00..14.70 rows=304 ...)
721
+ ```
722
+
723
+ → _"The sequential scan on `orders` is the bottleneck — adding an index on `orders.status` would speed this up significantly."_
724
+
570
725
  ## Supported ORMs
571
726
 
572
727
  - **SQLModel** — auto-detected from `from sqlmodel import ...`
Binary file
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alterdb"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Visual schema design for SQLModel and SQLAlchemy. Edit your database as a diagram, write it back as code."
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE"]