@lix-js/sdk 0.6.0-preview.3 → 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 (235) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +105 -65
  3. package/dist/engine-wasm/index.js +4 -4
  4. package/dist/engine-wasm/wasm/lix_engine.d.ts +30 -6
  5. package/dist/engine-wasm/wasm/lix_engine.js +187 -117
  6. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  7. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +14 -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 +42 -28
  11. package/dist/open-lix.js +49 -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 +819 -124
  94. package/dist-engine-src/src/session/create_branch.rs +94 -0
  95. package/dist-engine-src/src/session/execute.rs +260 -57
  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 +19 -16
  102. package/dist-engine-src/src/session/switch_branch.rs +113 -0
  103. package/dist-engine-src/src/session/transaction.rs +557 -0
  104. package/dist-engine-src/src/sql2/bind/classify.rs +102 -0
  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} +98 -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 +4 -5
  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 +30 -24
  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 -109
  218. package/dist-engine-src/src/sql2/classify.rs +0 -182
  219. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  220. package/dist-engine-src/src/sql2/execute.rs +0 -3440
  221. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  222. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  223. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -166
  224. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -25
  225. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  226. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  227. package/dist-engine-src/src/storage/types.rs +0 -501
  228. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  229. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  230. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  231. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  232. package/dist-engine-src/src/version/mod.rs +0 -13
  233. package/dist-engine-src/src/version/refs.rs +0 -330
  234. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  235. 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,12 @@ 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};
10
+ use super::transaction::SessionTransaction;
9
11
 
10
12
  /// Result of executing one SQL statement through engine.
11
13
  ///
@@ -286,30 +288,38 @@ impl RowRef<'_> {
286
288
  }
287
289
  }
288
290
 
289
- 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
+ {
290
297
  /// Executes one DataFusion SQL statement against this Lix session.
291
298
  ///
292
299
  /// The SQL dialect is DataFusion SQL, not SQLite SQL. Positional
293
- /// placeholders use `$1`, `$2`, and so on. SQLite-specific catalog tables
300
+ /// placeholders use `?` or `$1`, `$2`, and so on. SQLite-specific catalog tables
294
301
  /// and transaction statements such as `sqlite_master`, `BEGIN`, and
295
302
  /// `COMMIT` are not part of this contract; use `information_schema` for
296
303
  /// catalog inspection. Lix owns transaction boundaries for each statement.
297
304
  pub async fn execute(&self, sql: &str, params: &[Value]) -> Result<ExecuteResult, LixError> {
298
305
  self.ensure_open()?;
299
- let kind = sql2::classify_statement(sql)?;
300
- if kind == sql2::SqlStatementKind::Write {
301
- let sql = sql.to_string();
302
- let sql_for_error = sql.clone();
306
+ let statement = sql2::parse_statement(sql)?;
307
+ if sql2::bind_statement_route(&statement)? == sql2::BoundStatementRoute::Write {
308
+ let write_access = self.begin_session_write_access().await?;
309
+ let sql_for_error = sql.to_string();
303
310
  let params = params.to_vec();
304
311
  return self
305
- .with_write_transaction(|transaction| {
312
+ .with_write_transaction_reserved(write_access, |transaction| {
306
313
  Box::pin(async move {
307
314
  // Re-plan against the transaction-backed write
308
315
  // session so provider hooks read and stage through the
309
316
  // transaction-owned SQL write context.
310
- let tx_plan = sql2::create_write_logical_plan(transaction, &sql).await?;
311
- let result = sql2::execute_logical_plan(tx_plan, &params).await?;
312
- let affected_rows = affected_rows_from_query_result(result)?;
317
+ transaction.prepare_sql_visible_schemas().await?;
318
+ let tx_plan =
319
+ sql2::create_write_logical_plan_from_parsed(transaction, statement)
320
+ .await?;
321
+ let affected_rows =
322
+ sql2::execute_write_logical_plan(transaction, tx_plan, &params).await?;
313
323
  Ok(ExecuteResult::from_rows_affected(affected_rows))
314
324
  })
315
325
  })
@@ -317,30 +327,39 @@ impl SessionContext {
317
327
  .map_err(|error| normalize_sql_surface_error(error, &sql_for_error));
318
328
  }
319
329
 
320
- 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())?;
321
341
  let read_result = async {
322
342
  let mut read_store = read_scope.store();
323
343
  let live_state: Arc<dyn crate::live_state::LiveStateReader> =
324
344
  Arc::new(self.live_state.reader(read_store.clone()));
325
345
  let runtime_functions = FunctionContext::prepare(live_state.as_ref()).await?;
326
346
  let functions = runtime_functions.provider();
327
- 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?;
328
348
  let visible_schemas = self
329
349
  .catalog_context
330
- .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)
331
351
  .await?;
332
352
  let ctx = SessionSqlExecutionContext {
333
- active_version_id: &active_version_id,
353
+ active_branch_id: &active_branch_id,
334
354
  read_store,
335
355
  live_state: Arc::clone(&self.live_state),
336
356
  binary_cas: Arc::clone(&self.binary_cas),
337
- commit_store: Arc::clone(&self.commit_store),
338
- version_ctx: Arc::clone(&self.version_ctx),
357
+ branch_ctx: Arc::clone(&self.branch_ctx),
339
358
  visible_schemas,
340
359
  functions: functions.clone(),
341
360
  };
342
361
 
343
- let plan = sql2::create_logical_plan(&ctx, sql).await?;
362
+ let plan = sql2::create_logical_plan_from_parsed(&ctx, sql, statement).await?;
344
363
  let result = sql2::execute_logical_plan(plan, params).await?;
345
364
  drop(ctx);
346
365
  drop(live_state);
@@ -348,19 +367,55 @@ impl SessionContext {
348
367
  };
349
368
  let (runtime_functions, result) = match read_result.await {
350
369
  Ok(result) => {
351
- read_scope.rollback().await?;
370
+ read_scope.close()?;
352
371
  result
353
372
  }
354
373
  Err(error) => {
355
- let _ = read_scope.rollback().await;
374
+ let _ = read_scope.close();
356
375
  return Err(normalize_sql_surface_error(error, sql));
357
376
  }
358
377
  };
359
- self.persist_runtime_functions_if_needed(&runtime_functions)
378
+ self.persist_runtime_functions_if_needed(&runtime_functions, runtime_write_access.as_ref())
360
379
  .await?;
361
380
  Ok(ExecuteResult::from_sql_query_result(result))
362
381
  }
363
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
+
364
419
  /// Persists execution-scoped runtime function state after a successful read.
365
420
  ///
366
421
  /// Reads do not otherwise own a write transaction, but SQL functions such as
@@ -370,19 +425,198 @@ impl SessionContext {
370
425
  async fn persist_runtime_functions_if_needed(
371
426
  &self,
372
427
  runtime_functions: &FunctionContext,
428
+ runtime_write_access: Option<&SessionWriteAccess>,
373
429
  ) -> Result<(), LixError> {
374
- let mut transaction = self.storage.begin_write_transaction().await?;
375
430
  let mut writes = StorageWriteSet::new();
376
431
  runtime_functions
377
432
  .stage_persist_if_needed(&mut writes)
378
433
  .await?;
379
- if !writes.is_empty() {
380
- writes.apply(&mut transaction.as_mut()).await?;
434
+ if writes.is_empty() {
435
+ return Ok(());
436
+ }
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
+ ));
381
442
  }
382
- transaction.commit().await
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(())
383
451
  }
384
452
  }
385
453
 
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
+ {
460
+ /// Executes one SQL statement inside this transaction.
461
+ ///
462
+ /// Write statements are staged until `commit()`. Read statements use the
463
+ /// transaction overlay, so they can observe writes staged by earlier calls
464
+ /// on this transaction handle.
465
+ pub async fn execute(
466
+ &mut self,
467
+ sql: &str,
468
+ params: &[Value],
469
+ ) -> Result<ExecuteResult, LixError> {
470
+ let _operation_guard = self.begin_session_operation()?;
471
+ let statement = sql2::parse_statement(sql)?;
472
+ let transaction = self.transaction_mut()?;
473
+ match sql2::bind_statement_route(&statement)? {
474
+ sql2::BoundStatementRoute::Write => {
475
+ execute_transaction_write_auto(transaction, statement, params)
476
+ .await
477
+ .map_err(|error| normalize_sql_surface_error(error, sql))
478
+ }
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
+ };
503
+ Ok(ExecuteResult::from_sql_query_result(result))
504
+ }
505
+ }
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))
579
+ }
580
+
581
+ #[cfg(test)]
582
+ async fn execute_transaction_write_with_mode<B>(
583
+ transaction: &mut crate::transaction::Transaction<B>,
584
+ statement: datafusion::sql::parser::Statement,
585
+ params: &[Value],
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?;
594
+ let tx_plan = sql2::create_write_logical_plan_from_parsed(transaction, statement).await?;
595
+ let affected_rows =
596
+ sql2::execute_write_logical_plan_with_mode(transaction, tx_plan, params, mode).await?;
597
+ Ok(ExecuteResult::from_rows_affected(affected_rows))
598
+ }
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
+
386
620
  fn normalize_sql_surface_error(error: LixError, sql: &str) -> LixError {
387
621
  if error.code.starts_with("LIX_ERROR_PATH_") && sql_uses_public_filesystem_path_surface(sql) {
388
622
  return LixError {
@@ -401,15 +635,6 @@ fn normalize_sql_surface_error(error: LixError, sql: &str) -> LixError {
401
635
  ..error
402
636
  };
403
637
  }
404
- if error.code == LixError::CODE_FOREIGN_KEY {
405
- let lower = error.message.to_ascii_lowercase();
406
- if lower.contains("schema 'lix_version_ref'") && lower.contains("target 'lix_commit.") {
407
- return LixError {
408
- code: LixError::CODE_VERSION_NOT_FOUND.to_string(),
409
- ..error
410
- };
411
- }
412
- }
413
638
  error
414
639
  }
415
640
 
@@ -418,28 +643,6 @@ fn sql_uses_public_filesystem_path_surface(sql: &str) -> bool {
418
643
  (lower.contains("lix_file") || lower.contains("lix_directory")) && lower.contains("path")
419
644
  }
420
645
 
421
- fn affected_rows_from_query_result(result: SqlQueryResult) -> Result<u64, LixError> {
422
- let Some(first_row) = result.rows.first() else {
423
- return Ok(0);
424
- };
425
- let Some(first_value) = first_row.first() else {
426
- return Ok(0);
427
- };
428
- match first_value {
429
- Value::Integer(value) if *value >= 0 => Ok(*value as u64),
430
- Value::Text(value) => value.parse::<u64>().map_err(|error| {
431
- LixError::new(
432
- "LIX_ERROR_UNKNOWN",
433
- format!("failed to parse affected row count from SQL result: {error}"),
434
- )
435
- }),
436
- other => Err(LixError::new(
437
- "LIX_ERROR_UNKNOWN",
438
- format!("expected affected row count, got {other:?}"),
439
- )),
440
- }
441
- }
442
-
443
646
  #[cfg(test)]
444
647
  mod tests {
445
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