@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
@@ -0,0 +1,94 @@
1
+ use crate::branch::{
2
+ branch_descriptor_stage_row, branch_ref_stage_row, BranchLifecycle, BranchOperation,
3
+ BranchReferenceRole,
4
+ };
5
+ use crate::storage::StorageBackend;
6
+ use crate::transaction::types::{TransactionWrite, TransactionWriteMode};
7
+ use crate::LixError;
8
+
9
+ use super::context::SessionContext;
10
+
11
+ /// Options for creating a new branch from the session's active branch.
12
+ #[derive(Debug, Clone, PartialEq, Eq)]
13
+ pub struct CreateBranchOptions {
14
+ /// Optional caller-provided branch id. If omitted, engine generates one.
15
+ pub id: Option<String>,
16
+ /// User-facing branch name.
17
+ pub name: String,
18
+ /// Optional commit id for the new branch head. If omitted, the current
19
+ /// active branch head is used.
20
+ pub from_commit_id: Option<String>,
21
+ }
22
+
23
+ /// Receipt returned after creating a branch.
24
+ #[derive(Debug, Clone, PartialEq, Eq)]
25
+ pub struct CreateBranchReceipt {
26
+ pub id: String,
27
+ pub name: String,
28
+ pub hidden: bool,
29
+ pub commit_id: String,
30
+ }
31
+
32
+ impl<B> SessionContext<B>
33
+ where
34
+ B: StorageBackend + Clone + Send + Sync + 'static,
35
+ for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
36
+ for<'backend> B::Write<'backend>: Send,
37
+ {
38
+ /// Creates a new branch from this session's current branch head.
39
+ ///
40
+ /// Branch descriptors are tracked global facts so every branch agrees on
41
+ /// which branches exist. Branch refs are untracked global moving pointers,
42
+ /// so creating a ref does not add another changelog fact.
43
+ pub async fn create_branch(
44
+ &self,
45
+ options: CreateBranchOptions,
46
+ ) -> Result<CreateBranchReceipt, LixError> {
47
+ self.with_write_transaction(|transaction| {
48
+ Box::pin(async move {
49
+ let branch_id = options
50
+ .id
51
+ .unwrap_or_else(|| transaction.functions().call_uuid_v7());
52
+ let source_head = if let Some(from_commit_id) = options.from_commit_id {
53
+ let mut commit_graph = transaction.commit_graph_reader();
54
+ BranchLifecycle::require_existing_commit(
55
+ &mut commit_graph,
56
+ &from_commit_id,
57
+ BranchOperation::CreateBranch,
58
+ BranchReferenceRole::CommitSource,
59
+ )
60
+ .await?;
61
+ from_commit_id
62
+ } else {
63
+ let active_branch_id = transaction.active_branch_id().to_string();
64
+ let reader = transaction.branch_ref_reader();
65
+ BranchLifecycle::new(&reader)
66
+ .require_existing_commit_id(
67
+ &active_branch_id,
68
+ BranchOperation::CreateBranch,
69
+ BranchReferenceRole::Source,
70
+ )
71
+ .await?
72
+ };
73
+
74
+ transaction
75
+ .stage_write(TransactionWrite::Rows {
76
+ mode: TransactionWriteMode::Insert,
77
+ rows: vec![
78
+ branch_descriptor_stage_row(&branch_id, &options.name, false),
79
+ branch_ref_stage_row(&branch_id, &source_head),
80
+ ],
81
+ })
82
+ .await?;
83
+
84
+ Ok(CreateBranchReceipt {
85
+ id: branch_id,
86
+ name: options.name,
87
+ hidden: false,
88
+ commit_id: source_head,
89
+ })
90
+ })
91
+ })
92
+ .await
93
+ }
94
+ }
@@ -2,10 +2,11 @@ use std::sync::Arc;
2
2
 
3
3
  use crate::functions::FunctionContext;
4
4
  use crate::sql2;
5
- use crate::storage::{StorageReadScope, StorageWriteSet};
5
+ use crate::storage::{StorageBackend, StorageReadOptions, StorageWriteOptions, StorageWriteSet};
6
+ use crate::transaction::{begin_commit_boundary, commit_at_boundary};
6
7
  use crate::{LixError, LixNotice, SqlQueryResult, Value};
7
8
 
8
- use super::context::{SessionContext, SessionSqlExecutionContext};
9
+ use super::context::{SessionContext, SessionSqlExecutionContext, SessionWriteAccess};
9
10
  use super::transaction::SessionTransaction;
10
11
 
11
12
  /// Result of executing one SQL statement through engine.
@@ -287,7 +288,12 @@ impl RowRef<'_> {
287
288
  }
288
289
  }
289
290
 
290
- impl SessionContext {
291
+ impl<B> SessionContext<B>
292
+ where
293
+ B: StorageBackend + Clone + Send + Sync + 'static,
294
+ for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
295
+ for<'backend> B::Write<'backend>: Send,
296
+ {
291
297
  /// Executes one DataFusion SQL statement against this Lix session.
292
298
  ///
293
299
  /// The SQL dialect is DataFusion SQL, not SQLite SQL. Positional
@@ -297,55 +303,58 @@ impl SessionContext {
297
303
  /// catalog inspection. Lix owns transaction boundaries for each statement.
298
304
  pub async fn execute(&self, sql: &str, params: &[Value]) -> Result<ExecuteResult, LixError> {
299
305
  self.ensure_open()?;
300
- let _transaction_guard = self.reserve_session_transaction()?;
301
306
  let statement = sql2::parse_statement(sql)?;
302
- let kind = sql2::classify_datafusion_statement(&statement);
303
- if kind == sql2::SqlStatementKind::Write {
307
+ if sql2::bind_statement_route(&statement)? == sql2::BoundStatementRoute::Write {
308
+ let write_access = self.begin_session_write_access().await?;
304
309
  let sql_for_error = sql.to_string();
305
310
  let params = params.to_vec();
306
311
  return self
307
- .with_write_transaction_reserved(|transaction| {
312
+ .with_write_transaction_reserved(write_access, |transaction| {
308
313
  Box::pin(async move {
309
314
  // Re-plan against the transaction-backed write
310
315
  // session so provider hooks read and stage through the
311
316
  // transaction-owned SQL write context.
317
+ transaction.prepare_sql_visible_schemas().await?;
312
318
  let tx_plan =
313
319
  sql2::create_write_logical_plan_from_parsed(transaction, statement)
314
320
  .await?;
315
- let result = sql2::execute_logical_plan(tx_plan, &params).await?;
316
- let affected_rows = affected_rows_from_query_result(result)?;
321
+ let affected_rows =
322
+ sql2::execute_write_logical_plan(transaction, tx_plan, &params).await?;
317
323
  Ok(ExecuteResult::from_rows_affected(affected_rows))
318
324
  })
319
325
  })
320
326
  .await
321
327
  .map_err(|error| normalize_sql_surface_error(error, &sql_for_error));
322
328
  }
323
- if kind == sql2::SqlStatementKind::Other {
324
- return Err(LixError::new(
325
- LixError::CODE_UNSUPPORTED_SQL,
326
- "SQL statement is not supported by Lix SQL",
327
- ));
328
- }
329
329
 
330
- let read_scope = StorageReadScope::new(self.storage.begin_read_transaction().await?);
330
+ let runtime_write_access = if sql2::statement_has_durable_runtime_function(&statement) {
331
+ Some(self.begin_session_write_access().await?)
332
+ } else {
333
+ None
334
+ };
335
+ let _operation_guard = if runtime_write_access.is_some() {
336
+ None
337
+ } else {
338
+ Some(self.begin_session_operation()?)
339
+ };
340
+ let read_scope = self.storage.begin_read(StorageReadOptions::default())?;
331
341
  let read_result = async {
332
342
  let mut read_store = read_scope.store();
333
343
  let live_state: Arc<dyn crate::live_state::LiveStateReader> =
334
344
  Arc::new(self.live_state.reader(read_store.clone()));
335
345
  let runtime_functions = FunctionContext::prepare(live_state.as_ref()).await?;
336
346
  let functions = runtime_functions.provider();
337
- let active_version_id = self.active_version_id_from_reader(&mut read_store).await?;
347
+ let active_branch_id = self.active_branch_id_from_reader(&mut read_store).await?;
338
348
  let visible_schemas = self
339
349
  .catalog_context
340
- .schema_jsons_for_sql_read_planning(live_state.as_ref(), &active_version_id)
350
+ .schema_jsons_for_sql_read_planning(live_state.as_ref(), &active_branch_id)
341
351
  .await?;
342
352
  let ctx = SessionSqlExecutionContext {
343
- active_version_id: &active_version_id,
353
+ active_branch_id: &active_branch_id,
344
354
  read_store,
345
355
  live_state: Arc::clone(&self.live_state),
346
356
  binary_cas: Arc::clone(&self.binary_cas),
347
- commit_store: Arc::clone(&self.commit_store),
348
- version_ctx: Arc::clone(&self.version_ctx),
357
+ branch_ctx: Arc::clone(&self.branch_ctx),
349
358
  visible_schemas,
350
359
  functions: functions.clone(),
351
360
  };
@@ -358,19 +367,55 @@ impl SessionContext {
358
367
  };
359
368
  let (runtime_functions, result) = match read_result.await {
360
369
  Ok(result) => {
361
- read_scope.rollback().await?;
370
+ read_scope.close()?;
362
371
  result
363
372
  }
364
373
  Err(error) => {
365
- let _ = read_scope.rollback().await;
374
+ let _ = read_scope.close();
366
375
  return Err(normalize_sql_surface_error(error, sql));
367
376
  }
368
377
  };
369
- self.persist_runtime_functions_if_needed(&runtime_functions)
378
+ self.persist_runtime_functions_if_needed(&runtime_functions, runtime_write_access.as_ref())
370
379
  .await?;
371
380
  Ok(ExecuteResult::from_sql_query_result(result))
372
381
  }
373
382
 
383
+ #[cfg(test)]
384
+ pub(crate) async fn execute_with_write_executor_mode(
385
+ &self,
386
+ sql: &str,
387
+ params: &[Value],
388
+ mode: sql2::WriteExecutorMode,
389
+ ) -> Result<ExecuteResult, LixError> {
390
+ self.ensure_open()?;
391
+ let statement = sql2::parse_statement(sql)?;
392
+ if sql2::bind_statement_route(&statement)? == sql2::BoundStatementRoute::Write {
393
+ let write_access = self.begin_session_write_access().await?;
394
+ let sql_for_error = sql.to_string();
395
+ let params = params.to_vec();
396
+ return self
397
+ .with_write_transaction_reserved(write_access, |transaction| {
398
+ Box::pin(async move {
399
+ transaction.prepare_sql_visible_schemas().await?;
400
+ let tx_plan =
401
+ sql2::create_write_logical_plan_from_parsed(transaction, statement)
402
+ .await?;
403
+ let affected_rows = sql2::execute_write_logical_plan_with_mode(
404
+ transaction,
405
+ tx_plan,
406
+ &params,
407
+ mode,
408
+ )
409
+ .await?;
410
+ Ok(ExecuteResult::from_rows_affected(affected_rows))
411
+ })
412
+ })
413
+ .await
414
+ .map_err(|error| normalize_sql_surface_error(error, &sql_for_error));
415
+ }
416
+ self.execute(sql, params).await
417
+ }
418
+
374
419
  /// Persists execution-scoped runtime function state after a successful read.
375
420
  ///
376
421
  /// Reads do not otherwise own a write transaction, but SQL functions such as
@@ -380,6 +425,7 @@ impl SessionContext {
380
425
  async fn persist_runtime_functions_if_needed(
381
426
  &self,
382
427
  runtime_functions: &FunctionContext,
428
+ runtime_write_access: Option<&SessionWriteAccess>,
383
429
  ) -> Result<(), LixError> {
384
430
  let mut writes = StorageWriteSet::new();
385
431
  runtime_functions
@@ -388,13 +434,29 @@ impl SessionContext {
388
434
  if writes.is_empty() {
389
435
  return Ok(());
390
436
  }
391
- let mut transaction = self.storage.begin_write_transaction().await?;
392
- writes.apply(&mut transaction.as_mut()).await?;
393
- transaction.commit().await
437
+ if runtime_write_access.is_none() {
438
+ return Err(LixError::new(
439
+ LixError::CODE_INTERNAL_ERROR,
440
+ "runtime function state changed without reserved write access",
441
+ ));
442
+ }
443
+ let commit_boundary = self.transaction_commit_boundary();
444
+ let _commit_guard = begin_commit_boundary(Some(&commit_boundary));
445
+ commit_at_boundary(Some(&commit_boundary), || {
446
+ self.storage
447
+ .commit_write_set(writes, StorageWriteOptions::default())?;
448
+ Ok(())
449
+ })?;
450
+ Ok(())
394
451
  }
395
452
  }
396
453
 
397
- impl SessionTransaction {
454
+ impl<B> SessionTransaction<B>
455
+ where
456
+ B: StorageBackend + Clone + Send + Sync + 'static,
457
+ for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
458
+ for<'backend> B::Write<'backend>: Send,
459
+ {
398
460
  /// Executes one SQL statement inside this transaction.
399
461
  ///
400
462
  /// Write statements are staged until `commit()`. Read statements use the
@@ -405,47 +467,156 @@ impl SessionTransaction {
405
467
  sql: &str,
406
468
  params: &[Value],
407
469
  ) -> Result<ExecuteResult, LixError> {
470
+ let _operation_guard = self.begin_session_operation()?;
408
471
  let statement = sql2::parse_statement(sql)?;
409
- let kind = sql2::classify_datafusion_statement(&statement);
410
472
  let transaction = self.transaction_mut()?;
411
- match kind {
412
- sql2::SqlStatementKind::Write => {
413
- execute_transaction_write(transaction, statement, params)
473
+ match sql2::bind_statement_route(&statement)? {
474
+ sql2::BoundStatementRoute::Write => {
475
+ execute_transaction_write_auto(transaction, statement, params)
414
476
  .await
415
477
  .map_err(|error| normalize_sql_surface_error(error, sql))
416
478
  }
417
- sql2::SqlStatementKind::Read => {
418
- let plan = sql2::create_transaction_read_logical_plan_from_parsed(
419
- transaction,
420
- sql,
421
- statement,
422
- )
423
- .await
424
- .map_err(|error| normalize_sql_surface_error(error, sql))?;
425
- let result = sql2::execute_logical_plan(plan, params)
426
- .await
427
- .map_err(|error| normalize_sql_surface_error(error, sql))?;
479
+ sql2::BoundStatementRoute::Read => {
480
+ let read_ctx = transaction.sql_read_execution_context().await?;
481
+ let read_result = async {
482
+ let plan = sql2::create_transaction_read_logical_plan_from_parsed(
483
+ &read_ctx,
484
+ transaction,
485
+ sql,
486
+ statement,
487
+ )
488
+ .await?;
489
+ sql2::execute_logical_plan(plan, params).await
490
+ }
491
+ .await;
492
+ let close_result = read_ctx.close();
493
+ let result = match read_result {
494
+ Ok(result) => {
495
+ close_result?;
496
+ result
497
+ }
498
+ Err(error) => {
499
+ let _ = close_result;
500
+ return Err(normalize_sql_surface_error(error, sql));
501
+ }
502
+ };
428
503
  Ok(ExecuteResult::from_sql_query_result(result))
429
504
  }
430
- sql2::SqlStatementKind::Other => Err(LixError::new(
431
- LixError::CODE_UNSUPPORTED_SQL,
432
- "SQL statement is not supported by Lix SQL",
433
- )),
434
505
  }
435
506
  }
507
+
508
+ #[cfg(test)]
509
+ pub(crate) async fn execute_with_write_executor_mode(
510
+ &mut self,
511
+ sql: &str,
512
+ params: &[Value],
513
+ mode: sql2::WriteExecutorMode,
514
+ ) -> Result<ExecuteResult, LixError> {
515
+ let _operation_guard = self.begin_session_operation()?;
516
+ let statement = sql2::parse_statement(sql)?;
517
+ let transaction = self.transaction_mut()?;
518
+ match sql2::bind_statement_route(&statement)? {
519
+ sql2::BoundStatementRoute::Write => {
520
+ execute_transaction_write_with_mode(transaction, statement, params, mode)
521
+ .await
522
+ .map_err(|error| normalize_sql_surface_error(error, sql))
523
+ }
524
+ sql2::BoundStatementRoute::Read => self.execute(sql, params).await,
525
+ }
526
+ }
527
+
528
+ #[cfg(test)]
529
+ pub(crate) async fn execute_with_write_executor_mode_and_trace(
530
+ &mut self,
531
+ sql: &str,
532
+ params: &[Value],
533
+ mode: sql2::WriteExecutorMode,
534
+ ) -> Result<(ExecuteResult, Option<sql2::WriteExecutorPath>), LixError> {
535
+ let _operation_guard = self.begin_session_operation()?;
536
+ let statement = sql2::parse_statement(sql)?;
537
+ let transaction = self.transaction_mut()?;
538
+ match sql2::bind_statement_route(&statement)? {
539
+ sql2::BoundStatementRoute::Write => {
540
+ execute_transaction_write_with_mode_and_trace(transaction, statement, params, mode)
541
+ .await
542
+ .map_err(|error| normalize_sql_surface_error(error, sql))
543
+ }
544
+ sql2::BoundStatementRoute::Read => {
545
+ self.execute(sql, params).await.map(|result| (result, None))
546
+ }
547
+ }
548
+ }
549
+
550
+ #[cfg(test)]
551
+ pub(crate) async fn scan_live_state_for_test(
552
+ &mut self,
553
+ request: &crate::live_state::LiveStateScanRequest,
554
+ ) -> Result<Vec<crate::live_state::MaterializedLiveStateRow>, LixError> {
555
+ let _operation_guard = self.begin_session_operation()?;
556
+ let transaction = self.transaction_mut()?;
557
+ <crate::transaction::Transaction<B> as sql2::SqlWriteExecutionContext>::scan_live_state(
558
+ transaction,
559
+ request,
560
+ )
561
+ .await
562
+ }
563
+ }
564
+
565
+ async fn execute_transaction_write_auto<B>(
566
+ transaction: &mut crate::transaction::Transaction<B>,
567
+ statement: datafusion::sql::parser::Statement,
568
+ params: &[Value],
569
+ ) -> Result<ExecuteResult, LixError>
570
+ where
571
+ B: StorageBackend + Clone + Send + Sync + 'static,
572
+ for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
573
+ for<'backend> B::Write<'backend>: Send,
574
+ {
575
+ transaction.prepare_sql_visible_schemas().await?;
576
+ let tx_plan = sql2::create_write_logical_plan_from_parsed(transaction, statement).await?;
577
+ let affected_rows = sql2::execute_write_logical_plan(transaction, tx_plan, params).await?;
578
+ Ok(ExecuteResult::from_rows_affected(affected_rows))
436
579
  }
437
580
 
438
- async fn execute_transaction_write(
439
- transaction: &mut crate::transaction::Transaction,
581
+ #[cfg(test)]
582
+ async fn execute_transaction_write_with_mode<B>(
583
+ transaction: &mut crate::transaction::Transaction<B>,
440
584
  statement: datafusion::sql::parser::Statement,
441
585
  params: &[Value],
442
- ) -> Result<ExecuteResult, LixError> {
586
+ mode: sql2::WriteExecutorMode,
587
+ ) -> Result<ExecuteResult, LixError>
588
+ where
589
+ B: StorageBackend + Clone + Send + Sync + 'static,
590
+ for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
591
+ for<'backend> B::Write<'backend>: Send,
592
+ {
593
+ transaction.prepare_sql_visible_schemas().await?;
443
594
  let tx_plan = sql2::create_write_logical_plan_from_parsed(transaction, statement).await?;
444
- let result = sql2::execute_logical_plan(tx_plan, params).await?;
445
- let affected_rows = affected_rows_from_query_result(result)?;
595
+ let affected_rows =
596
+ sql2::execute_write_logical_plan_with_mode(transaction, tx_plan, params, mode).await?;
446
597
  Ok(ExecuteResult::from_rows_affected(affected_rows))
447
598
  }
448
599
 
600
+ #[cfg(test)]
601
+ async fn execute_transaction_write_with_mode_and_trace<B>(
602
+ transaction: &mut crate::transaction::Transaction<B>,
603
+ statement: datafusion::sql::parser::Statement,
604
+ params: &[Value],
605
+ mode: sql2::WriteExecutorMode,
606
+ ) -> Result<(ExecuteResult, Option<sql2::WriteExecutorPath>), LixError>
607
+ where
608
+ B: StorageBackend + Clone + Send + Sync + 'static,
609
+ for<'backend> B::Read<'backend>: Clone + Send + Sync + 'static,
610
+ for<'backend> B::Write<'backend>: Send,
611
+ {
612
+ transaction.prepare_sql_visible_schemas().await?;
613
+ let tx_plan = sql2::create_write_logical_plan_from_parsed(transaction, statement).await?;
614
+ let (affected_rows, path) =
615
+ sql2::execute_write_logical_plan_with_mode_and_trace(transaction, tx_plan, params, mode)
616
+ .await?;
617
+ Ok((ExecuteResult::from_rows_affected(affected_rows), Some(path)))
618
+ }
619
+
449
620
  fn normalize_sql_surface_error(error: LixError, sql: &str) -> LixError {
450
621
  if error.code.starts_with("LIX_ERROR_PATH_") && sql_uses_public_filesystem_path_surface(sql) {
451
622
  return LixError {
@@ -464,15 +635,6 @@ fn normalize_sql_surface_error(error: LixError, sql: &str) -> LixError {
464
635
  ..error
465
636
  };
466
637
  }
467
- if error.code == LixError::CODE_FOREIGN_KEY {
468
- let lower = error.message.to_ascii_lowercase();
469
- if lower.contains("schema 'lix_version_ref'") && lower.contains("target 'lix_commit.") {
470
- return LixError {
471
- code: LixError::CODE_VERSION_NOT_FOUND.to_string(),
472
- ..error
473
- };
474
- }
475
- }
476
638
  error
477
639
  }
478
640
 
@@ -481,28 +643,6 @@ fn sql_uses_public_filesystem_path_surface(sql: &str) -> bool {
481
643
  (lower.contains("lix_file") || lower.contains("lix_directory")) && lower.contains("path")
482
644
  }
483
645
 
484
- fn affected_rows_from_query_result(result: SqlQueryResult) -> Result<u64, LixError> {
485
- let Some(first_row) = result.rows.first() else {
486
- return Ok(0);
487
- };
488
- let Some(first_value) = first_row.first() else {
489
- return Ok(0);
490
- };
491
- match first_value {
492
- Value::Integer(value) if *value >= 0 => Ok(*value as u64),
493
- Value::Text(value) => value.parse::<u64>().map_err(|error| {
494
- LixError::new(
495
- "LIX_ERROR_UNKNOWN",
496
- format!("failed to parse affected row count from SQL result: {error}"),
497
- )
498
- }),
499
- other => Err(LixError::new(
500
- "LIX_ERROR_UNKNOWN",
501
- format!("expected affected row count, got {other:?}"),
502
- )),
503
- }
504
- }
505
-
506
646
  #[cfg(test)]
507
647
  mod tests {
508
648
  use super::*;
@@ -1,4 +1,4 @@
1
- use crate::storage::StorageReader;
1
+ use crate::storage::StorageRead;
2
2
  use crate::tracked_state::{
3
3
  plan_merge, TrackedStateDiff, TrackedStateDiffRequest, TrackedStateMergePlan,
4
4
  TrackedStateStoreReader,
@@ -44,7 +44,7 @@ pub(crate) async fn analyze<S>(
44
44
  commits: MergeCommits,
45
45
  ) -> Result<MergeAnalysis, LixError>
46
46
  where
47
- S: StorageReader,
47
+ S: StorageRead + Send + Sync,
48
48
  {
49
49
  let request = TrackedStateDiffRequest::default();
50
50
  let source_diff = reader
@@ -56,7 +56,13 @@ where
56
56
  TrackedStateDiff::default()
57
57
  } else {
58
58
  reader
59
- .diff_commits(&commits.base_commit_id, &commits.target_commit_id, &request)
59
+ .diff_commits_with_validation(
60
+ &commits.base_commit_id,
61
+ &commits.target_commit_id,
62
+ &request,
63
+ false,
64
+ true,
65
+ )
60
66
  .await?
61
67
  };
62
68