Asqueel Migration
Database schema migration engine — compare a desired database structure against a live database and generate (or apply) the SQL commands that align them.
ORM-agnostic by design: the library never reads your model. You describe the database in a normalized JSON contract — directly, or through the friendlier human JSON / XML external formats — and the engine does the rest: introspection, diff, DDL generation, execution.
Status: Alpha — API may change.
Features
- Four dialects: PostgreSQL (primary), SQLite, MySQL, MSSQL — one reader/writer/adapter trio per dialect, capability-gated.
- Two equivalent external formats: human JSON and XSD-validated XML; the same model in either compiles to the identical internal structure.
- Deterministic entity matching: FK/UNIQUE/index names are structural hashes computed from schema + table + columns, so renaming never causes spurious diffs.
- Additive compatibility check: success means the database can host the application without further additive changes, not that both structures are identical. Extra database objects are preserved; renames are never inferred.
- Safe by default: destructive commands (DROP) are disabled unless explicitly enabled; type conversions can be forced and backed up.
- Minimal runtime dependencies:
dictdiffer, plus the DB driver of your dialect as an optional extra.
Install
Install from PyPI:
pip install "asqueel-migration[postgresql]"
Extras: postgresql (psycopg 3), mysql (PyMySQL), mssql
(pymssql), validation (jsonschema), docs, dev, all.
SQLite needs no extra (stdlib driver).
Quickstart
from asqueel_migration import (
JsonStructureProducer, PgDatabase, SqlMigrator, StructureValidator,
)
model = {
"db": "mydb",
"schemas": [
{"name": "public", "tables": [
{"name": "author", "pkey": "id", "columns": [
{"name": "id", "dtype": "serial"},
{"name": "name", "dtype": "A", "size": "0:120",
"notnull": True},
]},
]},
],
}
structure = JsonStructureProducer(model).get_json_struct()
db = PgDatabase({"dbname": "mydb", "host": "localhost"},
application_schemas=["public"])
migrator = SqlMigrator(db)
migrator.ormStructure = StructureValidator().validate(structure)
migrator.prepareMigrationCommands()
print(migrator.getChanges()) # review the SQL...
# migrator.applyChanges() # ...or apply it
Swap PgDatabase for SqliteDatabase, MysqlDatabase or
MssqlDatabase for the other dialects.
Describing the database
Three input formats, from the friendliest to the most technical:
- Human JSON — readable JSON with names and lists, compiled by
JsonStructureProducer(above). - XML — the same information validated by the packaged XSD
(
sql_model-1.0.xsd), ideal for editors and GUIs; compiled byXmlStructureProducer. The inversestruct_to_xml()turns an introspected database back into editable XML. - Normalized JSON — the internal contract, built directly with
the
new_*_itemfactories (the route an ORM extractor takes).
The complete format reference — columns, dtypes, foreign keys (including multi-column), constraints, indexes with per-column sort order and WITH options, extensions, event triggers — is in the Producer Guide, also available as docs/producer_guide.md.
How it works
your model ──(producer)──► normalized JSON ──┐
├──► diff ──► SQL commands ──► apply
live database ──(reader)──► normalized JSON ─┘
Both sides project to the same normalized JSON (hierarchy:
root → schemas → tables → columns/relations/constraints/indexes,
each entity carrying entity, entity_name, attributes). The diff
engine (dictdiffer) emits added/changed/removed events; the command
builder turns them into dialect-specific DDL; the executor assembles
and applies it. The JSON Schema of the contract is packaged as
structure-1.0.json.
Execution safety and recovery
applyChanges() is atomic by default where the generated DDL supports a
transaction. PostgreSQL, SQLite and SQL Server execute extensions and all
structural changes as one DDL unit. MySQL runs best-effort because its DDL can
implicitly commit; applyChanges() records an explicit warning and returns
best_effort: true.
Database creation is always a separate manager operation and cannot be part of the target database transaction. If creating a new database succeeds but its DDL later fails, the empty or partially initialized database remains, while the transactional DDL unit is rolled back where supported.
Every failure stops execution immediately and raises
MigrationExecutionError. Its phase, rolled_back and
partial_state_possible attributes describe the outcome. The same invocation
does not try to repair or reanalyse the database. Run the migration again: it
re-introspects the current database and generates only the residual additive
changes. Objects already created are retained; no DROP or rename is inferred.
| Dialect | Default DDL mode | Failure in the DDL unit |
|---|---|---|
| PostgreSQL | One transaction | Rolled back |
| SQLite | One transaction on the attached connection | Rolled back on an ordinary execution error; crash-level atomicity across multiple attached files depends on SQLite journal configuration |
| SQL Server | One transaction for the DDL generated by this library | Rolled back |
| MySQL | Best-effort, explicit warning | Earlier statements may remain committed |
Database management service (0.2 migration)
The Kajenn application now lives in the separate
asqueel-dbmanager repository.
Install that application to use REST, generated OpenAPI, MCP, or PostgreSQL
backup/restore. This engine no longer depends on Kajenn or genro-routes.
The former [app] extra and asqueel_migration.app.EditorApp import are removed
in 0.2. Python callers and the database CLI remain in this package.
from asqueel_migration import plan_migration, apply_migration
# db is a caller-owned PgDatabase/SqliteDatabase/etc.
# desired is normalized JSON text or a Python dictionary.
try:
plan = plan_migration(db, desired)
print(plan["sql"], plan["warnings"])
# After reviewing the plan:
result = apply_migration(db, desired, expected_plan=plan["plan_id"])
assert result["verified"], result["remaining"]
finally:
db.closeConnection()
SqlMigrator(db, structure=desired) validates and snapshots the input.
The legacy migrator.ormStructure = desired form remains available and now
copies its input. Each preparation filters a fresh working copy; source data
and repeated warnings are preserved. To change the desired schema, assign it
again: edits to the exposed working dictionary do not update the source.
Plan identifiers cover the desired/current structure and generated commands.
They detect drift before execution, but do not prevent concurrent external DDL.
Unsupported operations block apply_migration; post-apply verification reports
remaining differences without pretending that already applied DDL was undone.
The legacy XML helpers in editor_service remain transport-independent.
Modules
| Module | Responsibility |
|---|---|
structures.py |
Contract constants, entity factories, name hashing |
validation.py |
StructureValidator — JSON Schema + semantic checks |
json_producer.py |
Human JSON → normalized JSON |
xml_producer.py |
XML → normalized JSON, and back (struct_to_xml) |
service.py |
Plan, reviewed execution and convergence results |
editor_service.py |
DB ↔ XML round-trip service for editors |
diff_engine.py |
Structure comparison → typed events |
command_builder.py |
Events → SQL command fragments |
executor.py |
SQL assembly, execution, backup verification |
migrator.py |
SqlMigrator — the orchestrator |
database.py |
Abstract Database / adapter interfaces |
readers/ |
Per-dialect introspection (live DB → normalized JSON) |
writers/ |
Per-dialect DDL generation |
adapters/ |
Concrete Database/adapter pairs per dialect |
Documentation
Full documentation on Read the Docs. Design notes and milestones live in roadmap/.
License
Apache License 2.0 — Copyright Softwell S.r.l. See LICENSE and NOTICE.
Metadata
Release files for asqueel-migration 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| asqueel_migration-0.2.0.tar.gz | 141.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| asqueel_migration-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 253.8 kB
Release files / asqueel_migration-0.2.0.tar.gz
| Download URL | asqueel_migration-0.2.0.tar.gz |
|---|---|
| Size | 141.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7cb51ecb88e1ff24dfb8b7f83973adf57edaf2efea91c254bdec1a377696851b
|
|
BLAKE2b-256 checksum How to use checksums |
f808c843a2a50bb21d7cf8d1e78b6a7cef7dc762026f2acc0e875d3175af8b2a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / asqueel_migration-0.2.0-py3-none-any.whl
| Download URL | asqueel_migration-0.2.0-py3-none-any.whl |
|---|---|
| Size | 112.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4900790b6c2ce466e0af9ed627ca2b600db12e23a85264cc38f806d9b1e009ba
|
|
BLAKE2b-256 checksum How to use checksums |
8eb221107df3cf1b2e35cfb4bd66c0bb7e2ddc5286b901c99a148507653fce0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log