@lix-js/sdk 0.6.0-preview.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +38 -207
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +38 -71
  16. package/SKILL.md +0 -507
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -833
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -359
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/capabilities.rs +0 -67
  31. package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
  32. package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
  33. package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
  34. package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
  35. package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
  36. package/dist-engine-src/src/backend/conformance/model.rs +0 -28
  37. package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
  38. package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
  39. package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
  40. package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
  41. package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
  42. package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
  43. package/dist-engine-src/src/backend/conformance/write.rs +0 -16
  44. package/dist-engine-src/src/backend/error.rs +0 -94
  45. package/dist-engine-src/src/backend/in_memory.rs +0 -670
  46. package/dist-engine-src/src/backend/mod.rs +0 -39
  47. package/dist-engine-src/src/backend/predicate.rs +0 -80
  48. package/dist-engine-src/src/backend/traits.rs +0 -260
  49. package/dist-engine-src/src/backend/types.rs +0 -239
  50. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  51. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  52. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  53. package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
  54. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  55. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  56. package/dist-engine-src/src/branch/context.rs +0 -40
  57. package/dist-engine-src/src/branch/lifecycle.rs +0 -221
  58. package/dist-engine-src/src/branch/mod.rs +0 -13
  59. package/dist-engine-src/src/branch/refs.rs +0 -321
  60. package/dist-engine-src/src/branch/stage_rows.rs +0 -67
  61. package/dist-engine-src/src/branch/types.rs +0 -21
  62. package/dist-engine-src/src/catalog/context.rs +0 -412
  63. package/dist-engine-src/src/catalog/mod.rs +0 -10
  64. package/dist-engine-src/src/catalog/schema.rs +0 -4
  65. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  66. package/dist-engine-src/src/cel/context.rs +0 -86
  67. package/dist-engine-src/src/cel/error.rs +0 -19
  68. package/dist-engine-src/src/cel/mod.rs +0 -8
  69. package/dist-engine-src/src/cel/provider.rs +0 -9
  70. package/dist-engine-src/src/cel/runtime.rs +0 -167
  71. package/dist-engine-src/src/cel/value.rs +0 -50
  72. package/dist-engine-src/src/changelog/bench_support.rs +0 -785
  73. package/dist-engine-src/src/changelog/change.rs +0 -1
  74. package/dist-engine-src/src/changelog/codec.rs +0 -497
  75. package/dist-engine-src/src/changelog/commit.rs +0 -1
  76. package/dist-engine-src/src/changelog/context.rs +0 -1614
  77. package/dist-engine-src/src/changelog/mod.rs +0 -29
  78. package/dist-engine-src/src/changelog/store.rs +0 -163
  79. package/dist-engine-src/src/changelog/test_support.rs +0 -54
  80. package/dist-engine-src/src/changelog/types.rs +0 -213
  81. package/dist-engine-src/src/commit_graph/context.rs +0 -944
  82. package/dist-engine-src/src/commit_graph/mod.rs +0 -9
  83. package/dist-engine-src/src/commit_graph/types.rs +0 -89
  84. package/dist-engine-src/src/commit_graph/walker.rs +0 -786
  85. package/dist-engine-src/src/common/error.rs +0 -347
  86. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  87. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  88. package/dist-engine-src/src/common/identity.rs +0 -145
  89. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  90. package/dist-engine-src/src/common/metadata.rs +0 -40
  91. package/dist-engine-src/src/common/mod.rs +0 -23
  92. package/dist-engine-src/src/common/types.rs +0 -105
  93. package/dist-engine-src/src/common/wire.rs +0 -222
  94. package/dist-engine-src/src/domain.rs +0 -320
  95. package/dist-engine-src/src/engine.rs +0 -203
  96. package/dist-engine-src/src/entity_pk.rs +0 -402
  97. package/dist-engine-src/src/functions/context.rs +0 -296
  98. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  99. package/dist-engine-src/src/functions/mod.rs +0 -18
  100. package/dist-engine-src/src/functions/provider.rs +0 -130
  101. package/dist-engine-src/src/functions/state.rs +0 -335
  102. package/dist-engine-src/src/functions/types.rs +0 -37
  103. package/dist-engine-src/src/init.rs +0 -692
  104. package/dist-engine-src/src/json_store/compression.rs +0 -77
  105. package/dist-engine-src/src/json_store/context.rs +0 -172
  106. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  107. package/dist-engine-src/src/json_store/mod.rs +0 -38
  108. package/dist-engine-src/src/json_store/store.rs +0 -494
  109. package/dist-engine-src/src/json_store/types.rs +0 -212
  110. package/dist-engine-src/src/lib.rs +0 -92
  111. package/dist-engine-src/src/live_state/context.rs +0 -1883
  112. package/dist-engine-src/src/live_state/mod.rs +0 -21
  113. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  114. package/dist-engine-src/src/live_state/reader.rs +0 -23
  115. package/dist-engine-src/src/live_state/types.rs +0 -231
  116. package/dist-engine-src/src/live_state/visibility.rs +0 -666
  117. package/dist-engine-src/src/plugin/archive.rs +0 -438
  118. package/dist-engine-src/src/plugin/component.rs +0 -183
  119. package/dist-engine-src/src/plugin/install.rs +0 -619
  120. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  121. package/dist-engine-src/src/plugin/materializer.rs +0 -202
  122. package/dist-engine-src/src/plugin/mod.rs +0 -33
  123. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
  124. package/dist-engine-src/src/plugin/storage.rs +0 -74
  125. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  126. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  127. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  128. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  129. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  130. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
  131. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
  132. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  133. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  134. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  135. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  136. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  137. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  138. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  139. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  140. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  141. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  142. package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
  143. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  144. package/dist-engine-src/src/schema/definition.json +0 -187
  145. package/dist-engine-src/src/schema/definition.rs +0 -742
  146. package/dist-engine-src/src/schema/key.rs +0 -138
  147. package/dist-engine-src/src/schema/mod.rs +0 -20
  148. package/dist-engine-src/src/schema/seed.rs +0 -14
  149. package/dist-engine-src/src/schema/tests.rs +0 -780
  150. package/dist-engine-src/src/session/context.rs +0 -1059
  151. package/dist-engine-src/src/session/create_branch.rs +0 -94
  152. package/dist-engine-src/src/session/execute.rs +0 -681
  153. package/dist-engine-src/src/session/merge/analysis.rs +0 -108
  154. package/dist-engine-src/src/session/merge/branch.rs +0 -417
  155. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  156. package/dist-engine-src/src/session/merge/mod.rs +0 -10
  157. package/dist-engine-src/src/session/merge/stats.rs +0 -61
  158. package/dist-engine-src/src/session/mod.rs +0 -30
  159. package/dist-engine-src/src/session/switch_branch.rs +0 -113
  160. package/dist-engine-src/src/session/transaction.rs +0 -557
  161. package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
  162. package/dist-engine-src/src/sql2/bind/error.rs +0 -5
  163. package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
  164. package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
  165. package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
  166. package/dist-engine-src/src/sql2/bind/read.rs +0 -65
  167. package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
  168. package/dist-engine-src/src/sql2/bind/table.rs +0 -273
  169. package/dist-engine-src/src/sql2/bind/write.rs +0 -86
  170. package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
  171. package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
  172. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
  173. package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
  174. package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
  175. package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
  176. package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
  177. package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
  178. package/dist-engine-src/src/sql2/context.rs +0 -317
  179. package/dist-engine-src/src/sql2/dml.rs +0 -148
  180. package/dist-engine-src/src/sql2/error.rs +0 -215
  181. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
  182. package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
  183. package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
  184. package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
  185. package/dist-engine-src/src/sql2/exec/write.rs +0 -661
  186. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
  187. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  188. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  189. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  190. package/dist-engine-src/src/sql2/history_route.rs +0 -661
  191. package/dist-engine-src/src/sql2/mod.rs +0 -52
  192. package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
  193. package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
  194. package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
  195. package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
  196. package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
  197. package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
  198. package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
  199. package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
  200. package/dist-engine-src/src/sql2/plan/write.rs +0 -147
  201. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
  202. package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
  203. package/dist-engine-src/src/sql2/providers/change.rs +0 -445
  204. package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
  205. package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
  206. package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
  207. package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
  208. package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
  209. package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
  210. package/dist-engine-src/src/sql2/providers/history.rs +0 -426
  211. package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
  212. package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
  213. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  214. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  215. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  216. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  217. package/dist-engine-src/src/sql2/session.rs +0 -83
  218. package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
  219. package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
  220. package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
  221. package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
  222. package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
  223. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  224. package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
  225. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  226. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  227. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  228. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  229. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  230. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  231. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  232. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  233. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
  234. package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
  235. package/dist-engine-src/src/storage/conformance.rs +0 -399
  236. package/dist-engine-src/src/storage/context.rs +0 -620
  237. package/dist-engine-src/src/storage/mod.rs +0 -52
  238. package/dist-engine-src/src/storage/point.rs +0 -440
  239. package/dist-engine-src/src/storage/read_scope.rs +0 -67
  240. package/dist-engine-src/src/storage/reader.rs +0 -867
  241. package/dist-engine-src/src/storage/scan.rs +0 -784
  242. package/dist-engine-src/src/storage/spaces.rs +0 -236
  243. package/dist-engine-src/src/storage/stats.rs +0 -80
  244. package/dist-engine-src/src/storage/write_set.rs +0 -962
  245. package/dist-engine-src/src/storage_bench.rs +0 -171
  246. package/dist-engine-src/src/test_support.rs +0 -450
  247. package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
  248. package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
  249. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
  250. package/dist-engine-src/src/tracked_state/context.rs +0 -2801
  251. package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
  252. package/dist-engine-src/src/tracked_state/merge.rs +0 -478
  253. package/dist-engine-src/src/tracked_state/mod.rs +0 -35
  254. package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
  255. package/dist-engine-src/src/tracked_state/storage.rs +0 -427
  256. package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
  257. package/dist-engine-src/src/tracked_state/types.rs +0 -238
  258. package/dist-engine-src/src/transaction/bench_support.rs +0 -407
  259. package/dist-engine-src/src/transaction/commit.rs +0 -1592
  260. package/dist-engine-src/src/transaction/context.rs +0 -1653
  261. package/dist-engine-src/src/transaction/mod.rs +0 -24
  262. package/dist-engine-src/src/transaction/normalization.rs +0 -877
  263. package/dist-engine-src/src/transaction/prep.rs +0 -37
  264. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
  265. package/dist-engine-src/src/transaction/staging.rs +0 -1525
  266. package/dist-engine-src/src/transaction/types.rs +0 -403
  267. package/dist-engine-src/src/transaction/validation.rs +0 -5766
  268. package/dist-engine-src/src/untracked_state/codec.rs +0 -615
  269. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  270. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  271. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  272. package/dist-engine-src/src/untracked_state/storage.rs +0 -898
  273. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  274. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,1059 +0,0 @@
1
- use std::future::Future;
2
- use std::pin::Pin;
3
- use std::sync::Arc;
4
-
5
- use serde_json::Value as JsonValue;
6
-
7
- use crate::binary_cas::{BinaryCasContext, BlobDataReader};
8
- use crate::branch::{
9
- BranchContext, BranchLifecycle, BranchOperation, BranchRefReader, BranchReferenceRole,
10
- };
11
- use crate::catalog::CatalogContext;
12
- use crate::commit_graph::{CommitGraphContext, CommitGraphReader};
13
- use crate::entity_pk::EntityPk;
14
- use crate::functions::FunctionProviderHandle;
15
- use crate::json_store::JsonStoreContext;
16
- use crate::live_state::{LiveStateContext, LiveStateReader, LiveStateRowRequest};
17
- use crate::sql2::{
18
- ChangelogQuerySource, HistoryQuerySource, SqlChangelogQuerySource, SqlExecutionContext,
19
- SqlHistoryQuerySource,
20
- };
21
- use crate::storage::{
22
- DurableWriteGuard, DurableWriteLock, InMemoryStorageBackend, StorageBackend, StorageReadOptions,
23
- };
24
- use crate::storage::{StorageContext, StorageRead, StorageReadScope};
25
- use crate::tracked_state::TrackedStateContext;
26
- use crate::transaction::{open_transaction, Transaction};
27
- use crate::GLOBAL_BRANCH_ID;
28
- use crate::{LixError, NullableKeyFilter};
29
-
30
- use super::transaction::{SessionOperationGuard, SessionTransactionManager, SessionWriteLease};
31
-
32
- pub(crate) const WORKSPACE_BRANCH_KEY: &str = "lix_workspace_branch_id";
33
-
34
- #[derive(Clone)]
35
- pub(crate) enum SessionMode {
36
- Pinned { branch_id: String },
37
- Workspace,
38
- }
39
-
40
- /// Session-context state for engine execution.
41
- ///
42
- /// A session context pins the active branch selector and shared execution
43
- /// services. Parent-handle `execute(...)` runs as an implicit single-statement
44
- /// transaction. Explicit transactions hold the session execution lease until
45
- /// commit or rollback, so all SQL during that window must run through the
46
- /// transaction handle.
47
- #[derive(Clone)]
48
- pub struct SessionContext<B: StorageBackend = InMemoryStorageBackend> {
49
- pub(super) mode: SessionMode,
50
- pub(super) storage: StorageContext<B>,
51
- pub(super) live_state: Arc<LiveStateContext>,
52
- pub(super) tracked_state: Arc<TrackedStateContext>,
53
- pub(super) binary_cas: Arc<BinaryCasContext>,
54
- pub(super) branch_ctx: Arc<BranchContext>,
55
- pub(super) catalog_context: Arc<CatalogContext>,
56
- pub(super) write_lock: DurableWriteLock,
57
- transaction_manager: SessionTransactionManager,
58
- }
59
-
60
- impl<B> SessionContext<B>
61
- where
62
- B: StorageBackend + Clone + Send + Sync + 'static,
63
- for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
64
- for<'backend> B::Write<'backend>: Send,
65
- {
66
- pub(crate) async fn open_workspace(
67
- storage: StorageContext<B>,
68
- live_state: Arc<LiveStateContext>,
69
- tracked_state: Arc<TrackedStateContext>,
70
- binary_cas: Arc<BinaryCasContext>,
71
- branch_ctx: Arc<BranchContext>,
72
- catalog_context: Arc<CatalogContext>,
73
- write_lock: DurableWriteLock,
74
- ) -> Result<Self, LixError> {
75
- let session = Self::new(
76
- SessionMode::Workspace,
77
- storage,
78
- live_state,
79
- tracked_state,
80
- binary_cas,
81
- branch_ctx,
82
- catalog_context,
83
- write_lock,
84
- );
85
- session.active_branch_id().await?;
86
- Ok(session)
87
- }
88
-
89
- pub(crate) async fn open(
90
- active_branch_id: String,
91
- storage: StorageContext<B>,
92
- live_state: Arc<LiveStateContext>,
93
- tracked_state: Arc<TrackedStateContext>,
94
- binary_cas: Arc<BinaryCasContext>,
95
- branch_ctx: Arc<BranchContext>,
96
- catalog_context: Arc<CatalogContext>,
97
- write_lock: DurableWriteLock,
98
- ) -> Result<Self, LixError> {
99
- Ok(Self::new(
100
- SessionMode::Pinned {
101
- branch_id: active_branch_id,
102
- },
103
- storage,
104
- live_state,
105
- tracked_state,
106
- binary_cas,
107
- branch_ctx,
108
- catalog_context,
109
- write_lock,
110
- ))
111
- }
112
-
113
- pub(super) fn new(
114
- mode: SessionMode,
115
- storage: StorageContext<B>,
116
- live_state: Arc<LiveStateContext>,
117
- tracked_state: Arc<TrackedStateContext>,
118
- binary_cas: Arc<BinaryCasContext>,
119
- branch_ctx: Arc<BranchContext>,
120
- catalog_context: Arc<CatalogContext>,
121
- write_lock: DurableWriteLock,
122
- ) -> Self {
123
- Self::new_with_transaction_manager(
124
- mode,
125
- storage,
126
- live_state,
127
- tracked_state,
128
- binary_cas,
129
- branch_ctx,
130
- catalog_context,
131
- write_lock,
132
- SessionTransactionManager::new(),
133
- )
134
- }
135
-
136
- pub(super) fn new_with_transaction_manager(
137
- mode: SessionMode,
138
- storage: StorageContext<B>,
139
- live_state: Arc<LiveStateContext>,
140
- tracked_state: Arc<TrackedStateContext>,
141
- binary_cas: Arc<BinaryCasContext>,
142
- branch_ctx: Arc<BranchContext>,
143
- catalog_context: Arc<CatalogContext>,
144
- write_lock: DurableWriteLock,
145
- transaction_manager: SessionTransactionManager,
146
- ) -> Self {
147
- Self {
148
- mode,
149
- storage,
150
- live_state,
151
- tracked_state,
152
- binary_cas,
153
- branch_ctx,
154
- catalog_context,
155
- write_lock,
156
- transaction_manager,
157
- }
158
- }
159
-
160
- /// Releases this logical session handle. This is a lifecycle boundary only:
161
- /// successful writes are committed before their operation returns.
162
- pub async fn close(&self) -> Result<(), LixError> {
163
- self.transaction_manager.close().await
164
- }
165
-
166
- pub fn is_closed(&self) -> bool {
167
- self.transaction_manager.is_closed()
168
- }
169
-
170
- #[cfg(test)]
171
- pub(crate) fn operation_in_progress_count_for_test(&self) -> usize {
172
- self.transaction_manager.operation_count_for_test()
173
- }
174
-
175
- #[cfg(test)]
176
- pub(crate) fn commit_in_progress_for_test(&self) -> bool {
177
- self.transaction_manager.commit_in_progress_for_test()
178
- }
179
-
180
- #[cfg(test)]
181
- pub(crate) fn active_transaction_for_test(&self) -> bool {
182
- self.transaction_manager.active_transaction_for_test()
183
- }
184
-
185
- pub(super) fn transaction_manager(&self) -> SessionTransactionManager {
186
- self.transaction_manager.clone()
187
- }
188
-
189
- pub(crate) fn ensure_open(&self) -> Result<(), LixError> {
190
- self.transaction_manager.ensure_open()
191
- }
192
-
193
- pub(super) fn begin_session_operation(&self) -> Result<SessionOperationGuard, LixError> {
194
- self.transaction_manager.begin_session_operation()
195
- }
196
-
197
- pub(super) fn begin_session_write_lease(&self) -> Result<SessionWriteLease, LixError> {
198
- self.transaction_manager.begin_write_lease()
199
- }
200
-
201
- pub(super) fn begin_explicit_session_write_lease(&self) -> Result<SessionWriteLease, LixError> {
202
- self.transaction_manager.begin_explicit_write_lease()
203
- }
204
-
205
- pub(super) async fn begin_session_write_access(&self) -> Result<SessionWriteAccess, LixError> {
206
- let write_lease = self.begin_session_write_lease()?;
207
- self.begin_session_write_access_with_lease(write_lease)
208
- .await
209
- }
210
-
211
- pub(super) async fn begin_explicit_session_write_access(
212
- &self,
213
- ) -> Result<SessionWriteAccess, LixError> {
214
- let write_lease = self.begin_explicit_session_write_lease()?;
215
- self.begin_session_write_access_with_lease(write_lease)
216
- .await
217
- }
218
-
219
- async fn begin_session_write_access_with_lease(
220
- &self,
221
- write_lease: SessionWriteLease,
222
- ) -> Result<SessionWriteAccess, LixError> {
223
- let write_guard = self.write_lock.lock_owned().await;
224
- let write_access = SessionWriteAccess {
225
- _write_lease: write_lease,
226
- _write_guard: write_guard,
227
- };
228
- self.ensure_open()?;
229
- Ok(write_access)
230
- }
231
-
232
- /// Resolves the branch this session should operate on right now.
233
- ///
234
- /// This is a read-path helper. Write flows must resolve the active branch
235
- /// through the transaction capability so the read is scoped to the
236
- /// same backend transaction as the writes it influences.
237
- ///
238
- /// Pinned sessions are pure in-memory views over one branch. Workspace
239
- /// sessions read the shared workspace selector from untracked global
240
- /// `lix_key_value` state so multiple open app sessions can observe the same
241
- /// active workspace branch.
242
- pub async fn active_branch_id(&self) -> Result<String, LixError> {
243
- let _operation_guard = self.begin_session_operation()?;
244
- let transaction = self.storage.begin_read(StorageReadOptions::default())?;
245
- let result = self.active_branch_id_from_reader(&transaction).await;
246
- match result {
247
- Ok(branch_id) => Ok(branch_id),
248
- Err(error) => Err(error),
249
- }
250
- }
251
-
252
- pub(super) async fn active_branch_id_from_reader<S>(
253
- &self,
254
- reader: &S,
255
- ) -> Result<String, LixError>
256
- where
257
- S: StorageRead + Send + Sync + ?Sized,
258
- {
259
- self.ensure_open()?;
260
- match &self.mode {
261
- SessionMode::Pinned { branch_id } => Ok(branch_id.clone()),
262
- SessionMode::Workspace => self.load_workspace_branch_id(reader).await,
263
- }
264
- }
265
-
266
- async fn load_workspace_branch_id<S>(&self, reader: &S) -> Result<String, LixError>
267
- where
268
- S: StorageRead + Send + Sync + ?Sized,
269
- {
270
- let row = self
271
- .live_state
272
- .reader(reader)
273
- .load_row(&LiveStateRowRequest {
274
- schema_key: "lix_key_value".to_string(),
275
- branch_id: GLOBAL_BRANCH_ID.to_string(),
276
- entity_pk: EntityPk::single(WORKSPACE_BRANCH_KEY),
277
- file_id: NullableKeyFilter::Null,
278
- })
279
- .await?
280
- .ok_or_else(|| {
281
- LixError::new(
282
- "LIX_ERROR_UNKNOWN",
283
- "workspace branch selector is missing lix_key_value:lix_workspace_branch_id",
284
- )
285
- })?;
286
- let snapshot_content = row.snapshot_content.as_deref().ok_or_else(|| {
287
- LixError::new(
288
- "LIX_ERROR_UNKNOWN",
289
- "workspace branch selector is missing snapshot_content",
290
- )
291
- })?;
292
- let snapshot = serde_json::from_str::<JsonValue>(snapshot_content).map_err(|error| {
293
- LixError::new(
294
- "LIX_ERROR_UNKNOWN",
295
- format!("workspace branch selector snapshot is invalid JSON: {error}"),
296
- )
297
- })?;
298
- let branch_id = snapshot
299
- .get("value")
300
- .and_then(JsonValue::as_str)
301
- .filter(|value| !value.is_empty())
302
- .ok_or_else(|| {
303
- LixError::new(
304
- "LIX_ERROR_UNKNOWN",
305
- "workspace branch selector value must be a non-empty string",
306
- )
307
- })?
308
- .to_string();
309
-
310
- let branch_ref = self.branch_ctx.ref_reader(reader);
311
- BranchLifecycle::new(&branch_ref)
312
- .require_existing_ref(
313
- &branch_id,
314
- BranchOperation::LoadWorkspaceSelector,
315
- BranchReferenceRole::WorkspaceSelector,
316
- )
317
- .await?;
318
-
319
- Ok(branch_id)
320
- }
321
-
322
- pub(crate) async fn with_write_transaction<T, F>(&self, f: F) -> Result<T, LixError>
323
- where
324
- F: for<'tx> FnOnce(
325
- &'tx mut Transaction<B>,
326
- ) -> Pin<Box<dyn Future<Output = Result<T, LixError>> + 'tx>>,
327
- {
328
- self.ensure_open()?;
329
- let write_access = self.begin_session_write_access().await?;
330
- self.with_write_transaction_reserved(write_access, f).await
331
- }
332
-
333
- pub(super) async fn with_write_transaction_reserved<T, F>(
334
- &self,
335
- _write_access: SessionWriteAccess,
336
- f: F,
337
- ) -> Result<T, LixError>
338
- where
339
- F: for<'tx> FnOnce(
340
- &'tx mut Transaction<B>,
341
- ) -> Pin<Box<dyn Future<Output = Result<T, LixError>> + 'tx>>,
342
- {
343
- let opened = open_transaction(
344
- &self.mode,
345
- self.storage.clone(),
346
- Arc::clone(&self.live_state),
347
- Arc::clone(&self.tracked_state),
348
- Arc::clone(&self.binary_cas),
349
- Arc::clone(&self.branch_ctx),
350
- Arc::clone(&self.catalog_context),
351
- )
352
- .await?;
353
- self.ensure_open()?;
354
- let mut transaction = opened.transaction;
355
- transaction.attach_commit_boundary(self.transaction_commit_boundary());
356
- let runtime_functions = opened.runtime_functions;
357
-
358
- match f(&mut transaction).await {
359
- Ok(value) => {
360
- self.ensure_open()?;
361
- transaction.commit(&runtime_functions).await?;
362
- Ok(value)
363
- }
364
- Err(error) => Err(error),
365
- }
366
- }
367
-
368
- #[cfg(test)]
369
- pub(super) fn begin_commit(&self) -> crate::transaction::CommitBoundaryGuard {
370
- self.transaction_manager.begin_commit()
371
- }
372
-
373
- pub(super) fn transaction_commit_boundary(
374
- &self,
375
- ) -> crate::transaction::TransactionCommitBoundary {
376
- self.transaction_manager.transaction_commit_boundary()
377
- }
378
- }
379
-
380
- pub(super) struct SessionWriteAccess {
381
- _write_guard: DurableWriteGuard,
382
- _write_lease: SessionWriteLease,
383
- }
384
-
385
- pub(super) fn closed_error() -> LixError {
386
- LixError::new(LixError::CODE_CLOSED, "Lix handle is closed")
387
- .with_hint("Open a new Lix handle before calling this method.")
388
- }
389
-
390
- /// Read-only SQL execution context derived from a session.
391
- ///
392
- /// Write statements re-plan against `Transaction`; this context intentionally
393
- /// has no write stager.
394
- pub(super) struct SessionSqlExecutionContext<'a, R> {
395
- pub(super) active_branch_id: &'a str,
396
- pub(super) read_store: StorageReadScope<R>,
397
- pub(super) live_state: Arc<LiveStateContext>,
398
- pub(super) binary_cas: Arc<BinaryCasContext>,
399
- pub(super) branch_ctx: Arc<BranchContext>,
400
- pub(super) visible_schemas: Vec<JsonValue>,
401
- pub(super) functions: FunctionProviderHandle,
402
- }
403
-
404
- impl<R> SqlExecutionContext for SessionSqlExecutionContext<'_, R>
405
- where
406
- R: crate::storage::StorageBackendRead + Clone + Send + Sync + 'static,
407
- {
408
- type ReadStore = StorageReadScope<R>;
409
-
410
- fn active_branch_id(&self) -> &str {
411
- self.active_branch_id
412
- }
413
-
414
- fn live_state(&self) -> Arc<dyn LiveStateReader> {
415
- Arc::new(self.live_state.reader(self.read_store.clone())) as Arc<dyn LiveStateReader>
416
- }
417
-
418
- fn history_query_source(&self) -> SqlHistoryQuerySource<Self::ReadStore> {
419
- HistoryQuerySource {
420
- json_reader: JsonStoreContext::new().reader(self.read_store.store()),
421
- }
422
- }
423
-
424
- fn changelog_query_source(&self) -> SqlChangelogQuerySource<Self::ReadStore> {
425
- ChangelogQuerySource {
426
- store: self.read_store.clone(),
427
- json_reader: JsonStoreContext::new().reader(self.read_store.store()),
428
- }
429
- }
430
-
431
- fn commit_graph(&self) -> Box<dyn CommitGraphReader> {
432
- Box::new(CommitGraphContext::new().reader(self.read_store.clone()))
433
- }
434
-
435
- fn branch_ref(&self) -> Arc<dyn BranchRefReader> {
436
- Arc::new(self.branch_ctx.ref_reader(self.read_store.clone()))
437
- }
438
-
439
- fn functions(&self) -> FunctionProviderHandle {
440
- self.functions.clone()
441
- }
442
-
443
- fn blob_reader(&self) -> Arc<dyn BlobDataReader> {
444
- Arc::new(self.binary_cas.reader(self.read_store.clone())) as Arc<dyn BlobDataReader>
445
- }
446
-
447
- fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
448
- Ok(self.visible_schemas.clone())
449
- }
450
- }
451
-
452
- #[cfg(test)]
453
- mod tests {
454
- use std::future::Future;
455
- use std::pin::Pin;
456
- use std::sync::Condvar;
457
- use std::sync::Mutex;
458
- use std::task::{Context, Poll};
459
- use std::thread;
460
- use std::time::{Duration, Instant};
461
-
462
- use crate::backend::{
463
- Backend, BackendCapabilities, BackendError, DurableWriteLock, InMemoryBackend,
464
- InMemoryRead, InMemoryWrite, ReadOptions, WriteOptions,
465
- };
466
- use crate::Engine;
467
- use futures_util::task::noop_waker_ref;
468
-
469
- const TEST_WAIT_TIMEOUT: Duration = Duration::from_secs(2);
470
-
471
- fn wait_until(description: &str, mut condition: impl FnMut() -> bool) {
472
- let deadline = Instant::now() + TEST_WAIT_TIMEOUT;
473
- while !condition() {
474
- assert!(
475
- Instant::now() < deadline,
476
- "timed out waiting for {description}"
477
- );
478
- thread::yield_now();
479
- }
480
- }
481
-
482
- fn assert_close_pending<F>(mut future: Pin<&mut F>)
483
- where
484
- F: Future<Output = Result<(), crate::LixError>>,
485
- {
486
- let mut cx = Context::from_waker(noop_waker_ref());
487
- assert!(
488
- matches!(future.as_mut().poll(&mut cx), Poll::Pending),
489
- "close should remain pending while guarded work is in progress"
490
- );
491
- }
492
-
493
- async fn assert_close_finishes<F>(future: Pin<&mut F>, description: &str)
494
- where
495
- F: Future<Output = Result<(), crate::LixError>>,
496
- {
497
- tokio::time::timeout(TEST_WAIT_TIMEOUT, future)
498
- .await
499
- .unwrap_or_else(|_| panic!("timed out waiting for {description}"))
500
- .unwrap_or_else(|error| panic!("{description} failed: {error:?}"));
501
- }
502
-
503
- fn join_thread<T>(handle: thread::JoinHandle<T>, description: &str) -> T {
504
- wait_until(description, || handle.is_finished());
505
- match handle.join() {
506
- Ok(result) => result,
507
- Err(_) => panic!("{description} panicked"),
508
- }
509
- }
510
-
511
- async fn open_session() -> std::sync::Arc<super::SessionContext<InMemoryBackend>> {
512
- let backend = InMemoryBackend::default();
513
- let _receipt = Engine::initialize(backend.clone())
514
- .await
515
- .expect("backend should initialize");
516
- let engine = Engine::new(backend)
517
- .await
518
- .expect("initialized backend should create engine");
519
- std::sync::Arc::new(
520
- engine
521
- .open_workspace_session()
522
- .await
523
- .expect("workspace session should open"),
524
- )
525
- }
526
-
527
- async fn open_blocking_read_session() -> (
528
- std::sync::Arc<super::SessionContext<BlockingBeginReadBackend>>,
529
- BlockingGate,
530
- ) {
531
- let backend = BlockingBeginReadBackend::new();
532
- let gate = backend.gate();
533
- let _receipt = Engine::initialize(backend.clone())
534
- .await
535
- .expect("backend should initialize");
536
- let engine = Engine::new(backend)
537
- .await
538
- .expect("initialized backend should create engine");
539
- (
540
- std::sync::Arc::new(
541
- engine
542
- .open_workspace_session()
543
- .await
544
- .expect("workspace session should open"),
545
- ),
546
- gate,
547
- )
548
- }
549
-
550
- async fn open_blocking_write_session() -> (
551
- std::sync::Arc<super::SessionContext<BlockingBeginWriteBackend>>,
552
- BlockingGate,
553
- ) {
554
- let backend = BlockingBeginWriteBackend::new();
555
- let gate = backend.gate();
556
- let _receipt = Engine::initialize(backend.clone())
557
- .await
558
- .expect("backend should initialize");
559
- let engine = Engine::new(backend)
560
- .await
561
- .expect("initialized backend should create engine");
562
- (
563
- std::sync::Arc::new(
564
- engine
565
- .open_workspace_session()
566
- .await
567
- .expect("workspace session should open"),
568
- ),
569
- gate,
570
- )
571
- }
572
-
573
- #[tokio::test]
574
- async fn close_waits_for_session_operation_guard_to_drop() {
575
- let session = open_session().await;
576
- let guard = session
577
- .begin_session_operation()
578
- .expect("session operation should begin");
579
- let mut close = Box::pin(session.close());
580
- assert_close_pending(close.as_mut());
581
-
582
- drop(guard);
583
- assert_close_finishes(close.as_mut(), "close after operation guard drops").await;
584
- }
585
-
586
- #[tokio::test]
587
- async fn close_waits_for_commit_guard_to_drop() {
588
- let session = open_session().await;
589
- let guard = session.begin_commit();
590
- let mut close = Box::pin(session.close());
591
- assert_close_pending(close.as_mut());
592
-
593
- drop(guard);
594
- assert_close_finishes(close.as_mut(), "close after commit guard drops").await;
595
- }
596
-
597
- #[tokio::test]
598
- async fn session_read_execute_holds_operation_guard() {
599
- let session = open_session().await;
600
- let result = session
601
- .execute("SELECT 1", &[])
602
- .await
603
- .expect("read should succeed");
604
- assert_eq!(result.len(), 1);
605
- assert_eq!(session.operation_in_progress_count_for_test(), 0);
606
- }
607
-
608
- #[tokio::test]
609
- async fn active_transaction_read_execute_holds_operation_guard() {
610
- let session = open_session().await;
611
- let mut transaction = session
612
- .begin_transaction()
613
- .await
614
- .expect("transaction should begin");
615
- assert!(session.active_transaction_for_test());
616
- let result = transaction
617
- .execute("SELECT 1", &[])
618
- .await
619
- .expect("transaction read should succeed");
620
- assert_eq!(result.len(), 1);
621
- assert_eq!(session.operation_in_progress_count_for_test(), 1);
622
- assert!(session.active_transaction_for_test());
623
- transaction
624
- .rollback()
625
- .await
626
- .expect("transaction rollback should succeed");
627
- assert_eq!(session.operation_in_progress_count_for_test(), 0);
628
- assert!(!session.active_transaction_for_test());
629
- }
630
-
631
- #[tokio::test]
632
- async fn close_rejects_idle_explicit_transaction_without_waiting() {
633
- let session = open_session().await;
634
- let transaction = session
635
- .begin_transaction()
636
- .await
637
- .expect("transaction should begin");
638
-
639
- let error = session
640
- .close()
641
- .await
642
- .expect_err("close should reject an idle explicit transaction");
643
- assert_eq!(error.code, "LIX_INVALID_TRANSACTION_STATE");
644
-
645
- transaction
646
- .rollback()
647
- .await
648
- .expect("rollback should remain available after rejected close");
649
- }
650
-
651
- #[tokio::test]
652
- async fn transaction_open_waits_for_write_lock() {
653
- let session = open_session().await;
654
- let write_guard = session.write_lock.lock_owned().await;
655
-
656
- let opener_session = std::sync::Arc::clone(&session);
657
- let opener = thread::spawn(move || {
658
- let runtime = tokio::runtime::Builder::new_current_thread()
659
- .build()
660
- .expect("test runtime should build");
661
- runtime.block_on(async move { opener_session.begin_transaction().await })
662
- });
663
- wait_until("explicit transaction open to reserve the session", || {
664
- session.operation_in_progress_count_for_test() > 0
665
- && session.active_transaction_for_test()
666
- && !opener.is_finished()
667
- });
668
-
669
- assert!(
670
- !opener.is_finished(),
671
- "transaction open should wait for the write lock"
672
- );
673
- assert!(session.active_transaction_for_test());
674
-
675
- drop(write_guard);
676
- let transaction = join_thread(opener, "queued transaction opener")
677
- .expect("transaction should begin after write lock is released");
678
- transaction
679
- .rollback()
680
- .await
681
- .expect("transaction rollback should succeed");
682
- }
683
-
684
- #[tokio::test]
685
- async fn close_waits_for_session_write_queued_on_write_lock() {
686
- let session = open_session().await;
687
- let write_guard = session.write_lock.lock_owned().await;
688
-
689
- let writer_session = std::sync::Arc::clone(&session);
690
- let writer = thread::spawn(move || {
691
- let runtime = tokio::runtime::Builder::new_current_thread()
692
- .build()
693
- .expect("test runtime should build");
694
- runtime.block_on(async move {
695
- writer_session
696
- .execute(
697
- "INSERT INTO lix_key_value (key, value) VALUES ('queued-write-close', 'value')",
698
- &[],
699
- )
700
- .await
701
- })
702
- });
703
- wait_until("queued session write to reserve the session", || {
704
- session.operation_in_progress_count_for_test() > 0
705
- && session.active_transaction_for_test()
706
- });
707
-
708
- let mut close = Box::pin(session.close());
709
- assert_close_pending(close.as_mut());
710
-
711
- drop(write_guard);
712
- let write_error =
713
- join_thread(writer, "queued writer").expect_err("queued write should observe close");
714
- assert_eq!(write_error.code, crate::LixError::CODE_CLOSED);
715
- assert_close_finishes(close.as_mut(), "close after queued write exits").await;
716
- }
717
-
718
- #[tokio::test]
719
- async fn session_read_does_not_wait_for_write_lock() {
720
- let session = open_session().await;
721
- let write_guard = session.write_lock.lock_owned().await;
722
-
723
- let result = tokio::time::timeout(TEST_WAIT_TIMEOUT, session.execute("SELECT 1", &[]))
724
- .await
725
- .expect("read should not wait for the write lock")
726
- .expect("read should succeed");
727
-
728
- assert_eq!(result.len(), 1);
729
- drop(write_guard);
730
- }
731
-
732
- #[tokio::test]
733
- async fn explicit_transaction_commit_sets_commit_guard() {
734
- let session = open_session().await;
735
- let mut transaction = session
736
- .begin_transaction()
737
- .await
738
- .expect("transaction should begin");
739
- transaction
740
- .execute(
741
- "INSERT INTO lix_key_value (key, value) VALUES ('commit-guard-test', 'value')",
742
- &[],
743
- )
744
- .await
745
- .expect("transaction write should stage");
746
- transaction
747
- .commit()
748
- .await
749
- .expect("transaction commit should succeed");
750
- assert!(!session.commit_in_progress_for_test());
751
- }
752
-
753
- #[tokio::test]
754
- async fn close_waits_for_explicit_transaction_open_queued_on_write_lock() {
755
- let session = open_session().await;
756
- let write_guard = session.write_lock.lock_owned().await;
757
-
758
- let opener_session = std::sync::Arc::clone(&session);
759
- let opener = thread::spawn(move || {
760
- let runtime = tokio::runtime::Builder::new_current_thread()
761
- .build()
762
- .expect("test runtime should build");
763
- runtime.block_on(async move { opener_session.begin_transaction().await })
764
- });
765
- wait_until("explicit transaction open to queue on write lock", || {
766
- session.operation_in_progress_count_for_test() > 0
767
- && session.active_transaction_for_test()
768
- && !opener.is_finished()
769
- });
770
- assert!(
771
- !opener.is_finished(),
772
- "transaction open should still be queued on write lock"
773
- );
774
-
775
- let mut close = Box::pin(session.close());
776
- assert_close_pending(close.as_mut());
777
-
778
- drop(write_guard);
779
- let open_error = match join_thread(opener, "queued explicit transaction opener") {
780
- Ok(_) => panic!("queued explicit transaction open should observe close"),
781
- Err(error) => error,
782
- };
783
- assert_eq!(open_error.code, crate::LixError::CODE_CLOSED);
784
- assert_close_finishes(close.as_mut(), "close after queued explicit open exits").await;
785
- }
786
-
787
- #[tokio::test]
788
- async fn close_waits_for_session_read_blocked_in_backend_read() {
789
- let (session, gate) = open_blocking_read_session().await;
790
-
791
- gate.block_next();
792
- let reader_session = std::sync::Arc::clone(&session);
793
- let reader = std::thread::spawn(move || {
794
- let runtime = tokio::runtime::Builder::new_current_thread()
795
- .build()
796
- .expect("test runtime should build");
797
- runtime.block_on(async move { reader_session.execute("SELECT 1", &[]).await })
798
- });
799
- gate.wait_until_blocked();
800
-
801
- let mut close = Box::pin(session.close());
802
- assert_close_pending(close.as_mut());
803
-
804
- gate.release();
805
- let error = join_thread(reader, "blocked reader")
806
- .expect_err("read should observe close after backend read resumes");
807
- assert_eq!(error.code, crate::LixError::CODE_CLOSED);
808
- assert_close_finishes(close.as_mut(), "close after blocked read exits").await;
809
- }
810
-
811
- #[tokio::test]
812
- async fn close_rejects_active_transaction_read_blocked_in_backend_read() {
813
- let (session, gate) = open_blocking_read_session().await;
814
- let mut transaction = session
815
- .begin_transaction()
816
- .await
817
- .expect("transaction should begin");
818
-
819
- gate.block_next();
820
- let reader = std::thread::spawn(move || {
821
- let runtime = tokio::runtime::Builder::new_current_thread()
822
- .build()
823
- .expect("test runtime should build");
824
- runtime.block_on(async move { transaction.execute("SELECT 1", &[]).await })
825
- });
826
- gate.wait_until_blocked();
827
-
828
- let close_error = session
829
- .close()
830
- .await
831
- .expect_err("close should reject an active explicit transaction read");
832
- assert_eq!(close_error.code, "LIX_INVALID_TRANSACTION_STATE");
833
-
834
- gate.release();
835
- let result = join_thread(reader, "blocked transaction reader")
836
- .expect("in-flight transaction read should finish after rejected close");
837
- assert_eq!(result.len(), 1);
838
- }
839
-
840
- #[tokio::test]
841
- async fn close_waits_for_explicit_transaction_blocked_in_backend_commit() {
842
- let (session, gate) = open_blocking_write_session().await;
843
- let mut transaction = session
844
- .begin_transaction()
845
- .await
846
- .expect("transaction should begin");
847
- transaction
848
- .execute(
849
- "INSERT INTO lix_key_value (key, value) VALUES ('blocked-commit', 'value')",
850
- &[],
851
- )
852
- .await
853
- .expect("transaction write should stage");
854
-
855
- gate.block_next();
856
- let committer = std::thread::spawn(move || {
857
- let runtime = tokio::runtime::Builder::new_current_thread()
858
- .build()
859
- .expect("test runtime should build");
860
- runtime.block_on(async move { transaction.commit().await })
861
- });
862
- gate.wait_until_blocked();
863
- assert!(
864
- session.commit_in_progress_for_test(),
865
- "blocked explicit transaction commit should set the commit guard"
866
- );
867
-
868
- let mut close = Box::pin(session.close());
869
- assert_close_pending(close.as_mut());
870
-
871
- gate.release();
872
- join_thread(committer, "blocked committer")
873
- .expect("commit already at durable boundary should finish");
874
- assert_close_finishes(close.as_mut(), "close after commit exits").await;
875
- assert!(
876
- !session.commit_in_progress_for_test(),
877
- "commit guard should clear after the blocked commit exits"
878
- );
879
- }
880
-
881
- #[derive(Clone)]
882
- struct BlockingBeginReadBackend {
883
- inner: InMemoryBackend,
884
- gate: BlockingGate,
885
- }
886
-
887
- impl BlockingBeginReadBackend {
888
- fn new() -> Self {
889
- Self {
890
- inner: InMemoryBackend::default(),
891
- gate: BlockingGate::new(),
892
- }
893
- }
894
-
895
- fn gate(&self) -> BlockingGate {
896
- self.gate.clone()
897
- }
898
- }
899
-
900
- impl Backend for BlockingBeginReadBackend {
901
- type Read<'a>
902
- = InMemoryRead
903
- where
904
- Self: 'a;
905
-
906
- type Write<'a>
907
- = InMemoryWrite
908
- where
909
- Self: 'a;
910
-
911
- fn capabilities(&self) -> BackendCapabilities {
912
- self.inner.capabilities()
913
- }
914
-
915
- fn begin_read(&self, opts: ReadOptions) -> Result<Self::Read<'_>, BackendError> {
916
- self.gate.maybe_block();
917
- self.inner.begin_read(opts)
918
- }
919
-
920
- fn begin_write(&self, opts: WriteOptions) -> Result<Self::Write<'_>, BackendError> {
921
- self.inner.begin_write(opts)
922
- }
923
-
924
- fn durable_write_lock(&self) -> DurableWriteLock {
925
- self.inner.durable_write_lock()
926
- }
927
- }
928
-
929
- #[derive(Clone)]
930
- struct BlockingBeginWriteBackend {
931
- inner: InMemoryBackend,
932
- gate: BlockingGate,
933
- }
934
-
935
- impl BlockingBeginWriteBackend {
936
- fn new() -> Self {
937
- Self {
938
- inner: InMemoryBackend::default(),
939
- gate: BlockingGate::new(),
940
- }
941
- }
942
-
943
- fn gate(&self) -> BlockingGate {
944
- self.gate.clone()
945
- }
946
- }
947
-
948
- impl Backend for BlockingBeginWriteBackend {
949
- type Read<'a>
950
- = InMemoryRead
951
- where
952
- Self: 'a;
953
-
954
- type Write<'a>
955
- = InMemoryWrite
956
- where
957
- Self: 'a;
958
-
959
- fn capabilities(&self) -> BackendCapabilities {
960
- self.inner.capabilities()
961
- }
962
-
963
- fn begin_read(&self, opts: ReadOptions) -> Result<Self::Read<'_>, BackendError> {
964
- self.inner.begin_read(opts)
965
- }
966
-
967
- fn begin_write(&self, opts: WriteOptions) -> Result<Self::Write<'_>, BackendError> {
968
- self.gate.maybe_block();
969
- self.inner.begin_write(opts)
970
- }
971
-
972
- fn durable_write_lock(&self) -> DurableWriteLock {
973
- self.inner.durable_write_lock()
974
- }
975
- }
976
-
977
- #[derive(Clone)]
978
- struct BlockingGate {
979
- state: std::sync::Arc<(Mutex<BlockingGateState>, Condvar)>,
980
- }
981
-
982
- impl BlockingGate {
983
- fn new() -> Self {
984
- Self {
985
- state: std::sync::Arc::new((
986
- Mutex::new(BlockingGateState::default()),
987
- Condvar::new(),
988
- )),
989
- }
990
- }
991
-
992
- fn block_next(&self) {
993
- let (lock, _) = &*self.state;
994
- let mut state = lock.lock().expect("blocking gate lock should not poison");
995
- state.block_next = true;
996
- state.blocked = false;
997
- state.released = false;
998
- }
999
-
1000
- fn maybe_block(&self) {
1001
- let (lock, condvar) = &*self.state;
1002
- let mut state = lock.lock().expect("blocking gate lock should not poison");
1003
- if !state.block_next {
1004
- return;
1005
- }
1006
- state.block_next = false;
1007
- state.blocked = true;
1008
- condvar.notify_all();
1009
- let deadline = Instant::now() + TEST_WAIT_TIMEOUT;
1010
- while !state.released {
1011
- let remaining = deadline.saturating_duration_since(Instant::now());
1012
- assert!(
1013
- !remaining.is_zero(),
1014
- "timed out waiting for blocking gate release"
1015
- );
1016
- let (next_state, wait_result) = condvar
1017
- .wait_timeout(state, remaining)
1018
- .expect("blocking gate lock should not poison after wait");
1019
- state = next_state;
1020
- assert!(
1021
- !wait_result.timed_out() || state.released,
1022
- "timed out waiting for blocking gate release"
1023
- );
1024
- }
1025
- }
1026
-
1027
- fn wait_until_blocked(&self) {
1028
- let (lock, condvar) = &*self.state;
1029
- let mut state = lock.lock().expect("blocking gate lock should not poison");
1030
- let deadline = Instant::now() + TEST_WAIT_TIMEOUT;
1031
- while !state.blocked {
1032
- let remaining = deadline.saturating_duration_since(Instant::now());
1033
- assert!(!remaining.is_zero(), "timed out waiting for blocking gate");
1034
- let (next_state, wait_result) = condvar
1035
- .wait_timeout(state, remaining)
1036
- .expect("blocking gate lock should not poison after wait");
1037
- state = next_state;
1038
- assert!(
1039
- !wait_result.timed_out() || state.blocked,
1040
- "timed out waiting for blocking gate"
1041
- );
1042
- }
1043
- }
1044
-
1045
- fn release(&self) {
1046
- let (lock, condvar) = &*self.state;
1047
- let mut state = lock.lock().expect("blocking gate lock should not poison");
1048
- state.released = true;
1049
- condvar.notify_all();
1050
- }
1051
- }
1052
-
1053
- #[derive(Default)]
1054
- struct BlockingGateState {
1055
- block_next: bool,
1056
- blocked: bool,
1057
- released: bool,
1058
- }
1059
- }