tina4-python 3.13.72__tar.gz → 3.13.73__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 (168) hide show
  1. {tina4_python-3.13.72 → tina4_python-3.13.73}/PKG-INFO +1 -1
  2. {tina4_python-3.13.72 → tina4_python-3.13.73}/pyproject.toml +1 -1
  3. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/CLAUDE.md +1 -1
  4. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/migration/runner.py +32 -28
  5. {tina4_python-3.13.72 → tina4_python-3.13.73}/.gitignore +0 -0
  6. {tina4_python-3.13.72 → tina4_python-3.13.73}/README.md +0 -0
  7. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/HtmlElement.py +0 -0
  8. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/Testing.py +0 -0
  9. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/__init__.py +0 -0
  10. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/ai/__init__.py +0 -0
  11. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/api/__init__.py +0 -0
  12. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/auth/__init__.py +0 -0
  13. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/cache/__init__.py +0 -0
  14. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/cli/__init__.py +0 -0
  15. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/container/__init__.py +0 -0
  16. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/context/__init__.py +0 -0
  17. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/context/chunker.py +0 -0
  18. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/__init__.py +0 -0
  19. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/cache.py +0 -0
  20. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/constants.py +0 -0
  21. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/events.py +0 -0
  22. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/middleware.py +0 -0
  23. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/rate_limiter.py +0 -0
  24. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/request.py +0 -0
  25. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/response.py +0 -0
  26. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/router.py +0 -0
  27. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/core/server.py +0 -0
  28. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/crud/__init__.py +0 -0
  29. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/__init__.py +0 -0
  30. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/adapter.py +0 -0
  31. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/connection.py +0 -0
  32. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/firebird.py +0 -0
  33. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/mongodb.py +0 -0
  34. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/dev_admin/__init__.py +0 -0
  42. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/dev_admin/metrics.py +0 -0
  43. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/dev_admin/plan.py +0 -0
  44. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/dev_admin/project_index.py +0 -0
  45. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/docs.py +0 -0
  46. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/docstore/__init__.py +0 -0
  47. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/dotenv/__init__.py +0 -0
  48. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/env.py +0 -0
  49. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/frond/FROND.md +0 -0
  50. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/frond/__init__.py +0 -0
  51. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/frond/engine.py +0 -0
  52. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/auth/meta.json +0 -0
  53. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  54. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/database/meta.json +0 -0
  55. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  56. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/error-overlay/meta.json +0 -0
  57. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  58. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/orm/meta.json +0 -0
  59. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  60. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  61. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/queue/meta.json +0 -0
  62. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  63. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/rest-api/meta.json +0 -0
  64. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  65. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/templates/meta.json +0 -0
  66. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  67. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  68. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/graphql/__init__.py +0 -0
  69. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/i18n/__init__.py +0 -0
  70. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/mcp/__init__.py +0 -0
  71. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/mcp/protocol.py +0 -0
  72. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/mcp/tools.py +0 -0
  73. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/messenger/__init__.py +0 -0
  74. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/migration/__init__.py +0 -0
  75. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/orm/__init__.py +0 -0
  76. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/orm/fields.py +0 -0
  77. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/orm/model.py +0 -0
  78. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/__feedback/widget.js +0 -0
  79. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/css/tina4.css +0 -0
  80. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/css/tina4.min.css +0 -0
  81. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/favicon.ico +0 -0
  82. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/images/logo.svg +0 -0
  83. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  84. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/js/frond.js +0 -0
  85. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/js/frond.min.js +0 -0
  86. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  87. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  88. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/js/tina4.min.js +0 -0
  89. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/js/tina4js.min.js +0 -0
  90. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/swagger/index.html +0 -0
  91. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  92. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/query_builder/__init__.py +0 -0
  93. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue/__init__.py +0 -0
  94. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue/job.py +0 -0
  95. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue/kafka_backend.py +0 -0
  96. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue/lite_backend.py +0 -0
  97. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue/mongo_backend.py +0 -0
  98. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue/rabbitmq_backend.py +0 -0
  99. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue_backends/__init__.py +0 -0
  100. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue_backends/kafka_backend.py +0 -0
  101. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue_backends/mongo_backend.py +0 -0
  102. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  103. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/__init__.py +0 -0
  104. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/models/Attachment.py +0 -0
  105. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/models/Channel.py +0 -0
  106. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/models/ChannelMember.py +0 -0
  107. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/models/Message.py +0 -0
  108. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/models/Workspace.py +0 -0
  109. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/models/__init__.py +0 -0
  110. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/realtime/storage.py +0 -0
  111. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/__init__.py +0 -0
  112. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  113. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_badges.scss +0 -0
  114. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  115. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_cards.scss +0 -0
  116. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_forms.scss +0 -0
  117. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_grid.scss +0 -0
  118. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_modals.scss +0 -0
  119. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_nav.scss +0 -0
  120. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_reset.scss +0 -0
  121. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_tables.scss +0 -0
  122. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_typography.scss +0 -0
  123. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  124. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/_variables.scss +0 -0
  125. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/base.scss +0 -0
  126. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/colors.scss +0 -0
  127. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/scss/tina4css/tina4.scss +0 -0
  128. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/seeder/__init__.py +0 -0
  129. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/service/__init__.py +0 -0
  130. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/session/__init__.py +0 -0
  131. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/session_handlers/__init__.py +0 -0
  132. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  133. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/session_handlers/redis_handler.py +0 -0
  134. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/session_handlers/valkey_handler.py +0 -0
  135. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/swagger/__init__.py +0 -0
  136. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/components/crud.twig +0 -0
  137. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  138. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  139. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/docker/python/Dockerfile +0 -0
  140. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  141. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/302.twig +0 -0
  142. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/401.twig +0 -0
  143. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/403.twig +0 -0
  144. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/404.twig +0 -0
  145. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/500.twig +0 -0
  146. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/502.twig +0 -0
  147. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/503.twig +0 -0
  148. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/errors/base.twig +0 -0
  149. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/frontend/README.md +0 -0
  150. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/templates/readme.md +0 -0
  151. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/test/__init__.py +0 -0
  152. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/test_client/__init__.py +0 -0
  153. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  154. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  155. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  156. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  157. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  158. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  159. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  160. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  161. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  162. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  163. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  164. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  165. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/validator/__init__.py +0 -0
  166. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/websocket/__init__.py +0 -0
  167. {tina4_python-3.13.72 → tina4_python-3.13.73}/tina4_python/websocket/backplane.py +0 -0
  168. {tina4_python-3.13.72 → tina4_python-3.13.73}/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.72
3
+ Version: 3.13.73
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.72"
3
+ version = "3.13.73"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -961,7 +961,7 @@ uv run tina4python migrate
961
961
 
962
962
  - SQL files live in `migrations/` folder, named `NNNNNN_description.sql` (6-digit sequence)
963
963
  - Files are executed in **numeric-prefix order** (`9_` before `10_`) and split on the `;` delimiter. A file without a numeric/timestamp prefix logs a warning — its order is undefined
964
- - State is tracked (row-existence) in the `tina4_migration` table (auto-created per engine): a migration runs onceif a row for it exists, it is skipped. (A vestigial `passed` column exists for back-compat; only applied = `passed=1` rows are ever writtenfailures are never recorded as `passed=0`.)
964
+ - State is tracked in the `tina4_migration` table (auto-created per engine, canonical columns `id, migration_name VARCHAR(500) NOT NULL UNIQUE, description VARCHAR(500), batch INTEGER NOT NULL DEFAULT 1, executed_at VARCHAR(50) NOT NULL, passed INTEGER NOT NULL DEFAULT 1` identical across all four frameworks). A migration is **applied** when a row exists for it with `passed = 1` (the applied-read is `WHERE passed = 1`). `migrate()` writes **only `passed = 1` rows**: on a failure the file's transaction is rolled back and **no row is written** for it (it is NOT recorded as `passed=0`), and `migrate()` raises. The public `record_migration(name, batch, passed=...)` API can write a `passed=0` row; any `passed=0` row (including one carried over from a v2 table) is treated as **not applied**, so it is reported pending. A leftover `passed=0` row (a prior failure or a v2 carry-over) **re-applies cleanly**: on the next `migrate()` the success path deletes any existing row for that `migration_name` before writing the fresh `passed=1` row, so the migration runs again instead of colliding on the unique `migration_name` the bookkeeping table holds at most one row per migration with its latest state. The v2->v3 upgrade logs a warning naming any `passed=0` rows so you know which ones will re-run.
965
965
  - **Each migration FILE is wrapped in its own transaction**: on a failure the file rolls back and `migrate()` **raises** (it does not write `passed=0`, delete anything, or `sys.exit`). Already-applied files stay applied — fix the bad file and re-run. The explicit `tina4 migrate` CLI surfaces the raise as a non-zero exit; startup auto-migration logs it and the service still boots (see TINA4_AUTO_MIGRATE above).
966
966
  - **Atomicity caveat:** per-file transactions are truly atomic only on engines with **transactional DDL (PostgreSQL)**. MySQL, Firebird, and SQLite auto-commit DDL, so a multi-statement migration that fails midway on those engines leaves earlier statements applied — keep one logical change per file. CREATE TABLE / ALTER-ADD are made idempotent on Firebird/MSSQL (existence-checked) so a re-run doesn't error.
967
967
 
@@ -265,7 +265,7 @@ def _upgrade_v2_to_v3(db, migration_folder: str | None) -> None:
265
265
  logger.error(f"v2→v3 upgrade: failed to backfill {desc!r}: {e}")
266
266
 
267
267
  note = (
268
- f" ({failed_in_v2} were marked passed=0 in v2 review manually)"
268
+ f" ({failed_in_v2} were marked passed=0 in v2 and will re-apply on the next migrate)"
269
269
  if failed_in_v2
270
270
  else ""
271
271
  )
@@ -629,6 +629,32 @@ def _should_skip_create_table(db, stmt: str) -> str | None:
629
629
  return None
630
630
 
631
631
 
632
+ def _record_applied(db, name: str, desc: str, batch: int, passed: int = 1) -> None:
633
+ """Write (or overwrite) the tracking row for a migration.
634
+
635
+ Deletes any existing row for this migration_name first, so a leftover
636
+ passed=0 row (a prior failure, or a v2 carry-over) is superseded and the
637
+ migration re-applies cleanly instead of colliding on the UNIQUE
638
+ migration_name. DELETE+INSERT is portable across every engine (no UPSERT
639
+ dialect variance), leaving at most one row per migration_name with the
640
+ latest recorded state.
641
+ """
642
+ now = datetime.now(timezone.utc).isoformat()
643
+ db.execute("DELETE FROM tina4_migration WHERE migration_name = ?", [name])
644
+ if _is_firebird(db):
645
+ row = db.fetch_one("SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS next_id FROM RDB$DATABASE")
646
+ next_id = row["next_id"] if row else 1
647
+ db.execute(
648
+ "INSERT INTO tina4_migration (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, ?)",
649
+ [next_id, name, desc, batch, now, passed],
650
+ )
651
+ else:
652
+ db.execute(
653
+ "INSERT INTO tina4_migration (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?)",
654
+ [name, desc, batch, now, passed],
655
+ )
656
+
657
+
632
658
  def _migrate(db, migration_folder: str = "migrations", delimiter: str = ";") -> list[str]:
633
659
  """Run all pending migrations (internal implementation).
634
660
 
@@ -688,21 +714,11 @@ def _migrate(db, migration_folder: str = "migrations", delimiter: str = ";") ->
688
714
  if db.execute(stmt) is False:
689
715
  raise RuntimeError(f"Migration failed: {db.get_error() or stmt[:80]}")
690
716
 
691
- # Record as passed
692
- now = datetime.now(timezone.utc).isoformat()
717
+ # Record as passed — supersede any leftover row for this migration
718
+ # (a prior passed=0 failure or a v2 carry-over) so it re-applies
719
+ # cleanly instead of colliding on the UNIQUE migration_name.
693
720
  desc = re.sub(r"^\d+_", "", migration_name, count=1).replace("_", " ")
694
- if _is_firebird(db):
695
- row = db.fetch_one("SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS next_id FROM RDB$DATABASE")
696
- next_id = row["next_id"] if row else 1
697
- db.execute(
698
- "INSERT INTO tina4_migration (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, 1)",
699
- [next_id, migration_name, desc, batch, now],
700
- )
701
- else:
702
- db.execute(
703
- "INSERT INTO tina4_migration (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, 1)",
704
- [migration_name, desc, batch, now],
705
- )
721
+ _record_applied(db, migration_name, desc, batch, passed=1)
706
722
  db.commit()
707
723
  ran.append(mig_file.name)
708
724
 
@@ -912,20 +928,8 @@ class Migration:
912
928
  passed: 1 if the migration succeeded (default), 0 if it failed.
913
929
  """
914
930
  _ensure_tracking_table(self._db, self._dir)
915
- now = datetime.now(timezone.utc).isoformat()
916
931
  desc = re.sub(r"^\d+_", "", name, count=1).replace("_", " ")
917
- if _is_firebird(self._db):
918
- row = self._db.fetch_one("SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS next_id FROM RDB$DATABASE")
919
- next_id = row["next_id"] if row else 1
920
- self._db.execute(
921
- "INSERT INTO tina4_migration (id, migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?, ?)",
922
- [next_id, name, desc, batch, now, passed],
923
- )
924
- else:
925
- self._db.execute(
926
- "INSERT INTO tina4_migration (migration_name, description, batch, executed_at, passed) VALUES (?, ?, ?, ?, ?)",
927
- [name, desc, batch, now, passed],
928
- )
932
+ _record_applied(self._db, name, desc, batch, passed=passed)
929
933
 
930
934
  def remove_migration_record(self, name: str) -> None:
931
935
  """Delete a migration record from the tracking table by migration ID.
File without changes