@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
@@ -7,11 +7,12 @@ use datafusion::logical_expr::{Expr, Operator};
7
7
  use tokio::sync::Mutex;
8
8
 
9
9
  use crate::commit_graph::{CommitGraphChangeHistoryRequest, CommitGraphReader};
10
- use crate::entity_identity::EntityIdentity;
10
+ use crate::entity_pk::EntityPk;
11
11
  use crate::LixError;
12
12
 
13
13
  use super::SqlJsonReader;
14
- use crate::commit_store::{materialize_change, MaterializedChange};
14
+ use crate::sql2::change_materialization::{materialize_located_history_change, MaterializedChange};
15
+ use crate::storage::StorageRead;
15
16
 
16
17
  /// Shared routing state for commit-shaped history SQL surfaces.
17
18
  ///
@@ -21,7 +22,7 @@ use crate::commit_store::{materialize_change, MaterializedChange};
21
22
  #[derive(Debug, Clone, Default, PartialEq, Eq)]
22
23
  pub(crate) struct HistoryRoute {
23
24
  pub(crate) start_commit_ids: Vec<String>,
24
- pub(crate) entity_ids: Vec<String>,
25
+ pub(crate) entity_pks: Vec<String>,
25
26
  pub(crate) schema_keys: Vec<String>,
26
27
  pub(crate) file_ids: Vec<String>,
27
28
  pub(crate) min_depth: Option<i64>,
@@ -82,7 +83,7 @@ impl HistoryRoute {
82
83
  pub(crate) fn matches_surface_row(
83
84
  &self,
84
85
  schema_key: &str,
85
- entity_id: &str,
86
+ entity_pk: &str,
86
87
  file_id: Option<&str>,
87
88
  depth: u32,
88
89
  ) -> bool {
@@ -97,11 +98,11 @@ impl HistoryRoute {
97
98
  {
98
99
  return false;
99
100
  }
100
- if !self.entity_ids.is_empty()
101
+ if !self.entity_pks.is_empty()
101
102
  && !self
102
- .entity_ids
103
+ .entity_pks
103
104
  .iter()
104
- .any(|candidate| candidate == entity_id)
105
+ .any(|candidate| candidate == entity_pk)
105
106
  {
106
107
  return false;
107
108
  }
@@ -140,7 +141,7 @@ pub(crate) struct HistoryEntry {
140
141
  pub(crate) depth: u32,
141
142
  }
142
143
 
143
- pub(crate) const HISTORY_COL_ENTITY_ID: &str = "lixcol_entity_id";
144
+ pub(crate) const HISTORY_COL_ENTITY_PK: &str = "lixcol_entity_pk";
144
145
  pub(crate) const HISTORY_COL_SCHEMA_KEY: &str = "lixcol_schema_key";
145
146
  pub(crate) const HISTORY_COL_FILE_ID: &str = "lixcol_file_id";
146
147
  pub(crate) const HISTORY_COL_SNAPSHOT_CONTENT: &str = "lixcol_snapshot_content";
@@ -187,10 +188,10 @@ pub(crate) fn commit_graph_history_request(
187
188
  ) -> Option<CommitGraphChangeHistoryRequest> {
188
189
  let schema_keys = effective_schema_keys(route, schema_keys)?;
189
190
  Some(CommitGraphChangeHistoryRequest {
190
- entity_ids: route
191
- .entity_ids
191
+ entity_pks: route
192
+ .entity_pks
192
193
  .iter()
193
- .filter_map(|entity_id| EntityIdentity::from_json_array_text(entity_id).ok())
194
+ .filter_map(|entity_pk| EntityPk::from_json_array_text(entity_pk).ok())
194
195
  .collect(),
195
196
  schema_keys,
196
197
  file_ids: route.file_ids.clone(),
@@ -200,17 +201,20 @@ pub(crate) fn commit_graph_history_request(
200
201
  })
201
202
  }
202
203
 
203
- /// Loads commit-graph history once for all SQL history providers.
204
+ /// Loads reachability-aware commit-graph history once for all SQL history providers.
204
205
  ///
205
206
  /// Providers pass the schema keys they know how to shape. An empty list means
206
207
  /// "do not constrain by provider schema"; this is what `lix_state_history` uses.
207
- pub(crate) async fn load_history_entries(
208
+ pub(crate) async fn load_history_entries<S>(
208
209
  descriptor: HistoryViewDescriptor<'_>,
209
210
  commit_graph: Arc<Mutex<Box<dyn CommitGraphReader>>>,
210
- mut json_reader: SqlJsonReader,
211
+ mut json_reader: SqlJsonReader<S>,
211
212
  route: &HistoryRoute,
212
213
  schema_keys: Vec<String>,
213
- ) -> Result<Vec<HistoryEntry>, LixError> {
214
+ ) -> Result<Vec<HistoryEntry>, LixError>
215
+ where
216
+ S: StorageRead + Clone + Send + Sync + 'static,
217
+ {
214
218
  if route.is_contradictory() {
215
219
  return Ok(Vec::new());
216
220
  }
@@ -223,7 +227,7 @@ pub(crate) async fn load_history_entries(
223
227
  ),
224
228
  )
225
229
  .with_hint(format!(
226
- "Use WHERE {} = lix_active_version_commit_id() to inspect {} from the active version head.",
230
+ "Use WHERE {} = lix_active_branch_commit_id() to inspect {} from the active branch head.",
227
231
  descriptor.start_commit_column, descriptor.view_name
228
232
  )));
229
233
  }
@@ -252,7 +256,7 @@ pub(crate) async fn load_history_entries(
252
256
  .collect::<BTreeMap<_, _>>();
253
257
 
254
258
  for entry in entries {
255
- let change = materialize_change(&mut json_reader, entry.located_change).await?;
259
+ let change = materialize_located_history_change(&mut json_reader, entry.change).await?;
256
260
  rows.push(HistoryEntry {
257
261
  commit_created_at: commit_created_at_by_id
258
262
  .get(&entry.observed_commit_id)
@@ -366,7 +370,7 @@ fn parse_history_disjunction(
366
370
  #[derive(Debug, Clone, PartialEq, Eq)]
367
371
  enum HistoryFilterTerm {
368
372
  StartCommitIds(Vec<String>),
369
- EntityIds(Vec<String>),
373
+ EntityPks(Vec<String>),
370
374
  SchemaKeys(Vec<String>),
371
375
  FileIds(Vec<String>),
372
376
  MinDepth(i64),
@@ -383,9 +387,9 @@ fn merge_history_disjunction_terms(
383
387
  extend_unique(&mut left, right);
384
388
  Some(HistoryFilterTerm::StartCommitIds(left))
385
389
  }
386
- (HistoryFilterTerm::EntityIds(mut left), HistoryFilterTerm::EntityIds(right)) => {
390
+ (HistoryFilterTerm::EntityPks(mut left), HistoryFilterTerm::EntityPks(right)) => {
387
391
  extend_unique(&mut left, right);
388
- Some(HistoryFilterTerm::EntityIds(left))
392
+ Some(HistoryFilterTerm::EntityPks(left))
389
393
  }
390
394
  (HistoryFilterTerm::FileIds(mut left), HistoryFilterTerm::FileIds(right)) => {
391
395
  extend_unique(&mut left, right);
@@ -419,8 +423,8 @@ fn parse_history_binary_filter(
419
423
  _ => unreachable!(),
420
424
  })
421
425
  }
422
- ("entity_id", Operator::Eq, Expr::Literal(ScalarValue::Utf8(Some(value)), _)) => {
423
- canonical_entity_id_value(value).map(|value| HistoryFilterTerm::EntityIds(vec![value]))
426
+ ("entity_pk", Operator::Eq, Expr::Literal(ScalarValue::Utf8(Some(value)), _)) => {
427
+ canonical_entity_pk_value(value).map(|value| HistoryFilterTerm::EntityPks(vec![value]))
424
428
  }
425
429
  ("depth", Operator::Eq, depth_expr) => {
426
430
  scalar_i64_literal(depth_expr).map(HistoryFilterTerm::ExactDepth)
@@ -464,7 +468,7 @@ fn parse_history_in_list_filter(
464
468
 
465
469
  match column_name {
466
470
  "start_commit_id" => Some(HistoryFilterTerm::StartCommitIds(values)),
467
- "entity_id" => canonical_entity_id_values(values).map(HistoryFilterTerm::EntityIds),
471
+ "entity_pk" => canonical_entity_pk_values(values).map(HistoryFilterTerm::EntityPks),
468
472
  "schema_key" => Some(HistoryFilterTerm::SchemaKeys(values)),
469
473
  "file_id" => Some(HistoryFilterTerm::FileIds(values)),
470
474
  _ => None,
@@ -478,9 +482,9 @@ fn apply_history_filter(expr: &Expr, route: &mut HistoryRoute, column_style: His
478
482
  route.contradictory |=
479
483
  apply_conjunctive_values_filter(&mut route.start_commit_ids, values)
480
484
  }
481
- HistoryFilterTerm::EntityIds(values) => {
485
+ HistoryFilterTerm::EntityPks(values) => {
482
486
  route.contradictory |=
483
- apply_conjunctive_values_filter(&mut route.entity_ids, values)
487
+ apply_conjunctive_values_filter(&mut route.entity_pks, values)
484
488
  }
485
489
  HistoryFilterTerm::SchemaKeys(values) => {
486
490
  route.contradictory |=
@@ -518,15 +522,15 @@ fn apply_conjunctive_values_filter(bucket: &mut Vec<String>, incoming_values: Ve
518
522
  bucket.is_empty()
519
523
  }
520
524
 
521
- fn canonical_entity_id_values(values: Vec<String>) -> Option<Vec<String>> {
525
+ fn canonical_entity_pk_values(values: Vec<String>) -> Option<Vec<String>> {
522
526
  values
523
527
  .into_iter()
524
- .map(|value| canonical_entity_id_value(&value))
528
+ .map(|value| canonical_entity_pk_value(&value))
525
529
  .collect()
526
530
  }
527
531
 
528
- fn canonical_entity_id_value(value: &str) -> Option<String> {
529
- EntityIdentity::from_json_array_text(value)
532
+ fn canonical_entity_pk_value(value: &str) -> Option<String> {
533
+ EntityPk::from_json_array_text(value)
530
534
  .ok()?
531
535
  .as_json_array_text()
532
536
  .ok()
@@ -536,8 +540,8 @@ fn canonical_history_column_name(name: &str, column_style: HistoryColumnStyle) -
536
540
  match (column_style, name) {
537
541
  (HistoryColumnStyle::Bare, "start_commit_id")
538
542
  | (HistoryColumnStyle::Prefixed, "lixcol_start_commit_id") => Some("start_commit_id"),
539
- (HistoryColumnStyle::Bare, "entity_id")
540
- | (HistoryColumnStyle::Prefixed, "lixcol_entity_id") => Some("entity_id"),
543
+ (HistoryColumnStyle::Bare, "entity_pk")
544
+ | (HistoryColumnStyle::Prefixed, "lixcol_entity_pk") => Some("entity_pk"),
541
545
  (HistoryColumnStyle::Bare, "schema_key")
542
546
  | (HistoryColumnStyle::Prefixed, "lixcol_schema_key") => Some("schema_key"),
543
547
  (HistoryColumnStyle::Bare, "file_id")
@@ -1,46 +1,52 @@
1
- mod change_provider;
2
- mod classify;
1
+ mod bind;
2
+ mod branch_scope;
3
+ mod catalog;
4
+ mod change_materialization;
3
5
  mod context;
4
- mod directory_history_provider;
5
- mod directory_provider;
6
6
  mod dml;
7
- mod entity_history_provider;
8
- mod entity_provider;
9
7
  mod error;
10
- mod execute;
11
- mod file_history_provider;
12
- mod file_provider;
8
+ mod exec;
13
9
  mod filesystem_planner;
14
10
  mod filesystem_predicates;
15
11
  mod filesystem_visibility;
16
12
  mod history_projection;
17
- mod history_provider;
18
13
  mod history_route;
19
- mod lix_state_provider;
14
+ mod optimize;
15
+ mod parse;
16
+ mod plan;
20
17
  mod predicate_typecheck;
21
- mod public_bind;
18
+ mod providers;
22
19
  mod read_only;
23
20
  mod record_batch;
24
21
  mod result_metadata;
25
22
  mod runtime;
26
23
  mod session;
24
+ pub(crate) mod storage;
25
+ #[cfg(test)]
26
+ mod test_support;
27
27
  mod udfs;
28
- mod version_provider;
29
- mod version_scope;
30
28
  mod write_normalization;
31
29
 
32
- pub(crate) use classify::{
33
- classify_statement, datafusion_statement_dml_target_table_names,
34
- validate_supported_datafusion_statement_ast, validate_supported_statement_ast,
35
- SqlStatementKind,
30
+ pub(crate) use bind::{
31
+ bind_read_statement, bind_statement, bind_statement_route,
32
+ statement_has_durable_runtime_function, BoundStatementRoute,
36
33
  };
37
34
  pub(crate) use context::{
38
- CommitStoreQuerySource, SqlCommitStoreQuerySource, SqlExecutionContext, SqlJsonReader,
39
- SqlWriteContext, SqlWriteExecutionContext, WriteAccess, WriteContextLiveStateReader,
40
- WriteContextVersionRefReader,
35
+ ChangelogQuerySource, HistoryQuerySource, SqlChangelogQuerySource, SqlExecutionContext,
36
+ SqlHistoryQuerySource, SqlJsonReader, SqlWriteContext, SqlWriteExecutionContext, WriteAccess,
37
+ WriteContextBranchRefReader, WriteContextLiveStateReader,
41
38
  };
42
39
  #[allow(unused_imports)]
43
- pub(crate) use execute::{
44
- create_logical_plan, create_write_logical_plan, execute_logical_plan, execute_sql,
45
- SqlLogicalPlan,
40
+ pub(crate) use exec::{
41
+ create_logical_plan, create_logical_plan_from_parsed,
42
+ create_transaction_read_logical_plan_from_parsed, create_write_logical_plan,
43
+ create_write_logical_plan_from_parsed, execute_logical_plan, execute_sql,
44
+ execute_write_logical_plan, SqlLogicalPlan,
46
45
  };
46
+ #[cfg(test)]
47
+ pub(crate) use exec::{
48
+ execute_write_logical_plan_with_mode, execute_write_logical_plan_with_mode_and_trace,
49
+ WriteExecutorMode, WriteExecutorPath,
50
+ };
51
+ pub(crate) use parse::parse_statement;
52
+ pub(crate) use plan::plan_write;
@@ -0,0 +1 @@
1
+ //! DataFusion optimization hooks for bound sql2 plans.
@@ -0,0 +1,2 @@
1
+ pub(crate) mod datafusion;
2
+ pub(crate) mod simple_write;
@@ -0,0 +1,116 @@
1
+ use crate::sql2::bind::write::{BoundWriteOp, BoundWriteTarget};
2
+ use crate::sql2::plan::branch_scope::BranchScope;
3
+ use crate::sql2::plan::predicate::FilterSet;
4
+ use crate::sql2::plan::LogicalWritePlan;
5
+ use crate::LixError;
6
+
7
+ #[derive(Clone, Debug, Eq, PartialEq)]
8
+ pub(crate) enum FastWritePlan {
9
+ Update(FastUpdatePlan),
10
+ Delete(FastDeletePlan),
11
+ }
12
+
13
+ #[derive(Clone, Debug, Eq, PartialEq)]
14
+ pub(crate) struct FastUpdatePlan;
15
+
16
+ #[derive(Clone, Debug, Eq, PartialEq)]
17
+ pub(crate) struct FastDeletePlan;
18
+
19
+ pub(crate) fn try_make_fast_write_plan(
20
+ plan: &LogicalWritePlan,
21
+ ) -> Result<Option<FastWritePlan>, LixError> {
22
+ if !is_supported_fast_target(plan) || !is_known_no_match(plan) {
23
+ return Ok(None);
24
+ }
25
+
26
+ Ok(match plan.bound.op {
27
+ BoundWriteOp::Insert => None,
28
+ BoundWriteOp::Update => Some(FastWritePlan::Update(FastUpdatePlan)),
29
+ BoundWriteOp::Delete => Some(FastWritePlan::Delete(FastDeletePlan)),
30
+ })
31
+ }
32
+
33
+ fn is_supported_fast_target(plan: &LogicalWritePlan) -> bool {
34
+ matches!(
35
+ plan.bound.target,
36
+ BoundWriteTarget::LixState | BoundWriteTarget::LixStateByBranch
37
+ )
38
+ }
39
+
40
+ fn is_known_no_match(plan: &LogicalWritePlan) -> bool {
41
+ matches!(plan.bound.branch_scope, BranchScope::Empty)
42
+ || matches!(plan.filters.rows, FilterSet::None)
43
+ }
44
+
45
+ #[cfg(test)]
46
+ mod tests {
47
+ use super::*;
48
+ use crate::sql2::bind::bind_statement;
49
+ use crate::sql2::parse_statement;
50
+ use crate::sql2::plan::plan_write;
51
+
52
+ #[test]
53
+ fn try_make_fast_write_plan_declines_column_contradictions() {
54
+ let plan = plan_sql(
55
+ "UPDATE lix_state SET metadata = '{}' \
56
+ WHERE schema_key = 'profile' AND schema_key = 'note'",
57
+ );
58
+
59
+ assert_eq!(
60
+ try_make_fast_write_plan(&plan).expect("optimization should not fail"),
61
+ None
62
+ );
63
+ }
64
+
65
+ #[test]
66
+ fn try_make_fast_write_plan_accepts_false_delete_as_noop() {
67
+ let plan = plan_sql("DELETE FROM lix_state WHERE false");
68
+
69
+ assert_eq!(
70
+ try_make_fast_write_plan(&plan).expect("optimization should not fail"),
71
+ Some(FastWritePlan::Delete(FastDeletePlan))
72
+ );
73
+ }
74
+
75
+ #[test]
76
+ fn try_make_fast_write_plan_declines_complex_update() {
77
+ let plan = plan_sql(
78
+ "UPDATE lix_state SET metadata = lix_json('{\"schema_key\":\"lix_key_value\"}') \
79
+ WHERE metadata = lix_json('{\"source\":\"match\"}')",
80
+ );
81
+
82
+ assert_eq!(
83
+ try_make_fast_write_plan(&plan).expect("optimization should not fail"),
84
+ None
85
+ );
86
+ }
87
+
88
+ #[test]
89
+ fn try_make_fast_write_plan_declines_literal_insert() {
90
+ let plan = plan_sql(
91
+ "INSERT INTO lix_state (entity_pk, schema_key, snapshot_content) \
92
+ VALUES (lix_json('[\"entity-1\"]'), 'lix_key_value', '{}')",
93
+ );
94
+
95
+ assert_eq!(
96
+ try_make_fast_write_plan(&plan).expect("optimization should not fail"),
97
+ None
98
+ );
99
+ }
100
+
101
+ #[test]
102
+ fn try_make_fast_write_plan_declines_unsupported_targets_even_when_no_match() {
103
+ let plan = plan_sql("DELETE FROM lix_branch WHERE false");
104
+
105
+ assert_eq!(
106
+ try_make_fast_write_plan(&plan).expect("optimization should not fail"),
107
+ None
108
+ );
109
+ }
110
+
111
+ fn plan_sql(sql: &str) -> LogicalWritePlan {
112
+ let statement = parse_statement(sql).expect("SQL parses");
113
+ let write = bind_statement(&statement, &[], "branch1").expect("SQL binds");
114
+ plan_write(write).expect("write plans")
115
+ }
116
+ }
@@ -0,0 +1,69 @@
1
+ mod normalize;
2
+
3
+ use datafusion::sql::parser::{DFParserBuilder, Statement as DataFusionStatement};
4
+ use datafusion::sql::sqlparser::dialect::GenericDialect;
5
+ use datafusion::sql::sqlparser::tokenizer::{Token, Tokenizer};
6
+ use serde_json::json;
7
+
8
+ use crate::LixError;
9
+
10
+ pub(crate) fn parse_statement(sql: &str) -> Result<DataFusionStatement, LixError> {
11
+ let dialect = GenericDialect {};
12
+ let mut next_index = 1usize;
13
+ let mut has_anonymous = false;
14
+ let mut explicit_placeholders = Vec::new();
15
+
16
+ let mut tokens = Vec::new();
17
+ Tokenizer::new(&dialect, sql)
18
+ .tokenize_with_location_into_buf_with_mapper(&mut tokens, |mut token_span| {
19
+ if let Token::Placeholder(placeholder) = &token_span.token {
20
+ if placeholder == "?" {
21
+ has_anonymous = true;
22
+ token_span.token = Token::Placeholder(format!("${next_index}"));
23
+ next_index += 1;
24
+ } else {
25
+ explicit_placeholders.push(placeholder.clone());
26
+ }
27
+ }
28
+ token_span
29
+ })
30
+ .map_err(|error| {
31
+ LixError::new(
32
+ LixError::CODE_PARSE_ERROR,
33
+ format!("sql2 SQL tokenize error: {error}"),
34
+ )
35
+ })?;
36
+
37
+ if has_anonymous && !explicit_placeholders.is_empty() {
38
+ return Err(LixError::new(
39
+ LixError::CODE_PARSE_ERROR,
40
+ "SQL mixes anonymous and explicit parameter placeholders",
41
+ )
42
+ .with_hint("Use either anonymous placeholders like ?, ? or numbered placeholders like $1, $2, but not both.")
43
+ .with_details(json!({
44
+ "operation": "execute",
45
+ "explicit_placeholders": explicit_placeholders,
46
+ })));
47
+ }
48
+
49
+ let mut statements = DFParserBuilder::new(tokens)
50
+ .with_dialect(&dialect)
51
+ .build()
52
+ .map_err(crate::sql2::error::datafusion_error_to_lix_error)?
53
+ .parse_statements()
54
+ .map_err(crate::sql2::error::datafusion_error_to_lix_error)?;
55
+
56
+ if statements.len() > 1 {
57
+ return Err(LixError::new(
58
+ LixError::CODE_UNSUPPORTED_SQL,
59
+ "Lix SQL only supports one statement per execute() call",
60
+ ));
61
+ }
62
+
63
+ statements.pop_front().ok_or_else(|| {
64
+ LixError::new(
65
+ LixError::CODE_PARSE_ERROR,
66
+ "sql2 DataFusion error: No SQL statements were provided in the query string",
67
+ )
68
+ })
69
+ }
@@ -0,0 +1 @@
1
+ //! SQL text normalization belongs in parse-time code, before semantic binding.
@@ -0,0 +1,24 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ #[derive(Clone, Debug, Eq, PartialEq)]
4
+ pub(crate) enum BranchScope {
5
+ Active {
6
+ branch_id: String,
7
+ },
8
+ Explicit {
9
+ branch_ids: BTreeSet<String>,
10
+ },
11
+ ExplicitDynamic {
12
+ branch_ids: BTreeSet<String>,
13
+ param_indexes: BTreeSet<usize>,
14
+ },
15
+ ExplicitRequired {
16
+ branch_ids: BTreeSet<String>,
17
+ },
18
+ ExplicitRequiredDynamic {
19
+ branch_ids: BTreeSet<String>,
20
+ param_indexes: BTreeSet<usize>,
21
+ },
22
+ Global,
23
+ Empty,
24
+ }
@@ -0,0 +1,5 @@
1
+ pub(crate) mod branch_scope;
2
+ pub(crate) mod predicate;
3
+ pub(crate) mod write;
4
+
5
+ pub(crate) use write::{plan_write, LogicalWritePlan};
@@ -0,0 +1,22 @@
1
+ use crate::sql2::bind::expr::BoundExpr;
2
+
3
+ #[derive(Clone, Debug, Eq, PartialEq)]
4
+ pub(crate) enum FilterSet {
5
+ All,
6
+ None,
7
+ }
8
+
9
+ #[derive(Clone, Debug, Eq, PartialEq)]
10
+ pub(crate) enum BoundPredicate {
11
+ True,
12
+ False,
13
+ And(Vec<BoundPredicate>),
14
+ Or(Vec<BoundPredicate>),
15
+ Eq(BoundExpr, BoundExpr),
16
+ IsNull(BoundExpr),
17
+ IsNotNull(BoundExpr),
18
+ In {
19
+ expr: BoundExpr,
20
+ values: Vec<BoundExpr>,
21
+ },
22
+ }
@@ -0,0 +1,147 @@
1
+ use crate::sql2::bind::write::BoundWrite;
2
+ use crate::sql2::plan::predicate::{BoundPredicate, FilterSet};
3
+ use crate::LixError;
4
+
5
+ #[derive(Clone, Debug, Eq, PartialEq)]
6
+ pub(crate) struct LogicalWritePlan {
7
+ pub(crate) bound: BoundWrite,
8
+ pub(crate) filters: PlannedWriteFilters,
9
+ }
10
+
11
+ #[derive(Clone, Debug, Eq, PartialEq)]
12
+ pub(crate) struct PlannedWriteFilters {
13
+ pub(crate) rows: FilterSet,
14
+ }
15
+
16
+ pub(crate) fn plan_write(bound: BoundWrite) -> Result<LogicalWritePlan, LixError> {
17
+ let mut filters = PlannedWriteFilters::default();
18
+ collect_predicate_filters(&bound.predicate, &mut filters)?;
19
+
20
+ Ok(LogicalWritePlan { bound, filters })
21
+ }
22
+
23
+ impl Default for PlannedWriteFilters {
24
+ fn default() -> Self {
25
+ Self {
26
+ rows: FilterSet::All,
27
+ }
28
+ }
29
+ }
30
+
31
+ impl PlannedWriteFilters {
32
+ fn set_none(&mut self) {
33
+ self.rows = FilterSet::None;
34
+ }
35
+ }
36
+
37
+ fn collect_predicate_filters(
38
+ predicate: &BoundPredicate,
39
+ filters: &mut PlannedWriteFilters,
40
+ ) -> Result<(), LixError> {
41
+ match predicate {
42
+ BoundPredicate::True => Ok(()),
43
+ BoundPredicate::False => {
44
+ filters.set_none();
45
+ Ok(())
46
+ }
47
+ BoundPredicate::And(predicates) => {
48
+ for predicate in predicates {
49
+ collect_predicate_filters(predicate, filters)?;
50
+ }
51
+ Ok(())
52
+ }
53
+ BoundPredicate::Or(_) => Ok(()),
54
+ BoundPredicate::Eq(_, _)
55
+ | BoundPredicate::IsNull(_)
56
+ | BoundPredicate::IsNotNull(_)
57
+ | BoundPredicate::In { .. } => Ok(()),
58
+ }
59
+ }
60
+
61
+ #[cfg(test)]
62
+ mod tests {
63
+ use super::*;
64
+ use crate::sql2::bind::bind_statement;
65
+ use crate::sql2::parse_statement;
66
+ use crate::sql2::plan::branch_scope::BranchScope;
67
+ use std::collections::BTreeSet;
68
+
69
+ #[test]
70
+ fn plan_write_contradiction_does_not_drop_bound_params() {
71
+ let plan = plan_sql(
72
+ "UPDATE lix_state SET metadata = $1 WHERE schema_key IN ('profile') AND schema_key IN ('note') AND entity_pk = $2",
73
+ );
74
+
75
+ assert_eq!(
76
+ plan.bound.params.params.keys().copied().collect::<Vec<_>>(),
77
+ vec![1, 2]
78
+ );
79
+ }
80
+
81
+ #[test]
82
+ fn plan_write_applies_active_branch_scope_to_base_writes() {
83
+ let plan = plan_sql("DELETE FROM lix_state WHERE schema_key = 'profile'");
84
+
85
+ assert_eq!(
86
+ plan.bound.branch_scope,
87
+ BranchScope::Active {
88
+ branch_id: "branch1".to_string()
89
+ }
90
+ );
91
+ }
92
+
93
+ #[test]
94
+ fn plan_write_keeps_explicit_required_scope_for_by_branch_writes() {
95
+ let plan = plan_sql("DELETE FROM lix_state_by_branch WHERE branch_id IN ('v1', 'v2')");
96
+
97
+ assert_eq!(
98
+ plan.bound.branch_scope,
99
+ BranchScope::ExplicitRequired {
100
+ branch_ids: BTreeSet::from(["v1".to_string(), "v2".to_string()])
101
+ }
102
+ );
103
+ }
104
+
105
+ #[test]
106
+ fn plan_write_false_conjunct_sets_no_match_sentinel() {
107
+ let plan = plan_sql("UPDATE lix_file SET hidden = true WHERE id = 'file1' AND false");
108
+
109
+ assert_eq!(plan.filters.rows, FilterSet::None);
110
+ }
111
+
112
+ #[test]
113
+ fn plan_write_user_column_names_do_not_become_storage_filters() {
114
+ let plan = plan_sql_with_schemas(
115
+ "UPDATE app_doc SET title = 'new' WHERE schema_key = 'draft'",
116
+ &[serde_json::json!({
117
+ "x-lix-key": "app_doc",
118
+ "type": "object",
119
+ "properties": {
120
+ "id": { "type": "string" },
121
+ "schema_key": { "type": "string" },
122
+ "title": { "type": "string" }
123
+ },
124
+ "x-lix-primary-key": ["/id"],
125
+ "required": ["id", "schema_key", "title"],
126
+ "additionalProperties": false
127
+ })],
128
+ );
129
+
130
+ assert_eq!(
131
+ plan.bound.branch_scope,
132
+ BranchScope::Active {
133
+ branch_id: "branch1".to_string()
134
+ }
135
+ );
136
+ }
137
+
138
+ fn plan_sql(sql: &str) -> LogicalWritePlan {
139
+ plan_sql_with_schemas(sql, &[])
140
+ }
141
+
142
+ fn plan_sql_with_schemas(sql: &str, schemas: &[serde_json::Value]) -> LogicalWritePlan {
143
+ let statement = parse_statement(sql).expect("parse SQL");
144
+ let write = bind_statement(&statement, schemas, "branch1").expect("bind SQL");
145
+ plan_write(write).expect("plan write")
146
+ }
147
+ }