tina4-python 3.13.10__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.
- {tina4_python-3.13.10 → tina4_python-3.13.12}/PKG-INFO +1 -1
- {tina4_python-3.13.10 → tina4_python-3.13.12}/pyproject.toml +1 -1
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/__init__.py +1 -1
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/adapter.py +26 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/connection.py +27 -5
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/firebird.py +13 -4
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/mssql.py +15 -7
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/mysql.py +12 -3
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/postgres.py +64 -4
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/sqlite.py +9 -2
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/orm/fields.py +25 -1
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/orm/model.py +83 -10
- {tina4_python-3.13.10 → tina4_python-3.13.12}/.gitignore +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/README.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/router.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/core/server.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/env.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/js/tina4-dev-admin.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -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."""
|
|
@@ -421,7 +421,14 @@ class Database:
|
|
|
421
421
|
|
|
422
422
|
def fetch(self, sql: str, params: list = None,
|
|
423
423
|
limit: int = 100, offset: int = 0) -> DatabaseResult:
|
|
424
|
-
"""Fetch rows with pagination.
|
|
424
|
+
"""Fetch rows with pagination.
|
|
425
|
+
|
|
426
|
+
v3.13.11 (issue #49 Gap 3): mirror :meth:`execute` and capture
|
|
427
|
+
``last_error`` here too — pre-v3.13.11 a failed ``fetch`` left
|
|
428
|
+
``db.get_error()`` returning ``None`` even though the adapter
|
|
429
|
+
had stored the failure on its own ``last_error``. Callers had
|
|
430
|
+
no way to read the cause via the public API.
|
|
431
|
+
"""
|
|
425
432
|
if self._cache_enabled:
|
|
426
433
|
key = self._cache_key(sql + f":L{limit}:S{offset}", params)
|
|
427
434
|
cached = self._cache_get(key)
|
|
@@ -430,17 +437,28 @@ class Database:
|
|
|
430
437
|
self._cache_hits += 1
|
|
431
438
|
return cached
|
|
432
439
|
adapter = self._get_adapter()
|
|
433
|
-
|
|
440
|
+
try:
|
|
441
|
+
result = adapter.fetch(sql, params, limit, offset)
|
|
442
|
+
self.last_error = None
|
|
443
|
+
except Exception:
|
|
444
|
+
self.last_error = getattr(adapter, "last_error", None) or self.last_error
|
|
445
|
+
raise
|
|
434
446
|
self._cache_set(key, result)
|
|
435
447
|
with self._cache_lock:
|
|
436
448
|
self._cache_misses += 1
|
|
437
449
|
return result
|
|
438
450
|
adapter = self._get_adapter()
|
|
439
|
-
|
|
451
|
+
try:
|
|
452
|
+
result = adapter.fetch(sql, params, limit, offset)
|
|
453
|
+
self.last_error = None
|
|
454
|
+
return result
|
|
455
|
+
except Exception:
|
|
456
|
+
self.last_error = getattr(adapter, "last_error", None) or self.last_error
|
|
457
|
+
raise
|
|
440
458
|
|
|
441
459
|
def fetch_all(self, sql: str, params: list = None,
|
|
442
|
-
limit: int =
|
|
443
|
-
"""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.
|
|
444
462
|
|
|
445
463
|
Symmetric with ``fetch_one``. For the common case where you just
|
|
446
464
|
want the rows and don't need the ``DatabaseResult`` metadata
|
|
@@ -451,6 +469,10 @@ class Database:
|
|
|
451
469
|
for row in rows:
|
|
452
470
|
print(row["name"])
|
|
453
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
|
+
|
|
454
476
|
Returns ``[]`` (not ``None``) when no rows match.
|
|
455
477
|
"""
|
|
456
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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 [])
|
|
@@ -137,7 +137,16 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
137
137
|
raise
|
|
138
138
|
|
|
139
139
|
def _on_query_error(self, exc: Exception, sql: str, params=None):
|
|
140
|
-
"""Handle a failed query: log, store, auto-rollback if implicit.
|
|
140
|
+
"""Handle a failed query: log, store, auto-rollback if implicit.
|
|
141
|
+
|
|
142
|
+
v3.13.11 (issue #49 Gap 1): inside an explicit transaction the
|
|
143
|
+
auto-rollback step defers to the user — they may be running a
|
|
144
|
+
SAVEPOINT/retry pattern. The visibility half stays: the original
|
|
145
|
+
cause is still logged via ``Log.error``, and an additional
|
|
146
|
+
``Log.warning`` makes the context explicit so a tail-the-log
|
|
147
|
+
operator can spot the upstream cause that's about to be buried
|
|
148
|
+
by a downstream ``InFailedSqlTransaction`` cascade.
|
|
149
|
+
"""
|
|
141
150
|
# Lazy import — avoid circular import at module load and let the
|
|
142
151
|
# framework boot without Log if something exotic is happening.
|
|
143
152
|
try:
|
|
@@ -148,6 +157,15 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
148
157
|
sql=preview,
|
|
149
158
|
params=params,
|
|
150
159
|
)
|
|
160
|
+
# v3.13.11: extra in-txn marker so the original cause is
|
|
161
|
+
# easy to spot above the cascade line.
|
|
162
|
+
if self._in_transaction:
|
|
163
|
+
Log.warning(
|
|
164
|
+
"Above failure occurred inside an explicit transaction — "
|
|
165
|
+
"auto-rollback skipped (caller owns the transaction). "
|
|
166
|
+
"Subsequent queries on this connection will cascade with "
|
|
167
|
+
"'current transaction is aborted' until rollback() is called."
|
|
168
|
+
)
|
|
151
169
|
except Exception:
|
|
152
170
|
pass
|
|
153
171
|
|
|
@@ -166,6 +184,29 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
166
184
|
# Don't mask the original error if rollback itself fails.
|
|
167
185
|
pass
|
|
168
186
|
|
|
187
|
+
def _log_silent_probe_failure(self, exc: Exception, sql: str, params=None):
|
|
188
|
+
"""v3.13.11 (issue #49 Gap 1): log a probe-query failure as a
|
|
189
|
+
warning WITHOUT touching ``last_error`` or rolling back.
|
|
190
|
+
|
|
191
|
+
Used by the COUNT probe in :meth:`fetch` which is best-effort —
|
|
192
|
+
it's allowed to fail without surfacing to the caller — but inside
|
|
193
|
+
an explicit transaction the connection is now poisoned, the
|
|
194
|
+
paginated query that follows will cascade, and the real cause
|
|
195
|
+
used to be lost entirely. This logs it so an operator scrolling
|
|
196
|
+
back through the log can spot the upstream cause.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
from tina4_python.debug import Log
|
|
200
|
+
preview = sql.strip().splitlines()[0][:200] if sql else "<no sql>"
|
|
201
|
+
Log.warning(
|
|
202
|
+
f"PostgreSQL probe query failed (original cause, before "
|
|
203
|
+
f"any cascade): {exc.__class__.__name__}: {exc}",
|
|
204
|
+
sql=preview,
|
|
205
|
+
params=params,
|
|
206
|
+
)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
169
210
|
def connect(self, connection_string: str, username: str = "", password: str = "", **kwargs):
|
|
170
211
|
"""Connect to PostgreSQL.
|
|
171
212
|
|
|
@@ -265,6 +306,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
265
306
|
limit: int = 100, offset: int = 0) -> DatabaseResult:
|
|
266
307
|
import psycopg2.extras
|
|
267
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)
|
|
268
313
|
sql = self._translate_sql(sql)
|
|
269
314
|
|
|
270
315
|
# v3.13.8: heal first so the COUNT probe below doesn't open on a
|
|
@@ -280,8 +325,15 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
280
325
|
try:
|
|
281
326
|
self._safe_execute(cursor, count_sql, params)
|
|
282
327
|
total = cursor.fetchone()["cnt"]
|
|
283
|
-
except Exception:
|
|
328
|
+
except Exception as e:
|
|
284
329
|
total = 0
|
|
330
|
+
# v3.13.11 (issue #49 Gap 1): log the probe failure as a
|
|
331
|
+
# warning so the original cause appears in the log even
|
|
332
|
+
# though the probe itself swallows. Without this line, a
|
|
333
|
+
# failure inside an explicit transaction is invisible —
|
|
334
|
+
# the cascade message from the paginated query below is
|
|
335
|
+
# all the operator sees.
|
|
336
|
+
self._log_silent_probe_failure(e, count_sql, params)
|
|
285
337
|
# If the probe failed because the connection arrived (or
|
|
286
338
|
# became) aborted, roll back so the real pagination query
|
|
287
339
|
# below sees a clean txn. Without this we'd cascade.
|
|
@@ -294,8 +346,14 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
294
346
|
# Apply pagination — the real query, so use the error-handling
|
|
295
347
|
# wrapper. The count probe above is best-effort and stays on
|
|
296
348
|
# _safe_execute (a failed probe shouldn't taint last_error).
|
|
297
|
-
|
|
298
|
-
|
|
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]
|
|
299
357
|
self._exec_with_handling(cursor, paginated_sql, paginated_params)
|
|
300
358
|
rows = [self._decode_blobs(dict(row)) for row in cursor.fetchall()]
|
|
301
359
|
|
|
@@ -304,6 +362,8 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
|
304
362
|
def fetch_one(self, sql: str, params: list = None) -> dict | None:
|
|
305
363
|
import psycopg2.extras
|
|
306
364
|
|
|
365
|
+
# v3.13.12: see fetch() — trailing semicolons break the wrappers.
|
|
366
|
+
sql = self._strip_trailing_semicolons(sql)
|
|
307
367
|
sql = self._translate_sql(sql)
|
|
308
368
|
cursor = self._conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
309
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
|
-
|
|
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
|
|
@@ -51,12 +51,36 @@ class Field:
|
|
|
51
51
|
self.choices = choices
|
|
52
52
|
self.validator = validator # Custom callable(value) → raises ValueError
|
|
53
53
|
|
|
54
|
+
# ── v3.13.11 (issue #50): callable-default resolution ──────────────
|
|
55
|
+
#
|
|
56
|
+
# Pre-v3.13.11 a callable default (e.g. ``DateTimeField(default=lambda:
|
|
57
|
+
# datetime.now())``) reached the driver verbatim and blew up with
|
|
58
|
+
# ``can't adapt type 'function'``. Now we evaluate the callable at the
|
|
59
|
+
# point of use, so every instance gets a freshly computed value (per-row
|
|
60
|
+
# timestamps actually differ). Types are excluded — ``default=int`` is
|
|
61
|
+
# almost never intended to mean "call int()".
|
|
62
|
+
def _resolve_default(self):
|
|
63
|
+
d = self.default
|
|
64
|
+
if callable(d) and not isinstance(d, type):
|
|
65
|
+
return d()
|
|
66
|
+
return d
|
|
67
|
+
|
|
54
68
|
def validate(self, value):
|
|
55
69
|
"""Validate and coerce value to the field type."""
|
|
56
70
|
if value is None:
|
|
57
71
|
if self.required and self.default is None:
|
|
58
72
|
raise ValueError(f"Field '{self.name}' is required")
|
|
59
|
-
return self.
|
|
73
|
+
return self._resolve_default()
|
|
74
|
+
|
|
75
|
+
# v3.13.11 (issue #50): if the user stored a callable as the field
|
|
76
|
+
# value (e.g. ``default=lambda: datetime.now()`` reached us without
|
|
77
|
+
# being resolved on instance init), evaluate it now so the driver
|
|
78
|
+
# gets a real value, not the function object. Types are excluded —
|
|
79
|
+
# ``default=int`` is almost never intended to mean "call int()".
|
|
80
|
+
if callable(value) and not isinstance(value, type):
|
|
81
|
+
value = value()
|
|
82
|
+
if value is None:
|
|
83
|
+
return None
|
|
60
84
|
|
|
61
85
|
# Type coercion.
|
|
62
86
|
#
|
|
@@ -177,9 +177,14 @@ class ORM(metaclass=ORMMeta):
|
|
|
177
177
|
# Initialize relationship cache
|
|
178
178
|
self._rel_cache = {}
|
|
179
179
|
|
|
180
|
-
# Set defaults from field definitions
|
|
180
|
+
# Set defaults from field definitions.
|
|
181
|
+
# v3.13.11 (issue #50): callable defaults are evaluated *per
|
|
182
|
+
# instance* so per-row timestamps (e.g. ``default=lambda:
|
|
183
|
+
# datetime.now()``) actually differ. Pre-v3.13.11 the function
|
|
184
|
+
# object reached the driver verbatim and blew up with
|
|
185
|
+
# ``can't adapt type 'function'``.
|
|
181
186
|
for name, field in self._fields.items():
|
|
182
|
-
setattr(self, name, field.
|
|
187
|
+
setattr(self, name, field._resolve_default())
|
|
183
188
|
|
|
184
189
|
# Accept JSON string or dict
|
|
185
190
|
if isinstance(data, str):
|
|
@@ -304,10 +309,22 @@ class ORM(metaclass=ORMMeta):
|
|
|
304
309
|
# ── CRUD ────────────────────────────────────────────────────
|
|
305
310
|
|
|
306
311
|
def save(self) -> Self | bool:
|
|
307
|
-
"""Insert or update. Returns self on success, False on failure.
|
|
312
|
+
"""Insert or update. Returns self on success, False on failure.
|
|
313
|
+
|
|
314
|
+
v3.13.11 (issue #50): for non-auto-increment primary keys (e.g.
|
|
315
|
+
user-supplied string IDs like ``"GC-100"``), the decision between
|
|
316
|
+
INSERT and UPDATE is made on whether the row *exists*, not on
|
|
317
|
+
whether the PK is set. Pre-v3.13.11 a natural-key save() always
|
|
318
|
+
chose UPDATE — matched zero rows — and silently returned success
|
|
319
|
+
without inserting anything.
|
|
320
|
+
|
|
321
|
+
Auto-increment behaviour is unchanged: ``pk_value is None`` means
|
|
322
|
+
"new row, let the engine assign an ID" and we still INSERT.
|
|
323
|
+
"""
|
|
308
324
|
db = self._get_db()
|
|
309
325
|
pk = self._get_pk()
|
|
310
326
|
pk_value = getattr(self, pk, None)
|
|
327
|
+
pk_field = self._fields[pk]
|
|
311
328
|
table = self._get_table()
|
|
312
329
|
pk_db_col = self.field_mapping.get(pk, self._fields[pk].column)
|
|
313
330
|
|
|
@@ -316,22 +333,52 @@ class ORM(metaclass=ORMMeta):
|
|
|
316
333
|
if field.auto_increment and pk_value is None:
|
|
317
334
|
continue # Skip auto-increment on insert
|
|
318
335
|
value = getattr(self, name)
|
|
336
|
+
# v3.13.11 (issue #50): resolve callable values at write time
|
|
337
|
+
# too, in case the user set ``self.created_at = lambda: ...``
|
|
338
|
+
# directly. Defensive — Field.validate() already handles this
|
|
339
|
+
# for the normal __init__ + _populate paths.
|
|
340
|
+
if callable(value) and not isinstance(value, type):
|
|
341
|
+
value = value()
|
|
319
342
|
if value is not None or not field.auto_increment:
|
|
320
343
|
# Use field_mapping for the column name, fall back to field.column
|
|
321
344
|
db_col = self.field_mapping.get(name, field.column)
|
|
322
345
|
data[db_col] = value
|
|
323
346
|
|
|
347
|
+
# v3.13.11 (issue #50): pick INSERT vs UPDATE on row existence
|
|
348
|
+
# for non-auto-increment PKs. Auto-increment keeps the legacy
|
|
349
|
+
# behaviour (PK is None → INSERT, PK is set → UPDATE).
|
|
350
|
+
is_update = False
|
|
351
|
+
if pk_value is not None:
|
|
352
|
+
if pk_field.auto_increment:
|
|
353
|
+
is_update = True
|
|
354
|
+
else:
|
|
355
|
+
# Natural-key model — check if the row already exists.
|
|
356
|
+
# type(self).exists() handles soft-delete + scope filters
|
|
357
|
+
# the same way the rest of the ORM does.
|
|
358
|
+
try:
|
|
359
|
+
is_update = type(self).exists(pk_value)
|
|
360
|
+
except Exception:
|
|
361
|
+
# If we can't tell (e.g. table doesn't exist yet),
|
|
362
|
+
# fall back to INSERT — the user will see the real
|
|
363
|
+
# error from the driver instead of a silent no-op.
|
|
364
|
+
is_update = False
|
|
365
|
+
|
|
324
366
|
db.start_transaction()
|
|
325
367
|
try:
|
|
326
|
-
if
|
|
368
|
+
if is_update:
|
|
327
369
|
update_data = {k: v for k, v in data.items() if k != pk_db_col}
|
|
328
370
|
if update_data:
|
|
329
371
|
db.update(table, update_data, f"{pk_db_col} = ?", [pk_value])
|
|
330
372
|
else:
|
|
331
373
|
db.insert(table, data)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
374
|
+
# Only adopt the engine-assigned ID for auto-increment PKs.
|
|
375
|
+
# Natural-key PKs were already set by the caller; don't
|
|
376
|
+
# overwrite them with the driver's last_id (which on PG
|
|
377
|
+
# may be a sequence value that doesn't apply here).
|
|
378
|
+
if pk_field.auto_increment:
|
|
379
|
+
last_id = db.get_last_id()
|
|
380
|
+
if last_id and pk in self._fields:
|
|
381
|
+
setattr(self, pk, last_id)
|
|
335
382
|
db.commit()
|
|
336
383
|
except Exception:
|
|
337
384
|
db.rollback()
|
|
@@ -673,10 +720,21 @@ class ORM(metaclass=ORMMeta):
|
|
|
673
720
|
StringField → VARCHAR(255)
|
|
674
721
|
TextField → TEXT
|
|
675
722
|
NumericField/FloatField → REAL
|
|
676
|
-
BooleanField →
|
|
723
|
+
BooleanField → engine-aware (see below)
|
|
677
724
|
DateTimeField → DATETIME
|
|
678
725
|
BlobField → BLOB
|
|
679
726
|
|
|
727
|
+
v3.13.11 BooleanField mapping (engine-aware):
|
|
728
|
+
SQLite → INTEGER (no native bool — historic convention)
|
|
729
|
+
PostgreSQL → BOOLEAN (native type; psycopg2 binds Python
|
|
730
|
+
``bool`` as BOOLEAN, so INTEGER columns
|
|
731
|
+
caused ``operator does not exist:
|
|
732
|
+
boolean = integer``)
|
|
733
|
+
MySQL → BOOLEAN (synonym for TINYINT(1))
|
|
734
|
+
MSSQL → BIT (the SQL Server bool type)
|
|
735
|
+
Firebird → INTEGER (driver round-trip is uneven; integer
|
|
736
|
+
is the safer choice on Firebird)
|
|
737
|
+
|
|
680
738
|
Auto-increment primary keys use engine-appropriate syntax.
|
|
681
739
|
Returns True on success.
|
|
682
740
|
"""
|
|
@@ -684,6 +742,21 @@ class ORM(metaclass=ORMMeta):
|
|
|
684
742
|
|
|
685
743
|
db = cls._get_db()
|
|
686
744
|
table = cls._get_table()
|
|
745
|
+
engine = (db.get_database_type() or "").lower()
|
|
746
|
+
|
|
747
|
+
# v3.13.11: BooleanField now uses each engine's native type
|
|
748
|
+
# where it's reliable. SQLite and Firebird stay on INTEGER —
|
|
749
|
+
# SQLite has no native bool, and Firebird's driver round-trip
|
|
750
|
+
# for native BOOLEAN is uneven across versions.
|
|
751
|
+
if engine == "postgres":
|
|
752
|
+
bool_sql = "BOOLEAN"
|
|
753
|
+
elif engine == "mysql":
|
|
754
|
+
bool_sql = "BOOLEAN" # MySQL alias for TINYINT(1)
|
|
755
|
+
elif engine == "mssql":
|
|
756
|
+
bool_sql = "BIT"
|
|
757
|
+
else:
|
|
758
|
+
# sqlite, firebird, odbc, anything else
|
|
759
|
+
bool_sql = "INTEGER"
|
|
687
760
|
|
|
688
761
|
# Don't recreate if table already exists
|
|
689
762
|
if db.table_exists(table):
|
|
@@ -706,7 +779,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
706
779
|
elif kind in ("NumericField", "FloatField"):
|
|
707
780
|
sql_type = "REAL"
|
|
708
781
|
elif kind == "BooleanField":
|
|
709
|
-
sql_type =
|
|
782
|
+
sql_type = bool_sql
|
|
710
783
|
elif kind == "DateTimeField":
|
|
711
784
|
sql_type = "DATETIME"
|
|
712
785
|
elif kind == "BlobField":
|
|
@@ -719,7 +792,7 @@ class ORM(metaclass=ORMMeta):
|
|
|
719
792
|
elif ft == float:
|
|
720
793
|
sql_type = "REAL"
|
|
721
794
|
elif ft == bool:
|
|
722
|
-
sql_type =
|
|
795
|
+
sql_type = bool_sql
|
|
723
796
|
elif ft == bytes:
|
|
724
797
|
sql_type = "BLOB"
|
|
725
798
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.10 → tina4_python-3.13.12}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|