lambda-erp 0.8.5__tar.gz → 0.8.7__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.
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/PKG-INFO +1 -1
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/documents.py +18 -6
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/services.py +45 -2
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/database.py +81 -1
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/pyproject.toml +1 -1
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/.gitignore +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/LICENSE +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/README.md +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/attachments.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/auth.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/bootstrap.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/chat.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/demo_limits.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/deps.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/errors.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/main.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/oauth.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/pdf.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/providers.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/remarks_md.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/accounting.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/admin.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/analytics.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/availability.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/masters.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/mcp.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/proposals.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/reports.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/routers/setup.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/templates/document.html +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/api/templates/proposal.html +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/docs/agents/README.md +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/frontend/LICENSE +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/frontend/README.md +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/assets/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/assets/asset.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/assets/reservation.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/model.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.8.5 → lambda_erp-0.8.7}/terraform/README.md +0 -0
|
@@ -28,7 +28,7 @@ _manager = Depends(require_role("manager"))
|
|
|
28
28
|
_LIST_RESERVED = {
|
|
29
29
|
"status", "party", "from_date", "to_date", "docstatus",
|
|
30
30
|
"include_discarded", "limit", "offset", "order_by", "order",
|
|
31
|
-
"date_field", "search", "search_fields",
|
|
31
|
+
"date_field", "search", "search_fields", "fields",
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
@@ -47,6 +47,7 @@ def list_docs(
|
|
|
47
47
|
date_field: str | None = None,
|
|
48
48
|
search: str | None = None,
|
|
49
49
|
search_fields: str | None = None,
|
|
50
|
+
fields: str | None = None,
|
|
50
51
|
limit: int = Query(default=50, le=500),
|
|
51
52
|
offset: int = Query(default=0, ge=0),
|
|
52
53
|
_user: dict = _viewer,
|
|
@@ -87,20 +88,31 @@ def list_docs(
|
|
|
87
88
|
# real columns, mirroring the arbitrary-filter / order_by contract.
|
|
88
89
|
if search:
|
|
89
90
|
filters["search"] = search
|
|
90
|
-
|
|
91
|
-
bad = [f for f in
|
|
91
|
+
sf = [f.strip() for f in (search_fields or "").split(",") if f.strip()]
|
|
92
|
+
bad = [f for f in sf if f not in columns]
|
|
92
93
|
if bad:
|
|
93
94
|
raise HTTPException(status_code=400, detail=f"Unknown search field(s): {', '.join(bad)}")
|
|
94
|
-
if
|
|
95
|
-
filters["search_fields"] =
|
|
95
|
+
if sf:
|
|
96
|
+
filters["search_fields"] = sf
|
|
96
97
|
|
|
97
98
|
if order_by is not None and order_by not in columns:
|
|
98
99
|
raise HTTPException(status_code=400, detail=f"Unknown order_by field: {order_by}")
|
|
99
100
|
if order.lower() not in ("asc", "desc"):
|
|
100
101
|
raise HTTPException(status_code=400, detail="order must be 'asc' or 'desc'")
|
|
101
102
|
|
|
103
|
+
# Column projection: the frontend sends the columns its list view actually
|
|
104
|
+
# renders, so a wide doctype doesn't ship every column for a few-column grid.
|
|
105
|
+
# Validated against real columns (name is always kept, added downstream).
|
|
106
|
+
projection = None
|
|
107
|
+
if fields:
|
|
108
|
+
projection = [f.strip() for f in fields.split(",") if f.strip()]
|
|
109
|
+
bad = [f for f in projection if f not in columns]
|
|
110
|
+
if bad:
|
|
111
|
+
raise HTTPException(status_code=400, detail=f"Unknown field(s): {', '.join(bad)}")
|
|
112
|
+
|
|
102
113
|
rows = list_documents(doctype_slug, filters=filters, limit=limit, offset=offset,
|
|
103
|
-
include_discarded=include_discarded, order_by=order_by, order=order
|
|
114
|
+
include_discarded=include_discarded, order_by=order_by, order=order,
|
|
115
|
+
fields=projection)
|
|
104
116
|
total = count_documents(doctype_slug, filters=filters, include_discarded=include_discarded)
|
|
105
117
|
return {"rows": rows, "total": total, "limit": limit, "offset": offset}
|
|
106
118
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Bridge between FastAPI request data and lambda_erp Document classes."""
|
|
2
2
|
|
|
3
|
+
import time
|
|
4
|
+
|
|
3
5
|
from lambda_erp.utils import _dict, now
|
|
4
|
-
from lambda_erp.database import get_db
|
|
6
|
+
from lambda_erp.database import get_db, get_write_generation
|
|
5
7
|
|
|
6
8
|
from lambda_erp.selling.quotation import (
|
|
7
9
|
Quotation, make_sales_order,
|
|
@@ -717,8 +719,10 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
717
719
|
else:
|
|
718
720
|
rows = db.get_all(
|
|
719
721
|
doctype,
|
|
722
|
+
# Project to the requested columns at the DB (not just *-then-trim) so a
|
|
723
|
+
# wide table doesn't ship every column for a few-column list view.
|
|
724
|
+
fields=(sorted(_projection_columns(doctype_slug, fields)) if fields else ["*"]),
|
|
720
725
|
filters=db_filters if db_filters else None,
|
|
721
|
-
fields=["*"],
|
|
722
726
|
order_by=order_clause,
|
|
723
727
|
limit=limit,
|
|
724
728
|
)
|
|
@@ -732,7 +736,46 @@ def list_documents(doctype_slug: str, filters: dict = None, limit: int = 50, off
|
|
|
732
736
|
return rows
|
|
733
737
|
|
|
734
738
|
|
|
739
|
+
# Exact list counts are cached per (doctype, filters). The count is the one O(N)
|
|
740
|
+
# part of a list load — a full scan even with the creation index, since the
|
|
741
|
+
# discard/search predicates aren't covered — and paging within a view repeats the
|
|
742
|
+
# identical count. The cache stays EXACT (unlike a reltuples estimate, which would
|
|
743
|
+
# make the page-jump "of N" bound approximate): every entry is tagged with the DB
|
|
744
|
+
# write-generation, so any in-process write invalidates it immediately; the TTL is
|
|
745
|
+
# only a backstop for cross-replica writes this process didn't see.
|
|
746
|
+
_COUNT_CACHE: dict = {}
|
|
747
|
+
_COUNT_CACHE_TTL = 60.0
|
|
748
|
+
_COUNT_CACHE_MAX = 512
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _count_cache_key(doctype_slug, filters, include_discarded):
|
|
752
|
+
items = tuple(sorted((k, str(v)) for k, v in (filters or {}).items()))
|
|
753
|
+
return (doctype_slug, items, bool(include_discarded))
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def invalidate_count_cache() -> None:
|
|
757
|
+
"""Clear the list-count cache (writes already invalidate via the write-
|
|
758
|
+
generation; call this only to force-drop cross-replica staleness early)."""
|
|
759
|
+
_COUNT_CACHE.clear()
|
|
760
|
+
|
|
761
|
+
|
|
735
762
|
def count_documents(doctype_slug: str, filters: dict = None, include_discarded: bool = False) -> int:
|
|
763
|
+
"""Exact count of documents matching the filters, cached per filter-set until
|
|
764
|
+
the next write (write-generation) or the TTL, whichever comes first."""
|
|
765
|
+
key = _count_cache_key(doctype_slug, filters, include_discarded)
|
|
766
|
+
gen = get_write_generation()
|
|
767
|
+
now_mono = time.monotonic()
|
|
768
|
+
hit = _COUNT_CACHE.get(key)
|
|
769
|
+
if hit is not None and hit[1] == gen and now_mono - hit[0] < _COUNT_CACHE_TTL:
|
|
770
|
+
return hit[2]
|
|
771
|
+
result = _count_documents_uncached(doctype_slug, filters, include_discarded)
|
|
772
|
+
if len(_COUNT_CACHE) >= _COUNT_CACHE_MAX:
|
|
773
|
+
_COUNT_CACHE.clear()
|
|
774
|
+
_COUNT_CACHE[key] = (now_mono, gen, result)
|
|
775
|
+
return result
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _count_documents_uncached(doctype_slug: str, filters: dict = None, include_discarded: bool = False) -> int:
|
|
736
779
|
"""Count documents matching the filters (ignores limit/offset)."""
|
|
737
780
|
doctype = SLUG_TO_DOCTYPE.get(doctype_slug)
|
|
738
781
|
if not doctype:
|
|
@@ -96,6 +96,28 @@ def _pg_row_factory(cursor):
|
|
|
96
96
|
return make
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
_write_generation = 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _stmt_is_read(sql: str) -> bool:
|
|
103
|
+
s = sql.lstrip().lower()
|
|
104
|
+
return s.startswith("select") and "for update" not in s and "for share" not in s
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _note_write(sql: str) -> None:
|
|
108
|
+
"""Bump the process write-generation on any non-SELECT statement. The list
|
|
109
|
+
count cache keys on it, so an in-process write invalidates cached counts at
|
|
110
|
+
once (cross-process staleness stays bounded by the cache TTL)."""
|
|
111
|
+
global _write_generation
|
|
112
|
+
if not _stmt_is_read(sql):
|
|
113
|
+
_write_generation += 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_write_generation() -> int:
|
|
117
|
+
"""Monotonic per-process write counter — see _note_write."""
|
|
118
|
+
return _write_generation
|
|
119
|
+
|
|
120
|
+
|
|
99
121
|
class _PgConn:
|
|
100
122
|
"""Adapts a psycopg (v3) connection to the sqlite3 connection API the code
|
|
101
123
|
uses. autocommit is off so the existing `_in_transaction` + single commit()
|
|
@@ -150,6 +172,7 @@ class _PgConn:
|
|
|
150
172
|
self._safe_rollback()
|
|
151
173
|
raise
|
|
152
174
|
self._maybe_release(sql)
|
|
175
|
+
_note_write(sql)
|
|
153
176
|
return cur
|
|
154
177
|
|
|
155
178
|
def executemany(self, sql, seq_params):
|
|
@@ -161,6 +184,7 @@ class _PgConn:
|
|
|
161
184
|
self._safe_rollback()
|
|
162
185
|
raise
|
|
163
186
|
self._dirty = True
|
|
187
|
+
_note_write(sql)
|
|
164
188
|
return cur
|
|
165
189
|
|
|
166
190
|
def _safe_rollback(self):
|
|
@@ -185,6 +209,20 @@ class _PgConn:
|
|
|
185
209
|
return self._raw.cursor(*args, **kwargs)
|
|
186
210
|
|
|
187
211
|
|
|
212
|
+
class _SqliteConn(sqlite3.Connection):
|
|
213
|
+
"""SQLite connection that bumps the write-generation like _PgConn does, so the
|
|
214
|
+
list count cache invalidates on writes under both backends (SQLite is a raw
|
|
215
|
+
connection where Postgres is wrapped)."""
|
|
216
|
+
|
|
217
|
+
def execute(self, sql, *args, **kwargs):
|
|
218
|
+
_note_write(sql)
|
|
219
|
+
return super().execute(sql, *args, **kwargs)
|
|
220
|
+
|
|
221
|
+
def executemany(self, sql, *args, **kwargs):
|
|
222
|
+
_note_write(sql)
|
|
223
|
+
return super().executemany(sql, *args, **kwargs)
|
|
224
|
+
|
|
225
|
+
|
|
188
226
|
class _NullLock:
|
|
189
227
|
"""No-op context manager used when SQLite-level locking is sufficient."""
|
|
190
228
|
|
|
@@ -240,7 +278,7 @@ class Database:
|
|
|
240
278
|
return conn
|
|
241
279
|
|
|
242
280
|
def _open_sqlite_conn(self):
|
|
243
|
-
conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
281
|
+
conn = sqlite3.connect(self.db_path, check_same_thread=False, factory=_SqliteConn)
|
|
244
282
|
conn.row_factory = sqlite3.Row
|
|
245
283
|
conn.execute(f"PRAGMA journal_mode={_journal_mode()}")
|
|
246
284
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
@@ -1457,6 +1495,7 @@ class Database:
|
|
|
1457
1495
|
self.conn.execute(self._ddl(stmt))
|
|
1458
1496
|
self.conn.commit()
|
|
1459
1497
|
self._migrate()
|
|
1498
|
+
self._ensure_list_indexes()
|
|
1460
1499
|
|
|
1461
1500
|
def _ddl(self, stmt: str) -> str:
|
|
1462
1501
|
"""Translate SQLite DDL to the active dialect. No-op for SQLite.
|
|
@@ -1605,6 +1644,47 @@ class Database:
|
|
|
1605
1644
|
# covers the happy path; migrations are only needed for drift.
|
|
1606
1645
|
self.conn.rollback()
|
|
1607
1646
|
|
|
1647
|
+
def _all_user_tables(self) -> list:
|
|
1648
|
+
"""Base tables in the active DB, minus internal bookkeeping."""
|
|
1649
|
+
if self.dialect == "postgres":
|
|
1650
|
+
rows = self.conn.execute(
|
|
1651
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
|
|
1652
|
+
).fetchall()
|
|
1653
|
+
else:
|
|
1654
|
+
rows = self.conn.execute(
|
|
1655
|
+
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
1656
|
+
).fetchall()
|
|
1657
|
+
return [
|
|
1658
|
+
r[0] for r in rows
|
|
1659
|
+
if not str(r[0]).startswith("_") and not str(r[0]).startswith("sqlite_")
|
|
1660
|
+
]
|
|
1661
|
+
|
|
1662
|
+
def _ensure_list_indexes(self) -> None:
|
|
1663
|
+
"""Index `creation` on every table that has it — the generic list-speed fix.
|
|
1664
|
+
|
|
1665
|
+
Every document/master list defaults to `ORDER BY creation DESC`, but tables
|
|
1666
|
+
ship with only a `name` primary key. So once a table grows large, each list
|
|
1667
|
+
load does a full sequential scan + top-N sort: the Leads page at 89K rows
|
|
1668
|
+
measured ~340ms; ~0.3ms with this index. Reconciled here (idempotent,
|
|
1669
|
+
CREATE INDEX IF NOT EXISTS) so it covers core, plugin AND future doctypes,
|
|
1670
|
+
and back-fills existing tables on the next deploy. `creation` is a
|
|
1671
|
+
fixed-width ISO string, so a plain btree sorts chronologically and the
|
|
1672
|
+
planner scans it backward for DESC. Best-effort per table — a failure on one
|
|
1673
|
+
(e.g. two replicas racing to build it at boot) never blocks startup.
|
|
1674
|
+
"""
|
|
1675
|
+
for table in self._all_user_tables():
|
|
1676
|
+
try:
|
|
1677
|
+
if "creation" not in self._get_table_columns(table):
|
|
1678
|
+
continue
|
|
1679
|
+
except Exception:
|
|
1680
|
+
continue
|
|
1681
|
+
idx = f"ix_{table.replace(' ', '_')}_creation"
|
|
1682
|
+
try:
|
|
1683
|
+
self.conn.execute(f'CREATE INDEX IF NOT EXISTS "{idx}" ON "{table}" ("creation")')
|
|
1684
|
+
self.conn.commit()
|
|
1685
|
+
except Exception:
|
|
1686
|
+
self.conn.rollback()
|
|
1687
|
+
|
|
1608
1688
|
# --- Core query interface (mirrors framework.db) ---
|
|
1609
1689
|
|
|
1610
1690
|
def sql(self, query, values=None, as_dict=True):
|
|
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
|
|
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
|