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.
Files changed (205) hide show
  1. declaro_persistum-0.1.0/.gitignore +56 -0
  2. declaro_persistum-0.1.0/BUG_REPORT_autoindex_stale.md +76 -0
  3. declaro_persistum-0.1.0/BUG_REPORT_cdc_mvcc_conflict.md +54 -0
  4. declaro_persistum-0.1.0/BUG_REPORT_deep_link_migration.md +58 -0
  5. declaro_persistum-0.1.0/BUG_REPORT_migrate_remote_connect.md +49 -0
  6. declaro_persistum-0.1.0/BUG_REPORT_migrate_remote_data_loss.md +83 -0
  7. declaro_persistum-0.1.0/BUG_REPORT_reconstruction_sync_corruption.md +67 -0
  8. declaro_persistum-0.1.0/FEATURE_REQUEST_sync_reads.md +34 -0
  9. declaro_persistum-0.1.0/PKG-INFO +423 -0
  10. declaro_persistum-0.1.0/README.md +373 -0
  11. declaro_persistum-0.1.0/docs/BUGFIX-sqlite-applier-consistency.md +236 -0
  12. declaro_persistum-0.1.0/docs/BUGFIX-sqlite-table-reconstruction.md +115 -0
  13. declaro_persistum-0.1.0/docs/SYNC_MIGRATION_ARCHITECTURE_FIX.md +110 -0
  14. declaro_persistum-0.1.0/docs/architecture/005-check-constraint-emulation-v1.md +714 -0
  15. declaro_persistum-0.1.0/docs/architecture/006-check-constraint-emulation-impl-plan-v1.md +1266 -0
  16. declaro_persistum-0.1.0/docs/architecture/007-table-reconstruction-v1.md +880 -0
  17. declaro_persistum-0.1.0/docs/architecture/008-honest-code-audit-v1.md +392 -0
  18. declaro_persistum-0.1.0/docs/architecture/009-honest-code-audit-impl-plan-v1.md +479 -0
  19. declaro_persistum-0.1.0/docs/architecture/declaro_persistum_architecture.md +1741 -0
  20. declaro_persistum-0.1.0/docs/architecture/declaro_persistum_architecture_addendum.md +1499 -0
  21. declaro_persistum-0.1.0/docs/audit/001-2026-02-01-persistence-facade-audit-v1.md +233 -0
  22. declaro_persistum-0.1.0/docs/audit/002-2026-02-01-persistence-facade-fix-impl-plan-v1.md +408 -0
  23. declaro_persistum-0.1.0/docs/hooks.md +227 -0
  24. declaro_persistum-0.1.0/docs/implementation-plan-addendum.md +1417 -0
  25. declaro_persistum-0.1.0/docs/pyturso-integration.md +197 -0
  26. declaro_persistum-0.1.0/docs/standards/architecture-doc-guidelines.md +628 -0
  27. declaro_persistum-0.1.0/docs/standards/documentation-maintenance-process.md +597 -0
  28. declaro_persistum-0.1.0/docs/standards/generated-code-standards.md +795 -0
  29. declaro_persistum-0.1.0/docs/standards/implementation-plan-guidelines.md +255 -0
  30. declaro_persistum-0.1.0/docs/standards/performance-monitoring-standards.md +764 -0
  31. declaro_persistum-0.1.0/docs/standards/python-coding-standards.md +754 -0
  32. declaro_persistum-0.1.0/docs/table_reconstruction.md +253 -0
  33. declaro_persistum-0.1.0/docs/turso-cloud-sync.md +134 -0
  34. declaro_persistum-0.1.0/docs/usage.md +2068 -0
  35. declaro_persistum-0.1.0/examples/todo_app_django_style/README.md +72 -0
  36. declaro_persistum-0.1.0/examples/todo_app_django_style/app.py +281 -0
  37. declaro_persistum-0.1.0/examples/todo_app_django_style/db.py +307 -0
  38. declaro_persistum-0.1.0/examples/todo_app_django_style/schema/todos.toml +23 -0
  39. declaro_persistum-0.1.0/examples/todo_app_django_style/templates/db_select.html +237 -0
  40. declaro_persistum-0.1.0/examples/todo_app_django_style/templates/db_status.html +30 -0
  41. declaro_persistum-0.1.0/examples/todo_app_django_style/templates/index.html +252 -0
  42. declaro_persistum-0.1.0/examples/todo_app_django_style/templates/partials/todo_count.html +1 -0
  43. declaro_persistum-0.1.0/examples/todo_app_django_style/templates/partials/todo_item.html +17 -0
  44. declaro_persistum-0.1.0/examples/todo_app_native/README.md +84 -0
  45. declaro_persistum-0.1.0/examples/todo_app_native/app.py +300 -0
  46. declaro_persistum-0.1.0/examples/todo_app_native/db.py +307 -0
  47. declaro_persistum-0.1.0/examples/todo_app_native/schema/todos.toml +23 -0
  48. declaro_persistum-0.1.0/examples/todo_app_native/templates/db_select.html +237 -0
  49. declaro_persistum-0.1.0/examples/todo_app_native/templates/db_status.html +30 -0
  50. declaro_persistum-0.1.0/examples/todo_app_native/templates/index.html +254 -0
  51. declaro_persistum-0.1.0/examples/todo_app_native/templates/partials/todo_count.html +1 -0
  52. declaro_persistum-0.1.0/examples/todo_app_native/templates/partials/todo_item.html +17 -0
  53. declaro_persistum-0.1.0/examples/todo_app_prisma_style/README.md +94 -0
  54. declaro_persistum-0.1.0/examples/todo_app_prisma_style/app.py +283 -0
  55. declaro_persistum-0.1.0/examples/todo_app_prisma_style/db.py +307 -0
  56. declaro_persistum-0.1.0/examples/todo_app_prisma_style/schema/todos.toml +23 -0
  57. declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/db_select.html +237 -0
  58. declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/db_status.html +30 -0
  59. declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/index.html +252 -0
  60. declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/partials/todo_count.html +1 -0
  61. declaro_persistum-0.1.0/examples/todo_app_prisma_style/templates/partials/todo_item.html +17 -0
  62. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/README.md +80 -0
  63. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/app.py +297 -0
  64. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/db.py +307 -0
  65. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/schema/todos.toml +23 -0
  66. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/db_select.html +237 -0
  67. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/db_status.html +30 -0
  68. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/index.html +252 -0
  69. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/partials/todo_count.html +1 -0
  70. declaro_persistum-0.1.0/examples/todo_app_sqlalchemy/templates/partials/todo_item.html +17 -0
  71. declaro_persistum-0.1.0/feature-requests/implementation-plan-instrumented-pool-write-queue.md +167 -0
  72. declaro_persistum-0.1.0/feature-requests/instrumented-pool-and-write-queue.md +223 -0
  73. declaro_persistum-0.1.0/feature-requests/row-level-security-policies.md +172 -0
  74. declaro_persistum-0.1.0/pyproject.toml +133 -0
  75. declaro_persistum-0.1.0/src/declaro_persistum/__init__.py +119 -0
  76. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/__init__.py +219 -0
  77. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/arrays.py +240 -0
  78. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/check_compat.py +927 -0
  79. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/enums.py +280 -0
  80. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/hierarchy.py +349 -0
  81. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/maps.py +211 -0
  82. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/materialized_views.py +276 -0
  83. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/pragma_compat.py +498 -0
  84. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/ranges.py +194 -0
  85. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/reconstruction.py +494 -0
  86. declaro_persistum-0.1.0/src/declaro_persistum/abstractions/table_reconstruction.py +449 -0
  87. declaro_persistum-0.1.0/src/declaro_persistum/applier/__init__.py +10 -0
  88. declaro_persistum-0.1.0/src/declaro_persistum/applier/postgresql.py +598 -0
  89. declaro_persistum-0.1.0/src/declaro_persistum/applier/protocol.py +139 -0
  90. declaro_persistum-0.1.0/src/declaro_persistum/applier/shared.py +406 -0
  91. declaro_persistum-0.1.0/src/declaro_persistum/applier/sqlite.py +386 -0
  92. declaro_persistum-0.1.0/src/declaro_persistum/applier/turso.py +349 -0
  93. declaro_persistum-0.1.0/src/declaro_persistum/bulk_loader.py +256 -0
  94. declaro_persistum-0.1.0/src/declaro_persistum/cli/__init__.py +14 -0
  95. declaro_persistum-0.1.0/src/declaro_persistum/cli/commands.py +728 -0
  96. declaro_persistum-0.1.0/src/declaro_persistum/cli/main.py +376 -0
  97. declaro_persistum-0.1.0/src/declaro_persistum/compat/__init__.py +25 -0
  98. declaro_persistum-0.1.0/src/declaro_persistum/compat/sqlalchemy_shim.py +133 -0
  99. declaro_persistum-0.1.0/src/declaro_persistum/cutover.py +124 -0
  100. declaro_persistum-0.1.0/src/declaro_persistum/differ/__init__.py +28 -0
  101. declaro_persistum-0.1.0/src/declaro_persistum/differ/ambiguity.py +360 -0
  102. declaro_persistum-0.1.0/src/declaro_persistum/differ/core.py +510 -0
  103. declaro_persistum-0.1.0/src/declaro_persistum/differ/extended.py +312 -0
  104. declaro_persistum-0.1.0/src/declaro_persistum/differ/toposort.py +425 -0
  105. declaro_persistum-0.1.0/src/declaro_persistum/errors.py +46 -0
  106. declaro_persistum-0.1.0/src/declaro_persistum/exceptions.py +376 -0
  107. declaro_persistum-0.1.0/src/declaro_persistum/fk_ordering.py +196 -0
  108. declaro_persistum-0.1.0/src/declaro_persistum/functions/__init__.py +77 -0
  109. declaro_persistum-0.1.0/src/declaro_persistum/functions/aggregates.py +242 -0
  110. declaro_persistum-0.1.0/src/declaro_persistum/functions/scalars.py +366 -0
  111. declaro_persistum-0.1.0/src/declaro_persistum/functions/translations.py +131 -0
  112. declaro_persistum-0.1.0/src/declaro_persistum/inspector/__init__.py +10 -0
  113. declaro_persistum-0.1.0/src/declaro_persistum/inspector/postgresql.py +516 -0
  114. declaro_persistum-0.1.0/src/declaro_persistum/inspector/protocol.py +136 -0
  115. declaro_persistum-0.1.0/src/declaro_persistum/inspector/shared.py +240 -0
  116. declaro_persistum-0.1.0/src/declaro_persistum/inspector/sqlite.py +280 -0
  117. declaro_persistum-0.1.0/src/declaro_persistum/inspector/turso.py +217 -0
  118. declaro_persistum-0.1.0/src/declaro_persistum/instrumentation.py +188 -0
  119. declaro_persistum-0.1.0/src/declaro_persistum/loader.py +673 -0
  120. declaro_persistum-0.1.0/src/declaro_persistum/migrations.py +474 -0
  121. declaro_persistum-0.1.0/src/declaro_persistum/observability/__init__.py +45 -0
  122. declaro_persistum-0.1.0/src/declaro_persistum/observability/slow_queries.py +213 -0
  123. declaro_persistum-0.1.0/src/declaro_persistum/observability/timing.py +139 -0
  124. declaro_persistum-0.1.0/src/declaro_persistum/pool.py +1798 -0
  125. declaro_persistum-0.1.0/src/declaro_persistum/pydantic_loader.py +398 -0
  126. declaro_persistum-0.1.0/src/declaro_persistum/query/__init__.py +131 -0
  127. declaro_persistum-0.1.0/src/declaro_persistum/query/builder.py +367 -0
  128. declaro_persistum-0.1.0/src/declaro_persistum/query/delete.py +154 -0
  129. declaro_persistum-0.1.0/src/declaro_persistum/query/django_style.py +382 -0
  130. declaro_persistum-0.1.0/src/declaro_persistum/query/executor.py +459 -0
  131. declaro_persistum-0.1.0/src/declaro_persistum/query/hooks.py +127 -0
  132. declaro_persistum-0.1.0/src/declaro_persistum/query/insert.py +221 -0
  133. declaro_persistum-0.1.0/src/declaro_persistum/query/prisma_style.py +444 -0
  134. declaro_persistum-0.1.0/src/declaro_persistum/query/select.py +356 -0
  135. declaro_persistum-0.1.0/src/declaro_persistum/query/sqlalchemy.py +739 -0
  136. declaro_persistum-0.1.0/src/declaro_persistum/query/table.py +909 -0
  137. declaro_persistum-0.1.0/src/declaro_persistum/query/update.py +221 -0
  138. declaro_persistum-0.1.0/src/declaro_persistum/transfer.py +523 -0
  139. declaro_persistum-0.1.0/src/declaro_persistum/types.py +357 -0
  140. declaro_persistum-0.1.0/src/declaro_persistum/validator.py +303 -0
  141. declaro_persistum-0.1.0/src/declaro_persistum/write_queue.py +440 -0
  142. declaro_persistum-0.1.0/tests/__init__.py +1 -0
  143. declaro_persistum-0.1.0/tests/bdd/conftest.py +150 -0
  144. declaro_persistum-0.1.0/tests/bdd/factories/__init__.py +42 -0
  145. declaro_persistum-0.1.0/tests/bdd/factories/connection_factory.py +308 -0
  146. declaro_persistum-0.1.0/tests/bdd/factories/data_factory.py +235 -0
  147. declaro_persistum-0.1.0/tests/bdd/factories/schema_factory.py +348 -0
  148. declaro_persistum-0.1.0/tests/bdd/features/database/multi_backend.feature +56 -0
  149. declaro_persistum-0.1.0/tests/bdd/features/pragma_compat.feature +286 -0
  150. declaro_persistum-0.1.0/tests/bdd/features/schema/materialized_views.feature +176 -0
  151. declaro_persistum-0.1.0/tests/bdd/features/table_reconstruction.feature +458 -0
  152. declaro_persistum-0.1.0/tests/bdd/steps/__init__.py +3 -0
  153. declaro_persistum-0.1.0/tests/bdd/steps/common_steps.py +99 -0
  154. declaro_persistum-0.1.0/tests/bdd/steps/database_steps.py +299 -0
  155. declaro_persistum-0.1.0/tests/bdd/steps/test_pragma_compat_steps_simple.py +325 -0
  156. declaro_persistum-0.1.0/tests/bdd/steps/test_table_reconstruction_steps.py +1467 -0
  157. declaro_persistum-0.1.0/tests/bdd/test_multi_backend.py +13 -0
  158. declaro_persistum-0.1.0/tests/bdd/test_pragma_compat.py +15 -0
  159. declaro_persistum-0.1.0/tests/conftest.py +309 -0
  160. declaro_persistum-0.1.0/tests/integration/__init__.py +1 -0
  161. declaro_persistum-0.1.0/tests/integration/test_postgresql.py +205 -0
  162. declaro_persistum-0.1.0/tests/integration/test_sqlite.py +242 -0
  163. declaro_persistum-0.1.0/tests/integration/test_turso.py +228 -0
  164. declaro_persistum-0.1.0/tests/stress/conftest.py +62 -0
  165. declaro_persistum-0.1.0/tests/stress/test_concurrent.py +208 -0
  166. declaro_persistum-0.1.0/tests/stress/test_edge_cases.py +262 -0
  167. declaro_persistum-0.1.0/tests/stress/test_large_data.py +309 -0
  168. declaro_persistum-0.1.0/tests/stress/test_load.py +217 -0
  169. declaro_persistum-0.1.0/tests/unit/__init__.py +1 -0
  170. declaro_persistum-0.1.0/tests/unit/test_aggregates.py +248 -0
  171. declaro_persistum-0.1.0/tests/unit/test_arrays.py +253 -0
  172. declaro_persistum-0.1.0/tests/unit/test_check_compat_parser.py +244 -0
  173. declaro_persistum-0.1.0/tests/unit/test_check_compat_registry.py +261 -0
  174. declaro_persistum-0.1.0/tests/unit/test_check_compat_validator.py +376 -0
  175. declaro_persistum-0.1.0/tests/unit/test_differ.py +286 -0
  176. declaro_persistum-0.1.0/tests/unit/test_django_style.py +227 -0
  177. declaro_persistum-0.1.0/tests/unit/test_enums.py +242 -0
  178. declaro_persistum-0.1.0/tests/unit/test_hierarchy.py +234 -0
  179. declaro_persistum-0.1.0/tests/unit/test_hooks.py +372 -0
  180. declaro_persistum-0.1.0/tests/unit/test_instrumented_pool.py +207 -0
  181. declaro_persistum-0.1.0/tests/unit/test_loader.py +207 -0
  182. declaro_persistum-0.1.0/tests/unit/test_maps.py +247 -0
  183. declaro_persistum-0.1.0/tests/unit/test_materialized_views.py +465 -0
  184. declaro_persistum-0.1.0/tests/unit/test_migrations.py +296 -0
  185. declaro_persistum-0.1.0/tests/unit/test_pool.py +358 -0
  186. declaro_persistum-0.1.0/tests/unit/test_prisma_style.py +259 -0
  187. declaro_persistum-0.1.0/tests/unit/test_procedures.py +365 -0
  188. declaro_persistum-0.1.0/tests/unit/test_query_builder.py +575 -0
  189. declaro_persistum-0.1.0/tests/unit/test_query_expressions.py +471 -0
  190. declaro_persistum-0.1.0/tests/unit/test_ranges.py +192 -0
  191. declaro_persistum-0.1.0/tests/unit/test_reconstruction.py +592 -0
  192. declaro_persistum-0.1.0/tests/unit/test_scalars.py +362 -0
  193. declaro_persistum-0.1.0/tests/unit/test_slow_queries.py +269 -0
  194. declaro_persistum-0.1.0/tests/unit/test_sqlalchemy_compat.py +559 -0
  195. declaro_persistum-0.1.0/tests/unit/test_sqlite_applier_reconstruction.py +226 -0
  196. declaro_persistum-0.1.0/tests/unit/test_table_reconstruction.py +252 -0
  197. declaro_persistum-0.1.0/tests/unit/test_timing.py +260 -0
  198. declaro_persistum-0.1.0/tests/unit/test_toposort.py +203 -0
  199. declaro_persistum-0.1.0/tests/unit/test_translations.py +263 -0
  200. declaro_persistum-0.1.0/tests/unit/test_triggers.py +330 -0
  201. declaro_persistum-0.1.0/tests/unit/test_turso_check_emulation.py +162 -0
  202. declaro_persistum-0.1.0/tests/unit/test_types.py +201 -0
  203. declaro_persistum-0.1.0/tests/unit/test_views.py +776 -0
  204. declaro_persistum-0.1.0/tests/unit/test_write_queue.py +450 -0
  205. 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.