storepy 4.4.2__tar.gz → 4.4.4__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.
- {storepy-4.4.2/src/storepy.egg-info → storepy-4.4.4}/PKG-INFO +26 -1
- {storepy-4.4.2 → storepy-4.4.4}/README.md +25 -0
- {storepy-4.4.2 → storepy-4.4.4}/pyproject.toml +1 -1
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/__init__.py +2 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/exec.py +9 -1
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/permission.py +15 -0
- {storepy-4.4.2 → storepy-4.4.4/src/storepy.egg-info}/PKG-INFO +26 -1
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_require_context.py +6 -6
- {storepy-4.4.2 → storepy-4.4.4}/LICENSE +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/setup.cfg +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/ask.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/ask_knowledge.md +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/bootstrap.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/cache.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/core.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/id.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/mutation.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/query.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/triggers.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/crud/write.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/datasource.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/ddl.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/executors/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/executors/_values.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/executors/mongo.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/executors/mysql.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/executors/postgres.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/executors/sqlite.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/feedback.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/introspect/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/introspect/mysql.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/introspect/postgres.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/introspect/sqlite.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/llm.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/load.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/local/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/local/handle.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/local/store.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/metadef.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/naming.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/resource/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/resource/providers/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/resource/providers/local.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/resource/providers/s3.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/scheduler/__init__.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/schema.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/scope.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/secure.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/sync.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/py_store/workflow.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/storepy.egg-info/SOURCES.txt +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/storepy.egg-info/dependency_links.txt +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/storepy.egg-info/requires.txt +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/src/storepy.egg-info/top_level.txt +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_ask.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_cache.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_coverage_margin.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_federation_e2e.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_feedback_flush.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_fns_binding.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_host_contract.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_host_paths.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_load.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_local_store.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_metadef.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_migration.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_mongo_executor.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_mongo_session.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_multi_datasource.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_parity_deny.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_parity_local.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_parity_scope.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_policy_isolation.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_profile.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_py_store.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_raw_sql_and_ddl.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_rbac_host.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_real_backends_e2e.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_resource.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_resource_e2e.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_resource_units.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_scenario_course_platform.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_scenario_manager_transaction.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_scheduler.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_session.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_sql_commit_and_registry.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_triggers.py +0 -0
- {storepy-4.4.2 → storepy-4.4.4}/tests/test_workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: storepy
|
|
3
|
-
Version: 4.4.
|
|
3
|
+
Version: 4.4.4
|
|
4
4
|
Summary: Multi-backend data layer for Python asyncio (MongoDB, MySQL, SQLite, PostgreSQL, local disk): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control
|
|
5
5
|
Author-email: leo <coen_ddt@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,6 +87,7 @@ from py_store import init, store
|
|
|
87
87
|
- [Feedback events](#feedback-events)
|
|
88
88
|
- [Schema reference](#schema-reference)
|
|
89
89
|
- [Transactions](#transaction-boundary)
|
|
90
|
+
- [Triggers](#triggers)
|
|
90
91
|
- [Transactional capabilities](#transactional-capabilities)
|
|
91
92
|
- [FAQ](#faq)
|
|
92
93
|
- [Related projects](#related-projects)
|
|
@@ -605,6 +606,30 @@ Computed columns live at the schema top level, `computes: { <key>: { type, fn |
|
|
|
605
606
|
- Read consistency: only multiple reads inside an explicit session share one transaction connection; reads outside a session do not open an extra transaction.
|
|
606
607
|
- **Cross-source writes (no session)**: a single write call touching ≥2 datasources **cannot be atomic**; it runs sequentially and emits one `non_atomic_write` feedback event (`code: nonAtomic`, with the source list) — degradation is allowed, silence is not. Converge writes onto a single source, or wrap them in `store.session()` (which fails closed on cross-source writes).
|
|
607
608
|
|
|
609
|
+
## Triggers
|
|
610
|
+
|
|
611
|
+
Declarative trigger chains on a schema: write events (`insert` / `update` / `remove`) are expanded by the Rust core at plan time into an ordered list of side-effect steps (`plan.triggers`) that the host runs in sequence inside the same atomic envelope as the source write. The declaration form (the `triggers` event keys plus the field table) and the placeholder grammar are defined by the "Triggers" section of the rust-store README; the core's expanded output is byte-for-byte identical across hosts (golden guard: `rust-store/fixtures/triggers/cases.json`).
|
|
612
|
+
|
|
613
|
+
**Callback wiring** (`py_store/crud/triggers.py`):
|
|
614
|
+
|
|
615
|
+
```python
|
|
616
|
+
async def _grant_points(args, ctx, host):
|
|
617
|
+
await host['store'].update('User', {'_id': args['userId']}, {'$inc': {'points': args['amount']}})
|
|
618
|
+
|
|
619
|
+
store.set_trigger_fn('grantPoints', _grant_points)
|
|
620
|
+
store.assert_trigger_fns_covered(defns) # startup: a declared fnRef without an implementation ⇒ ERR_TRIGGER_FN_MISSING
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Execution semantics**:
|
|
624
|
+
|
|
625
|
+
- **Hit test** — an `update` event first checks that the `onFields` values actually changed (a structural deep compare, no-op suppression — an unchanged value does not fire) → then the `when` guard (`eq/ne/gt/gte/lt/lte/in/and/or/not`); `insert` has no `before` and skips the field-level check; `remove` uses the archived (pre-delete) document as root. System fields (`createdAt` / `updatedAt` / `deletedAt`) are excluded from the trigger probe projection, so their changes never fire.
|
|
626
|
+
- **Placeholders** — `{{root.*}}` (post-change value) / `{{before.*}}` (pre-change value) / `{{now}}` undergo whole-value substitution only; embedding a placeholder inside a string raises `ERR_TRIGGER_PLACEHOLDER` — no silent drift.
|
|
627
|
+
- **De-duplication** — within one top-level call, `(step.name, _id)` runs at most once.
|
|
628
|
+
- **Callbacks** — `impl(args, ctx, {'store': store})`; `store.*` inside the callback hits the current transaction connection → the same transaction as the source write.
|
|
629
|
+
- **Scheduled (cron)** — the `schedule` event is enumerated by the host scheduler plugin (`py_store/scheduler/__init__.py`) and reuses the same trigger chain; only `{{now}}` is allowed as a placeholder.
|
|
630
|
+
|
|
631
|
+
**Boundaries**: single-source real transaction / a cross-source write declares `nonAtomic` / cross-source writes inside `store.session()` fail closed (see [Transaction boundary](#transaction-boundary)); **no cascading** (a trigger write does not fire further triggers); `update_many` does not support field-level triggers (explicitly refused, no silent degradation); the command step `op: "upsert"` is a registration-time `Err`.
|
|
632
|
+
|
|
608
633
|
## Transactional capabilities
|
|
609
634
|
|
|
610
635
|
Capabilities aimed at transactional workloads (orders, inventory — write contention plus
|
|
@@ -45,6 +45,7 @@ from py_store import init, store
|
|
|
45
45
|
- [Feedback events](#feedback-events)
|
|
46
46
|
- [Schema reference](#schema-reference)
|
|
47
47
|
- [Transactions](#transaction-boundary)
|
|
48
|
+
- [Triggers](#triggers)
|
|
48
49
|
- [Transactional capabilities](#transactional-capabilities)
|
|
49
50
|
- [FAQ](#faq)
|
|
50
51
|
- [Related projects](#related-projects)
|
|
@@ -563,6 +564,30 @@ Computed columns live at the schema top level, `computes: { <key>: { type, fn |
|
|
|
563
564
|
- Read consistency: only multiple reads inside an explicit session share one transaction connection; reads outside a session do not open an extra transaction.
|
|
564
565
|
- **Cross-source writes (no session)**: a single write call touching ≥2 datasources **cannot be atomic**; it runs sequentially and emits one `non_atomic_write` feedback event (`code: nonAtomic`, with the source list) — degradation is allowed, silence is not. Converge writes onto a single source, or wrap them in `store.session()` (which fails closed on cross-source writes).
|
|
565
566
|
|
|
567
|
+
## Triggers
|
|
568
|
+
|
|
569
|
+
Declarative trigger chains on a schema: write events (`insert` / `update` / `remove`) are expanded by the Rust core at plan time into an ordered list of side-effect steps (`plan.triggers`) that the host runs in sequence inside the same atomic envelope as the source write. The declaration form (the `triggers` event keys plus the field table) and the placeholder grammar are defined by the "Triggers" section of the rust-store README; the core's expanded output is byte-for-byte identical across hosts (golden guard: `rust-store/fixtures/triggers/cases.json`).
|
|
570
|
+
|
|
571
|
+
**Callback wiring** (`py_store/crud/triggers.py`):
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
async def _grant_points(args, ctx, host):
|
|
575
|
+
await host['store'].update('User', {'_id': args['userId']}, {'$inc': {'points': args['amount']}})
|
|
576
|
+
|
|
577
|
+
store.set_trigger_fn('grantPoints', _grant_points)
|
|
578
|
+
store.assert_trigger_fns_covered(defns) # startup: a declared fnRef without an implementation ⇒ ERR_TRIGGER_FN_MISSING
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Execution semantics**:
|
|
582
|
+
|
|
583
|
+
- **Hit test** — an `update` event first checks that the `onFields` values actually changed (a structural deep compare, no-op suppression — an unchanged value does not fire) → then the `when` guard (`eq/ne/gt/gte/lt/lte/in/and/or/not`); `insert` has no `before` and skips the field-level check; `remove` uses the archived (pre-delete) document as root. System fields (`createdAt` / `updatedAt` / `deletedAt`) are excluded from the trigger probe projection, so their changes never fire.
|
|
584
|
+
- **Placeholders** — `{{root.*}}` (post-change value) / `{{before.*}}` (pre-change value) / `{{now}}` undergo whole-value substitution only; embedding a placeholder inside a string raises `ERR_TRIGGER_PLACEHOLDER` — no silent drift.
|
|
585
|
+
- **De-duplication** — within one top-level call, `(step.name, _id)` runs at most once.
|
|
586
|
+
- **Callbacks** — `impl(args, ctx, {'store': store})`; `store.*` inside the callback hits the current transaction connection → the same transaction as the source write.
|
|
587
|
+
- **Scheduled (cron)** — the `schedule` event is enumerated by the host scheduler plugin (`py_store/scheduler/__init__.py`) and reuses the same trigger chain; only `{{now}}` is allowed as a placeholder.
|
|
588
|
+
|
|
589
|
+
**Boundaries**: single-source real transaction / a cross-source write declares `nonAtomic` / cross-source writes inside `store.session()` fail closed (see [Transaction boundary](#transaction-boundary)); **no cascading** (a trigger write does not fire further triggers); `update_many` does not support field-level triggers (explicitly refused, no silent degradation); the command step `op: "upsert"` is a registration-time `Err`.
|
|
590
|
+
|
|
566
591
|
## Transactional capabilities
|
|
567
592
|
|
|
568
593
|
Capabilities aimed at transactional workloads (orders, inventory — write contention plus
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "storepy"
|
|
7
|
-
version = "4.4.
|
|
7
|
+
version = "4.4.4"
|
|
8
8
|
description = "Multi-backend data layer for Python asyncio (MongoDB, MySQL, SQLite, PostgreSQL, local disk): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -350,6 +350,8 @@ class Store:
|
|
|
350
350
|
run_as_internal = staticmethod(permission.run_as_internal)
|
|
351
351
|
# 自定义权限错误(实例可被 store.PermissionError 捕获)
|
|
352
352
|
PermissionError = permission.PermissionError
|
|
353
|
+
# 上下文缺失错误(实例可被 store.NoContextError 捕获;fail-secure 下未注入 ctx)
|
|
354
|
+
NoContextError = permission.NoContextError
|
|
353
355
|
# 原生 SQL 入口错误(实例可被 store.RawSqlError 捕获)
|
|
354
356
|
RawSqlError = datasource.RawSqlError
|
|
355
357
|
# 原生 Mongo 命令入口错误(实例可被 store.NativeCommandError 捕获)
|
|
@@ -19,7 +19,7 @@ from .. import datasource as _datasource
|
|
|
19
19
|
from ..executors.mongo import exec_mongo as _exec_mongo
|
|
20
20
|
from ..feedback import emit as _emit_feedback
|
|
21
21
|
from ..naming import _to_logical, _to_mongo
|
|
22
|
-
from ..permission import PermissionError, get_context
|
|
22
|
+
from ..permission import NoContextError, PermissionError, get_context
|
|
23
23
|
from ..schema import get as _schema_get
|
|
24
24
|
|
|
25
25
|
_PHASE1_IDS = re.compile(r'^\{\{phase1\.ids\}\}$')
|
|
@@ -35,6 +35,11 @@ _PERM_PREFIX = 'ERR_PERMISSION:'
|
|
|
35
35
|
# 反馈事件 `profile_blocked`(自动反馈原则:允许拦截,禁止静默)。
|
|
36
36
|
_PROFILE_PREFIX = 'ERR_TEXT2QUERY:'
|
|
37
37
|
|
|
38
|
+
# 上下文缺失识别:core fail-secure(require_context / secure_mode)下无 ctx 时统一携带
|
|
39
|
+
# `ERR_NO_CONTEXT:` 稳定前缀(见 core `command/mod.rs`),同上按前缀映射。命中即抛
|
|
40
|
+
# `NoContextError`(与 PermissionError 同档 403;前缀剥离)。
|
|
41
|
+
_NO_CONTEXT_PREFIX = 'ERR_NO_CONTEXT:'
|
|
42
|
+
|
|
38
43
|
# 从 core 文案 `... [$feature](功能收缩)` 中提取门禁项名;无 `[..]` 时留白(None),
|
|
39
44
|
# 不伪造 feature —— 缺值必须显式暴露(禁静默兜底)。
|
|
40
45
|
_FEATURE_RE = re.compile(r'\[(.+?)\]')
|
|
@@ -85,6 +90,7 @@ def _call(fn):
|
|
|
85
90
|
"""绑定层调用包装:
|
|
86
91
|
|
|
87
92
|
- 权限类错误(``ERR_PERMISSION:`` 前缀)→ ``PermissionError``
|
|
93
|
+
- 上下文缺失(``ERR_NO_CONTEXT:`` 前缀)→ ``NoContextError``(同档 403)
|
|
88
94
|
- 档位类错误(``ERR_TEXT2QUERY:`` 前缀)→ emit ``profile_blocked`` 反馈 + ``ProfileViolation``
|
|
89
95
|
|
|
90
96
|
按前缀映射而非具体文案(core 文案可自由调整,映射不随文案漂移而静默失效)。
|
|
@@ -96,6 +102,8 @@ def _call(fn):
|
|
|
96
102
|
msg = str(e)
|
|
97
103
|
if msg.startswith(_PERM_PREFIX):
|
|
98
104
|
raise PermissionError(msg[len(_PERM_PREFIX):]) from e
|
|
105
|
+
if msg.startswith(_NO_CONTEXT_PREFIX):
|
|
106
|
+
raise NoContextError(msg[len(_NO_CONTEXT_PREFIX):]) from e
|
|
99
107
|
if msg.startswith(_PROFILE_PREFIX):
|
|
100
108
|
detail = msg[len(_PROFILE_PREFIX):]
|
|
101
109
|
m = _FEATURE_RE.search(detail)
|
|
@@ -184,3 +184,18 @@ class PermissionError(Exception):
|
|
|
184
184
|
def __init__(self, message, status=403):
|
|
185
185
|
super().__init__(message)
|
|
186
186
|
self.status = status
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class NoContextError(Exception):
|
|
190
|
+
"""上下文缺失错误(fail-secure:require_context 开启 / secure_mode 下未注入 ctx)
|
|
191
|
+
|
|
192
|
+
与 PermissionError 同属权限上下文类(403):core 抛 ``ERR_NO_CONTEXT:`` 稳定前缀
|
|
193
|
+
(见 core ``command/mod.rs``),由 ``crud.exec._call`` 归一为本类型(前缀已剥离)。
|
|
194
|
+
``code`` 与 core machine code 对齐(``no_context``),供皮按枚举判定(禁按文案匹配)。
|
|
195
|
+
对齐 nodejs-store ``NoContextError``。
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, message, status=403):
|
|
199
|
+
super().__init__(message)
|
|
200
|
+
self.code = 'no_context'
|
|
201
|
+
self.status = status
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: storepy
|
|
3
|
-
Version: 4.4.
|
|
3
|
+
Version: 4.4.4
|
|
4
4
|
Summary: Multi-backend data layer for Python asyncio (MongoDB, MySQL, SQLite, PostgreSQL, local disk): pure JSON schemas, GQL tree queries compiled to a single native query, GROUP BY/HAVING aggregation, computed columns, soft-delete and role-based access control
|
|
5
5
|
Author-email: leo <coen_ddt@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -87,6 +87,7 @@ from py_store import init, store
|
|
|
87
87
|
- [Feedback events](#feedback-events)
|
|
88
88
|
- [Schema reference](#schema-reference)
|
|
89
89
|
- [Transactions](#transaction-boundary)
|
|
90
|
+
- [Triggers](#triggers)
|
|
90
91
|
- [Transactional capabilities](#transactional-capabilities)
|
|
91
92
|
- [FAQ](#faq)
|
|
92
93
|
- [Related projects](#related-projects)
|
|
@@ -605,6 +606,30 @@ Computed columns live at the schema top level, `computes: { <key>: { type, fn |
|
|
|
605
606
|
- Read consistency: only multiple reads inside an explicit session share one transaction connection; reads outside a session do not open an extra transaction.
|
|
606
607
|
- **Cross-source writes (no session)**: a single write call touching ≥2 datasources **cannot be atomic**; it runs sequentially and emits one `non_atomic_write` feedback event (`code: nonAtomic`, with the source list) — degradation is allowed, silence is not. Converge writes onto a single source, or wrap them in `store.session()` (which fails closed on cross-source writes).
|
|
607
608
|
|
|
609
|
+
## Triggers
|
|
610
|
+
|
|
611
|
+
Declarative trigger chains on a schema: write events (`insert` / `update` / `remove`) are expanded by the Rust core at plan time into an ordered list of side-effect steps (`plan.triggers`) that the host runs in sequence inside the same atomic envelope as the source write. The declaration form (the `triggers` event keys plus the field table) and the placeholder grammar are defined by the "Triggers" section of the rust-store README; the core's expanded output is byte-for-byte identical across hosts (golden guard: `rust-store/fixtures/triggers/cases.json`).
|
|
612
|
+
|
|
613
|
+
**Callback wiring** (`py_store/crud/triggers.py`):
|
|
614
|
+
|
|
615
|
+
```python
|
|
616
|
+
async def _grant_points(args, ctx, host):
|
|
617
|
+
await host['store'].update('User', {'_id': args['userId']}, {'$inc': {'points': args['amount']}})
|
|
618
|
+
|
|
619
|
+
store.set_trigger_fn('grantPoints', _grant_points)
|
|
620
|
+
store.assert_trigger_fns_covered(defns) # startup: a declared fnRef without an implementation ⇒ ERR_TRIGGER_FN_MISSING
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Execution semantics**:
|
|
624
|
+
|
|
625
|
+
- **Hit test** — an `update` event first checks that the `onFields` values actually changed (a structural deep compare, no-op suppression — an unchanged value does not fire) → then the `when` guard (`eq/ne/gt/gte/lt/lte/in/and/or/not`); `insert` has no `before` and skips the field-level check; `remove` uses the archived (pre-delete) document as root. System fields (`createdAt` / `updatedAt` / `deletedAt`) are excluded from the trigger probe projection, so their changes never fire.
|
|
626
|
+
- **Placeholders** — `{{root.*}}` (post-change value) / `{{before.*}}` (pre-change value) / `{{now}}` undergo whole-value substitution only; embedding a placeholder inside a string raises `ERR_TRIGGER_PLACEHOLDER` — no silent drift.
|
|
627
|
+
- **De-duplication** — within one top-level call, `(step.name, _id)` runs at most once.
|
|
628
|
+
- **Callbacks** — `impl(args, ctx, {'store': store})`; `store.*` inside the callback hits the current transaction connection → the same transaction as the source write.
|
|
629
|
+
- **Scheduled (cron)** — the `schedule` event is enumerated by the host scheduler plugin (`py_store/scheduler/__init__.py`) and reuses the same trigger chain; only `{{now}}` is allowed as a placeholder.
|
|
630
|
+
|
|
631
|
+
**Boundaries**: single-source real transaction / a cross-source write declares `nonAtomic` / cross-source writes inside `store.session()` fail closed (see [Transaction boundary](#transaction-boundary)); **no cascading** (a trigger write does not fire further triggers); `update_many` does not support field-level triggers (explicitly refused, no silent degradation); the command step `op: "upsert"` is a registration-time `Err`.
|
|
632
|
+
|
|
608
633
|
## Transactional capabilities
|
|
609
634
|
|
|
610
635
|
Capabilities aimed at transactional workloads (orders, inventory — write contention plus
|
|
@@ -123,13 +123,13 @@ def test_require_context_on_blocks_without_ctx():
|
|
|
123
123
|
_mock([{'_id': '1', 'title': 'a'}])
|
|
124
124
|
_sc.set_require_context(True)
|
|
125
125
|
try:
|
|
126
|
-
with pytest.raises(
|
|
126
|
+
with pytest.raises(perm.NoContextError):
|
|
127
127
|
_run(_crud_mod.query('RcModel{title}'))
|
|
128
|
-
with pytest.raises(
|
|
128
|
+
with pytest.raises(perm.NoContextError):
|
|
129
129
|
_run(_crud_mod.insert('RcModel', {'title': 'x'}))
|
|
130
|
-
with pytest.raises(
|
|
130
|
+
with pytest.raises(perm.NoContextError):
|
|
131
131
|
_run(_crud_mod.update('RcModel', {'_id': '1'}, {'title': 'y'}))
|
|
132
|
-
with pytest.raises(
|
|
132
|
+
with pytest.raises(perm.NoContextError):
|
|
133
133
|
_run(_crud_mod.remove('RcModel', {'_id': '1'}))
|
|
134
134
|
finally:
|
|
135
135
|
_sc.set_require_context(False)
|
|
@@ -177,9 +177,9 @@ def test_secure_mode_flips_switches_blocks_ctxless():
|
|
|
177
177
|
assert secure.is_secure() is True
|
|
178
178
|
assert _sc.require_context() is True
|
|
179
179
|
# 开关1 require_context:无 ctx 读写拒绝
|
|
180
|
-
with pytest.raises(
|
|
180
|
+
with pytest.raises(perm.NoContextError):
|
|
181
181
|
_run(_crud_mod.query('RcModel{title}'))
|
|
182
|
-
with pytest.raises(
|
|
182
|
+
with pytest.raises(perm.NoContextError):
|
|
183
183
|
_run(_crud_mod.insert('RcModel', {'title': 'x'}))
|
|
184
184
|
# 开关3 meta closed:无 ctx 注册新定义拒绝
|
|
185
185
|
with pytest.raises(RuntimeError, match='ERR_PERMISSION'):
|
|
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
|