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

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 (234) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +65 -64
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -5
  5. package/dist/engine-wasm/wasm/lix_engine.js +130 -118
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +9 -8
  8. package/dist/generated/builtin-schemas.d.ts +69 -69
  9. package/dist/generated/builtin-schemas.js +94 -94
  10. package/dist/open-lix.d.ts +33 -26
  11. package/dist/open-lix.js +10 -10
  12. package/dist/sqlite/index.js +86 -30
  13. package/dist-engine-src/README.md +3 -3
  14. package/dist-engine-src/src/backend/capabilities.rs +67 -0
  15. package/dist-engine-src/src/backend/conformance/baseline.rs +1127 -0
  16. package/dist-engine-src/src/backend/conformance/factory.rs +93 -0
  17. package/dist-engine-src/src/backend/conformance/failure_tests.rs +608 -0
  18. package/dist-engine-src/src/backend/conformance/fixtures.rs +26 -0
  19. package/dist-engine-src/src/backend/conformance/mod.rs +75 -0
  20. package/dist-engine-src/src/backend/conformance/model.rs +28 -0
  21. package/dist-engine-src/src/backend/conformance/model_based.rs +257 -0
  22. package/dist-engine-src/src/backend/conformance/persistence.rs +204 -0
  23. package/dist-engine-src/src/backend/conformance/projection.rs +21 -0
  24. package/dist-engine-src/src/backend/conformance/pushdown.rs +24 -0
  25. package/dist-engine-src/src/backend/conformance/runner.rs +90 -0
  26. package/dist-engine-src/src/backend/conformance/scan.rs +24 -0
  27. package/dist-engine-src/src/backend/conformance/write.rs +16 -0
  28. package/dist-engine-src/src/backend/error.rs +94 -0
  29. package/dist-engine-src/src/backend/in_memory.rs +670 -0
  30. package/dist-engine-src/src/backend/mod.rs +36 -9
  31. package/dist-engine-src/src/backend/predicate.rs +80 -0
  32. package/dist-engine-src/src/backend/traits.rs +260 -0
  33. package/dist-engine-src/src/backend/types.rs +224 -81
  34. package/dist-engine-src/src/binary_cas/context.rs +8 -8
  35. package/dist-engine-src/src/binary_cas/kv.rs +234 -259
  36. package/dist-engine-src/src/{version → branch}/context.rs +12 -12
  37. package/dist-engine-src/src/branch/lifecycle.rs +221 -0
  38. package/dist-engine-src/src/branch/mod.rs +13 -0
  39. package/dist-engine-src/src/branch/refs.rs +321 -0
  40. package/dist-engine-src/src/branch/stage_rows.rs +67 -0
  41. package/dist-engine-src/src/branch/types.rs +21 -0
  42. package/dist-engine-src/src/catalog/context.rs +18 -18
  43. package/dist-engine-src/src/catalog/snapshot.rs +8 -8
  44. package/dist-engine-src/src/changelog/bench_support.rs +785 -0
  45. package/dist-engine-src/src/changelog/change.rs +1 -0
  46. package/dist-engine-src/src/changelog/codec.rs +497 -0
  47. package/dist-engine-src/src/changelog/commit.rs +1 -0
  48. package/dist-engine-src/src/changelog/context.rs +1614 -0
  49. package/dist-engine-src/src/changelog/mod.rs +29 -0
  50. package/dist-engine-src/src/changelog/store.rs +163 -0
  51. package/dist-engine-src/src/changelog/test_support.rs +54 -0
  52. package/dist-engine-src/src/changelog/types.rs +213 -0
  53. package/dist-engine-src/src/commit_graph/context.rs +317 -274
  54. package/dist-engine-src/src/commit_graph/mod.rs +2 -4
  55. package/dist-engine-src/src/commit_graph/types.rs +22 -42
  56. package/dist-engine-src/src/commit_graph/walker.rs +133 -103
  57. package/dist-engine-src/src/common/error.rs +52 -18
  58. package/dist-engine-src/src/common/identity.rs +2 -2
  59. package/dist-engine-src/src/common/mod.rs +1 -1
  60. package/dist-engine-src/src/domain.rs +42 -46
  61. package/dist-engine-src/src/engine.rs +74 -96
  62. package/dist-engine-src/src/{entity_identity.rs → entity_pk.rs} +89 -92
  63. package/dist-engine-src/src/functions/context.rs +56 -52
  64. package/dist-engine-src/src/functions/state.rs +51 -52
  65. package/dist-engine-src/src/init.rs +288 -154
  66. package/dist-engine-src/src/json_store/context.rs +15 -266
  67. package/dist-engine-src/src/json_store/mod.rs +26 -0
  68. package/dist-engine-src/src/json_store/store.rs +103 -718
  69. package/dist-engine-src/src/json_store/types.rs +4 -9
  70. package/dist-engine-src/src/lib.rs +49 -19
  71. package/dist-engine-src/src/live_state/context.rs +654 -790
  72. package/dist-engine-src/src/live_state/mod.rs +9 -3
  73. package/dist-engine-src/src/live_state/overlay.rs +4 -4
  74. package/dist-engine-src/src/live_state/types.rs +30 -21
  75. package/dist-engine-src/src/live_state/visibility.rs +514 -71
  76. package/dist-engine-src/src/plugin/install.rs +48 -48
  77. package/dist-engine-src/src/plugin/manifest.rs +7 -7
  78. package/dist-engine-src/src/plugin/materializer.rs +0 -275
  79. package/dist-engine-src/src/plugin/plugin_manifest.json +4 -3
  80. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +2 -2
  81. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +34 -0
  82. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +48 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change.json +3 -3
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +1 -1
  85. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +6 -6
  86. package/dist-engine-src/src/schema/builtin/mod.rs +18 -20
  87. package/dist-engine-src/src/schema/compatibility.rs +11 -11
  88. package/dist-engine-src/src/schema/definition.json +2 -2
  89. package/dist-engine-src/src/schema/definition.rs +5 -5
  90. package/dist-engine-src/src/schema/key.rs +3 -3
  91. package/dist-engine-src/src/schema/mod.rs +1 -1
  92. package/dist-engine-src/src/schema/tests.rs +18 -18
  93. package/dist-engine-src/src/session/context.rs +803 -148
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +223 -83
  96. package/dist-engine-src/src/session/merge/analysis.rs +9 -3
  97. package/dist-engine-src/src/session/merge/{version.rs → branch.rs} +119 -129
  98. package/dist-engine-src/src/session/merge/conflicts.rs +2 -2
  99. package/dist-engine-src/src/session/merge/mod.rs +5 -6
  100. package/dist-engine-src/src/session/merge/stats.rs +7 -11
  101. package/dist-engine-src/src/session/mod.rs +15 -12
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +495 -14
  104. package/dist-engine-src/src/sql2/{classify.rs → bind/classify.rs} +3 -75
  105. package/dist-engine-src/src/sql2/bind/error.rs +5 -0
  106. package/dist-engine-src/src/sql2/bind/expr.rs +29 -0
  107. package/dist-engine-src/src/sql2/bind/mod.rs +12 -0
  108. package/dist-engine-src/src/sql2/{udfs/public_call.rs → bind/public_udf.rs} +71 -3
  109. package/dist-engine-src/src/sql2/bind/read.rs +65 -0
  110. package/dist-engine-src/src/sql2/bind/statement.rs +2236 -0
  111. package/dist-engine-src/src/sql2/bind/table.rs +273 -0
  112. package/dist-engine-src/src/sql2/bind/write.rs +86 -0
  113. package/dist-engine-src/src/sql2/branch_scope.rs +436 -0
  114. package/dist-engine-src/src/sql2/catalog/capability.rs +20 -0
  115. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +296 -0
  116. package/dist-engine-src/src/sql2/catalog/mod.rs +15 -0
  117. package/dist-engine-src/src/sql2/catalog/registry.rs +556 -0
  118. package/dist-engine-src/src/sql2/catalog/schema.rs +88 -0
  119. package/dist-engine-src/src/sql2/catalog/surface.rs +41 -0
  120. package/dist-engine-src/src/sql2/change_materialization.rs +122 -0
  121. package/dist-engine-src/src/sql2/context.rs +36 -30
  122. package/dist-engine-src/src/sql2/error.rs +1 -1
  123. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +1593 -0
  124. package/dist-engine-src/src/sql2/exec/datafusion.rs +5266 -0
  125. package/dist-engine-src/src/sql2/exec/fast_write.rs +82 -0
  126. package/dist-engine-src/src/sql2/exec/mod.rs +24 -0
  127. package/dist-engine-src/src/sql2/exec/write.rs +661 -0
  128. package/dist-engine-src/src/sql2/filesystem_planner.rs +72 -77
  129. package/dist-engine-src/src/sql2/filesystem_visibility.rs +21 -21
  130. package/dist-engine-src/src/sql2/history_projection.rs +8 -8
  131. package/dist-engine-src/src/sql2/history_route.rs +35 -31
  132. package/dist-engine-src/src/sql2/mod.rs +28 -23
  133. package/dist-engine-src/src/sql2/optimize/datafusion.rs +1 -0
  134. package/dist-engine-src/src/sql2/optimize/mod.rs +2 -0
  135. package/dist-engine-src/src/sql2/optimize/simple_write.rs +116 -0
  136. package/dist-engine-src/src/sql2/parse/mod.rs +69 -0
  137. package/dist-engine-src/src/sql2/parse/normalize.rs +1 -0
  138. package/dist-engine-src/src/sql2/plan/branch_scope.rs +24 -0
  139. package/dist-engine-src/src/sql2/plan/mod.rs +5 -0
  140. package/dist-engine-src/src/sql2/plan/predicate.rs +22 -0
  141. package/dist-engine-src/src/sql2/plan/write.rs +147 -0
  142. package/dist-engine-src/src/sql2/predicate_typecheck.rs +258 -0
  143. package/dist-engine-src/src/sql2/{version_provider.rs → providers/branch.rs} +218 -214
  144. package/dist-engine-src/src/sql2/{change_provider.rs → providers/change.rs} +156 -42
  145. package/dist-engine-src/src/sql2/{directory_provider.rs → providers/directory.rs} +291 -322
  146. package/dist-engine-src/src/sql2/{directory_history_provider.rs → providers/directory_history.rs} +56 -42
  147. package/dist-engine-src/src/sql2/providers/entity.rs +1484 -0
  148. package/dist-engine-src/src/sql2/{entity_history_provider.rs → providers/entity_history.rs} +43 -31
  149. package/dist-engine-src/src/sql2/{file_provider.rs → providers/file.rs} +323 -316
  150. package/dist-engine-src/src/sql2/{file_history_provider.rs → providers/file_history.rs} +60 -46
  151. package/dist-engine-src/src/sql2/{history_provider.rs → providers/history.rs} +46 -32
  152. package/dist-engine-src/src/sql2/{lix_state_provider.rs → providers/lix_state.rs} +359 -329
  153. package/dist-engine-src/src/sql2/providers/mod.rs +508 -0
  154. package/dist-engine-src/src/sql2/read_only.rs +2 -2
  155. package/dist-engine-src/src/sql2/session.rs +47 -96
  156. package/dist-engine-src/src/sql2/storage/constraints.rs +1 -0
  157. package/dist-engine-src/src/sql2/storage/mod.rs +1 -0
  158. package/dist-engine-src/src/sql2/test_support/differential.rs +712 -0
  159. package/dist-engine-src/src/sql2/test_support/generators.rs +354 -0
  160. package/dist-engine-src/src/sql2/test_support/mod.rs +2 -0
  161. package/dist-engine-src/src/sql2/udfs/{lix_active_version_commit_id.rs → lix_active_branch_commit_id.rs} +7 -7
  162. package/dist-engine-src/src/sql2/udfs/mod.rs +3 -6
  163. package/dist-engine-src/src/sql2/write_normalization.rs +45 -22
  164. package/dist-engine-src/src/storage/conformance.rs +399 -0
  165. package/dist-engine-src/src/storage/context.rs +552 -288
  166. package/dist-engine-src/src/storage/mod.rs +48 -10
  167. package/dist-engine-src/src/storage/point.rs +440 -0
  168. package/dist-engine-src/src/storage/read_scope.rs +43 -64
  169. package/dist-engine-src/src/storage/reader.rs +867 -0
  170. package/dist-engine-src/src/storage/scan.rs +784 -0
  171. package/dist-engine-src/src/storage/spaces.rs +236 -0
  172. package/dist-engine-src/src/storage/stats.rs +80 -0
  173. package/dist-engine-src/src/storage/write_set.rs +962 -0
  174. package/dist-engine-src/src/storage_bench.rs +136 -4828
  175. package/dist-engine-src/src/test_support.rs +360 -138
  176. package/dist-engine-src/src/tracked_state/bench_support.rs +394 -0
  177. package/dist-engine-src/src/tracked_state/codec.rs +155 -1057
  178. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +358 -0
  179. package/dist-engine-src/src/tracked_state/context.rs +1927 -993
  180. package/dist-engine-src/src/tracked_state/diff.rs +1715 -261
  181. package/dist-engine-src/src/tracked_state/merge.rs +74 -88
  182. package/dist-engine-src/src/tracked_state/mod.rs +19 -16
  183. package/dist-engine-src/src/tracked_state/{materialization.rs → row_materialization.rs} +50 -178
  184. package/dist-engine-src/src/tracked_state/storage.rs +243 -191
  185. package/dist-engine-src/src/tracked_state/tree.rs +247 -371
  186. package/dist-engine-src/src/tracked_state/types.rs +49 -42
  187. package/dist-engine-src/src/transaction/bench_support.rs +407 -0
  188. package/dist-engine-src/src/transaction/commit.rs +821 -713
  189. package/dist-engine-src/src/transaction/context.rs +705 -600
  190. package/dist-engine-src/src/transaction/mod.rs +13 -2
  191. package/dist-engine-src/src/transaction/normalization.rs +63 -76
  192. package/dist-engine-src/src/transaction/prep.rs +13 -13
  193. package/dist-engine-src/src/transaction/schema_resolver.rs +19 -5
  194. package/dist-engine-src/src/transaction/staging.rs +228 -434
  195. package/dist-engine-src/src/transaction/types.rs +41 -98
  196. package/dist-engine-src/src/transaction/validation.rs +382 -446
  197. package/dist-engine-src/src/untracked_state/codec.rs +337 -29
  198. package/dist-engine-src/src/untracked_state/context.rs +7 -7
  199. package/dist-engine-src/src/untracked_state/materialization.rs +2 -2
  200. package/dist-engine-src/src/untracked_state/mod.rs +1 -1
  201. package/dist-engine-src/src/untracked_state/storage.rs +659 -157
  202. package/dist-engine-src/src/untracked_state/types.rs +21 -21
  203. package/package.json +71 -68
  204. package/dist-engine-src/src/backend/kv.rs +0 -358
  205. package/dist-engine-src/src/backend/testing.rs +0 -658
  206. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  207. package/dist-engine-src/src/commit_store/context.rs +0 -944
  208. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  209. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  210. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  211. package/dist-engine-src/src/commit_store/types.rs +0 -215
  212. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  213. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  214. package/dist-engine-src/src/session/create_version.rs +0 -88
  215. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  216. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  217. package/dist-engine-src/src/session/switch_version.rs +0 -110
  218. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  219. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  220. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  221. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  222. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  223. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  224. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  225. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  226. package/dist-engine-src/src/storage/types.rs +0 -501
  227. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  228. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  229. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  230. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  231. package/dist-engine-src/src/version/mod.rs +0 -13
  232. package/dist-engine-src/src/version/refs.rs +0 -330
  233. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  234. package/dist-engine-src/src/version/types.rs +0 -21
@@ -1,70 +1,76 @@
1
1
  use std::future::Future;
2
2
  use std::pin::Pin;
3
- use std::sync::atomic::{AtomicBool, Ordering};
4
3
  use std::sync::Arc;
5
4
 
6
5
  use serde_json::Value as JsonValue;
7
6
 
8
7
  use crate::binary_cas::{BinaryCasContext, BlobDataReader};
8
+ use crate::branch::{
9
+ BranchContext, BranchLifecycle, BranchOperation, BranchRefReader, BranchReferenceRole,
10
+ };
9
11
  use crate::catalog::CatalogContext;
10
12
  use crate::commit_graph::{CommitGraphContext, CommitGraphReader};
11
- use crate::commit_store::CommitStoreContext;
12
- use crate::entity_identity::EntityIdentity;
13
+ use crate::entity_pk::EntityPk;
13
14
  use crate::functions::FunctionProviderHandle;
14
15
  use crate::json_store::JsonStoreContext;
15
16
  use crate::live_state::{LiveStateContext, LiveStateReader, LiveStateRowRequest};
16
- use crate::sql2::{CommitStoreQuerySource, SqlCommitStoreQuerySource, SqlExecutionContext};
17
+ use crate::sql2::{
18
+ ChangelogQuerySource, HistoryQuerySource, SqlChangelogQuerySource, SqlExecutionContext,
19
+ SqlHistoryQuerySource,
20
+ };
17
21
  use crate::storage::{
18
- ScopedStorageReader, StorageContext, StorageReadScope, StorageReadTransaction, StorageReader,
22
+ DurableWriteGuard, DurableWriteLock, InMemoryStorageBackend, StorageBackend, StorageReadOptions,
19
23
  };
24
+ use crate::storage::{StorageContext, StorageRead, StorageReadScope};
20
25
  use crate::tracked_state::TrackedStateContext;
21
26
  use crate::transaction::{open_transaction, Transaction};
22
- use crate::version::{
23
- VersionContext, VersionLifecycle, VersionOperation, VersionRefReader, VersionReferenceRole,
24
- };
25
- use crate::GLOBAL_VERSION_ID;
27
+ use crate::GLOBAL_BRANCH_ID;
26
28
  use crate::{LixError, NullableKeyFilter};
27
29
 
28
- use super::transaction::transaction_state_error;
30
+ use super::transaction::{SessionOperationGuard, SessionTransactionManager, SessionWriteLease};
29
31
 
30
- pub(crate) const WORKSPACE_VERSION_KEY: &str = "lix_workspace_version_id";
32
+ pub(crate) const WORKSPACE_BRANCH_KEY: &str = "lix_workspace_branch_id";
31
33
 
32
34
  #[derive(Clone)]
33
35
  pub(crate) enum SessionMode {
34
- Pinned { version_id: String },
36
+ Pinned { branch_id: String },
35
37
  Workspace,
36
38
  }
37
39
 
38
40
  /// Session-context state for engine execution.
39
41
  ///
40
- /// A session context pins the active version selector and shared execution
42
+ /// A session context pins the active branch selector and shared execution
41
43
  /// services. Parent-handle `execute(...)` runs as an implicit single-statement
42
44
  /// transaction. Explicit transactions hold the session execution lease until
43
45
  /// commit or rollback, so all SQL during that window must run through the
44
46
  /// transaction handle.
45
47
  #[derive(Clone)]
46
- pub struct SessionContext {
48
+ pub struct SessionContext<B: StorageBackend = InMemoryStorageBackend> {
47
49
  pub(super) mode: SessionMode,
48
- pub(super) storage: StorageContext,
50
+ pub(super) storage: StorageContext<B>,
49
51
  pub(super) live_state: Arc<LiveStateContext>,
50
52
  pub(super) tracked_state: Arc<TrackedStateContext>,
51
53
  pub(super) binary_cas: Arc<BinaryCasContext>,
52
- pub(super) commit_store: Arc<CommitStoreContext>,
53
- pub(super) version_ctx: Arc<VersionContext>,
54
+ pub(super) branch_ctx: Arc<BranchContext>,
54
55
  pub(super) catalog_context: Arc<CatalogContext>,
55
- closed: Arc<AtomicBool>,
56
- active_transaction: Arc<AtomicBool>,
56
+ pub(super) write_lock: DurableWriteLock,
57
+ transaction_manager: SessionTransactionManager,
57
58
  }
58
59
 
59
- impl SessionContext {
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
+ {
60
66
  pub(crate) async fn open_workspace(
61
- storage: StorageContext,
67
+ storage: StorageContext<B>,
62
68
  live_state: Arc<LiveStateContext>,
63
69
  tracked_state: Arc<TrackedStateContext>,
64
70
  binary_cas: Arc<BinaryCasContext>,
65
- commit_store: Arc<CommitStoreContext>,
66
- version_ctx: Arc<VersionContext>,
71
+ branch_ctx: Arc<BranchContext>,
67
72
  catalog_context: Arc<CatalogContext>,
73
+ write_lock: DurableWriteLock,
68
74
  ) -> Result<Self, LixError> {
69
75
  let session = Self::new(
70
76
  SessionMode::Workspace,
@@ -72,73 +78,71 @@ impl SessionContext {
72
78
  live_state,
73
79
  tracked_state,
74
80
  binary_cas,
75
- commit_store,
76
- version_ctx,
81
+ branch_ctx,
77
82
  catalog_context,
83
+ write_lock,
78
84
  );
79
- session.active_version_id().await?;
85
+ session.active_branch_id().await?;
80
86
  Ok(session)
81
87
  }
82
88
 
83
89
  pub(crate) async fn open(
84
- active_version_id: String,
85
- storage: StorageContext,
90
+ active_branch_id: String,
91
+ storage: StorageContext<B>,
86
92
  live_state: Arc<LiveStateContext>,
87
93
  tracked_state: Arc<TrackedStateContext>,
88
94
  binary_cas: Arc<BinaryCasContext>,
89
- commit_store: Arc<CommitStoreContext>,
90
- version_ctx: Arc<VersionContext>,
95
+ branch_ctx: Arc<BranchContext>,
91
96
  catalog_context: Arc<CatalogContext>,
97
+ write_lock: DurableWriteLock,
92
98
  ) -> Result<Self, LixError> {
93
99
  Ok(Self::new(
94
100
  SessionMode::Pinned {
95
- version_id: active_version_id,
101
+ branch_id: active_branch_id,
96
102
  },
97
103
  storage,
98
104
  live_state,
99
105
  tracked_state,
100
106
  binary_cas,
101
- commit_store,
102
- version_ctx,
107
+ branch_ctx,
103
108
  catalog_context,
109
+ write_lock,
104
110
  ))
105
111
  }
106
112
 
107
113
  pub(super) fn new(
108
114
  mode: SessionMode,
109
- storage: StorageContext,
115
+ storage: StorageContext<B>,
110
116
  live_state: Arc<LiveStateContext>,
111
117
  tracked_state: Arc<TrackedStateContext>,
112
118
  binary_cas: Arc<BinaryCasContext>,
113
- commit_store: Arc<CommitStoreContext>,
114
- version_ctx: Arc<VersionContext>,
119
+ branch_ctx: Arc<BranchContext>,
115
120
  catalog_context: Arc<CatalogContext>,
121
+ write_lock: DurableWriteLock,
116
122
  ) -> Self {
117
- Self::new_with_closed(
123
+ Self::new_with_transaction_manager(
118
124
  mode,
119
125
  storage,
120
126
  live_state,
121
127
  tracked_state,
122
128
  binary_cas,
123
- commit_store,
124
- version_ctx,
129
+ branch_ctx,
125
130
  catalog_context,
126
- Arc::new(AtomicBool::new(false)),
127
- Arc::new(AtomicBool::new(false)),
131
+ write_lock,
132
+ SessionTransactionManager::new(),
128
133
  )
129
134
  }
130
135
 
131
- pub(super) fn new_with_closed(
136
+ pub(super) fn new_with_transaction_manager(
132
137
  mode: SessionMode,
133
- storage: StorageContext,
138
+ storage: StorageContext<B>,
134
139
  live_state: Arc<LiveStateContext>,
135
140
  tracked_state: Arc<TrackedStateContext>,
136
141
  binary_cas: Arc<BinaryCasContext>,
137
- commit_store: Arc<CommitStoreContext>,
138
- version_ctx: Arc<VersionContext>,
142
+ branch_ctx: Arc<BranchContext>,
139
143
  catalog_context: Arc<CatalogContext>,
140
- closed: Arc<AtomicBool>,
141
- active_transaction: Arc<AtomicBool>,
144
+ write_lock: DurableWriteLock,
145
+ transaction_manager: SessionTransactionManager,
142
146
  ) -> Self {
143
147
  Self {
144
148
  mode,
@@ -146,152 +150,194 @@ impl SessionContext {
146
150
  live_state,
147
151
  tracked_state,
148
152
  binary_cas,
149
- commit_store,
150
- version_ctx,
153
+ branch_ctx,
151
154
  catalog_context,
152
- closed,
153
- active_transaction,
155
+ write_lock,
156
+ transaction_manager,
154
157
  }
155
158
  }
156
159
 
157
160
  /// Releases this logical session handle. This is a lifecycle boundary only:
158
161
  /// successful writes are committed before their operation returns.
159
162
  pub async fn close(&self) -> Result<(), LixError> {
160
- self.closed.store(true, Ordering::SeqCst);
161
- Ok(())
163
+ self.transaction_manager.close().await
162
164
  }
163
165
 
164
166
  pub fn is_closed(&self) -> bool {
165
- self.closed.load(Ordering::SeqCst)
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()
166
178
  }
167
179
 
168
- pub(crate) fn closed_flag(&self) -> Arc<AtomicBool> {
169
- Arc::clone(&self.closed)
180
+ #[cfg(test)]
181
+ pub(crate) fn active_transaction_for_test(&self) -> bool {
182
+ self.transaction_manager.active_transaction_for_test()
170
183
  }
171
184
 
172
- pub(crate) fn active_transaction_flag(&self) -> Arc<AtomicBool> {
173
- Arc::clone(&self.active_transaction)
185
+ pub(super) fn transaction_manager(&self) -> SessionTransactionManager {
186
+ self.transaction_manager.clone()
174
187
  }
175
188
 
176
189
  pub(crate) fn ensure_open(&self) -> Result<(), LixError> {
177
- if self.is_closed() {
178
- return Err(closed_error());
179
- }
180
- Ok(())
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)
181
230
  }
182
231
 
183
- /// Resolves the version this session should operate on right now.
232
+ /// Resolves the branch this session should operate on right now.
184
233
  ///
185
- /// This is a read-path helper. Write flows must resolve the active version
234
+ /// This is a read-path helper. Write flows must resolve the active branch
186
235
  /// through the transaction capability so the read is scoped to the
187
236
  /// same backend transaction as the writes it influences.
188
237
  ///
189
- /// Pinned sessions are pure in-memory views over one version. Workspace
238
+ /// Pinned sessions are pure in-memory views over one branch. Workspace
190
239
  /// sessions read the shared workspace selector from untracked global
191
240
  /// `lix_key_value` state so multiple open app sessions can observe the same
192
- /// active workspace version.
193
- pub async fn active_version_id(&self) -> Result<String, LixError> {
194
- let mut transaction = self.storage.begin_read_transaction().await?;
195
- let result = self
196
- .active_version_id_from_reader(transaction.as_mut())
197
- .await;
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;
198
246
  match result {
199
- Ok(version_id) => {
200
- transaction.rollback().await?;
201
- Ok(version_id)
202
- }
203
- Err(error) => {
204
- let _ = transaction.rollback().await;
205
- Err(error)
206
- }
247
+ Ok(branch_id) => Ok(branch_id),
248
+ Err(error) => Err(error),
207
249
  }
208
250
  }
209
251
 
210
- pub(super) async fn active_version_id_from_reader<S>(
252
+ pub(super) async fn active_branch_id_from_reader<S>(
211
253
  &self,
212
- reader: &mut S,
254
+ reader: &S,
213
255
  ) -> Result<String, LixError>
214
256
  where
215
- S: StorageReader + ?Sized,
257
+ S: StorageRead + Send + Sync + ?Sized,
216
258
  {
217
259
  self.ensure_open()?;
218
260
  match &self.mode {
219
- SessionMode::Pinned { version_id } => Ok(version_id.clone()),
220
- SessionMode::Workspace => self.load_workspace_version_id(reader).await,
261
+ SessionMode::Pinned { branch_id } => Ok(branch_id.clone()),
262
+ SessionMode::Workspace => self.load_workspace_branch_id(reader).await,
221
263
  }
222
264
  }
223
265
 
224
- async fn load_workspace_version_id<S>(&self, reader: &mut S) -> Result<String, LixError>
266
+ async fn load_workspace_branch_id<S>(&self, reader: &S) -> Result<String, LixError>
225
267
  where
226
- S: StorageReader + ?Sized,
268
+ S: StorageRead + Send + Sync + ?Sized,
227
269
  {
228
270
  let row = self
229
271
  .live_state
230
- .reader(&mut *reader)
272
+ .reader(reader)
231
273
  .load_row(&LiveStateRowRequest {
232
274
  schema_key: "lix_key_value".to_string(),
233
- version_id: GLOBAL_VERSION_ID.to_string(),
234
- entity_id: EntityIdentity::single(WORKSPACE_VERSION_KEY),
275
+ branch_id: GLOBAL_BRANCH_ID.to_string(),
276
+ entity_pk: EntityPk::single(WORKSPACE_BRANCH_KEY),
235
277
  file_id: NullableKeyFilter::Null,
236
278
  })
237
279
  .await?
238
280
  .ok_or_else(|| {
239
281
  LixError::new(
240
282
  "LIX_ERROR_UNKNOWN",
241
- "workspace version selector is missing lix_key_value:lix_workspace_version_id",
283
+ "workspace branch selector is missing lix_key_value:lix_workspace_branch_id",
242
284
  )
243
285
  })?;
244
286
  let snapshot_content = row.snapshot_content.as_deref().ok_or_else(|| {
245
287
  LixError::new(
246
288
  "LIX_ERROR_UNKNOWN",
247
- "workspace version selector is missing snapshot_content",
289
+ "workspace branch selector is missing snapshot_content",
248
290
  )
249
291
  })?;
250
292
  let snapshot = serde_json::from_str::<JsonValue>(snapshot_content).map_err(|error| {
251
293
  LixError::new(
252
294
  "LIX_ERROR_UNKNOWN",
253
- format!("workspace version selector snapshot is invalid JSON: {error}"),
295
+ format!("workspace branch selector snapshot is invalid JSON: {error}"),
254
296
  )
255
297
  })?;
256
- let version_id = snapshot
298
+ let branch_id = snapshot
257
299
  .get("value")
258
300
  .and_then(JsonValue::as_str)
259
301
  .filter(|value| !value.is_empty())
260
302
  .ok_or_else(|| {
261
303
  LixError::new(
262
304
  "LIX_ERROR_UNKNOWN",
263
- "workspace version selector value must be a non-empty string",
305
+ "workspace branch selector value must be a non-empty string",
264
306
  )
265
307
  })?
266
308
  .to_string();
267
309
 
268
- let version_ref = self.version_ctx.ref_reader(&mut *reader);
269
- VersionLifecycle::new(&version_ref)
310
+ let branch_ref = self.branch_ctx.ref_reader(reader);
311
+ BranchLifecycle::new(&branch_ref)
270
312
  .require_existing_ref(
271
- &version_id,
272
- VersionOperation::LoadWorkspaceSelector,
273
- VersionReferenceRole::WorkspaceSelector,
313
+ &branch_id,
314
+ BranchOperation::LoadWorkspaceSelector,
315
+ BranchReferenceRole::WorkspaceSelector,
274
316
  )
275
317
  .await?;
276
318
 
277
- Ok(version_id)
319
+ Ok(branch_id)
278
320
  }
279
321
 
280
322
  pub(crate) async fn with_write_transaction<T, F>(&self, f: F) -> Result<T, LixError>
281
323
  where
282
324
  F: for<'tx> FnOnce(
283
- &'tx mut Transaction,
325
+ &'tx mut Transaction<B>,
284
326
  ) -> Pin<Box<dyn Future<Output = Result<T, LixError>> + 'tx>>,
285
327
  {
286
328
  self.ensure_open()?;
287
- let _transaction_guard = self.reserve_session_transaction()?;
288
- self.with_write_transaction_reserved(f).await
329
+ let write_access = self.begin_session_write_access().await?;
330
+ self.with_write_transaction_reserved(write_access, f).await
289
331
  }
290
332
 
291
- pub(crate) async fn with_write_transaction_reserved<T, F>(&self, f: F) -> Result<T, LixError>
333
+ pub(super) async fn with_write_transaction_reserved<T, F>(
334
+ &self,
335
+ _write_access: SessionWriteAccess,
336
+ f: F,
337
+ ) -> Result<T, LixError>
292
338
  where
293
339
  F: for<'tx> FnOnce(
294
- &'tx mut Transaction,
340
+ &'tx mut Transaction<B>,
295
341
  ) -> Pin<Box<dyn Future<Output = Result<T, LixError>> + 'tx>>,
296
342
  {
297
343
  let opened = open_transaction(
@@ -300,85 +346,85 @@ impl SessionContext {
300
346
  Arc::clone(&self.live_state),
301
347
  Arc::clone(&self.tracked_state),
302
348
  Arc::clone(&self.binary_cas),
303
- Arc::clone(&self.commit_store),
304
- Arc::clone(&self.version_ctx),
349
+ Arc::clone(&self.branch_ctx),
305
350
  Arc::clone(&self.catalog_context),
306
351
  )
307
352
  .await?;
353
+ self.ensure_open()?;
308
354
  let mut transaction = opened.transaction;
355
+ transaction.attach_commit_boundary(self.transaction_commit_boundary());
309
356
  let runtime_functions = opened.runtime_functions;
310
357
 
311
358
  match f(&mut transaction).await {
312
359
  Ok(value) => {
360
+ self.ensure_open()?;
313
361
  transaction.commit(&runtime_functions).await?;
314
362
  Ok(value)
315
363
  }
316
- Err(error) => {
317
- let _ = transaction.rollback().await;
318
- Err(error)
319
- }
364
+ Err(error) => Err(error),
320
365
  }
321
366
  }
322
367
 
323
- pub(super) fn reserve_session_transaction(&self) -> Result<SessionTransactionGuard, LixError> {
324
- let active_transaction = self.active_transaction_flag();
325
- if active_transaction
326
- .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
327
- .is_err()
328
- {
329
- return Err(transaction_state_error(
330
- "Lix handle has an active transaction; use the transaction handle for reads and writes until it is committed or rolled back",
331
- ));
332
- }
333
- Ok(SessionTransactionGuard { active_transaction })
368
+ #[cfg(test)]
369
+ pub(super) fn begin_commit(&self) -> crate::transaction::CommitBoundaryGuard {
370
+ self.transaction_manager.begin_commit()
334
371
  }
335
- }
336
372
 
337
- fn closed_error() -> LixError {
338
- LixError::new(LixError::CODE_CLOSED, "Lix handle is closed")
339
- .with_hint("Open a new Lix handle before calling this method.")
373
+ pub(super) fn transaction_commit_boundary(
374
+ &self,
375
+ ) -> crate::transaction::TransactionCommitBoundary {
376
+ self.transaction_manager.transaction_commit_boundary()
377
+ }
340
378
  }
341
379
 
342
- pub(super) struct SessionTransactionGuard {
343
- active_transaction: Arc<AtomicBool>,
380
+ pub(super) struct SessionWriteAccess {
381
+ _write_guard: DurableWriteGuard,
382
+ _write_lease: SessionWriteLease,
344
383
  }
345
384
 
346
- impl Drop for SessionTransactionGuard {
347
- fn drop(&mut self) {
348
- self.active_transaction.store(false, Ordering::SeqCst);
349
- }
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.")
350
388
  }
351
389
 
352
390
  /// Read-only SQL execution context derived from a session.
353
391
  ///
354
392
  /// Write statements re-plan against `Transaction`; this context intentionally
355
393
  /// has no write stager.
356
- pub(super) struct SessionSqlExecutionContext<'a> {
357
- pub(super) active_version_id: &'a str,
358
- pub(super) read_store:
359
- ScopedStorageReader<Box<dyn StorageReadTransaction + Send + Sync + 'static>>,
394
+ pub(super) struct SessionSqlExecutionContext<'a, R> {
395
+ pub(super) active_branch_id: &'a str,
396
+ pub(super) read_store: StorageReadScope<R>,
360
397
  pub(super) live_state: Arc<LiveStateContext>,
361
398
  pub(super) binary_cas: Arc<BinaryCasContext>,
362
- pub(super) commit_store: Arc<CommitStoreContext>,
363
- pub(super) version_ctx: Arc<VersionContext>,
399
+ pub(super) branch_ctx: Arc<BranchContext>,
364
400
  pub(super) visible_schemas: Vec<JsonValue>,
365
401
  pub(super) functions: FunctionProviderHandle,
366
402
  }
367
403
 
368
- impl SqlExecutionContext for SessionSqlExecutionContext<'_> {
369
- fn active_version_id(&self) -> &str {
370
- self.active_version_id
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
371
412
  }
372
413
 
373
414
  fn live_state(&self) -> Arc<dyn LiveStateReader> {
374
415
  Arc::new(self.live_state.reader(self.read_store.clone())) as Arc<dyn LiveStateReader>
375
416
  }
376
417
 
377
- fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
378
- let read_scope = StorageReadScope::new(self.read_store.clone());
379
- CommitStoreQuerySource {
380
- commit_store_reader: Arc::new(self.commit_store.reader(read_scope.store())),
381
- json_reader: JsonStoreContext::new().reader(read_scope.store()),
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()),
382
428
  }
383
429
  }
384
430
 
@@ -386,8 +432,8 @@ impl SqlExecutionContext for SessionSqlExecutionContext<'_> {
386
432
  Box::new(CommitGraphContext::new().reader(self.read_store.clone()))
387
433
  }
388
434
 
389
- fn version_ref(&self) -> Arc<dyn VersionRefReader> {
390
- Arc::new(self.version_ctx.ref_reader(self.read_store.clone()))
435
+ fn branch_ref(&self) -> Arc<dyn BranchRefReader> {
436
+ Arc::new(self.branch_ctx.ref_reader(self.read_store.clone()))
391
437
  }
392
438
 
393
439
  fn functions(&self) -> FunctionProviderHandle {
@@ -402,3 +448,612 @@ impl SqlExecutionContext for SessionSqlExecutionContext<'_> {
402
448
  Ok(self.visible_schemas.clone())
403
449
  }
404
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
+ }