tina4-python 3.13.53__tar.gz → 3.13.55__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 (158) hide show
  1. {tina4_python-3.13.53 → tina4_python-3.13.55}/PKG-INFO +1 -1
  2. {tina4_python-3.13.53 → tina4_python-3.13.55}/pyproject.toml +1 -1
  3. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/migration/runner.py +107 -47
  4. {tina4_python-3.13.53 → tina4_python-3.13.55}/.gitignore +0 -0
  5. {tina4_python-3.13.53 → tina4_python-3.13.55}/README.md +0 -0
  6. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/CLAUDE.md +0 -0
  7. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/HtmlElement.py +0 -0
  8. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/Testing.py +0 -0
  9. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/__init__.py +0 -0
  10. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/__init__.py +0 -0
  17. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/cache.py +0 -0
  18. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/constants.py +0 -0
  19. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/events.py +0 -0
  20. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/middleware.py +0 -0
  21. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/rate_limiter.py +0 -0
  22. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/request.py +0 -0
  23. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/response.py +0 -0
  24. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/router.py +0 -0
  25. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/core/server.py +0 -0
  26. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/crud/__init__.py +0 -0
  27. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/__init__.py +0 -0
  28. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/adapter.py +0 -0
  29. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/connection.py +0 -0
  30. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/firebird.py +0 -0
  31. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/mongodb.py +0 -0
  32. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/mssql.py +0 -0
  33. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/mysql.py +0 -0
  34. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/odbc.py +0 -0
  35. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/postgres.py +0 -0
  36. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/database/sqlite.py +0 -0
  37. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/debug/__init__.py +0 -0
  38. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/debug/error_overlay.py +0 -0
  39. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/dev_admin/__init__.py +0 -0
  40. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/dev_admin/metrics.py +0 -0
  41. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/dev_admin/plan.py +0 -0
  42. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/dev_admin/project_index.py +0 -0
  43. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/docs.py +0 -0
  44. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/docstore/__init__.py +0 -0
  45. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/dotenv/__init__.py +0 -0
  46. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/env.py +0 -0
  47. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/frond/FROND.md +0 -0
  48. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/frond/__init__.py +0 -0
  49. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/frond/engine.py +0 -0
  50. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/auth/meta.json +0 -0
  51. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  52. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/database/meta.json +0 -0
  53. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  54. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/error-overlay/meta.json +0 -0
  55. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  56. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/orm/meta.json +0 -0
  57. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  58. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  59. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/queue/meta.json +0 -0
  60. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  61. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/rest-api/meta.json +0 -0
  62. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  63. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/templates/meta.json +0 -0
  64. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  65. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  66. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/graphql/__init__.py +0 -0
  67. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/i18n/__init__.py +0 -0
  68. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/mcp/__init__.py +0 -0
  69. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/mcp/protocol.py +0 -0
  70. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/mcp/tools.py +0 -0
  71. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/messenger/__init__.py +0 -0
  72. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/migration/__init__.py +0 -0
  73. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/orm/__init__.py +0 -0
  74. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/orm/fields.py +0 -0
  75. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/orm/model.py +0 -0
  76. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/__feedback/widget.js +0 -0
  77. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/css/tina4.css +0 -0
  78. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/css/tina4.min.css +0 -0
  79. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/favicon.ico +0 -0
  80. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/images/logo.svg +0 -0
  81. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  82. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/js/frond.js +0 -0
  83. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/js/frond.min.js +0 -0
  84. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  85. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  86. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/js/tina4.min.js +0 -0
  87. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/js/tina4js.min.js +0 -0
  88. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/swagger/index.html +0 -0
  89. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  90. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/query_builder/__init__.py +0 -0
  91. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue/__init__.py +0 -0
  92. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue/job.py +0 -0
  93. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue/kafka_backend.py +0 -0
  94. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue/lite_backend.py +0 -0
  95. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue/mongo_backend.py +0 -0
  96. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue/rabbitmq_backend.py +0 -0
  97. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue_backends/__init__.py +0 -0
  98. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue_backends/kafka_backend.py +0 -0
  99. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue_backends/mongo_backend.py +0 -0
  100. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  101. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/__init__.py +0 -0
  102. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  103. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_badges.scss +0 -0
  104. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  105. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_cards.scss +0 -0
  106. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_forms.scss +0 -0
  107. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_grid.scss +0 -0
  108. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_modals.scss +0 -0
  109. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_nav.scss +0 -0
  110. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_reset.scss +0 -0
  111. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_tables.scss +0 -0
  112. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_typography.scss +0 -0
  113. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  114. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/_variables.scss +0 -0
  115. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/base.scss +0 -0
  116. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/colors.scss +0 -0
  117. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/scss/tina4css/tina4.scss +0 -0
  118. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/seeder/__init__.py +0 -0
  119. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/service/__init__.py +0 -0
  120. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/session/__init__.py +0 -0
  121. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/session_handlers/__init__.py +0 -0
  122. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  123. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/session_handlers/redis_handler.py +0 -0
  124. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/session_handlers/valkey_handler.py +0 -0
  125. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/swagger/__init__.py +0 -0
  126. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/components/crud.twig +0 -0
  127. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  128. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  129. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/docker/python/Dockerfile +0 -0
  130. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  131. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/302.twig +0 -0
  132. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/401.twig +0 -0
  133. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/403.twig +0 -0
  134. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/404.twig +0 -0
  135. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/500.twig +0 -0
  136. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/502.twig +0 -0
  137. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/503.twig +0 -0
  138. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/errors/base.twig +0 -0
  139. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/frontend/README.md +0 -0
  140. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/templates/readme.md +0 -0
  141. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/test/__init__.py +0 -0
  142. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/test_client/__init__.py +0 -0
  143. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  144. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  145. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  146. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  147. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  148. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  149. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  150. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  151. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  152. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  153. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/validator/__init__.py +0 -0
  156. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/websocket/__init__.py +0 -0
  157. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/websocket/backplane.py +0 -0
  158. {tina4_python-3.13.53 → tina4_python-3.13.55}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.53
3
+ Version: 3.13.55
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.53"
3
+ version = "3.13.55"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -87,7 +87,7 @@ def _create_v3_table(db) -> None:
87
87
  db.execute("""
88
88
  CREATE TABLE tina4_migration (
89
89
  id INTEGER NOT NULL PRIMARY KEY,
90
- migration_id VARCHAR(500) NOT NULL UNIQUE,
90
+ migration_name VARCHAR(500) NOT NULL UNIQUE,
91
91
  description VARCHAR(500),
92
92
  batch INTEGER DEFAULT 1 NOT NULL,
93
93
  executed_at VARCHAR(50) NOT NULL,
@@ -98,7 +98,7 @@ def _create_v3_table(db) -> None:
98
98
  # Engine-aware bookkeeping DDL. Each engine spells an auto-increment
99
99
  # integer PK differently (SQLite AUTOINCREMENT, PostgreSQL SERIAL, MySQL
100
100
  # AUTO_INCREMENT, MSSQL IDENTITY), and a TEXT column cannot carry a UNIQUE
101
- # constraint on MySQL -- so migration_id is VARCHAR. (SQLite gives VARCHAR
101
+ # constraint on MySQL -- so migration_name is VARCHAR. (SQLite gives VARCHAR
102
102
  # TEXT affinity, so this stays behaviour-identical there.) Mirrors the
103
103
  # engine-aware DDL in ORM.create_table; without it `migrate()` died with
104
104
  # "syntax error at AUTOINCREMENT" on PostgreSQL/MySQL/MSSQL.
@@ -111,7 +111,7 @@ def _create_v3_table(db) -> None:
111
111
  db.execute(f"""
112
112
  CREATE TABLE tina4_migration (
113
113
  {id_column},
114
- migration_id VARCHAR(500) NOT NULL UNIQUE,
114
+ migration_name VARCHAR(500) NOT NULL UNIQUE,
115
115
  description VARCHAR(500),
116
116
  batch INTEGER NOT NULL DEFAULT 1,
117
117
  executed_at VARCHAR(50) NOT NULL,
@@ -136,7 +136,7 @@ def _build_stem_map(migration_folder: str | None) -> list[str]:
136
136
  """Scan the migrations folder and return a list of file stems.
137
137
 
138
138
  Used by the v2→v3 backfill to match a v2 row's `description` to the
139
- actual file on disk, so the resulting `migration_id` matches what
139
+ actual file on disk, so the resulting `migration_name` matches what
140
140
  `migrate()` will compare against next.
141
141
  """
142
142
  if not migration_folder:
@@ -153,7 +153,7 @@ def _build_stem_map(migration_folder: str | None) -> list[str]:
153
153
  return stems
154
154
 
155
155
 
156
- def _resolve_migration_id(description: str, stems: list[str]) -> str:
156
+ def _resolve_migration_name(description: str, stems: list[str]) -> str:
157
157
  """Find the file stem that corresponds to a v2 description, or fall back.
158
158
 
159
159
  Match strategy, in order:
@@ -179,11 +179,11 @@ def _resolve_migration_id(description: str, stems: list[str]) -> str:
179
179
  def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
180
180
  """In-place upgrade of a Python-v2 tina4_migration table to v3.
181
181
 
182
- 1. ALTER TABLE ADD the v3 columns (migration_id, batch, executed_at)
182
+ 1. ALTER TABLE ADD the v3 columns (migration_name, batch, executed_at)
183
183
  alongside the existing v2 columns. v2 columns stay in place so a
184
184
  manual rollback path remains open; they are simply ignored from
185
185
  now on.
186
- 2. Backfill every v2 row's migration_id by matching `description`
186
+ 2. Backfill every v2 row's migration_name by matching `description`
187
187
  against the on-disk file stems. Falls back to the description
188
188
  verbatim when no file matches. All legacy rows get batch=1 and
189
189
  executed_at='legacy'.
@@ -192,15 +192,25 @@ def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
192
192
  "Detected legacy v2 tina4_migration schema — performing in-place upgrade to v3"
193
193
  )
194
194
 
195
+ # A v2 table has no id generator; on Firebird, ids for rows recorded after
196
+ # the upgrade come from GEN_TINA4_MIGRATION_ID (see _record_migration), so
197
+ # make sure it exists. Fresh v3 tables get it in _create_v3_table.
198
+ if _is_firebird(db):
199
+ try:
200
+ db.execute("CREATE GENERATOR GEN_TINA4_MIGRATION_ID")
201
+ db.commit()
202
+ except Exception:
203
+ pass # Generator may already exist
204
+
195
205
  cols = _column_names(db)
196
206
 
197
- if "migration_id" not in cols:
207
+ if "migration_name" not in cols:
198
208
  col_type = "VARCHAR(500)" if _is_firebird(db) else "TEXT"
199
209
  try:
200
- db.execute(f"ALTER TABLE tina4_migration ADD migration_id {col_type}")
210
+ db.execute(f"ALTER TABLE tina4_migration ADD migration_name {col_type}")
201
211
  db.commit()
202
212
  except Exception as e:
203
- logger.debug(f"v2→v3 upgrade: ALTER ADD migration_id skipped: {e}")
213
+ logger.debug(f"v2→v3 upgrade: ALTER ADD migration_name skipped: {e}")
204
214
 
205
215
  if "batch" not in cols:
206
216
  try:
@@ -224,7 +234,7 @@ def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
224
234
 
225
235
  try:
226
236
  rows = db.fetch(
227
- "SELECT description, passed FROM tina4_migration WHERE migration_id IS NULL",
237
+ "SELECT description, passed FROM tina4_migration WHERE migration_name IS NULL",
228
238
  limit=100000,
229
239
  ).records
230
240
  except Exception as e:
@@ -239,13 +249,13 @@ def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
239
249
  if not desc:
240
250
  continue
241
251
 
242
- migration_id = _resolve_migration_id(desc, stems)
252
+ migration_name = _resolve_migration_name(desc, stems)
243
253
 
244
254
  try:
245
255
  db.execute(
246
- "UPDATE tina4_migration SET migration_id = ?, batch = 1 "
247
- "WHERE description = ? AND migration_id IS NULL",
248
- [migration_id, desc],
256
+ "UPDATE tina4_migration SET migration_name = ?, batch = 1 "
257
+ "WHERE description = ? AND migration_name IS NULL",
258
+ [migration_name, desc],
249
259
  )
250
260
  db.commit()
251
261
  backfilled += 1
@@ -267,8 +277,8 @@ def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
267
277
  def _ensure_tracking_table(db, migration_folder: str | None = None):
268
278
  """Create or upgrade the migration tracking table.
269
279
 
270
- Handles v2→v3 upgrade: v2 tables have `description` but no `migration_id`.
271
- When detected, adds the missing v3 columns and backfills `migration_id`
280
+ Handles v2→v3 upgrade: v2 tables have `description` but no `migration_name`.
281
+ When detected, adds the missing v3 columns and backfills `migration_name`
272
282
  by matching `description` against the on-disk file stems so subsequent
273
283
  `migrate()` calls do not re-apply already-applied migrations.
274
284
 
@@ -279,8 +289,26 @@ def _ensure_tracking_table(db, migration_folder: str | None = None):
279
289
  return
280
290
 
281
291
  cols = _column_names(db)
292
+
293
+ # In-place rename for a table created by an older v3 (<= 3.13.54), which
294
+ # named the column `migration_id`. The canonical name is now
295
+ # `migration_name` (unified across all four frameworks). Add the new column
296
+ # and copy the values across so already-applied migrations are still seen as
297
+ # applied — without this the `migration_name`-based read below finds nothing
298
+ # and every migration re-runs. The old `migration_id` column is left in
299
+ # place (harmless, ignored from now on).
300
+ if "migration_id" in cols and "migration_name" not in cols:
301
+ col_type = "VARCHAR(500)" if _is_firebird(db) else "TEXT"
302
+ try:
303
+ db.execute(f"ALTER TABLE tina4_migration ADD migration_name {col_type}")
304
+ db.execute("UPDATE tina4_migration SET migration_name = migration_id")
305
+ db.commit()
306
+ cols = _column_names(db)
307
+ except Exception as e:
308
+ logger.debug(f"v3 migration_id->migration_name rename skipped: {e}")
309
+
282
310
  has_v3_shape = (
283
- "migration_id" in cols
311
+ "migration_name" in cols
284
312
  and "batch" in cols
285
313
  and "executed_at" in cols
286
314
  )
@@ -292,12 +320,12 @@ def _get_executed(db) -> set[str]:
292
320
  """Get set of already-executed migration IDs."""
293
321
  try:
294
322
  result = db.fetch(
295
- "SELECT migration_id FROM tina4_migration WHERE passed = 1",
323
+ "SELECT migration_name FROM tina4_migration WHERE passed = 1",
296
324
  limit=10000,
297
325
  )
298
- return {row["migration_id"] for row in result.records if row.get("migration_id")}
326
+ return {row["migration_name"] for row in result.records if row.get("migration_name")}
299
327
  except Exception:
300
- # Fallback for v2 tables where migration_id may not exist yet
328
+ # Fallback for v2 tables where migration_name may not exist yet
301
329
  result = db.fetch(
302
330
  "SELECT description FROM tina4_migration WHERE passed = 1",
303
331
  limit=10000,
@@ -331,6 +359,27 @@ def _normalize_quotes(sql: str) -> str:
331
359
  return _SMART_QUOTE_RE.sub(lambda m: _SMART_QUOTES[m.group(0)], sql)
332
360
 
333
361
 
362
+ _SET_TERM_RE = re.compile(r"^SET\s+TERM\s+(\S+)$", re.IGNORECASE)
363
+
364
+
365
+ def _parse_set_term(statement: str) -> str | None:
366
+ """Return the new terminator from a ``SET TERM <new> <current>`` directive.
367
+
368
+ ``SET TERM`` is a script-level directive (recognised by isql and other
369
+ InterBase/Firebird tooling, not run by the engine) that changes the
370
+ terminator separating statements. Recognising it lets a statement whose own
371
+ body contains the default ``;`` terminator — a trigger, stored procedure or
372
+ ``EXECUTE BLOCK`` — be kept intact rather than split on those inner ``;``.
373
+ The terminator may be more than one character (e.g. ``!!``).
374
+
375
+ :param statement: A single, already-stripped statement.
376
+ :returns: The new terminator, or ``None`` when *statement* is not a
377
+ ``SET TERM`` directive.
378
+ """
379
+ match = _SET_TERM_RE.match(statement.strip())
380
+ return match.group(1) if match else None
381
+
382
+
334
383
  def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
335
384
  """Split SQL into individual statements with a single-pass, quote- and
336
385
  comment-aware scanner.
@@ -356,6 +405,10 @@ def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
356
405
  a delimiter or comment, so it is preserved untouched.
357
406
  - The **delimiter** ends a statement only when reached outside all of the
358
407
  above. Empty statements are dropped; each kept statement is trimmed.
408
+ - A **SET TERM directive** (``SET TERM <new> <current>``) switches the active
409
+ terminator and is consumed, never emitted, so a statement whose own body
410
+ contains the default terminator survives as one. Multi-character
411
+ terminators (e.g. ``!!``) are supported.
359
412
 
360
413
  Smart/curly quotes are normalized to straight ASCII first. Mirrors the
361
414
  tina4-php ``Migration::splitStatements`` scanner for cross-framework parity.
@@ -367,7 +420,6 @@ def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
367
420
  statements: list[str] = []
368
421
  current: list[str] = []
369
422
  n = len(sql)
370
- dlen = len(delimiter)
371
423
  i = 0
372
424
  in_dollar_block = False
373
425
  in_slash_block = False
@@ -448,20 +500,28 @@ def _split_statements(sql: str, delimiter: str = ";") -> list[str]:
448
500
  i += 1
449
501
  continue
450
502
 
451
- # Statement delimiter — only reached outside blocks/comments/strings.
503
+ # Statement delimiter — only reached outside blocks/comments/strings. A
504
+ # SET TERM directive switches the active terminator and is consumed
505
+ # (never emitted); any other completed statement is collected.
452
506
  if delimiter and sql.startswith(delimiter, i):
507
+ i += len(delimiter)
453
508
  stmt = "".join(current).strip()
454
- if stmt:
455
- statements.append(stmt)
456
509
  current = []
457
- i += dlen
510
+ if stmt:
511
+ new_term = _parse_set_term(stmt)
512
+ if new_term is not None:
513
+ delimiter = new_term
514
+ else:
515
+ statements.append(stmt)
458
516
  continue
459
517
 
460
518
  current.append(ch)
461
519
  i += 1
462
520
 
521
+ # Trailing statement (may not end with a delimiter). A trailing SET TERM
522
+ # directive is a no-op — consume it, don't emit it.
463
523
  stmt = "".join(current).strip()
464
- if stmt:
524
+ if stmt and _parse_set_term(stmt) is None:
465
525
  statements.append(stmt)
466
526
  return statements
467
527
 
@@ -605,9 +665,9 @@ def _migrate(db, migration_folder: str = "migrations", delimiter: str = ";") ->
605
665
  )
606
666
 
607
667
  for mig_file in migration_files:
608
- migration_id = mig_file.stem # e.g., "000001_create_users_table"
668
+ migration_name = mig_file.stem # e.g., "000001_create_users_table"
609
669
 
610
- if migration_id in executed:
670
+ if migration_name in executed:
611
671
  continue
612
672
 
613
673
  try:
@@ -630,18 +690,18 @@ def _migrate(db, migration_folder: str = "migrations", delimiter: str = ";") ->
630
690
 
631
691
  # Record as passed
632
692
  now = datetime.now(timezone.utc).isoformat()
633
- desc = re.sub(r"^\d+_", "", migration_id, count=1).replace("_", " ")
693
+ desc = re.sub(r"^\d+_", "", migration_name, count=1).replace("_", " ")
634
694
  if _is_firebird(db):
635
695
  row = db.fetch_one("SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS next_id FROM RDB$DATABASE")
636
696
  next_id = row["next_id"] if row else 1
637
697
  db.execute(
638
- "INSERT INTO tina4_migration (id, migration_id, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, 1)",
639
- [next_id, migration_id, desc, batch, now],
698
+ "INSERT INTO tina4_migration (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, 1)",
699
+ [next_id, migration_name, desc, batch, now],
640
700
  )
641
701
  else:
642
702
  db.execute(
643
- "INSERT INTO tina4_migration (migration_id, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, 1)",
644
- [migration_id, desc, batch, now],
703
+ "INSERT INTO tina4_migration (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, 1)",
704
+ [migration_name, desc, batch, now],
645
705
  )
646
706
  db.commit()
647
707
  ran.append(mig_file.name)
@@ -671,7 +731,7 @@ def _rollback(db, migration_folder: str = "migrations", delimiter: str = ";") ->
671
731
 
672
732
  last_batch = row["max_batch"]
673
733
  result = db.fetch(
674
- "SELECT migration_id FROM tina4_migration WHERE batch = ? AND passed = 1 ORDER BY migration_id DESC",
734
+ "SELECT migration_name FROM tina4_migration WHERE batch = ? AND passed = 1 ORDER BY migration_name DESC",
675
735
  [last_batch],
676
736
  limit=10000,
677
737
  )
@@ -680,7 +740,7 @@ def _rollback(db, migration_folder: str = "migrations", delimiter: str = ";") ->
680
740
  rolled_back = []
681
741
 
682
742
  for migration in result.records:
683
- mid = migration["migration_id"]
743
+ mid = migration["migration_name"]
684
744
  py_file = folder / f"{mid}.py"
685
745
  down_file = folder / f"{mid}.down.sql"
686
746
 
@@ -703,7 +763,7 @@ def _rollback(db, migration_folder: str = "migrations", delimiter: str = ";") ->
703
763
  )
704
764
 
705
765
  db.execute(
706
- "DELETE FROM tina4_migration WHERE migration_id = ?",
766
+ "DELETE FROM tina4_migration WHERE migration_name = ?",
707
767
  [mid],
708
768
  )
709
769
  db.commit()
@@ -858,12 +918,12 @@ class Migration:
858
918
  row = self._db.fetch_one("SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS next_id FROM RDB$DATABASE")
859
919
  next_id = row["next_id"] if row else 1
860
920
  self._db.execute(
861
- "INSERT INTO tina4_migration (id, migration_id, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, ?)",
921
+ "INSERT INTO tina4_migration (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, ?)",
862
922
  [next_id, name, desc, batch, now, passed],
863
923
  )
864
924
  else:
865
925
  self._db.execute(
866
- "INSERT INTO tina4_migration (migration_id, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?)",
926
+ "INSERT INTO tina4_migration (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?)",
867
927
  [name, desc, batch, now, passed],
868
928
  )
869
929
 
@@ -874,7 +934,7 @@ class Migration:
874
934
  name: Migration ID to remove (stem of the filename).
875
935
  """
876
936
  self._db.execute(
877
- "DELETE FROM tina4_migration WHERE migration_id = ?",
937
+ "DELETE FROM tina4_migration WHERE migration_name = ?",
878
938
  [name],
879
939
  )
880
940
 
@@ -883,7 +943,7 @@ def _status(db, migration_folder: str = "migrations") -> dict:
883
943
  """Get migration status (internal implementation).
884
944
 
885
945
  Returns {"completed": [...], "pending": [...]}, where each entry is
886
- a dict with 'migration_id', 'description', and (for completed)
946
+ a dict with 'migration_name', 'description', and (for completed)
887
947
  'executed_at' and 'batch'.
888
948
  """
889
949
  _ensure_tracking_table(db, migration_folder)
@@ -902,12 +962,12 @@ def _status(db, migration_folder: str = "migrations") -> dict:
902
962
 
903
963
  # Build completed list from the database with execution metadata
904
964
  result = db.fetch(
905
- "SELECT migration_id, description, batch, executed_at FROM tina4_migration WHERE passed = 1 ORDER BY migration_id",
965
+ "SELECT migration_name, description, batch, executed_at FROM tina4_migration WHERE passed = 1 ORDER BY migration_name",
906
966
  limit=10000,
907
967
  )
908
968
  completed = [
909
969
  {
910
- "migration_id": row["migration_id"],
970
+ "migration_name": row["migration_name"],
911
971
  "description": row["description"],
912
972
  "batch": row["batch"],
913
973
  "executed_at": row["executed_at"],
@@ -918,11 +978,11 @@ def _status(db, migration_folder: str = "migrations") -> dict:
918
978
  # Build pending list from files not yet executed
919
979
  pending = []
920
980
  for sql_file in sql_files:
921
- migration_id = sql_file.stem
922
- if migration_id not in executed:
923
- desc = re.sub(r"^\d+_", "", migration_id, count=1).replace("_", " ")
981
+ migration_name = sql_file.stem
982
+ if migration_name not in executed:
983
+ desc = re.sub(r"^\d+_", "", migration_name, count=1).replace("_", " ")
924
984
  pending.append({
925
- "migration_id": migration_id,
985
+ "migration_name": migration_name,
926
986
  "description": desc,
927
987
  })
928
988
 
File without changes