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.
- {alterdb-0.2.2 → alterdb-0.2.4}/CHANGELOG.md +165 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/PKG-INFO +159 -4
- {alterdb-0.2.2 → alterdb-0.2.4}/README.md +158 -3
- alterdb-0.2.4/docs/alter_logo.png +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/pyproject.toml +1 -1
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/server.py +91 -6
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/static/canvas.js +15 -4
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/cli.py +10 -1
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/_surgical.py +37 -6
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/mcp_server.py +135 -9
- alterdb-0.2.4/src/alter/query.py +612 -0
- alterdb-0.2.4/tests/test_query.py +462 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/.env.example +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/.gitignore +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/.python-version +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/LICENSE +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/docker-compose.yml +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/docs/Canvas.png +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/alembic/alembic.ini +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/alembic/env.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/alembic/script.py.mako +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/database.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/enums.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/main.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/models/parents.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/app/models/starter.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/pyproject.toml +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/tests/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/tests/test_integration.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/examples/saas-starter/tests/test_round_trip.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/static/index.html +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/canvas/static/style.css +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/data/demo_schema.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/diff.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/diff_format.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/errors.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/alter_file.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/mermaid.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/exporters/sql.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/base.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/sqlalchemy.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/generators/sqlmodel.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/alter_file.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/database.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/importers/sql.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/layout.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/merge_driver.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/base.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/sqlalchemy.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/parsers/sqlmodel.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/schema.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/staging.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/auth.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/cms.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/ecommerce.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/templates/saas-base.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/types.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alter/validate.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/src/alterdb/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/templates/auth.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/templates/cms.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/templates/ecommerce.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/templates/saas-base.alter +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/conftest.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/fixtures/__init__.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/fixtures/sqlalchemy_models.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_alembic_wrapper.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug10_sa_column_type.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug17_apply_preserve.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug6_list_any_json.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug7_table_args.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug7_unreferenced_enums.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug8_surgical_preserve.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bug_table_args_tuple.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_bugs_v013.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_canvas_actions.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_canvas_cors.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_cli.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_diff.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_e2e.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_enum_routing.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_exporters.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_generator_sqlalchemy.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_generator_sqlmodel.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_importer_database.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_importers.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_layout.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_mcp_server.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_merge_driver.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_migration_sql.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_parser_sqlalchemy.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_parser_sqlmodel.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_round_trip.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_schema.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_smoke.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_staging.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_surgical.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_sync_from_code.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_types.py +0 -0
- {alterdb-0.2.2 → alterdb-0.2.4}/tests/test_validate.py +0 -0
- {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.
|
|
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
|
|
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 —
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
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
|