tina4-python 3.13.11__tar.gz → 3.13.12__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 (157) hide show
  1. {tina4_python-3.13.11 → tina4_python-3.13.12}/PKG-INFO +1 -1
  2. {tina4_python-3.13.11 → tina4_python-3.13.12}/pyproject.toml +1 -1
  3. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/__init__.py +1 -1
  4. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/adapter.py +26 -0
  5. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/connection.py +6 -2
  6. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/firebird.py +13 -4
  7. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/mssql.py +15 -7
  8. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/mysql.py +12 -3
  9. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/postgres.py +14 -2
  10. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/sqlite.py +9 -2
  11. {tina4_python-3.13.11 → tina4_python-3.13.12}/.gitignore +0 -0
  12. {tina4_python-3.13.11 → tina4_python-3.13.12}/README.md +0 -0
  13. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/CLAUDE.md +0 -0
  14. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/HtmlElement.py +0 -0
  15. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/Testing.py +0 -0
  16. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/ai/__init__.py +0 -0
  17. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/api/__init__.py +0 -0
  18. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/auth/__init__.py +0 -0
  19. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/cache/__init__.py +0 -0
  20. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/cli/__init__.py +0 -0
  21. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/container/__init__.py +0 -0
  22. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/__init__.py +0 -0
  23. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/cache.py +0 -0
  24. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/constants.py +0 -0
  25. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/events.py +0 -0
  26. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/middleware.py +0 -0
  27. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/rate_limiter.py +0 -0
  28. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/request.py +0 -0
  29. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/response.py +0 -0
  30. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/router.py +0 -0
  31. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/core/server.py +0 -0
  32. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/crud/__init__.py +0 -0
  33. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/__init__.py +0 -0
  34. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/mongodb.py +0 -0
  35. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/database/odbc.py +0 -0
  36. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/dev_admin/plan.py +0 -0
  41. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/dev_admin/project_index.py +0 -0
  42. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/docs.py +0 -0
  43. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/dotenv/__init__.py +0 -0
  44. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/env.py +0 -0
  45. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/frond/engine.py +0 -0
  48. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.11 → tina4_python-3.13.12}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.11 → tina4_python-3.13.12}/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.11
3
+ Version: 3.13.12
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.11"
3
+ version = "3.13.12"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.11"
11
+ __version__ = "3.13.12"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -349,6 +349,32 @@ class DatabaseAdapter:
349
349
  last_id=last_id,
350
350
  )
351
351
 
352
+ @staticmethod
353
+ def _strip_trailing_semicolons(sql: str) -> str:
354
+ """v3.13.12: normalize user-supplied SQL by stripping trailing
355
+ semicolons and whitespace.
356
+
357
+ ``fetch()`` and ``fetch_one()`` wrap the user's SQL — fetch()
358
+ builds a ``SELECT COUNT(*) FROM ({sql}) AS _count_subquery``
359
+ for the pagination probe and appends ``LIMIT/OFFSET`` to the
360
+ real query. A trailing ``;`` in the user's SQL breaks both:
361
+
362
+ user input: "SELECT * FROM users;"
363
+ wrapped: "SELECT COUNT(*) FROM (SELECT * FROM users;) ..." ← syntax error
364
+ paginated: "SELECT * FROM users; LIMIT 100 OFFSET 0" ← invalid
365
+
366
+ Stripping trailing semicolons before any wrapping is the
367
+ single-line defence. Internal semicolons (between meaningful
368
+ SQL statements) are left alone — the driver will reject those
369
+ if multi-statement isn't supported on the engine.
370
+ """
371
+ if not sql:
372
+ return sql
373
+ stripped = sql.rstrip()
374
+ while stripped.endswith(";"):
375
+ stripped = stripped[:-1].rstrip()
376
+ return stripped
377
+
352
378
  def fetch(self, sql: str, params: list = None,
353
379
  limit: int = 100, offset: int = 0) -> DatabaseResult:
354
380
  """Execute a read query and return multiple rows."""
@@ -457,8 +457,8 @@ class Database:
457
457
  raise
458
458
 
459
459
  def fetch_all(self, sql: str, params: list = None,
460
- limit: int = 100, offset: int = 0) -> list[dict]:
461
- """Fetch rows and return the records list directly.
460
+ limit: int = 0, offset: int = 0) -> list[dict]:
461
+ """Fetch ALL rows and return the records list directly.
462
462
 
463
463
  Symmetric with ``fetch_one``. For the common case where you just
464
464
  want the rows and don't need the ``DatabaseResult`` metadata
@@ -469,6 +469,10 @@ class Database:
469
469
  for row in rows:
470
470
  print(row["name"])
471
471
 
472
+ v3.13.12: default ``limit`` is **0** (no truncation) — the method
473
+ name says ``fetch_all``, so it returns all matching rows. Pre-v3.13.12
474
+ silently truncated to 100. Pass an explicit ``limit=N`` to cap.
475
+
472
476
  Returns ``[]`` (not ``None``) when no rows match.
473
477
  """
474
478
  return self.fetch(sql, params, limit, offset).records
@@ -283,6 +283,9 @@ class FirebirdAdapter(DatabaseAdapter):
283
283
 
284
284
  def fetch(self, sql: str, params: list = None,
285
285
  limit: int = 100, offset: int = 0) -> DatabaseResult:
286
+ # v3.13.12: strip trailing `;` — Firebird wraps with COUNT(*)
287
+ # and appends ROWS pagination; a trailing semicolon breaks both.
288
+ sql = self._strip_trailing_semicolons(sql)
286
289
  sql = self._translate_sql(sql)
287
290
  cursor = self._conn.cursor()
288
291
 
@@ -297,10 +300,15 @@ class FirebirdAdapter(DatabaseAdapter):
297
300
  # paginated query below regardless of whether count succeeded.
298
301
  cursor = self._conn.cursor()
299
302
 
300
- # Apply Firebird pagination — ROWS start TO end
301
- start = offset + 1
302
- end = offset + limit
303
- paginated_sql = f"{sql} ROWS {start} TO {end}"
303
+ # Apply Firebird pagination — ROWS start TO end.
304
+ # v3.13.12: limit <= 0 means "no pagination" (fetch_all's
305
+ # default give me ALL rows).
306
+ if limit is None or limit <= 0:
307
+ paginated_sql = sql
308
+ else:
309
+ start = offset + 1
310
+ end = offset + limit
311
+ paginated_sql = f"{sql} ROWS {start} TO {end}"
304
312
  cursor = self._safe_cursor_execute(cursor, paginated_sql, params)
305
313
 
306
314
  desc = cursor.description
@@ -310,6 +318,7 @@ class FirebirdAdapter(DatabaseAdapter):
310
318
  return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
311
319
 
312
320
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
321
+ sql = self._strip_trailing_semicolons(sql)
313
322
  sql = self._translate_sql(sql)
314
323
  cursor = self._conn.cursor()
315
324
  cursor = self._safe_cursor_execute(cursor, sql, params)
@@ -105,6 +105,8 @@ class MSSQLAdapter(DatabaseAdapter):
105
105
 
106
106
  def fetch(self, sql: str, params: list = None,
107
107
  limit: int = 100, offset: int = 0) -> DatabaseResult:
108
+ # v3.13.12: strip trailing `;` — see DatabaseAdapter helper.
109
+ sql = self._strip_trailing_semicolons(sql)
108
110
  sql = self._translate_sql(sql)
109
111
  cursor = self._conn.cursor(as_dict=True)
110
112
 
@@ -116,20 +118,26 @@ class MSSQLAdapter(DatabaseAdapter):
116
118
  except Exception:
117
119
  total = 0
118
120
 
119
- # Apply pagination — MSSQL uses OFFSET/FETCH
120
- # This requires an ORDER BY; if none exists, add a default
121
- if not re.search(r"\bORDER\s+BY\b", sql, re.IGNORECASE):
122
- paginated_sql = f"{sql} ORDER BY (SELECT NULL) OFFSET %s ROWS FETCH NEXT %s ROWS ONLY"
121
+ # Apply pagination — MSSQL uses OFFSET/FETCH.
122
+ # v3.13.12: limit <= 0 means "no pagination" (fetch_all's
123
+ # default give me ALL rows).
124
+ if limit is None or limit <= 0:
125
+ paginated_sql = sql
126
+ paginated_params = tuple(params or [])
123
127
  else:
124
- paginated_sql = f"{sql} OFFSET %s ROWS FETCH NEXT %s ROWS ONLY"
125
-
126
- paginated_params = tuple(params or []) + (offset, limit)
128
+ # OFFSET/FETCH requires an ORDER BY; if none exists, add a default
129
+ if not re.search(r"\bORDER\s+BY\b", sql, re.IGNORECASE):
130
+ paginated_sql = f"{sql} ORDER BY (SELECT NULL) OFFSET %s ROWS FETCH NEXT %s ROWS ONLY"
131
+ else:
132
+ paginated_sql = f"{sql} OFFSET %s ROWS FETCH NEXT %s ROWS ONLY"
133
+ paginated_params = tuple(params or []) + (offset, limit)
127
134
  cursor.execute(paginated_sql, paginated_params)
128
135
  rows = [dict(row) for row in cursor.fetchall()]
129
136
 
130
137
  return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
131
138
 
132
139
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
140
+ sql = self._strip_trailing_semicolons(sql)
133
141
  sql = self._translate_sql(sql)
134
142
  cursor = self._conn.cursor(as_dict=True)
135
143
  cursor.execute(sql, tuple(params) if params else ())
@@ -94,6 +94,9 @@ class MySQLAdapter(DatabaseAdapter):
94
94
 
95
95
  def fetch(self, sql: str, params: list = None,
96
96
  limit: int = 100, offset: int = 0) -> DatabaseResult:
97
+ # v3.13.12: strip trailing `;` before wrapping with COUNT(*)
98
+ # and appending LIMIT/OFFSET — see DatabaseAdapter helper.
99
+ sql = self._strip_trailing_semicolons(sql)
97
100
  sql = self._translate_sql(sql)
98
101
  cursor = self._conn.cursor(dictionary=True)
99
102
 
@@ -105,15 +108,21 @@ class MySQLAdapter(DatabaseAdapter):
105
108
  except Exception:
106
109
  total = 0
107
110
 
108
- # Apply pagination
109
- paginated_sql = f"{sql} LIMIT %s OFFSET %s"
110
- paginated_params = (params or []) + [limit, offset]
111
+ # Apply pagination — v3.13.12: limit <= 0 means "no pagination"
112
+ # (fetch_all's default give me ALL rows).
113
+ if limit is None or limit <= 0:
114
+ paginated_sql = sql
115
+ paginated_params = params or []
116
+ else:
117
+ paginated_sql = f"{sql} LIMIT %s OFFSET %s"
118
+ paginated_params = (params or []) + [limit, offset]
111
119
  cursor.execute(paginated_sql, paginated_params)
112
120
  rows = [dict(row) for row in cursor.fetchall()]
113
121
 
114
122
  return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
115
123
 
116
124
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
125
+ sql = self._strip_trailing_semicolons(sql)
117
126
  sql = self._translate_sql(sql)
118
127
  cursor = self._conn.cursor(dictionary=True)
119
128
  cursor.execute(sql, params or [])
@@ -306,6 +306,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
306
306
  limit: int = 100, offset: int = 0) -> DatabaseResult:
307
307
  import psycopg2.extras
308
308
 
309
+ # v3.13.12: strip trailing `;` before the framework wraps with
310
+ # COUNT(*) and appends LIMIT/OFFSET — otherwise a user-supplied
311
+ # `"SELECT * FROM users;"` produces invalid wrapped SQL.
312
+ sql = self._strip_trailing_semicolons(sql)
309
313
  sql = self._translate_sql(sql)
310
314
 
311
315
  # v3.13.8: heal first so the COUNT probe below doesn't open on a
@@ -342,8 +346,14 @@ class PostgreSQLAdapter(DatabaseAdapter):
342
346
  # Apply pagination — the real query, so use the error-handling
343
347
  # wrapper. The count probe above is best-effort and stays on
344
348
  # _safe_execute (a failed probe shouldn't taint last_error).
345
- paginated_sql = f"{sql} LIMIT %s OFFSET %s"
346
- paginated_params = (params or []) + [limit, offset]
349
+ # v3.13.12: limit <= 0 means "no pagination" (fetch_all's
350
+ # default give me ALL rows, not a silent first-100 slice).
351
+ if limit is None or limit <= 0:
352
+ paginated_sql = sql
353
+ paginated_params = params or []
354
+ else:
355
+ paginated_sql = f"{sql} LIMIT %s OFFSET %s"
356
+ paginated_params = (params or []) + [limit, offset]
347
357
  self._exec_with_handling(cursor, paginated_sql, paginated_params)
348
358
  rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
349
359
 
@@ -352,6 +362,8 @@ class PostgreSQLAdapter(DatabaseAdapter):
352
362
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
353
363
  import psycopg2.extras
354
364
 
365
+ # v3.13.12: see fetch() — trailing semicolons break the wrappers.
366
+ sql = self._strip_trailing_semicolons(sql)
355
367
  sql = self._translate_sql(sql)
356
368
  cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
357
369
  self._exec_with_handling(cursor, sql, params)
@@ -108,6 +108,8 @@ class SQLiteAdapter(DatabaseAdapter):
108
108
 
109
109
  def fetch(self, sql: str, params: list = None,
110
110
  limit: int = 100, offset: int = 0) -> DatabaseResult:
111
+ # v3.13.12: strip trailing `;` before wrapping — see DatabaseAdapter.
112
+ sql = self._strip_trailing_semicolons(sql)
111
113
  # Count total rows (without LIMIT/OFFSET)
112
114
  count_sql = f"SELECT COUNT(*) as cnt FROM ({sql})"
113
115
  try:
@@ -115,8 +117,12 @@ class SQLiteAdapter(DatabaseAdapter):
115
117
  except Exception:
116
118
  total = 0
117
119
 
118
- # Apply pagination — skip if SQL already has LIMIT
119
- if "LIMIT" in sql.upper().split("--")[0]:
120
+ # Apply pagination — skip if SQL already has LIMIT, or if
121
+ # limit <= 0 (v3.13.12: fetch_all's "give me all rows" path).
122
+ if limit is None or limit <= 0:
123
+ paginated_sql = sql
124
+ paginated_params = params or []
125
+ elif "LIMIT" in sql.upper().split("--")[0]:
120
126
  paginated_sql = sql
121
127
  paginated_params = params or []
122
128
  else:
@@ -128,6 +134,7 @@ class SQLiteAdapter(DatabaseAdapter):
128
134
  return DatabaseResult(records=rows, count=total, limit=limit, offset=offset, sql=sql, adapter=self)
129
135
 
130
136
  def fetch_one(self, sql: str, params: list = None) -> dict | None:
137
+ sql = self._strip_trailing_semicolons(sql)
131
138
  cursor = self._conn.execute(sql, params or [])
132
139
  row = cursor.fetchone()
133
140
  return dict(row) if row else None
File without changes