declaro_persistum 0.1.0__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.
- declaro_persistum-0.1.0/.gitignore +56 -0
- declaro_persistum-0.1.0/BUG_REPORT_autoindex_stale.md +76 -0
- declaro_persistum-0.1.0/BUG_REPORT_cdc_mvcc_conflict.md +54 -0
- declaro_persistum-0.1.0/BUG_REPORT_deep_link_migration.md +58 -0
- declaro_persistum-0.1.0/BUG_REPORT_migrate_remote_connect.md +49 -0
- declaro_persistum-0.1.0/BUG_REPORT_migrate_remote_data_loss.md +83 -0
- declaro_persistum-0.1.0/BUG_REPORT_reconstruction_sync_corruption.md +67 -0
- declaro_persistum-0.1.0/FEATURE_REQUEST_sync_reads.md +34 -0
- declaro_persistum-0.1.0/PKG-INFO +423 -0
- declaro_persistum-0.1.0/README.md +373 -0
- declaro_persistum-0.1.0/docs/BUGFIX-sqlite-applier-consistency.md +236 -0
- declaro_persistum-0.1.0/docs/BUGFIX-sqlite-table-reconstruction.md +115 -0
- declaro_persistum-0.1.0/docs/SYNC_MIGRATION_ARCHITECTURE_FIX.md +110 -0
- declaro_persistum-0.1.0/docs/architecture/005-check-constraint-emulation-v1.md +714 -0
- declaro_persistum-0.1.0/docs/architecture/006-check-constraint-emulation-impl-plan-v1.md +1266 -0
- declaro_persistum-0.1.0/docs/architecture/007-table-reconstruction-v1.md +880 -0
- declaro_persistum-0.1.0/docs/architecture/008-honest-code-audit-v1.md +392 -0
- declaro_persistum-0.1.0/docs/architecture/009-honest-code-audit-impl-plan-v1.md +479 -0
- declaro_persistum-0.1.0/docs/architecture/declaro_persistum_architecture.md +1741 -0
- declaro_persistum-0.1.0/docs/architecture/declaro_persistum_architecture_addendum.md +1499 -0
- declaro_persistum-0.1.0/docs/audit/001-2026-02-01-persistence-facade-audit-v1.md +233 -0
- declaro_persistum-0.1.0/docs/audit/002-2026-02-01-persistence-facade-fix-impl-plan-v1.md +408 -0
- declaro_persistum-0.1.0/docs/hooks.md +227 -0
- declaro_persistum-0.1.0/docs/implementation-plan-addendum.md +1417 -0
- declaro_persistum-0.1.0/docs/pyturso-integration.md +197 -0
- declaro_persistum-0.1.0/docs/standards/architecture-doc-guidelines.md +628 -0
- declaro_persistum-0.1.0/docs/standards/documentation-maintenance-process.md +597 -0
- declaro_persistum-0.1.0/docs/standards/generated-code-standards.md +795 -0
- declaro_persistum-0.1.0/docs/standards/implementation-plan-guidelines.md +255 -0
- declaro_persistum-0.1.0/docs/standards/performance-monitoring-standards.md +764 -0
- declaro_persistum-0.1.0/docs/standards/python-coding-standards.md +754 -0
- declaro_persistum-0.1.0/docs/table_reconstruction.md +253 -0
- declaro_persistum-0.1.0/docs/turso-cloud-sync.md +134 -0
- declaro_persistum-0.1.0/docs/usage.md +2068 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/README.md +72 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/app.py +281 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/db.py +307 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/schema/todos.toml +23 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/templates/db_select.html +237 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/templates/db_status.html +30 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/templates/index.html +252 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/templates/partials/todo_count.html +1 -0
- declaro_persistum-0.1.0/examples/todo_app_django_style/templates/partials/todo_item.html +17 -0
- declaro_persistum-0.1.0/examples/todo_app_native/README.md +84 -0
- declaro_persistum-0.1.0/examples/todo_app_native/app.py +300 -0
- declaro_persistum-0.1.0/examples/todo_app_native/db.py +307 -0
- declaro_persistum-0.1.0/examples/todo_app_native/schema/todos.toml +23 -0
- declaro_persistum-0.1.0/examples/todo_app_native/templates/db_select.html +237 -0
- declaro_persistum-0.1.0/examples/todo_app_native/templates/db_status.html +30 -0
- declaro_persistum-0.1.0/examples/todo_app_native/templates/index.html +254 -0
- declaro_persistum-0.1.0/examples/todo_app_native/templates/partials/todo_count.html +1 -0
- declaro_persistum-0.1.0/examples/todo_app_native/templates/partials/todo_item.html +17 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/README.md +94 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/app.py +283 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/db.py +307 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/schema/todos.toml +23 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/db_select.html +237 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/db_status.html +30 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/index.html +252 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/partials/todo_count.html +1 -0
- declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/partials/todo_item.html +17 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/README.md +80 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/app.py +297 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/db.py +307 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/schema/todos.toml +23 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/db_select.html +237 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/db_status.html +30 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/index.html +252 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/partials/todo_count.html +1 -0
- declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/partials/todo_item.html +17 -0
- declaro_persistum-0.1.0/feature-requests/implementation-plan-instrumented-pool-write-queue.md +167 -0
- declaro_persistum-0.1.0/feature-requests/instrumented-pool-and-write-queue.md +223 -0
- declaro_persistum-0.1.0/feature-requests/row-level-security-policies.md +172 -0
- declaro_persistum-0.1.0/pyproject.toml +133 -0
- declaro_persistum-0.1.0/src/declaro_persistum/__init__.py +119 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/__init__.py +219 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/arrays.py +240 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/check_compat.py +927 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/enums.py +280 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/hierarchy.py +349 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/maps.py +211 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/materialized_views.py +276 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/pragma_compat.py +498 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/ranges.py +194 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/reconstruction.py +494 -0
- declaro_persistum-0.1.0/src/declaro_persistum/abstractions/table_reconstruction.py +449 -0
- declaro_persistum-0.1.0/src/declaro_persistum/applier/__init__.py +10 -0
- declaro_persistum-0.1.0/src/declaro_persistum/applier/postgresql.py +598 -0
- declaro_persistum-0.1.0/src/declaro_persistum/applier/protocol.py +139 -0
- declaro_persistum-0.1.0/src/declaro_persistum/applier/shared.py +406 -0
- declaro_persistum-0.1.0/src/declaro_persistum/applier/sqlite.py +386 -0
- declaro_persistum-0.1.0/src/declaro_persistum/applier/turso.py +349 -0
- declaro_persistum-0.1.0/src/declaro_persistum/bulk_loader.py +256 -0
- declaro_persistum-0.1.0/src/declaro_persistum/cli/__init__.py +14 -0
- declaro_persistum-0.1.0/src/declaro_persistum/cli/commands.py +728 -0
- declaro_persistum-0.1.0/src/declaro_persistum/cli/main.py +376 -0
- declaro_persistum-0.1.0/src/declaro_persistum/compat/__init__.py +25 -0
- declaro_persistum-0.1.0/src/declaro_persistum/compat/sqlalchemy_shim.py +133 -0
- declaro_persistum-0.1.0/src/declaro_persistum/cutover.py +124 -0
- declaro_persistum-0.1.0/src/declaro_persistum/differ/__init__.py +28 -0
- declaro_persistum-0.1.0/src/declaro_persistum/differ/ambiguity.py +360 -0
- declaro_persistum-0.1.0/src/declaro_persistum/differ/core.py +510 -0
- declaro_persistum-0.1.0/src/declaro_persistum/differ/extended.py +312 -0
- declaro_persistum-0.1.0/src/declaro_persistum/differ/toposort.py +425 -0
- declaro_persistum-0.1.0/src/declaro_persistum/errors.py +46 -0
- declaro_persistum-0.1.0/src/declaro_persistum/exceptions.py +376 -0
- declaro_persistum-0.1.0/src/declaro_persistum/fk_ordering.py +196 -0
- declaro_persistum-0.1.0/src/declaro_persistum/functions/__init__.py +77 -0
- declaro_persistum-0.1.0/src/declaro_persistum/functions/aggregates.py +242 -0
- declaro_persistum-0.1.0/src/declaro_persistum/functions/scalars.py +366 -0
- declaro_persistum-0.1.0/src/declaro_persistum/functions/translations.py +131 -0
- declaro_persistum-0.1.0/src/declaro_persistum/inspector/__init__.py +10 -0
- declaro_persistum-0.1.0/src/declaro_persistum/inspector/postgresql.py +516 -0
- declaro_persistum-0.1.0/src/declaro_persistum/inspector/protocol.py +136 -0
- declaro_persistum-0.1.0/src/declaro_persistum/inspector/shared.py +240 -0
- declaro_persistum-0.1.0/src/declaro_persistum/inspector/sqlite.py +280 -0
- declaro_persistum-0.1.0/src/declaro_persistum/inspector/turso.py +217 -0
- declaro_persistum-0.1.0/src/declaro_persistum/instrumentation.py +188 -0
- declaro_persistum-0.1.0/src/declaro_persistum/loader.py +673 -0
- declaro_persistum-0.1.0/src/declaro_persistum/migrations.py +474 -0
- declaro_persistum-0.1.0/src/declaro_persistum/observability/__init__.py +45 -0
- declaro_persistum-0.1.0/src/declaro_persistum/observability/slow_queries.py +213 -0
- declaro_persistum-0.1.0/src/declaro_persistum/observability/timing.py +139 -0
- declaro_persistum-0.1.0/src/declaro_persistum/pool.py +1798 -0
- declaro_persistum-0.1.0/src/declaro_persistum/pydantic_loader.py +398 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/__init__.py +131 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/builder.py +367 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/delete.py +154 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/django_style.py +382 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/executor.py +459 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/hooks.py +127 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/insert.py +221 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/prisma_style.py +444 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/select.py +356 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/sqlalchemy.py +739 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/table.py +909 -0
- declaro_persistum-0.1.0/src/declaro_persistum/query/update.py +221 -0
- declaro_persistum-0.1.0/src/declaro_persistum/transfer.py +523 -0
- declaro_persistum-0.1.0/src/declaro_persistum/types.py +357 -0
- declaro_persistum-0.1.0/src/declaro_persistum/validator.py +303 -0
- declaro_persistum-0.1.0/src/declaro_persistum/write_queue.py +440 -0
- declaro_persistum-0.1.0/tests/__init__.py +1 -0
- declaro_persistum-0.1.0/tests/bdd/conftest.py +150 -0
- declaro_persistum-0.1.0/tests/bdd/factories/__init__.py +42 -0
- declaro_persistum-0.1.0/tests/bdd/factories/connection_factory.py +308 -0
- declaro_persistum-0.1.0/tests/bdd/factories/data_factory.py +235 -0
- declaro_persistum-0.1.0/tests/bdd/factories/schema_factory.py +348 -0
- declaro_persistum-0.1.0/tests/bdd/features/database/multi_backend.feature +56 -0
- declaro_persistum-0.1.0/tests/bdd/features/pragma_compat.feature +286 -0
- declaro_persistum-0.1.0/tests/bdd/features/schema/materialized_views.feature +176 -0
- declaro_persistum-0.1.0/tests/bdd/features/table_reconstruction.feature +458 -0
- declaro_persistum-0.1.0/tests/bdd/steps/__init__.py +3 -0
- declaro_persistum-0.1.0/tests/bdd/steps/common_steps.py +99 -0
- declaro_persistum-0.1.0/tests/bdd/steps/database_steps.py +299 -0
- declaro_persistum-0.1.0/tests/bdd/steps/test_pragma_compat_steps_simple.py +325 -0
- declaro_persistum-0.1.0/tests/bdd/steps/test_table_reconstruction_steps.py +1467 -0
- declaro_persistum-0.1.0/tests/bdd/test_multi_backend.py +13 -0
- declaro_persistum-0.1.0/tests/bdd/test_pragma_compat.py +15 -0
- declaro_persistum-0.1.0/tests/conftest.py +309 -0
- declaro_persistum-0.1.0/tests/integration/__init__.py +1 -0
- declaro_persistum-0.1.0/tests/integration/test_postgresql.py +205 -0
- declaro_persistum-0.1.0/tests/integration/test_sqlite.py +242 -0
- declaro_persistum-0.1.0/tests/integration/test_turso.py +228 -0
- declaro_persistum-0.1.0/tests/stress/conftest.py +62 -0
- declaro_persistum-0.1.0/tests/stress/test_concurrent.py +208 -0
- declaro_persistum-0.1.0/tests/stress/test_edge_cases.py +262 -0
- declaro_persistum-0.1.0/tests/stress/test_large_data.py +309 -0
- declaro_persistum-0.1.0/tests/stress/test_load.py +217 -0
- declaro_persistum-0.1.0/tests/unit/__init__.py +1 -0
- declaro_persistum-0.1.0/tests/unit/test_aggregates.py +248 -0
- declaro_persistum-0.1.0/tests/unit/test_arrays.py +253 -0
- declaro_persistum-0.1.0/tests/unit/test_check_compat_parser.py +244 -0
- declaro_persistum-0.1.0/tests/unit/test_check_compat_registry.py +261 -0
- declaro_persistum-0.1.0/tests/unit/test_check_compat_validator.py +376 -0
- declaro_persistum-0.1.0/tests/unit/test_differ.py +286 -0
- declaro_persistum-0.1.0/tests/unit/test_django_style.py +227 -0
- declaro_persistum-0.1.0/tests/unit/test_enums.py +242 -0
- declaro_persistum-0.1.0/tests/unit/test_hierarchy.py +234 -0
- declaro_persistum-0.1.0/tests/unit/test_hooks.py +372 -0
- declaro_persistum-0.1.0/tests/unit/test_instrumented_pool.py +207 -0
- declaro_persistum-0.1.0/tests/unit/test_loader.py +207 -0
- declaro_persistum-0.1.0/tests/unit/test_maps.py +247 -0
- declaro_persistum-0.1.0/tests/unit/test_materialized_views.py +465 -0
- declaro_persistum-0.1.0/tests/unit/test_migrations.py +296 -0
- declaro_persistum-0.1.0/tests/unit/test_pool.py +358 -0
- declaro_persistum-0.1.0/tests/unit/test_prisma_style.py +259 -0
- declaro_persistum-0.1.0/tests/unit/test_procedures.py +365 -0
- declaro_persistum-0.1.0/tests/unit/test_query_builder.py +575 -0
- declaro_persistum-0.1.0/tests/unit/test_query_expressions.py +471 -0
- declaro_persistum-0.1.0/tests/unit/test_ranges.py +192 -0
- declaro_persistum-0.1.0/tests/unit/test_reconstruction.py +592 -0
- declaro_persistum-0.1.0/tests/unit/test_scalars.py +362 -0
- declaro_persistum-0.1.0/tests/unit/test_slow_queries.py +269 -0
- declaro_persistum-0.1.0/tests/unit/test_sqlalchemy_compat.py +559 -0
- declaro_persistum-0.1.0/tests/unit/test_sqlite_applier_reconstruction.py +226 -0
- declaro_persistum-0.1.0/tests/unit/test_table_reconstruction.py +252 -0
- declaro_persistum-0.1.0/tests/unit/test_timing.py +260 -0
- declaro_persistum-0.1.0/tests/unit/test_toposort.py +203 -0
- declaro_persistum-0.1.0/tests/unit/test_translations.py +263 -0
- declaro_persistum-0.1.0/tests/unit/test_triggers.py +330 -0
- declaro_persistum-0.1.0/tests/unit/test_turso_check_emulation.py +162 -0
- declaro_persistum-0.1.0/tests/unit/test_types.py +201 -0
- declaro_persistum-0.1.0/tests/unit/test_views.py +776 -0
- declaro_persistum-0.1.0/tests/unit/test_write_queue.py +450 -0
- declaro_persistum-0.1.0/uv.lock +996 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
|
|
34
|
+
# Testing
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
.tox/
|
|
39
|
+
.nox/
|
|
40
|
+
|
|
41
|
+
# Mypy
|
|
42
|
+
.mypy_cache/
|
|
43
|
+
|
|
44
|
+
# OS
|
|
45
|
+
.DS_Store
|
|
46
|
+
Thumbs.db
|
|
47
|
+
|
|
48
|
+
# Project specific
|
|
49
|
+
*.db
|
|
50
|
+
*.sqlite
|
|
51
|
+
*-client_wal_index
|
|
52
|
+
|
|
53
|
+
# Claude
|
|
54
|
+
.claude/
|
|
55
|
+
.hypothesis/
|
|
56
|
+
.env
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Bug: Stale `sqlite_autoindex_*_new_1` indexes block table reconstruction
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Table reconstruction fails with `Parse error: index "sqlite_autoindex_{table}_new_1" already exists` when a second `alter_column` op targets the same table in a single migration batch. The error also persists across migration runs if a previous run left stale autoindexes.
|
|
6
|
+
|
|
7
|
+
## Reproduction
|
|
8
|
+
|
|
9
|
+
Schema diff emits two `alter_column` ops for the same table (e.g., `database_routes`). The applier runs them sequentially:
|
|
10
|
+
|
|
11
|
+
1. First `alter_column` → reconstruction succeeds → `sqlite_autoindex_database_routes_new_1` gets created by SQLite for the temp table, then survives the `ALTER TABLE ... RENAME TO` as a stale artifact
|
|
12
|
+
2. Second `alter_column` → reconstruction fails because `sqlite_autoindex_database_routes_new_1` already exists
|
|
13
|
+
|
|
14
|
+
## Observed logs (Render production)
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
INFO:declaro_persistum.abstractions.reconstruction:Successfully reconstructed table 'database_routes'
|
|
18
|
+
INFO:declaro_persistum.applier.turso:Applied: alter_column on database_routes
|
|
19
|
+
ERROR:declaro_persistum.abstractions.reconstruction:Table reconstruction failed for 'database_routes': Parse error: index "sqlite_autoindex_database_routes_new_1" already exists
|
|
20
|
+
WARNING:declaro_persistum.applier.turso:Skipped unsupported operation: alter_column on database_routes: Parse error: index "sqlite_autoindex_database_routes_new_1" already exists
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Same pattern for `pricing_invites`, `users`, `subscriptions`, `billing_events`.
|
|
24
|
+
|
|
25
|
+
## Analysis
|
|
26
|
+
|
|
27
|
+
The reconstruction code in `reconstruction.py` now uses UUID-based temp names (`_declaro_tmp_{table}_{uuid}`), which should prevent this. However:
|
|
28
|
+
|
|
29
|
+
1. **Legacy stale indexes**: Previous versions may have used `{table}_new` naming. If those indexes survive in the database, any new reconstruction attempt that creates a temp table will collide if SQLite internally reuses the `_new` suffix pattern.
|
|
30
|
+
|
|
31
|
+
2. **SQLite autoindex naming**: When SQLite creates a table with UNIQUE or PK constraints, it auto-creates indexes named `sqlite_autoindex_{table}_N`. After `ALTER TABLE temp RENAME TO real`, these autoindexes keep their original name. So a table created as `_declaro_tmp_database_routes_abc123` would get `sqlite_autoindex__declaro_tmp_database_routes_abc123_1`, which is fine. But if there's a stale `sqlite_autoindex_database_routes_new_1` from a legacy `database_routes_new` temp table, it collides.
|
|
32
|
+
|
|
33
|
+
3. **`_recover_orphaned_tmp_tables`** (migrations.py line 304) cleans up `_declaro_tmp_*` tables but may not clean up legacy `*_new` tables or their stale autoindexes.
|
|
34
|
+
|
|
35
|
+
## Suggested fixes
|
|
36
|
+
|
|
37
|
+
### Fix A: Drop stale autoindexes before reconstruction
|
|
38
|
+
|
|
39
|
+
In `execute_reconstruction_async()`, before creating the temp table, query `sqlite_master` for any `sqlite_autoindex_*_new_*` indexes referencing the target table and drop them:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# Before creating temp table, clean up stale autoindexes from legacy naming
|
|
43
|
+
cursor = await connection.execute(
|
|
44
|
+
"SELECT name FROM sqlite_master WHERE type='index' "
|
|
45
|
+
"AND name LIKE 'sqlite_autoindex_%_new_%'"
|
|
46
|
+
)
|
|
47
|
+
stale = await cursor.fetchall()
|
|
48
|
+
for (idx_name,) in stale:
|
|
49
|
+
await connection.execute(f'DROP INDEX IF EXISTS "{idx_name}"')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Fix B: Coalesce same-table operations in the applier
|
|
53
|
+
|
|
54
|
+
If the differ emits N `alter_column` ops for the same table, the applier should merge them into a single reconstruction pass. This is both safer (one reconstruction instead of N) and faster.
|
|
55
|
+
|
|
56
|
+
In `TursoApplier.apply()`, group operations by table before the execution loop:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# Group reconstruction ops by table, apply as single reconstruction
|
|
60
|
+
from itertools import groupby
|
|
61
|
+
# ... merge alter_column ops targeting same table into one combined op
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Fix C: Expand orphan recovery to handle legacy `_new` tables
|
|
65
|
+
|
|
66
|
+
In `_recover_orphaned_tmp_tables`, also look for `{table}_new` pattern tables and drop them along with their autoindexes.
|
|
67
|
+
|
|
68
|
+
## Affected versions
|
|
69
|
+
|
|
70
|
+
Observed in production with declaro_persistum installed from pip (version in multicardz .venv). The UUID temp naming was added to prevent this, but legacy databases still have stale artifacts.
|
|
71
|
+
|
|
72
|
+
## Impact
|
|
73
|
+
|
|
74
|
+
- Non-fatal: the second `alter_column` is skipped, and the migration reports partial success
|
|
75
|
+
- But the skipped operations mean schema drift accumulates — the same ops re-run on every deploy
|
|
76
|
+
- Combined with the migration hash not being pushed to cloud (separate issue, fixed in multicardz), this caused 31 migration ops on every single deploy
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Bug Report: CDC/MVCC conflict breaks cloud sync
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
`TursoPool._initialize()` unconditionally sets `PRAGMA journal_mode = 'mvcc'`, which crashes when cloud sync (CDC replication) is active.
|
|
6
|
+
|
|
7
|
+
## Error
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
sync engine operation failed: database error: Parse error: CDC is not supported in MVCC mode
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Reproduction
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
pool = await ConnectionPool.turso(
|
|
17
|
+
"./data/central.db",
|
|
18
|
+
remote_url="libsql://mc-central-xxx.turso.io",
|
|
19
|
+
auth_token="eyJ...",
|
|
20
|
+
)
|
|
21
|
+
# Crashes during _initialize() because:
|
|
22
|
+
# 1. connect_async() opens with turso.aio.sync.connect() (CDC mode)
|
|
23
|
+
# 2. _initialize() runs PRAGMA journal_mode = 'mvcc'
|
|
24
|
+
# 3. MVCC conflicts with CDC → exception
|
|
25
|
+
# 4. The exception handler catches it, but the commit() on line 601
|
|
26
|
+
# triggers the sync engine which fails
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Root Cause
|
|
30
|
+
|
|
31
|
+
In `pool.py` `TursoPool._initialize()` (line ~584):
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
cur = await self._write_holder.conn.execute("PRAGMA journal_mode = 'mvcc'")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This runs regardless of whether `remote_url` is set. When cloud sync is active, the connection uses CDC replication, which is incompatible with MVCC journal mode.
|
|
38
|
+
|
|
39
|
+
## Suggested Fix
|
|
40
|
+
|
|
41
|
+
Skip the MVCC pragma when `self._remote_url` is set. CDC replication has its own journaling; MVCC is only useful for local-only connections.
|
|
42
|
+
|
|
43
|
+
Additionally, the `PRAGMA cache_size` and subsequent `commit()` may also interact badly with CDC — the maintainer should verify.
|
|
44
|
+
|
|
45
|
+
## Environment
|
|
46
|
+
|
|
47
|
+
- pyturso 0.5.1 (PyPI, pre-built wheels)
|
|
48
|
+
- Also reproduced with pyturso 0.6.0rc1 (git source)
|
|
49
|
+
- Turso Cloud URL format: `libsql://...turso.io`
|
|
50
|
+
- Deployed on Render (Linux x86_64)
|
|
51
|
+
|
|
52
|
+
## Impact
|
|
53
|
+
|
|
54
|
+
Blocks all cloud sync usage. Admin, public, and prod services cannot share a central Turso database.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: bug
|
|
3
|
+
priority: 1
|
|
4
|
+
reporter: downstream consumer
|
|
5
|
+
date: 2026-03-15
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Migration batch aborts on unsupported operation, preventing valid ADD COLUMN
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`apply_migrations_async()` correctly detects a new column but fails because it batches all 47 detected schema differences into a single transaction. One unsupported operation (e.g., `add_foreign_key` on pyturso/SQLite) causes `MigrationError`, which aborts the entire batch — including the `ADD COLUMN` that would have succeeded independently.
|
|
13
|
+
|
|
14
|
+
## Observed Behavior
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
INFO:declaro_persistum.migrations:Loading schema from .../project_tables.py
|
|
18
|
+
INFO:declaro_persistum.migrations:Found 47 schema differences
|
|
19
|
+
INFO:declaro_persistum.migrations: - add_index on table import_batches
|
|
20
|
+
INFO:declaro_persistum.migrations: - add_foreign_key on table comments
|
|
21
|
+
... (43 more index/FK/alter_column operations) ...
|
|
22
|
+
INFO:declaro_persistum.migrations: - add_column on table cards <-- needed
|
|
23
|
+
INFO:declaro_persistum.migrations: - add_index on table cards
|
|
24
|
+
...
|
|
25
|
+
ERROR: Failed to execute operation
|
|
26
|
+
declaro_persistum.exceptions.MigrationError: Failed to execute operation
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The pool factory catches the error and continues with the old schema. The `add_column` is never applied.
|
|
30
|
+
|
|
31
|
+
On subsequent pool creations, the same 47 differences are detected again, the same operation fails again, and the column is never added. The migration is stuck.
|
|
32
|
+
|
|
33
|
+
## Root Cause
|
|
34
|
+
|
|
35
|
+
1. The schema defines foreign keys and indexes that pyturso/SQLite cannot apply (e.g., `ADD FOREIGN KEY` is not supported in SQLite `ALTER TABLE`)
|
|
36
|
+
2. These unsupported operations accumulate as permanent "phantom" differences
|
|
37
|
+
3. Every migration attempt re-detects them and fails on them
|
|
38
|
+
4. Valid operations (like `ADD COLUMN`) are bundled in the same batch and never execute
|
|
39
|
+
|
|
40
|
+
## Expected Behavior
|
|
41
|
+
|
|
42
|
+
Any of these would fix it:
|
|
43
|
+
1. **Best**: Apply each operation independently — skip failures, continue with the rest, report which succeeded/failed
|
|
44
|
+
2. **Good**: Skip operations known to be unsupported on the current dialect before attempting them
|
|
45
|
+
3. **Acceptable**: Order operations so safe ones (`ADD COLUMN`, `CREATE TABLE`) run first, then attempt riskier ones (`ADD FOREIGN KEY`, `ALTER COLUMN`)
|
|
46
|
+
|
|
47
|
+
## Steps to Reproduce
|
|
48
|
+
|
|
49
|
+
1. Create a schema with tables that define foreign keys and indexes
|
|
50
|
+
2. Create a database using pyturso backend — some FK/index operations will silently fail or be ignored
|
|
51
|
+
3. Add a new nullable field to one of the models
|
|
52
|
+
4. Call `apply_migrations_async(pool, "sqlite", schema_path, expand_enums=True)`
|
|
53
|
+
5. Observe: 47 differences detected, batch fails on an FK operation, `ADD COLUMN` never applied
|
|
54
|
+
|
|
55
|
+
## Environment
|
|
56
|
+
|
|
57
|
+
- Database backend: pyturso (SQLite-compatible, no `ALTER TABLE ADD FOREIGN KEY` support)
|
|
58
|
+
- Migration call: `apply_migrations_async(pool, "sqlite", schema_path, expand_enums=True)`
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Bug: migrate-remote uses turso.aio.connect() which doesn't support remote URLs
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
`cmd_migrate_remote` at line 344 calls:
|
|
6
|
+
```python
|
|
7
|
+
conn = await turso.aio.connect(remote_url, auth_token=auth_token)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
But `turso.aio.connect()` only accepts a local file path — it has no `auth_token` parameter. The call fails with `Error: open: NotFound`.
|
|
11
|
+
|
|
12
|
+
## pyturso API
|
|
13
|
+
|
|
14
|
+
- `turso.aio.connect(database)` — local file only, no auth_token param
|
|
15
|
+
- `turso.aio.sync.connect(path, remote_url, auth_token=...)` — requires local path + remote URL
|
|
16
|
+
|
|
17
|
+
There is no direct-to-cloud-only connection in pyturso.
|
|
18
|
+
|
|
19
|
+
## Fix
|
|
20
|
+
|
|
21
|
+
Use `turso.aio.sync.connect()` with a temp local file:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import tempfile, os
|
|
25
|
+
|
|
26
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
27
|
+
local_path = os.path.join(tmpdir, "migrate_remote.db")
|
|
28
|
+
conn = await turso.aio.sync.connect(
|
|
29
|
+
local_path,
|
|
30
|
+
remote_url=remote_url,
|
|
31
|
+
auth_token=auth_token,
|
|
32
|
+
)
|
|
33
|
+
# pull from cloud to get current state
|
|
34
|
+
await conn.sync()
|
|
35
|
+
# ... introspect, diff, apply DDL ...
|
|
36
|
+
# push changes back to cloud
|
|
37
|
+
await conn.sync()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Reproduction
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
uv run declaro migrate-remote \
|
|
44
|
+
--remote "libsql://mc-central-adamzwasserman.aws-us-west-2.turso.io" \
|
|
45
|
+
--token "$CENTRAL_DB_TOKEN" \
|
|
46
|
+
--schema apps/shared/schema/central_tables.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Output: `Error: open: NotFound`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Bug: migrate-remote drops and recreates tables instead of altering them (DATA LOSS)
|
|
2
|
+
|
|
3
|
+
## Severity: CRITICAL — Production data loss
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
`declaro migrate-remote` emits `create_table` operations for tables that already exist on the cloud DB, instead of `add_column` or `alter_column` for the actual schema diff. This drops all rows in the affected tables.
|
|
8
|
+
|
|
9
|
+
## Reproduction
|
|
10
|
+
|
|
11
|
+
1. Run `migrate-remote` to create initial schema on cloud (6 tables created — correct)
|
|
12
|
+
2. Add one column (`invite_token`) to the `users` model in the schema file
|
|
13
|
+
3. Run `migrate-remote` again
|
|
14
|
+
|
|
15
|
+
### Expected
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Found 1 schema difference:
|
|
19
|
+
- add_column on users (invite_token)
|
|
20
|
+
Applied 1 operation to cloud DB
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Actual
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Found 6 schema differences:
|
|
27
|
+
- create_table on pricing_invites
|
|
28
|
+
- create_table on subscriptions
|
|
29
|
+
- create_table on users
|
|
30
|
+
- create_table on subscription_plans
|
|
31
|
+
- create_table on database_routes
|
|
32
|
+
- create_table on billing_events
|
|
33
|
+
|
|
34
|
+
Applied 6 operations to cloud DB
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
All 6 tables were dropped and recreated. All rows in all tables were lost.
|
|
38
|
+
|
|
39
|
+
## Impact
|
|
40
|
+
|
|
41
|
+
- All user records deleted
|
|
42
|
+
- All database_routes deleted (workspace provisioning broken)
|
|
43
|
+
- All pricing_invites deleted (invite links broken)
|
|
44
|
+
- All subscriptions and billing_events deleted
|
|
45
|
+
- Production login loop caused by missing user records
|
|
46
|
+
|
|
47
|
+
## Root Cause
|
|
48
|
+
|
|
49
|
+
The `cmd_migrate_remote` function connects via `turso.aio.sync.connect()` with a temporary local file. It pulls from cloud into the temp file, introspects, diffs, and applies. But the introspection found 0 tables in the temp file — meaning the pull from cloud did not bring the existing tables into the local replica.
|
|
50
|
+
|
|
51
|
+
Possible causes:
|
|
52
|
+
1. The pull into a fresh temp file doesn't actually sync the cloud state — the temp DB starts empty regardless of pull
|
|
53
|
+
2. The sync connection's pull is a no-op for a brand new local file (no replication frame to start from)
|
|
54
|
+
3. The introspection runs before the pull completes
|
|
55
|
+
|
|
56
|
+
Because the introspect sees 0 tables locally, the differ compares 0 existing tables vs 6 schema tables and emits 6 `create_table` operations. When these are pushed to cloud, the cloud's existing tables are replaced with empty ones.
|
|
57
|
+
|
|
58
|
+
## Suggested Fix
|
|
59
|
+
|
|
60
|
+
Before diffing, verify the introspected table count matches expectations. If the cloud DB should have tables but introspection finds 0, abort with an error rather than proceeding with `create_table` operations that will cause data loss.
|
|
61
|
+
|
|
62
|
+
Additionally, `create_table` on a table that already exists in the cloud should be rejected or converted to an `alter_table` diff. The applier should never silently drop a table that has rows.
|
|
63
|
+
|
|
64
|
+
## Workaround
|
|
65
|
+
|
|
66
|
+
None. Data is lost. Users, routes, invites, and subscriptions must be re-created manually or restored from backups.
|
|
67
|
+
|
|
68
|
+
## Command Used
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run declaro migrate-remote \
|
|
72
|
+
--remote "libsql://mc-central-adamzwasserman.aws-us-west-2.turso.io" \
|
|
73
|
+
--token "$CENTRAL_DB_TOKEN" \
|
|
74
|
+
--schema apps/shared/schema/central_tables.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Timeline
|
|
78
|
+
|
|
79
|
+
1. 00:27 UTC — First `migrate-remote` run: created 6 tables on cloud (correct, cloud was empty)
|
|
80
|
+
2. 00:32 UTC — App verified working: invite query returned 404 (table exists, token not found)
|
|
81
|
+
3. ~03:20 UTC — Added `invite_token` column to User schema
|
|
82
|
+
4. ~03:20 UTC — Second `migrate-remote` run: recreated all 6 tables (DATA LOSS)
|
|
83
|
+
5. ~03:25 UTC — Login loop: users table empty, database_routes empty, all provisioning fails
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Bug: Table reconstruction via embedded replica corrupts both local and cloud DBs
|
|
2
|
+
|
|
3
|
+
## Severity: CRITICAL — Tables silently destroyed, migration reports success
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
When `apply_migrations_async` detects `alter_column` differences on a Turso embedded replica, it reconstructs the table (create temp, copy data, drop original, rename temp). The DROP syncs to cloud but the CREATE/RENAME does not. After the push failure, the connection is "refreshed" from cloud, which now has the table dropped. Result: table is gone from both local and cloud, but the migration reports success.
|
|
8
|
+
|
|
9
|
+
## Reproduction
|
|
10
|
+
|
|
11
|
+
1. Create tables on Turso Cloud via `migrate-remote --init` (creates basic tables without NOT NULL constraints, indexes, or foreign keys)
|
|
12
|
+
2. Start an app that uses `ConnectionPool.turso()` with auto-migration enabled
|
|
13
|
+
3. Auto-migration detects `alter_column` differences (e.g., NOT NULL constraints missing from `--init` tables)
|
|
14
|
+
4. Migration reconstructs tables locally (drop + recreate with correct schema)
|
|
15
|
+
5. Push to cloud fails: `sync engine operation failed: database sync engine error: failed to execute sql: Error { message: "SQLite error: no such table: main.<table_name>", code: "SQLITE_UNKNOWN" }`
|
|
16
|
+
|
|
17
|
+
### Expected
|
|
18
|
+
|
|
19
|
+
Either:
|
|
20
|
+
- Migration detects that push will fail and skips destructive reconstruction, OR
|
|
21
|
+
- Reconstruction is atomic (drop + create sync together or not at all), OR
|
|
22
|
+
- Migration reports failure when push fails after reconstruction
|
|
23
|
+
|
|
24
|
+
### Actual
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
WARNING:declaro_persistum.pool:push after acquire_write commit failed
|
|
28
|
+
turso.lib.DatabaseError: sync engine operation failed: ...no such table: subscription_plans
|
|
29
|
+
INFO:declaro_persistum.pool:Write holder connection refreshed after migration
|
|
30
|
+
INFO:declaro_persistum.migrations:Successfully applied 21 migrations
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Migration reports "Successfully applied 21 migrations" but the tables no longer exist. Subsequent queries fail with `Parse error: no such table: users`.
|
|
34
|
+
|
|
35
|
+
The cloud DB also loses the tables — the DROP was synced but the CREATE was not.
|
|
36
|
+
|
|
37
|
+
## Root cause
|
|
38
|
+
|
|
39
|
+
Table reconstruction involves these steps:
|
|
40
|
+
1. `CREATE TABLE _declaro_tmp_<name>_<hash>` (new schema)
|
|
41
|
+
2. `INSERT INTO _declaro_tmp ... SELECT FROM <original>`
|
|
42
|
+
3. `DROP TABLE <original>`
|
|
43
|
+
4. `ALTER TABLE _declaro_tmp ... RENAME TO <original>`
|
|
44
|
+
5. `CREATE INDEX ...`
|
|
45
|
+
|
|
46
|
+
The Turso sync engine pushes step 3 (DROP) to cloud but fails on subsequent steps. The `push after acquire_write commit failed` error triggers a connection refresh, which pulls the now-dropped state from cloud, destroying the local table too.
|
|
47
|
+
|
|
48
|
+
## Compounding factor
|
|
49
|
+
|
|
50
|
+
This creates a vicious cycle:
|
|
51
|
+
1. `migrate-remote --init` creates basic tables in cloud
|
|
52
|
+
2. App migration sees 28 differences, reconstructs tables, push fails → tables destroyed
|
|
53
|
+
3. Next `migrate-remote --init` recreates them → app destroys them again on startup
|
|
54
|
+
|
|
55
|
+
## Environment
|
|
56
|
+
|
|
57
|
+
- declaro-persistum: installed via uv from GitHub
|
|
58
|
+
- pyturso (turso SDK)
|
|
59
|
+
- Turso Cloud embedded replica
|
|
60
|
+
|
|
61
|
+
## Suggested fixes (pick any)
|
|
62
|
+
|
|
63
|
+
1. **Wrap reconstruction in a savepoint** and roll back if push fails
|
|
64
|
+
2. **Don't push DDL through sync engine** — use direct HTTP for schema changes
|
|
65
|
+
3. **Detect embedded replica mode** and skip alter_column operations (log warning instead)
|
|
66
|
+
4. **Report failure accurately** — if push fails after DROP, don't report "Successfully applied"
|
|
67
|
+
5. **Make `migrate-remote --init` create full schema** (with NOT NULL, FKs, indexes) so embedded replicas see 0 differences
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Feature Request: Read connections should use sync driver without pull
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
When `TursoPool` has cloud sync enabled (`remote_url` set), the write holder connects via `turso.aio.sync.connect(path, remote_url, auth_token)`. Read connections from `acquire()` currently use `turso.aio.connect(path)` (plain local driver, no remote_url — changed in 106a4ae).
|
|
6
|
+
|
|
7
|
+
These two pyturso driver types don't share WAL state on the same file. Tables created by the sync write holder during migration are invisible to plain local reads. Result: `no such table` errors for tables that exist.
|
|
8
|
+
|
|
9
|
+
## Observed behavior
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# Write holder (sync driver) creates tables during migration — succeeds
|
|
13
|
+
# Read connection (plain driver) queries those tables — fails
|
|
14
|
+
turso.lib.DatabaseError: Parse error: no such table: pricing_invites
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Requested change
|
|
18
|
+
|
|
19
|
+
In `TursoPool.acquire()`, create the read connection holder with the same driver type as the write holder — `turso.aio.sync.connect(path, remote_url, auth_token)` — but **skip the pull**. This ensures both sides use the same driver and see the same local state.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
# Current (106a4ae):
|
|
23
|
+
holder = _TursoConnectionHolder(self._database_path) # plain driver
|
|
24
|
+
|
|
25
|
+
# Requested:
|
|
26
|
+
holder = _TursoConnectionHolder(self._database_path, self._remote_url, self._auth_token)
|
|
27
|
+
# But do NOT call holder.pull() — just connect and read local state
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This may require a flag on `_TursoConnectionHolder` or `connect_async()` to skip the automatic pull that `turso.aio.sync.connect()` may do on connection open. If `turso.aio.sync.connect()` doesn't pull automatically (only on explicit `pull()` call), then simply passing `remote_url` without calling `pull()` should be sufficient.
|
|
31
|
+
|
|
32
|
+
## Impact
|
|
33
|
+
|
|
34
|
+
Blocking: public app's invite page returns 503 on every request because `pricing_invites` table is invisible to reads.
|