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

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 (169) hide show
  1. package/SKILL.md +46 -8
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +25 -1
  3. package/dist/engine-wasm/wasm/lix_engine.js +60 -2
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +5 -0
  6. package/dist/generated/builtin-schemas.d.ts +87 -162
  7. package/dist/generated/builtin-schemas.js +139 -236
  8. package/dist/open-lix.d.ts +10 -3
  9. package/dist/open-lix.js +39 -0
  10. package/dist-engine-src/src/binary_cas/types.rs +0 -6
  11. package/dist-engine-src/src/catalog/context.rs +412 -0
  12. package/dist-engine-src/src/catalog/mod.rs +10 -0
  13. package/dist-engine-src/src/catalog/schema.rs +4 -0
  14. package/dist-engine-src/src/catalog/snapshot.rs +1114 -0
  15. package/dist-engine-src/src/cel/mod.rs +1 -1
  16. package/dist-engine-src/src/cel/provider.rs +1 -1
  17. package/dist-engine-src/src/commit_graph/context.rs +328 -1015
  18. package/dist-engine-src/src/commit_graph/mod.rs +2 -3
  19. package/dist-engine-src/src/commit_graph/types.rs +7 -43
  20. package/dist-engine-src/src/commit_graph/walker.rs +57 -81
  21. package/dist-engine-src/src/commit_store/codec.rs +887 -0
  22. package/dist-engine-src/src/commit_store/context.rs +944 -0
  23. package/dist-engine-src/src/commit_store/materialization.rs +84 -0
  24. package/dist-engine-src/src/commit_store/mod.rs +16 -0
  25. package/dist-engine-src/src/commit_store/storage.rs +600 -0
  26. package/dist-engine-src/src/commit_store/types.rs +215 -0
  27. package/dist-engine-src/src/common/identity.rs +15 -5
  28. package/dist-engine-src/src/common/json_pointer.rs +67 -0
  29. package/dist-engine-src/src/common/metadata.rs +17 -12
  30. package/dist-engine-src/src/common/mod.rs +5 -5
  31. package/dist-engine-src/src/domain.rs +324 -0
  32. package/dist-engine-src/src/engine.rs +29 -43
  33. package/dist-engine-src/src/entity_identity.rs +238 -118
  34. package/dist-engine-src/src/functions/context.rs +17 -52
  35. package/dist-engine-src/src/functions/deterministic.rs +1 -1
  36. package/dist-engine-src/src/functions/mod.rs +1 -1
  37. package/dist-engine-src/src/functions/provider.rs +4 -4
  38. package/dist-engine-src/src/functions/state.rs +39 -66
  39. package/dist-engine-src/src/functions/types.rs +1 -1
  40. package/dist-engine-src/src/init.rs +204 -151
  41. package/dist-engine-src/src/json_store/context.rs +354 -60
  42. package/dist-engine-src/src/json_store/encoded.rs +6 -6
  43. package/dist-engine-src/src/json_store/mod.rs +4 -1
  44. package/dist-engine-src/src/json_store/store.rs +884 -11
  45. package/dist-engine-src/src/json_store/types.rs +166 -1
  46. package/dist-engine-src/src/lib.rs +11 -10
  47. package/dist-engine-src/src/live_state/context.rs +608 -830
  48. package/dist-engine-src/src/live_state/mod.rs +3 -3
  49. package/dist-engine-src/src/live_state/overlay.rs +7 -7
  50. package/dist-engine-src/src/live_state/reader.rs +5 -5
  51. package/dist-engine-src/src/live_state/types.rs +19 -36
  52. package/dist-engine-src/src/live_state/visibility.rs +19 -14
  53. package/dist-engine-src/src/plugin/archive.rs +3 -6
  54. package/dist-engine-src/src/plugin/install.rs +0 -18
  55. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -1
  56. package/dist-engine-src/src/schema/annotations/defaults.rs +2 -7
  57. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -1
  58. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -1
  59. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -1
  60. package/dist-engine-src/src/schema/builtin/lix_change.json +11 -10
  61. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -1
  62. package/dist-engine-src/src/schema/builtin/lix_commit.json +8 -46
  63. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +29 -22
  64. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -1
  65. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -1
  66. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -1
  67. package/dist-engine-src/src/schema/builtin/lix_label.json +10 -3
  68. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +74 -0
  69. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +2 -8
  70. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -1
  71. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -1
  72. package/dist-engine-src/src/schema/builtin/mod.rs +10 -59
  73. package/dist-engine-src/src/schema/compatibility.rs +787 -0
  74. package/dist-engine-src/src/schema/definition.json +47 -17
  75. package/dist-engine-src/src/schema/definition.rs +202 -96
  76. package/dist-engine-src/src/schema/key.rs +9 -77
  77. package/dist-engine-src/src/schema/mod.rs +4 -4
  78. package/dist-engine-src/src/schema/tests.rs +133 -92
  79. package/dist-engine-src/src/session/context.rs +86 -48
  80. package/dist-engine-src/src/session/create_version.rs +22 -14
  81. package/dist-engine-src/src/session/execute.rs +117 -23
  82. package/dist-engine-src/src/session/merge/apply.rs +4 -4
  83. package/dist-engine-src/src/session/merge/conflicts.rs +3 -2
  84. package/dist-engine-src/src/session/merge/stats.rs +1 -1
  85. package/dist-engine-src/src/session/merge/version.rs +35 -45
  86. package/dist-engine-src/src/session/mod.rs +9 -7
  87. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +100 -0
  88. package/dist-engine-src/src/session/switch_version.rs +17 -28
  89. package/dist-engine-src/src/session/transaction.rs +76 -0
  90. package/dist-engine-src/src/sql2/change_provider.rs +14 -20
  91. package/dist-engine-src/src/sql2/classify.rs +75 -48
  92. package/dist-engine-src/src/sql2/context.rs +22 -18
  93. package/dist-engine-src/src/sql2/directory_history_provider.rs +28 -20
  94. package/dist-engine-src/src/sql2/directory_provider.rs +131 -83
  95. package/dist-engine-src/src/sql2/entity_history_provider.rs +10 -14
  96. package/dist-engine-src/src/sql2/entity_provider.rs +680 -169
  97. package/dist-engine-src/src/sql2/error.rs +24 -5
  98. package/dist-engine-src/src/sql2/execute.rs +426 -272
  99. package/dist-engine-src/src/sql2/file_history_provider.rs +29 -21
  100. package/dist-engine-src/src/sql2/file_provider.rs +533 -108
  101. package/dist-engine-src/src/sql2/filesystem_planner.rs +58 -94
  102. package/dist-engine-src/src/sql2/filesystem_visibility.rs +37 -23
  103. package/dist-engine-src/src/sql2/history_projection.rs +3 -27
  104. package/dist-engine-src/src/sql2/history_provider.rs +11 -17
  105. package/dist-engine-src/src/sql2/history_route.rs +22 -8
  106. package/dist-engine-src/src/sql2/lix_state_provider.rs +178 -96
  107. package/dist-engine-src/src/sql2/mod.rs +8 -4
  108. package/dist-engine-src/src/sql2/predicate_typecheck.rs +246 -0
  109. package/dist-engine-src/src/sql2/public_bind/assignment.rs +46 -0
  110. package/dist-engine-src/src/sql2/public_bind/capability.rs +41 -0
  111. package/dist-engine-src/src/sql2/public_bind/dml.rs +172 -0
  112. package/dist-engine-src/src/sql2/public_bind/mod.rs +26 -0
  113. package/dist-engine-src/src/sql2/public_bind/table.rs +168 -0
  114. package/dist-engine-src/src/sql2/read_only.rs +10 -12
  115. package/dist-engine-src/src/sql2/session.rs +7 -10
  116. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +76 -0
  117. package/dist-engine-src/src/sql2/udfs/mod.rs +8 -1
  118. package/dist-engine-src/src/sql2/udfs/public_call.rs +238 -0
  119. package/dist-engine-src/src/sql2/version_provider.rs +46 -31
  120. package/dist-engine-src/src/sql2/version_scope.rs +4 -4
  121. package/dist-engine-src/src/storage_bench.rs +1782 -325
  122. package/dist-engine-src/src/test_support.rs +183 -36
  123. package/dist-engine-src/src/tracked_state/by_file_index.rs +20 -24
  124. package/dist-engine-src/src/tracked_state/codec.rs +1519 -181
  125. package/dist-engine-src/src/tracked_state/context.rs +1155 -271
  126. package/dist-engine-src/src/tracked_state/diff.rs +249 -57
  127. package/dist-engine-src/src/tracked_state/materialization.rs +365 -103
  128. package/dist-engine-src/src/tracked_state/materializer.rs +488 -0
  129. package/dist-engine-src/src/tracked_state/merge.rs +37 -19
  130. package/dist-engine-src/src/tracked_state/mod.rs +8 -7
  131. package/dist-engine-src/src/tracked_state/storage.rs +138 -6
  132. package/dist-engine-src/src/tracked_state/tree.rs +695 -252
  133. package/dist-engine-src/src/tracked_state/types.rs +176 -6
  134. package/dist-engine-src/src/transaction/commit.rs +695 -435
  135. package/dist-engine-src/src/transaction/context.rs +551 -310
  136. package/dist-engine-src/src/transaction/live_state_overlay.rs +9 -8
  137. package/dist-engine-src/src/transaction/mod.rs +2 -0
  138. package/dist-engine-src/src/transaction/normalization.rs +311 -447
  139. package/dist-engine-src/src/transaction/prep.rs +37 -0
  140. package/dist-engine-src/src/transaction/schema_resolver.rs +93 -71
  141. package/dist-engine-src/src/transaction/staging.rs +701 -406
  142. package/dist-engine-src/src/transaction/types.rs +231 -122
  143. package/dist-engine-src/src/transaction/validation.rs +2717 -1698
  144. package/dist-engine-src/src/untracked_state/codec.rs +40 -96
  145. package/dist-engine-src/src/untracked_state/context.rs +21 -5
  146. package/dist-engine-src/src/untracked_state/materialization.rs +10 -104
  147. package/dist-engine-src/src/untracked_state/mod.rs +3 -5
  148. package/dist-engine-src/src/untracked_state/storage.rs +105 -57
  149. package/dist-engine-src/src/untracked_state/types.rs +63 -13
  150. package/dist-engine-src/src/version/context.rs +1 -13
  151. package/dist-engine-src/src/version/lifecycle.rs +221 -0
  152. package/dist-engine-src/src/version/mod.rs +3 -2
  153. package/dist-engine-src/src/version/refs.rs +12 -103
  154. package/dist-engine-src/src/version/stage_rows.rs +15 -19
  155. package/package.json +1 -1
  156. package/dist-engine-src/src/changelog/codec.rs +0 -321
  157. package/dist-engine-src/src/changelog/context.rs +0 -92
  158. package/dist-engine-src/src/changelog/materialization.rs +0 -121
  159. package/dist-engine-src/src/changelog/mod.rs +0 -13
  160. package/dist-engine-src/src/changelog/reader.rs +0 -20
  161. package/dist-engine-src/src/changelog/storage.rs +0 -220
  162. package/dist-engine-src/src/changelog/types.rs +0 -38
  163. package/dist-engine-src/src/schema/builtin/lix_change_set.json +0 -18
  164. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +0 -75
  165. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +0 -63
  166. package/dist-engine-src/src/schema_registry.rs +0 -294
  167. package/dist-engine-src/src/sql2/commit_derived_provider.rs +0 -591
  168. package/dist-engine-src/src/tracked_state/rebuild.rs +0 -771
  169. package/dist-engine-src/src/tracked_state/tree_types.rs +0 -176
@@ -1,15 +1,20 @@
1
1
  use datafusion::arrow::datatypes::Field;
2
2
  use datafusion::arrow::record_batch::RecordBatch;
3
- use datafusion::common::ScalarValue;
3
+ use datafusion::common::metadata::{FieldMetadata, ScalarAndMetadata};
4
+ use datafusion::common::{ParamValues, ScalarValue};
4
5
  use datafusion::logical_expr::{Expr, LogicalPlan, WriteOp};
5
6
  use datafusion::prelude::SessionContext;
7
+ use datafusion::sql::parser::{DFParserBuilder, Statement as DataFusionStatement};
8
+ use datafusion::sql::sqlparser::dialect::GenericDialect;
9
+ use datafusion::sql::sqlparser::tokenizer::{Token, Tokenizer};
6
10
  use serde_json::{json, Value as JsonValue};
7
- use std::collections::{BTreeSet, HashSet};
11
+ use std::collections::{BTreeMap, BTreeSet, HashSet};
8
12
 
9
13
  use crate::schema::schema_key_from_definition;
10
14
  use crate::{LixError, LixNotice, SqlQueryResult, Value};
11
15
 
12
- use super::result_metadata::field_is_json;
16
+ use super::predicate_typecheck::validate_json_predicate_expr_with_dfschema;
17
+ use super::result_metadata::{field_is_json, LIX_VALUE_TYPE_JSON, LIX_VALUE_TYPE_METADATA_KEY};
13
18
  use super::session::{build_read_session, build_write_session};
14
19
  use super::write_normalization::{
15
20
  is_binary_type, lix_file_data_type_lix_error, logical_expr_is_binary_or_null,
@@ -57,14 +62,26 @@ pub(crate) async fn create_logical_plan(
57
62
  ctx: &dyn SqlExecutionContext,
58
63
  sql: &str,
59
64
  ) -> Result<SqlLogicalPlan, LixError> {
60
- super::validate_supported_statement_ast(sql)?;
65
+ let statement = parse_statement(sql)?;
66
+ create_logical_plan_from_parsed(ctx, sql, statement).await
67
+ }
68
+
69
+ pub(crate) fn parse_statement(sql: &str) -> Result<DataFusionStatement, LixError> {
70
+ parse_datafusion_statement(sql)
71
+ }
72
+
73
+ pub(crate) async fn create_logical_plan_from_parsed(
74
+ ctx: &dyn SqlExecutionContext,
75
+ sql: &str,
76
+ statement: DataFusionStatement,
77
+ ) -> Result<SqlLogicalPlan, LixError> {
78
+ validate_public_read_sql_surface(sql)?;
79
+ super::validate_supported_datafusion_statement_ast(&statement)?;
80
+ super::udfs::validate_public_udf_calls_in_datafusion_statement(&statement)?;
61
81
  let session = build_read_session(ctx).await?;
62
- let plan = session
63
- .state()
64
- .create_logical_plan(sql)
65
- .await
66
- .map_err(datafusion_error_to_lix_error)?;
82
+ let plan = create_logical_plan_from_statement(&session, statement).await?;
67
83
  validate_supported_logical_plan(&plan)?;
84
+ validate_json_predicates_in_logical_plan(&plan)?;
68
85
  let kind = classify_logical_plan(&plan);
69
86
  let notices = history_filter_notices(&plan);
70
87
 
@@ -82,15 +99,24 @@ pub(crate) async fn create_write_logical_plan(
82
99
  ctx: &mut dyn SqlWriteExecutionContext,
83
100
  sql: &str,
84
101
  ) -> Result<SqlLogicalPlan, LixError> {
85
- super::validate_supported_statement_ast(sql)?;
86
- reject_read_only_history_view_dml(sql, &ctx.list_visible_schemas()?)?;
102
+ let statement = parse_statement(sql)?;
103
+ create_write_logical_plan_from_parsed(ctx, statement).await
104
+ }
105
+
106
+ pub(crate) async fn create_write_logical_plan_from_parsed(
107
+ ctx: &mut dyn SqlWriteExecutionContext,
108
+ statement: DataFusionStatement,
109
+ ) -> Result<SqlLogicalPlan, LixError> {
110
+ super::udfs::validate_public_udf_calls_in_datafusion_statement(&statement)?;
111
+ let visible_schemas = ctx.list_visible_schemas()?;
112
+ super::public_bind::validate_public_dml_statement(&statement, &visible_schemas)?;
113
+ super::validate_supported_datafusion_statement_ast(&statement)?;
114
+ reject_read_only_history_view_dml_from_statement(&statement, &visible_schemas)?;
87
115
  let session = build_write_session(ctx).await?;
88
- let plan = session
89
- .state()
90
- .create_logical_plan(sql)
91
- .await
92
- .map_err(datafusion_error_to_lix_error)?;
116
+ let plan = create_logical_plan_from_statement(&session, statement).await?;
93
117
  validate_supported_logical_plan(&plan)?;
118
+ super::public_bind::validate_public_dml_plan(&plan, &visible_schemas)?;
119
+ validate_json_predicates_in_logical_plan(&plan)?;
94
120
  let strict_binary_params = validate_strict_lix_file_data_writes(&plan)?;
95
121
  let kind = classify_logical_plan(&plan);
96
122
 
@@ -103,6 +129,141 @@ pub(crate) async fn create_write_logical_plan(
103
129
  })
104
130
  }
105
131
 
132
+ pub(crate) async fn create_transaction_read_logical_plan_from_parsed(
133
+ ctx: &mut dyn SqlWriteExecutionContext,
134
+ sql: &str,
135
+ statement: DataFusionStatement,
136
+ ) -> Result<SqlLogicalPlan, LixError> {
137
+ validate_public_read_sql_surface(sql)?;
138
+ super::validate_supported_datafusion_statement_ast(&statement)?;
139
+ super::udfs::validate_public_udf_calls_in_datafusion_statement(&statement)?;
140
+ let session = build_write_session(ctx).await?;
141
+ let plan = create_logical_plan_from_statement(&session, statement).await?;
142
+ validate_supported_logical_plan(&plan)?;
143
+ validate_json_predicates_in_logical_plan(&plan)?;
144
+ let kind = classify_logical_plan(&plan);
145
+ let notices = history_filter_notices(&plan);
146
+
147
+ Ok(SqlLogicalPlan {
148
+ session,
149
+ plan,
150
+ kind,
151
+ notices,
152
+ strict_binary_params: BTreeSet::new(),
153
+ })
154
+ }
155
+
156
+ fn validate_public_read_sql_surface(sql: &str) -> Result<(), LixError> {
157
+ let normalized = sql.to_ascii_lowercase();
158
+ if normalized.contains("lower(path)") {
159
+ return Err(LixError::new(
160
+ LixError::CODE_UNSUPPORTED_SQL,
161
+ "public column 'path' must be compared directly to a literal or parameter",
162
+ ));
163
+ }
164
+ if normalized.contains("lixcol_version_id")
165
+ && (normalized.contains("= lower(") || normalized.contains(" in (lower("))
166
+ {
167
+ return Err(LixError::new(
168
+ LixError::CODE_UNSUPPORTED_SQL,
169
+ "public column 'lixcol_version_id' must be compared directly to a literal or parameter",
170
+ ));
171
+ }
172
+ Ok(())
173
+ }
174
+
175
+ fn parse_datafusion_statement(sql: &str) -> Result<DataFusionStatement, LixError> {
176
+ let dialect = GenericDialect {};
177
+ let mut next_index = 1usize;
178
+ let mut has_anonymous = false;
179
+ let mut explicit_placeholders = Vec::new();
180
+
181
+ let mut tokens = Vec::new();
182
+ Tokenizer::new(&dialect, sql)
183
+ .tokenize_with_location_into_buf_with_mapper(&mut tokens, |mut token_span| {
184
+ if let Token::Placeholder(placeholder) = &token_span.token {
185
+ if placeholder == "?" {
186
+ has_anonymous = true;
187
+ token_span.token = Token::Placeholder(format!("${next_index}"));
188
+ next_index += 1;
189
+ } else {
190
+ explicit_placeholders.push(placeholder.clone());
191
+ }
192
+ }
193
+ token_span
194
+ })
195
+ .map_err(|error| {
196
+ LixError::new(
197
+ LixError::CODE_PARSE_ERROR,
198
+ format!("sql2 SQL tokenize error: {error}"),
199
+ )
200
+ })?;
201
+
202
+ if has_anonymous && !explicit_placeholders.is_empty() {
203
+ return Err(LixError::new(
204
+ LixError::CODE_PARSE_ERROR,
205
+ "SQL mixes anonymous and explicit parameter placeholders",
206
+ )
207
+ .with_hint("Use either anonymous placeholders like ?, ? or numbered placeholders like $1, $2, but not both.")
208
+ .with_details(json!({
209
+ "operation": "execute",
210
+ "explicit_placeholders": explicit_placeholders,
211
+ })));
212
+ }
213
+
214
+ let mut statements = DFParserBuilder::new(tokens)
215
+ .with_dialect(&dialect)
216
+ .build()
217
+ .map_err(datafusion_error_to_lix_error)?
218
+ .parse_statements()
219
+ .map_err(datafusion_error_to_lix_error)?;
220
+
221
+ if statements.len() > 1 {
222
+ return Err(LixError::new(
223
+ LixError::CODE_UNSUPPORTED_SQL,
224
+ "Lix SQL only supports one statement per execute() call",
225
+ ));
226
+ }
227
+
228
+ statements.pop_front().ok_or_else(|| {
229
+ LixError::new(
230
+ LixError::CODE_PARSE_ERROR,
231
+ "sql2 DataFusion error: No SQL statements were provided in the query string",
232
+ )
233
+ })
234
+ }
235
+
236
+ async fn create_logical_plan_from_statement(
237
+ session: &SessionContext,
238
+ statement: DataFusionStatement,
239
+ ) -> Result<LogicalPlan, LixError> {
240
+ session
241
+ .state()
242
+ .statement_to_plan(statement)
243
+ .await
244
+ .map_err(datafusion_error_to_lix_error)
245
+ }
246
+
247
+ fn validate_json_predicates_in_logical_plan(plan: &LogicalPlan) -> Result<(), LixError> {
248
+ match plan {
249
+ LogicalPlan::Filter(filter) => {
250
+ validate_json_predicate_expr_with_dfschema(filter.input.schema(), &filter.predicate)?;
251
+ }
252
+ LogicalPlan::TableScan(scan) => {
253
+ for filter in &scan.filters {
254
+ validate_json_predicate_expr_with_dfschema(scan.projected_schema.as_ref(), filter)?;
255
+ }
256
+ }
257
+ _ => {}
258
+ }
259
+
260
+ for input in plan.inputs() {
261
+ validate_json_predicates_in_logical_plan(input)?;
262
+ }
263
+
264
+ Ok(())
265
+ }
266
+
106
267
  fn validate_strict_lix_file_data_writes(plan: &LogicalPlan) -> Result<BTreeSet<usize>, LixError> {
107
268
  let mut strict_binary_params = BTreeSet::new();
108
269
  let LogicalPlan::Dml(dml) = plan else {
@@ -200,7 +361,7 @@ fn placeholder_index(id: &str) -> Result<usize, LixError> {
200
361
  LixError::CODE_PARSE_ERROR,
201
362
  format!("unsupported SQL parameter placeholder '{id}'"),
202
363
  )
203
- .with_hint("Use numbered placeholders like $1, $2, ...")
364
+ .with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
204
365
  })
205
366
  }
206
367
 
@@ -224,12 +385,9 @@ pub(crate) async fn execute_logical_plan(
224
385
  .map_err(datafusion_error_to_lix_error)?;
225
386
  if !params.is_empty() {
226
387
  dataframe = dataframe
227
- .with_param_values(
228
- params
229
- .iter()
230
- .map(scalar_value_from_lix_value)
231
- .collect::<Vec<_>>(),
232
- )
388
+ .with_param_values(ParamValues::List(
389
+ params.iter().map(scalar_value_from_lix_value).collect(),
390
+ ))
233
391
  .map_err(datafusion_error_to_lix_error)?;
234
392
  }
235
393
 
@@ -298,7 +456,7 @@ fn expected_positional_parameter_count(
298
456
  LixError::CODE_PARSE_ERROR,
299
457
  format!("unsupported SQL parameter placeholder '{name}'"),
300
458
  )
301
- .with_hint("Use numbered placeholders like $1, $2, ...")
459
+ .with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
302
460
  .with_details(json!({
303
461
  "operation": "execute",
304
462
  "placeholder": name,
@@ -309,7 +467,7 @@ fn expected_positional_parameter_count(
309
467
  LixError::CODE_PARSE_ERROR,
310
468
  "SQL parameter placeholders are 1-indexed",
311
469
  )
312
- .with_hint("Use numbered placeholders like $1, $2, ...")
470
+ .with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
313
471
  .with_details(json!({
314
472
  "operation": "execute",
315
473
  "placeholder": name,
@@ -326,11 +484,11 @@ fn sorted_parameter_names(parameter_names: &HashSet<String>) -> Vec<String> {
326
484
  names
327
485
  }
328
486
 
329
- fn reject_read_only_history_view_dml(
330
- sql: &str,
487
+ fn reject_read_only_history_view_dml_from_statement(
488
+ statement: &DataFusionStatement,
331
489
  visible_schemas: &[JsonValue],
332
490
  ) -> Result<(), LixError> {
333
- let target_names = super::dml_target_table_names(sql)?;
491
+ let target_names = super::datafusion_statement_dml_target_table_names(statement);
334
492
  for target_name in target_names {
335
493
  if is_history_view_name(&target_name, visible_schemas)? {
336
494
  return Err(read_only_history_view_error(&target_name));
@@ -419,18 +577,28 @@ fn validate_supported_logical_plan(plan: &LogicalPlan) -> Result<(), LixError> {
419
577
  Ok(())
420
578
  }
421
579
 
422
- fn scalar_value_from_lix_value(value: &Value) -> ScalarValue {
580
+ fn scalar_value_from_lix_value(value: &Value) -> ScalarAndMetadata {
423
581
  match value {
424
- Value::Null => ScalarValue::Null,
425
- Value::Boolean(value) => ScalarValue::Boolean(Some(*value)),
426
- Value::Integer(value) => ScalarValue::Int64(Some(*value)),
427
- Value::Real(value) => ScalarValue::Float64(Some(*value)),
428
- Value::Text(value) => ScalarValue::Utf8(Some(value.clone())),
429
- Value::Json(value) => ScalarValue::Utf8(Some(value.to_string())),
430
- Value::Blob(value) => ScalarValue::Binary(Some(value.clone())),
582
+ Value::Null => ScalarValue::Null.into(),
583
+ Value::Boolean(value) => ScalarValue::Boolean(Some(*value)).into(),
584
+ Value::Integer(value) => ScalarValue::Int64(Some(*value)).into(),
585
+ Value::Real(value) => ScalarValue::Float64(Some(*value)).into(),
586
+ Value::Text(value) => ScalarValue::Utf8(Some(value.clone())).into(),
587
+ Value::Json(value) => ScalarAndMetadata::new(
588
+ ScalarValue::Utf8(Some(value.to_string())),
589
+ Some(json_field_metadata()),
590
+ ),
591
+ Value::Blob(value) => ScalarValue::Binary(Some(value.clone())).into(),
431
592
  }
432
593
  }
433
594
 
595
+ fn json_field_metadata() -> FieldMetadata {
596
+ FieldMetadata::new(BTreeMap::from([(
597
+ LIX_VALUE_TYPE_METADATA_KEY.to_string(),
598
+ LIX_VALUE_TYPE_JSON.to_string(),
599
+ )]))
600
+ }
601
+
434
602
  fn datafusion_error_to_lix_error(error: datafusion::error::DataFusionError) -> LixError {
435
603
  super::error::datafusion_error_to_lix_error(error)
436
604
  }
@@ -660,27 +828,30 @@ mod tests {
660
828
  SqlWriteExecutionContext,
661
829
  };
662
830
  use crate::binary_cas::BlobDataReader;
663
- use crate::changelog::{CanonicalChange, ChangelogReader, ChangelogScanRequest};
664
831
  use crate::commit_graph::{
665
- CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphChangeSet,
666
- CommitGraphChangeSetElement, CommitGraphCommit, CommitGraphEdge, CommitGraphReader,
667
- ReachableCommitGraphCommit,
832
+ CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphCommit,
833
+ CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
668
834
  };
835
+ use crate::commit_store::CommitStoreContext;
669
836
  use crate::functions::{
670
837
  FunctionProvider, FunctionProviderHandle, SharedFunctionProvider, SystemFunctionProvider,
671
838
  };
672
839
  use crate::json_store::JsonStoreContext;
673
840
  use crate::live_state::{
674
- LiveStateContext, LiveStateReader, LiveStateRow, LiveStateRowRequest, LiveStateScanRequest,
841
+ LiveStateContext, LiveStateReader, LiveStateRowRequest, LiveStateScanRequest,
842
+ MaterializedLiveStateRow,
675
843
  };
676
- use crate::sql2::{ChangelogQuerySource, SqlChangelogQuerySource};
844
+ use crate::sql2::{CommitStoreQuerySource, SqlCommitStoreQuerySource};
677
845
  use crate::storage::{
678
846
  KvEntryPage, KvExistsBatch, KvGetRequest, KvKeyPage, KvScanRequest, KvValueBatch,
679
847
  KvValuePage, StorageContext, StorageReadScope, StorageReadTransaction, StorageReader,
680
848
  StorageWriteSet,
681
849
  };
682
850
  use crate::tracked_state::TrackedStateContext;
683
- use crate::transaction::types::{StageRow, StageWrite, StageWriteOutcome};
851
+ use crate::transaction::prepare_version_ref_row;
852
+ use crate::transaction::types::{
853
+ TransactionWrite, TransactionWriteOutcome, TransactionWriteRow,
854
+ };
684
855
  use crate::untracked_state::UntrackedStateContext;
685
856
  use crate::version::VersionRefReader;
686
857
  use crate::{Engine, ExecuteResult, SessionContext};
@@ -689,10 +860,9 @@ mod tests {
689
860
  struct DummyBlobReader;
690
861
  struct DummyLiveStateReader;
691
862
  struct RowsLiveStateReader {
692
- rows: Vec<LiveStateRow>,
863
+ rows: Vec<MaterializedLiveStateRow>,
693
864
  }
694
865
  struct BackendBlobReader(StorageContext);
695
- struct DummyChangelogReader;
696
866
  struct DummyCommitGraphReader;
697
867
  struct DummyVersionRefReader;
698
868
  struct TestReadTransaction(StorageContext);
@@ -747,7 +917,7 @@ mod tests {
747
917
 
748
918
  #[derive(Clone)]
749
919
  struct CapturedStageWrite {
750
- rows: Vec<StageRow>,
920
+ rows: Vec<TransactionWriteRow>,
751
921
  }
752
922
 
753
923
  impl CapturedStageWrite {
@@ -759,7 +929,7 @@ mod tests {
759
929
  }
760
930
 
761
931
  struct CapturedStageOverlay {
762
- rows: Vec<StageRow>,
932
+ rows: Vec<TransactionWriteRow>,
763
933
  }
764
934
 
765
935
  impl CapturedStageOverlay {
@@ -787,33 +957,31 @@ mod tests {
787
957
  struct CapturedStageRow {
788
958
  entity_id: String,
789
959
  schema_key: String,
790
- schema_version: String,
791
960
  version_id: String,
792
961
  file_id: Option<String>,
793
962
  snapshot_content: Option<String>,
794
- metadata: Option<JsonValue>,
963
+ metadata: Option<String>,
795
964
  global: bool,
796
965
  untracked: bool,
797
966
  tombstone: bool,
798
967
  }
799
968
 
800
- impl From<StageRow> for CapturedStageRow {
801
- fn from(row: StageRow) -> Self {
969
+ impl From<TransactionWriteRow> for CapturedStageRow {
970
+ fn from(row: TransactionWriteRow) -> Self {
802
971
  Self {
803
972
  entity_id: row
804
973
  .entity_id
805
974
  .expect("captured staged row should carry entity_id")
806
- .as_string()
975
+ .as_json_array_text()
807
976
  .expect("captured staged row should project entity_id"),
808
977
  schema_key: row.schema_key,
809
- schema_version: row.schema_version,
810
978
  version_id: row.version_id,
811
979
  file_id: row.file_id,
812
980
  global: row.global,
813
981
  untracked: row.untracked,
814
- tombstone: row.snapshot_content.is_none(),
815
- snapshot_content: row.snapshot_content,
816
- metadata: row.metadata,
982
+ tombstone: row.snapshot.is_none(),
983
+ snapshot_content: row.snapshot.map(|snapshot| snapshot.to_string()),
984
+ metadata: row.metadata.map(|metadata| metadata.to_string()),
817
985
  }
818
986
  }
819
987
  }
@@ -842,13 +1010,13 @@ mod tests {
842
1010
  Arc::clone(&self.blob_reader)
843
1011
  }
844
1012
 
845
- fn changelog_query_source(&self) -> SqlChangelogQuerySource {
1013
+ fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
846
1014
  let base_scope = test_read_scope(StorageContext::new(Arc::new(
847
1015
  crate::backend::testing::UnitTestBackend::new(),
848
1016
  )));
849
1017
  let read_scope = StorageReadScope::new(base_scope.store());
850
- ChangelogQuerySource {
851
- changelog_reader: Arc::new(DummyChangelogReader),
1018
+ CommitStoreQuerySource {
1019
+ commit_store_reader: Arc::new(CommitStoreContext::new().reader(read_scope.store())),
852
1020
  json_reader: JsonStoreContext::new().reader(read_scope.store()),
853
1021
  }
854
1022
  }
@@ -898,7 +1066,7 @@ mod tests {
898
1066
  async fn scan_live_state(
899
1067
  &mut self,
900
1068
  request: &LiveStateScanRequest,
901
- ) -> Result<Vec<LiveStateRow>, LixError> {
1069
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
902
1070
  self.live_state.scan_rows(request).await
903
1071
  }
904
1072
 
@@ -909,23 +1077,26 @@ mod tests {
909
1077
  Ok(Some(format!("commit-{version_id}")))
910
1078
  }
911
1079
 
912
- async fn stage_write(&mut self, write: StageWrite) -> Result<StageWriteOutcome, LixError> {
1080
+ async fn stage_write(
1081
+ &mut self,
1082
+ write: TransactionWrite,
1083
+ ) -> Result<TransactionWriteOutcome, LixError> {
913
1084
  let count = match &write {
914
- StageWrite::Rows { rows, .. } => rows.len() as u64,
915
- StageWrite::RowsWithFileData { count, .. } => *count,
916
- StageWrite::AdoptedChanges { changes } => changes.len() as u64,
1085
+ TransactionWrite::Rows { rows, .. } => rows.len() as u64,
1086
+ TransactionWrite::RowsWithFileData { count, .. } => *count,
1087
+ TransactionWrite::AdoptedChanges { changes } => changes.len() as u64,
917
1088
  };
918
1089
  let rows = match write {
919
- StageWrite::Rows { rows, .. } => rows,
920
- StageWrite::RowsWithFileData { rows, .. } => rows,
921
- StageWrite::AdoptedChanges { .. } => Vec::new(),
1090
+ TransactionWrite::Rows { rows, .. } => rows,
1091
+ TransactionWrite::RowsWithFileData { rows, .. } => rows,
1092
+ TransactionWrite::AdoptedChanges { .. } => Vec::new(),
922
1093
  };
923
1094
  self.staged_writes
924
1095
  .lock()
925
1096
  .expect("staged writes lock")
926
1097
  .deltas
927
1098
  .push(CapturedStageWrite { rows });
928
- Ok(StageWriteOutcome { count })
1099
+ Ok(TransactionWriteOutcome { count })
929
1100
  }
930
1101
  }
931
1102
 
@@ -938,20 +1109,6 @@ mod tests {
938
1109
  execute_logical_plan(plan, params).await
939
1110
  }
940
1111
 
941
- #[async_trait]
942
- impl ChangelogReader for DummyChangelogReader {
943
- async fn load_change(&self, _change_id: &str) -> Result<Option<CanonicalChange>, LixError> {
944
- Ok(None)
945
- }
946
-
947
- async fn scan_changes(
948
- &self,
949
- _request: &ChangelogScanRequest,
950
- ) -> Result<Vec<CanonicalChange>, LixError> {
951
- Ok(Vec::new())
952
- }
953
- }
954
-
955
1112
  #[async_trait]
956
1113
  impl VersionRefReader for DummyVersionRefReader {
957
1114
  async fn load_head(
@@ -1009,17 +1166,6 @@ mod tests {
1009
1166
  Vec::new()
1010
1167
  }
1011
1168
 
1012
- fn change_sets(&self, _commits: &[CommitGraphCommit]) -> Vec<CommitGraphChangeSet> {
1013
- Vec::new()
1014
- }
1015
-
1016
- async fn change_set_elements(
1017
- &mut self,
1018
- _commits: &[CommitGraphCommit],
1019
- ) -> Result<Vec<CommitGraphChangeSetElement>, LixError> {
1020
- Ok(Vec::new())
1021
- }
1022
-
1023
1169
  async fn change_history_from_commit(
1024
1170
  &mut self,
1025
1171
  _start_commit_id: &str,
@@ -1034,14 +1180,14 @@ mod tests {
1034
1180
  async fn scan_rows(
1035
1181
  &self,
1036
1182
  _request: &LiveStateScanRequest,
1037
- ) -> Result<Vec<LiveStateRow>, LixError> {
1183
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
1038
1184
  Ok(vec![])
1039
1185
  }
1040
1186
 
1041
1187
  async fn load_row(
1042
1188
  &self,
1043
1189
  _request: &LiveStateRowRequest,
1044
- ) -> Result<Option<LiveStateRow>, LixError> {
1190
+ ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
1045
1191
  Ok(None)
1046
1192
  }
1047
1193
  }
@@ -1051,14 +1197,14 @@ mod tests {
1051
1197
  async fn scan_rows(
1052
1198
  &self,
1053
1199
  _request: &LiveStateScanRequest,
1054
- ) -> Result<Vec<LiveStateRow>, LixError> {
1200
+ ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
1055
1201
  Ok(self.rows.clone())
1056
1202
  }
1057
1203
 
1058
1204
  async fn load_row(
1059
1205
  &self,
1060
1206
  _request: &LiveStateRowRequest,
1061
- ) -> Result<Option<LiveStateRow>, LixError> {
1207
+ ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
1062
1208
  Ok(None)
1063
1209
  }
1064
1210
  }
@@ -1069,7 +1215,10 @@ mod tests {
1069
1215
  &self,
1070
1216
  hashes: &[crate::binary_cas::BlobHash],
1071
1217
  ) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
1072
- Ok(crate::binary_cas::BlobBytesBatch::missing(hashes.len()))
1218
+ Ok(crate::binary_cas::BlobBytesBatch::new(vec![
1219
+ None;
1220
+ hashes.len()
1221
+ ]))
1073
1222
  }
1074
1223
  }
1075
1224
 
@@ -1085,17 +1234,14 @@ mod tests {
1085
1234
  }
1086
1235
  }
1087
1236
 
1088
- fn live_lix_state_row(entity_id: &str, metadata: Option<&str>) -> LiveStateRow {
1089
- LiveStateRow {
1090
- entity_id: crate::entity_identity::EntityIdentity::from_string(entity_id)
1091
- .expect("entity id should decode"),
1237
+ fn live_lix_state_row(entity_id: &str, metadata: Option<&str>) -> MaterializedLiveStateRow {
1238
+ MaterializedLiveStateRow {
1239
+ entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1092
1240
  schema_key: "lix_key_value".to_string(),
1093
1241
  file_id: None,
1094
1242
  snapshot_content: Some("{\"key\":\"hello\",\"value\":\"world\"}".to_string()),
1095
- metadata: metadata.map(|value| {
1096
- serde_json::from_str(value).expect("test metadata should be valid JSON")
1097
- }),
1098
- schema_version: "1".to_string(),
1243
+ metadata: metadata.map(str::to_string),
1244
+ deleted: false,
1099
1245
  version_id: "version-a".to_string(),
1100
1246
  change_id: Some(format!("change-{entity_id}")),
1101
1247
  commit_id: Some(format!("commit-{entity_id}")),
@@ -1106,15 +1252,14 @@ mod tests {
1106
1252
  }
1107
1253
  }
1108
1254
 
1109
- fn live_entity_row(entity_id: &str, version_id: &str, value: &str) -> LiveStateRow {
1110
- LiveStateRow {
1111
- entity_id: crate::entity_identity::EntityIdentity::from_string(entity_id)
1112
- .expect("entity id should decode"),
1255
+ fn live_entity_row(entity_id: &str, version_id: &str, value: &str) -> MaterializedLiveStateRow {
1256
+ MaterializedLiveStateRow {
1257
+ entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1113
1258
  schema_key: "test_state_schema".to_string(),
1114
1259
  file_id: None,
1115
1260
  snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
1116
- metadata: Some(json!({ "source": entity_id })),
1117
- schema_version: "1".to_string(),
1261
+ metadata: Some(json!({ "source": entity_id }).to_string()),
1262
+ deleted: false,
1118
1263
  version_id: version_id.to_string(),
1119
1264
  change_id: Some(format!("change-{entity_id}")),
1120
1265
  commit_id: Some(format!("commit-{entity_id}")),
@@ -1131,10 +1276,9 @@ mod tests {
1131
1276
  parent_id: Option<&str>,
1132
1277
  name: &str,
1133
1278
  hidden: bool,
1134
- ) -> LiveStateRow {
1135
- LiveStateRow {
1136
- entity_id: crate::entity_identity::EntityIdentity::from_string(entity_id)
1137
- .expect("entity id should decode"),
1279
+ ) -> MaterializedLiveStateRow {
1280
+ MaterializedLiveStateRow {
1281
+ entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1138
1282
  schema_key: "lix_directory_descriptor".to_string(),
1139
1283
  file_id: None,
1140
1284
  snapshot_content: Some(
@@ -1146,8 +1290,8 @@ mod tests {
1146
1290
  })
1147
1291
  .to_string(),
1148
1292
  ),
1149
- metadata: Some(json!({ "source": entity_id })),
1150
- schema_version: "1".to_string(),
1293
+ metadata: Some(json!({ "source": entity_id }).to_string()),
1294
+ deleted: false,
1151
1295
  version_id: version_id.to_string(),
1152
1296
  change_id: Some(format!("change-{entity_id}")),
1153
1297
  commit_id: Some(format!("commit-{entity_id}")),
@@ -1164,10 +1308,9 @@ mod tests {
1164
1308
  directory_id: Option<&str>,
1165
1309
  name: &str,
1166
1310
  hidden: bool,
1167
- ) -> LiveStateRow {
1168
- LiveStateRow {
1169
- entity_id: crate::entity_identity::EntityIdentity::from_string(entity_id)
1170
- .expect("entity id should decode"),
1311
+ ) -> MaterializedLiveStateRow {
1312
+ MaterializedLiveStateRow {
1313
+ entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1171
1314
  schema_key: "lix_file_descriptor".to_string(),
1172
1315
  file_id: None,
1173
1316
  snapshot_content: Some(
@@ -1179,8 +1322,8 @@ mod tests {
1179
1322
  })
1180
1323
  .to_string(),
1181
1324
  ),
1182
- metadata: Some(json!({ "source": entity_id })),
1183
- schema_version: "1".to_string(),
1325
+ metadata: Some(json!({ "source": entity_id }).to_string()),
1326
+ deleted: false,
1184
1327
  version_id: version_id.to_string(),
1185
1328
  change_id: Some(format!("change-{entity_id}")),
1186
1329
  commit_id: Some(format!("commit-{entity_id}")),
@@ -1317,7 +1460,7 @@ mod tests {
1317
1460
  }));
1318
1461
  }
1319
1462
 
1320
- async fn setup_engine2_history_fixture() -> Result<(SessionContext, String), LixError> {
1463
+ async fn setup_engine_history_fixture() -> Result<(SessionContext, String), LixError> {
1321
1464
  let backend = crate::backend::testing::UnitTestBackend::new();
1322
1465
  let init_receipt = Engine::initialize(Box::new(backend.clone())).await?;
1323
1466
  let engine = Engine::new(Box::new(backend)).await?;
@@ -1327,9 +1470,9 @@ mod tests {
1327
1470
  .execute(
1328
1471
  "INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
1329
1472
  VALUES (\
1330
- lix_json('{\"x-lix-key\":\"test_state_schema\",\"x-lix-version\":\"1\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}},\"required\":[\"value\",\"count\"],\"additionalProperties\":false}'),\
1473
+ lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}},\"required\":[\"value\",\"count\"],\"additionalProperties\":false}'),\
1331
1474
  false,\
1332
- true\
1475
+ false\
1333
1476
  )",
1334
1477
  &[],
1335
1478
  )
@@ -1337,8 +1480,8 @@ mod tests {
1337
1480
  session
1338
1481
  .execute(
1339
1482
  "INSERT INTO test_state_schema \
1340
- (lixcol_entity_id, value, count, lixcol_metadata, lixcol_untracked) \
1341
- VALUES ('entity-history', 'A', 7, '{\"source\":\"history\"}', false)",
1483
+ (lixcol_entity_id, value, count, lixcol_metadata, lixcol_untracked) \
1484
+ VALUES (lix_json('[\"entity-history\"]'), 'A', 7, '{\"source\":\"history\"}', false)",
1342
1485
  &[],
1343
1486
  )
1344
1487
  .await?;
@@ -1616,23 +1759,23 @@ mod tests {
1616
1759
 
1617
1760
  #[tokio::test]
1618
1761
  async fn execute_sql_reads_lix_state_history_from_history_context() {
1619
- let (session, head_commit_id) = setup_engine2_history_fixture()
1762
+ let (session, head_commit_id) = setup_engine_history_fixture()
1620
1763
  .await
1621
1764
  .expect("history fixture should initialize");
1622
1765
  let result = session
1623
1766
  .execute(
1624
1767
  &format!(
1625
1768
  "SELECT entity_id, snapshot_content, metadata, depth, start_commit_id \
1626
- FROM lix_state_history \
1627
- WHERE schema_key = 'test_state_schema' \
1628
- AND entity_id = 'entity-history' \
1629
- AND start_commit_id = '{head_commit_id}' \
1630
- AND depth >= 0"
1769
+ FROM lix_state_history \
1770
+ WHERE schema_key = 'test_state_schema' \
1771
+ AND entity_id = lix_json('[\"entity-history\"]') \
1772
+ AND start_commit_id = '{head_commit_id}' \
1773
+ AND depth >= 0"
1631
1774
  ),
1632
1775
  &[],
1633
1776
  )
1634
1777
  .await
1635
- .expect("sql2 execute should read lix_state_history through real engine2 context");
1778
+ .expect("sql2 execute should read lix_state_history through real engine context");
1636
1779
  let (columns, rows) = rows_from_execute_result(result);
1637
1780
 
1638
1781
  assert_eq!(
@@ -1646,7 +1789,7 @@ mod tests {
1646
1789
  ]
1647
1790
  );
1648
1791
  assert_eq!(rows.len(), 1);
1649
- assert_eq!(rows[0][0], Value::Text("entity-history".to_string()));
1792
+ assert_eq!(rows[0][0], Value::Json(json!(["entity-history"])));
1650
1793
  assert_eq!(rows[0][1], Value::Json(json!({"count": 7, "value": "A"})));
1651
1794
  assert_eq!(rows[0][2], Value::Json(json!({"source": "history"})));
1652
1795
  assert!(matches!(rows[0][3], Value::Integer(_)));
@@ -1655,21 +1798,21 @@ mod tests {
1655
1798
 
1656
1799
  #[tokio::test]
1657
1800
  async fn execute_sql_reads_entity_history_view_from_history_context() {
1658
- let (session, head_commit_id) = setup_engine2_history_fixture()
1801
+ let (session, head_commit_id) = setup_engine_history_fixture()
1659
1802
  .await
1660
1803
  .expect("history fixture should initialize");
1661
1804
  let result = session
1662
1805
  .execute(
1663
1806
  &format!(
1664
1807
  "SELECT value, count, lixcol_entity_id, lixcol_start_commit_id, lixcol_depth \
1665
- FROM test_state_schema_history \
1666
- WHERE lixcol_start_commit_id = '{head_commit_id}' \
1667
- AND lixcol_entity_id = 'entity-history'"
1808
+ FROM test_state_schema_history \
1809
+ WHERE lixcol_start_commit_id = '{head_commit_id}' \
1810
+ AND lixcol_entity_id = lix_json('[\"entity-history\"]')"
1668
1811
  ),
1669
1812
  &[],
1670
1813
  )
1671
1814
  .await
1672
- .expect("sql2 execute should read entity history through real engine2 context");
1815
+ .expect("sql2 execute should read entity history through real engine context");
1673
1816
  let (columns, rows) = rows_from_execute_result(result);
1674
1817
 
1675
1818
  assert_eq!(
@@ -1685,14 +1828,14 @@ mod tests {
1685
1828
  assert_eq!(rows.len(), 1);
1686
1829
  assert_eq!(rows[0][0], Value::Text("A".to_string()));
1687
1830
  assert_eq!(rows[0][1], Value::Integer(7));
1688
- assert_eq!(rows[0][2], Value::Text("entity-history".to_string()));
1831
+ assert_eq!(rows[0][2], Value::Json(json!(["entity-history"])));
1689
1832
  assert_eq!(rows[0][3], Value::Text(head_commit_id));
1690
1833
  assert!(matches!(rows[0][4], Value::Integer(_)));
1691
1834
  }
1692
1835
 
1693
1836
  #[tokio::test]
1694
1837
  async fn execute_sql_reads_directory_history_view_from_history_context() {
1695
- let (session, head_commit_id) = setup_engine2_history_fixture()
1838
+ let (session, head_commit_id) = setup_engine_history_fixture()
1696
1839
  .await
1697
1840
  .expect("history fixture should initialize");
1698
1841
  let result = session
@@ -1705,7 +1848,7 @@ mod tests {
1705
1848
  &[],
1706
1849
  )
1707
1850
  .await
1708
- .expect("sql2 execute should read directory history through real engine2 context");
1851
+ .expect("sql2 execute should read directory history through real engine context");
1709
1852
  assert!(
1710
1853
  result.notices().is_empty(),
1711
1854
  "identity-filtered directory history should not emit soft notices"
@@ -1754,7 +1897,7 @@ mod tests {
1754
1897
 
1755
1898
  #[tokio::test]
1756
1899
  async fn execute_sql_reads_file_history_view_from_history_context() {
1757
- let (session, head_commit_id) = setup_engine2_history_fixture()
1900
+ let (session, head_commit_id) = setup_engine_history_fixture()
1758
1901
  .await
1759
1902
  .expect("history fixture should initialize");
1760
1903
  let result = session
@@ -1770,7 +1913,7 @@ mod tests {
1770
1913
  &[],
1771
1914
  )
1772
1915
  .await
1773
- .expect("sql2 execute should read file history through real engine2 context");
1916
+ .expect("sql2 execute should read file history through real engine context");
1774
1917
  assert!(
1775
1918
  result.notices().is_empty(),
1776
1919
  "identity-filtered file history should not emit soft notices"
@@ -1815,6 +1958,36 @@ mod tests {
1815
1958
  );
1816
1959
  }
1817
1960
 
1961
+ #[tokio::test]
1962
+ async fn execute_sql_rejects_writes_to_history_views_before_planning() {
1963
+ for sql in [
1964
+ "DELETE FROM lix_state_history",
1965
+ "DELETE FROM LIX_STATE_HISTORY",
1966
+ "DELETE FROM main.LIX_STATE_HISTORY",
1967
+ ] {
1968
+ let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1969
+ let live_state = Arc::new(DummyLiveStateReader);
1970
+ let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
1971
+ let mut ctx = DummySqlWriteExecutionContext {
1972
+ active_version_id: "version-a",
1973
+ blob_reader,
1974
+ live_state,
1975
+ staged_writes,
1976
+ schema_definitions: vec![],
1977
+ };
1978
+
1979
+ let error = execute_write_sql(&mut ctx, sql, &[])
1980
+ .await
1981
+ .expect_err("history views are read-only");
1982
+
1983
+ assert_eq!(error.code, LixError::CODE_READ_ONLY, "{sql}");
1984
+ assert_eq!(
1985
+ error.message, "DML cannot write read-only history view 'lix_state_history'",
1986
+ "{sql}"
1987
+ );
1988
+ }
1989
+ }
1990
+
1818
1991
  #[tokio::test]
1819
1992
  async fn execute_sql_insert_into_lix_state_values_stages_write() {
1820
1993
  let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
@@ -1829,14 +2002,14 @@ mod tests {
1829
2002
  };
1830
2003
 
1831
2004
  let result = execute_write_sql(
1832
- &mut ctx,
1833
- "INSERT INTO lix_state (\
1834
- entity_id, schema_key, file_id, snapshot_content, metadata, schema_version, global, untracked\
1835
- ) VALUES (\
1836
- 'entity-1', 'lix_key_value', NULL, '{\"key\":\"hello\",\"value\":\"world\"}', '{\"source\":\"sql\"}', '1', false, false\
1837
- )",
1838
- &[],
1839
- )
2005
+ &mut ctx,
2006
+ "INSERT INTO lix_state (\
2007
+ entity_id, schema_key, file_id, snapshot_content, metadata, global, untracked\
2008
+ ) VALUES (\
2009
+ lix_json('[\"entity-1\"]'), 'lix_key_value', NULL, '{\"key\":\"hello\",\"value\":\"world\"}', '{\"source\":\"sql\"}', false, false\
2010
+ )",
2011
+ &[],
2012
+ )
1840
2013
  .await
1841
2014
  .expect("INSERT INTO lix_state VALUES should stage write");
1842
2015
 
@@ -1850,8 +2023,7 @@ mod tests {
1850
2023
  .expect("staged delta should expose pending overlay");
1851
2024
  let rows = overlay.visible_semantic_rows(false, "lix_key_value");
1852
2025
  assert_eq!(rows.len(), 1);
1853
- assert_eq!(rows[0].entity_id, "entity-1");
1854
- assert_eq!(rows[0].schema_version, "1");
2026
+ assert_eq!(rows[0].entity_id, "[\"entity-1\"]");
1855
2027
  assert_eq!(rows[0].version_id, "version-a");
1856
2028
  assert!(!rows[0].global);
1857
2029
  assert!(!rows[0].untracked);
@@ -1859,7 +2031,7 @@ mod tests {
1859
2031
  rows[0].snapshot_content.as_deref(),
1860
2032
  Some("{\"key\":\"hello\",\"value\":\"world\"}")
1861
2033
  );
1862
- assert_eq!(rows[0].metadata.as_ref(), Some(&json!({"source": "sql"})));
2034
+ assert_eq!(rows[0].metadata.as_deref(), Some("{\"source\":\"sql\"}"));
1863
2035
  }
1864
2036
 
1865
2037
  #[tokio::test]
@@ -1876,14 +2048,14 @@ mod tests {
1876
2048
  };
1877
2049
 
1878
2050
  let result = execute_write_sql(
1879
- &mut ctx,
1880
- "INSERT INTO lix_state (\
1881
- entity_id, schema_key, file_id, snapshot_content, metadata, schema_version\
1882
- ) VALUES (\
1883
- 'entity-defaults', 'lix_key_value', NULL, '{\"key\":\"hello\",\"value\":\"defaults\"}', NULL, '1'\
1884
- )",
1885
- &[],
1886
- )
2051
+ &mut ctx,
2052
+ "INSERT INTO lix_state (\
2053
+ entity_id, schema_key, file_id, snapshot_content, metadata\
2054
+ ) VALUES (\
2055
+ lix_json('[\"entity-defaults\"]'), 'lix_key_value', NULL, '{\"key\":\"hello\",\"value\":\"defaults\"}', NULL\
2056
+ )",
2057
+ &[],
2058
+ )
1887
2059
  .await
1888
2060
  .expect("INSERT INTO lix_state should default bookkeeping flags");
1889
2061
 
@@ -1897,7 +2069,7 @@ mod tests {
1897
2069
  .expect("staged delta should expose pending overlay");
1898
2070
  let rows = overlay.visible_semantic_rows(false, "lix_key_value");
1899
2071
  assert_eq!(rows.len(), 1);
1900
- assert_eq!(rows[0].entity_id, "entity-defaults");
2072
+ assert_eq!(rows[0].entity_id, "[\"entity-defaults\"]");
1901
2073
  assert_eq!(rows[0].version_id, "version-a");
1902
2074
  assert!(!rows[0].global);
1903
2075
  assert!(!rows[0].untracked);
@@ -1919,15 +2091,14 @@ mod tests {
1919
2091
  let result = execute_write_sql(
1920
2092
  &mut ctx,
1921
2093
  "INSERT INTO lix_state (\
1922
- entity_id, schema_key, file_id, snapshot_content, metadata, schema_version, global, untracked\
1923
- ) \
1924
- SELECT \
1925
- 'entity-from-select' AS entity_id, \
1926
- 'lix_key_value' AS schema_key, \
1927
- NULL AS file_id, \
2094
+ entity_id, schema_key, file_id, snapshot_content, metadata, global, untracked\
2095
+ ) \
2096
+ SELECT \
2097
+ lix_json('[\"entity-from-select\"]') AS entity_id, \
2098
+ 'lix_key_value' AS schema_key, \
2099
+ NULL AS file_id, \
1928
2100
  '{\"key\":\"hello\",\"value\":\"from-select\"}' AS snapshot_content, \
1929
2101
  '{\"source\":\"select\"}' AS metadata, \
1930
- '1' AS schema_version, \
1931
2102
  false AS global, \
1932
2103
  false AS untracked",
1933
2104
  &[],
@@ -1945,17 +2116,13 @@ mod tests {
1945
2116
  .expect("staged delta should expose pending overlay");
1946
2117
  let rows = overlay.visible_semantic_rows(false, "lix_key_value");
1947
2118
  assert_eq!(rows.len(), 1);
1948
- assert_eq!(rows[0].entity_id, "entity-from-select");
1949
- assert_eq!(rows[0].schema_version, "1");
2119
+ assert_eq!(rows[0].entity_id, "[\"entity-from-select\"]");
1950
2120
  assert_eq!(rows[0].version_id, "version-a");
1951
2121
  assert_eq!(
1952
2122
  rows[0].snapshot_content.as_deref(),
1953
2123
  Some("{\"key\":\"hello\",\"value\":\"from-select\"}")
1954
2124
  );
1955
- assert_eq!(
1956
- rows[0].metadata.as_ref(),
1957
- Some(&json!({"source": "select"}))
1958
- );
2125
+ assert_eq!(rows[0].metadata.as_deref(), Some("{\"source\":\"select\"}"));
1959
2126
  }
1960
2127
 
1961
2128
  #[tokio::test]
@@ -1970,7 +2137,6 @@ mod tests {
1970
2137
  staged_writes: Arc::clone(&staged_writes),
1971
2138
  schema_definitions: vec![json!({
1972
2139
  "x-lix-key": "test_state_schema",
1973
- "x-lix-version": "1",
1974
2140
  "type": "object",
1975
2141
  "properties": {
1976
2142
  "value": { "type": "string" }
@@ -1981,8 +2147,8 @@ mod tests {
1981
2147
  let result = execute_write_sql(
1982
2148
  &mut ctx,
1983
2149
  "INSERT INTO test_state_schema_by_version (\
1984
- lixcol_entity_id, lixcol_version_id, value\
1985
- ) VALUES ('entity-c', 'version-b', 'C')",
2150
+ lixcol_entity_id, lixcol_version_id, value\
2151
+ ) VALUES (lix_json('[\"entity-c\"]'), 'version-b', 'C')",
1986
2152
  &[],
1987
2153
  )
1988
2154
  .await
@@ -1998,8 +2164,7 @@ mod tests {
1998
2164
  .expect("staged delta should expose pending overlay");
1999
2165
  let rows = overlay.visible_semantic_rows(false, "test_state_schema");
2000
2166
  assert_eq!(rows.len(), 1);
2001
- assert_eq!(rows[0].entity_id, "entity-c");
2002
- assert_eq!(rows[0].schema_version, "1");
2167
+ assert_eq!(rows[0].entity_id, "[\"entity-c\"]");
2003
2168
  assert_eq!(rows[0].version_id, "version-b");
2004
2169
  assert!(!rows[0].global);
2005
2170
  assert!(!rows[0].untracked);
@@ -2021,7 +2186,6 @@ mod tests {
2021
2186
  staged_writes: Arc::clone(&staged_writes),
2022
2187
  schema_definitions: vec![json!({
2023
2188
  "x-lix-key": "test_state_schema",
2024
- "x-lix-version": "1",
2025
2189
  "type": "object",
2026
2190
  "properties": {
2027
2191
  "value": { "type": "string" }
@@ -2032,7 +2196,7 @@ mod tests {
2032
2196
  let result = execute_write_sql(
2033
2197
  &mut ctx,
2034
2198
  "INSERT INTO test_state_schema (lixcol_entity_id, value) \
2035
- VALUES ('entity-c', 'C')",
2199
+ VALUES (lix_json('[\"entity-c\"]'), 'C')",
2036
2200
  &[],
2037
2201
  )
2038
2202
  .await
@@ -2048,7 +2212,7 @@ mod tests {
2048
2212
  .expect("staged delta should expose pending overlay");
2049
2213
  let rows = overlay.visible_semantic_rows(false, "test_state_schema");
2050
2214
  assert_eq!(rows.len(), 1);
2051
- assert_eq!(rows[0].entity_id, "entity-c");
2215
+ assert_eq!(rows[0].entity_id, "[\"entity-c\"]");
2052
2216
  assert_eq!(rows[0].version_id, "version-a");
2053
2217
  assert!(!rows[0].global);
2054
2218
  assert!(!rows[0].untracked);
@@ -2091,8 +2255,7 @@ mod tests {
2091
2255
  .expect("staged delta should expose pending overlay");
2092
2256
  let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
2093
2257
  assert_eq!(rows.len(), 1);
2094
- assert_eq!(rows[0].entity_id, "dir-docs");
2095
- assert_eq!(rows[0].schema_version, "1");
2258
+ assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
2096
2259
  assert_eq!(rows[0].version_id, "version-b");
2097
2260
  assert!(!rows[0].global);
2098
2261
  assert!(!rows[0].untracked);
@@ -2134,7 +2297,7 @@ mod tests {
2134
2297
  .expect("staged delta should expose pending overlay");
2135
2298
  let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
2136
2299
  assert_eq!(rows.len(), 1);
2137
- assert_eq!(rows[0].entity_id, "dir-docs");
2300
+ assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
2138
2301
  assert_eq!(rows[0].version_id, "version-a");
2139
2302
  assert!(!rows[0].global);
2140
2303
  assert!(!rows[0].untracked);
@@ -2178,15 +2341,15 @@ mod tests {
2178
2341
  .expect("staged delta should expose pending overlay");
2179
2342
  let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
2180
2343
  assert_eq!(rows.len(), 1);
2181
- assert_eq!(rows[0].entity_id, "dir-docs");
2344
+ assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
2182
2345
  assert_eq!(rows[0].version_id, "version-a");
2183
2346
  assert_eq!(
2184
2347
  rows[0].snapshot_content.as_deref(),
2185
2348
  Some("{\"hidden\":true,\"id\":\"dir-docs\",\"name\":\"docs\",\"parent_id\":null}")
2186
2349
  );
2187
2350
  assert_eq!(
2188
- rows[0].metadata.as_ref(),
2189
- Some(&json!({"source": "directory-update"}))
2351
+ rows[0].metadata.as_deref(),
2352
+ Some("{\"source\":\"directory-update\"}")
2190
2353
  );
2191
2354
  }
2192
2355
 
@@ -2267,7 +2430,7 @@ mod tests {
2267
2430
  .expect("staged delta should expose pending overlay");
2268
2431
  let rows = overlay.visible_all_semantic_rows();
2269
2432
  assert_eq!(rows.len(), 1);
2270
- assert_eq!(rows[0].entity_id, "dir-guides");
2433
+ assert_eq!(rows[0].entity_id, "[\"dir-guides\"]");
2271
2434
  assert_eq!(rows[0].version_id, "version-b");
2272
2435
  assert!(rows[0].tombstone);
2273
2436
  assert_eq!(rows[0].snapshot_content, None);
@@ -2306,8 +2469,7 @@ mod tests {
2306
2469
  .expect("staged delta should expose pending overlay");
2307
2470
  let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2308
2471
  assert_eq!(rows.len(), 1);
2309
- assert_eq!(rows[0].entity_id, "file-readme");
2310
- assert_eq!(rows[0].schema_version, "1");
2472
+ assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
2311
2473
  assert_eq!(rows[0].version_id, "version-b");
2312
2474
  assert!(!rows[0].global);
2313
2475
  assert!(!rows[0].untracked);
@@ -2352,7 +2514,7 @@ mod tests {
2352
2514
  .expect("staged delta should expose pending overlay");
2353
2515
  let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2354
2516
  assert_eq!(rows.len(), 1);
2355
- assert_eq!(rows[0].entity_id, "file-readme");
2517
+ assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
2356
2518
  assert_eq!(rows[0].version_id, "version-a");
2357
2519
  assert!(!rows[0].global);
2358
2520
  assert!(!rows[0].untracked);
@@ -2391,10 +2553,10 @@ mod tests {
2391
2553
  .expect("staged delta should expose pending overlay");
2392
2554
  let descriptor_rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2393
2555
  assert_eq!(descriptor_rows.len(), 1);
2394
- assert_eq!(descriptor_rows[0].entity_id, "file-readme");
2556
+ assert_eq!(descriptor_rows[0].entity_id, "[\"file-readme\"]");
2395
2557
  let blob_ref_rows = overlay.visible_semantic_rows(false, "lix_binary_blob_ref");
2396
2558
  assert_eq!(blob_ref_rows.len(), 1);
2397
- assert_eq!(blob_ref_rows[0].entity_id, "file-readme");
2559
+ assert_eq!(blob_ref_rows[0].entity_id, "[\"file-readme\"]");
2398
2560
  assert_eq!(blob_ref_rows[0].file_id.as_deref(), Some("file-readme"));
2399
2561
  assert_eq!(blob_ref_rows[0].version_id, "version-b");
2400
2562
  let snapshot: JsonValue =
@@ -2458,7 +2620,7 @@ mod tests {
2458
2620
  .expect("staged delta should expose pending overlay");
2459
2621
  let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2460
2622
  assert_eq!(rows.len(), 1);
2461
- assert_eq!(rows[0].entity_id, "file-readme");
2623
+ assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
2462
2624
  assert_eq!(rows[0].version_id, "version-a");
2463
2625
  let snapshot: JsonValue =
2464
2626
  serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
@@ -2468,8 +2630,8 @@ mod tests {
2468
2630
  assert_eq!(snapshot["name"], "readme-updated.txt");
2469
2631
  assert_eq!(snapshot["hidden"], true);
2470
2632
  assert_eq!(
2471
- rows[0].metadata.as_ref(),
2472
- Some(&json!({"source": "file-update"}))
2633
+ rows[0].metadata.as_deref(),
2634
+ Some("{\"source\":\"file-update\"}")
2473
2635
  );
2474
2636
  }
2475
2637
 
@@ -2518,7 +2680,7 @@ mod tests {
2518
2680
  .is_empty());
2519
2681
  let blob_ref_rows = overlay.visible_semantic_rows(false, "lix_binary_blob_ref");
2520
2682
  assert_eq!(blob_ref_rows.len(), 1);
2521
- assert_eq!(blob_ref_rows[0].entity_id, "file-readme");
2683
+ assert_eq!(blob_ref_rows[0].entity_id, "[\"file-readme\"]");
2522
2684
  let snapshot: JsonValue =
2523
2685
  serde_json::from_str(blob_ref_rows[0].snapshot_content.as_deref().unwrap())
2524
2686
  .expect("blob ref snapshot JSON");
@@ -2626,7 +2788,7 @@ mod tests {
2626
2788
  .expect("staged delta should expose pending overlay");
2627
2789
  let rows = overlay.visible_all_semantic_rows();
2628
2790
  assert_eq!(rows.len(), 1);
2629
- assert_eq!(rows[0].entity_id, "file-guide");
2791
+ assert_eq!(rows[0].entity_id, "[\"file-guide\"]");
2630
2792
  assert_eq!(rows[0].version_id, "version-b");
2631
2793
  assert!(rows[0].tombstone);
2632
2794
  assert_eq!(rows[0].snapshot_content, None);
@@ -2649,7 +2811,6 @@ mod tests {
2649
2811
  staged_writes: Arc::clone(&staged_writes),
2650
2812
  schema_definitions: vec![json!({
2651
2813
  "x-lix-key": "test_state_schema",
2652
- "x-lix-version": "1",
2653
2814
  "type": "object",
2654
2815
  "properties": {
2655
2816
  "value": { "type": "string" }
@@ -2677,15 +2838,15 @@ mod tests {
2677
2838
  .expect("staged delta should expose pending overlay");
2678
2839
  let rows = overlay.visible_semantic_rows(false, "test_state_schema");
2679
2840
  assert_eq!(rows.len(), 1);
2680
- assert_eq!(rows[0].entity_id, "entity-a");
2841
+ assert_eq!(rows[0].entity_id, "[\"entity-a\"]");
2681
2842
  assert_eq!(rows[0].version_id, "version-a");
2682
2843
  assert_eq!(
2683
2844
  rows[0].snapshot_content.as_deref(),
2684
2845
  Some("{\"value\":\"updated\"}")
2685
2846
  );
2686
2847
  assert_eq!(
2687
- rows[0].metadata.as_ref(),
2688
- Some(&json!({"source": "entity-update"}))
2848
+ rows[0].metadata.as_deref(),
2849
+ Some("{\"source\":\"entity-update\"}")
2689
2850
  );
2690
2851
  }
2691
2852
 
@@ -2706,7 +2867,6 @@ mod tests {
2706
2867
  staged_writes: Arc::clone(&staged_writes),
2707
2868
  schema_definitions: vec![json!({
2708
2869
  "x-lix-key": "test_state_schema",
2709
- "x-lix-version": "1",
2710
2870
  "type": "object",
2711
2871
  "properties": {
2712
2872
  "value": { "type": "string" }
@@ -2733,7 +2893,7 @@ mod tests {
2733
2893
  .expect("staged delta should expose pending overlay");
2734
2894
  let rows = overlay.visible_all_semantic_rows();
2735
2895
  assert_eq!(rows.len(), 1);
2736
- assert_eq!(rows[0].entity_id, "entity-b");
2896
+ assert_eq!(rows[0].entity_id, "[\"entity-b\"]");
2737
2897
  assert_eq!(rows[0].version_id, "version-b");
2738
2898
  assert!(rows[0].tombstone);
2739
2899
  assert_eq!(rows[0].snapshot_content, None);
@@ -2762,7 +2922,7 @@ mod tests {
2762
2922
  "UPDATE lix_state \
2763
2923
  SET snapshot_content = '{\"key\":\"hello\",\"value\":\"updated\"}', \
2764
2924
  metadata = '{\"schema_key\":\"lix_key_value\"}' \
2765
- WHERE metadata = '{\"source\":\"match\"}'",
2925
+ WHERE metadata = lix_json('{\"source\":\"match\"}')",
2766
2926
  &[],
2767
2927
  )
2768
2928
  .await
@@ -2778,15 +2938,15 @@ mod tests {
2778
2938
  .expect("staged delta should expose pending overlay");
2779
2939
  let rows = overlay.visible_semantic_rows(false, "lix_key_value");
2780
2940
  assert_eq!(rows.len(), 1);
2781
- assert_eq!(rows[0].entity_id, "entity-1");
2941
+ assert_eq!(rows[0].entity_id, "[\"entity-1\"]");
2782
2942
  assert_eq!(rows[0].version_id, "version-a");
2783
2943
  assert_eq!(
2784
2944
  rows[0].snapshot_content.as_deref(),
2785
2945
  Some("{\"key\":\"hello\",\"value\":\"updated\"}")
2786
2946
  );
2787
2947
  assert_eq!(
2788
- rows[0].metadata.as_ref(),
2789
- Some(&json!({"schema_key": "lix_key_value"}))
2948
+ rows[0].metadata.as_deref(),
2949
+ Some("{\"schema_key\":\"lix_key_value\"}")
2790
2950
  );
2791
2951
  }
2792
2952
 
@@ -2824,8 +2984,8 @@ mod tests {
2824
2984
  assert_eq!(rows.len(), 2);
2825
2985
  assert!(rows.iter().all(|row| row.tombstone));
2826
2986
  assert!(rows.iter().all(|row| row.snapshot_content.is_none()));
2827
- assert!(rows.iter().any(|row| row.entity_id == "entity-1"));
2828
- assert!(rows.iter().any(|row| row.entity_id == "entity-2"));
2987
+ assert!(rows.iter().any(|row| row.entity_id == "[\"entity-1\"]"));
2988
+ assert!(rows.iter().any(|row| row.entity_id == "[\"entity-2\"]"));
2829
2989
  }
2830
2990
 
2831
2991
  struct BackendSqlExecutionContext<'a> {
@@ -2853,13 +3013,11 @@ mod tests {
2853
3013
  Arc::clone(&self.blob_reader)
2854
3014
  }
2855
3015
 
2856
- fn changelog_query_source(&self) -> SqlChangelogQuerySource {
3016
+ fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
2857
3017
  let base_scope = test_read_scope(self.storage.clone());
2858
3018
  let read_scope = StorageReadScope::new(base_scope.store());
2859
- ChangelogQuerySource {
2860
- changelog_reader: Arc::new(
2861
- crate::changelog::ChangelogContext::new().reader(read_scope.store()),
2862
- ),
3019
+ CommitStoreQuerySource {
3020
+ commit_store_reader: Arc::new(CommitStoreContext::new().reader(read_scope.store())),
2863
3021
  json_reader: JsonStoreContext::new().reader(read_scope.store()),
2864
3022
  }
2865
3023
  }
@@ -2891,26 +3049,23 @@ mod tests {
2891
3049
  crate::untracked_state::UntrackedStateContext::new(),
2892
3050
  ));
2893
3051
  let mut writes = StorageWriteSet::new();
2894
- let canonical_rows = {
2895
- let mut json_writer = JsonStoreContext::new().writer();
2896
- vec![
2897
- version_ctx.canonical_ref_row(
2898
- &mut writes,
2899
- &mut json_writer,
2900
- "version-a",
2901
- &init_receipt.initial_commit_id,
2902
- "1970-01-01T00:00:00.000Z",
2903
- )?,
2904
- version_ctx.canonical_ref_row(
2905
- &mut writes,
2906
- &mut json_writer,
2907
- "version-b",
2908
- &init_receipt.initial_commit_id,
2909
- "1970-01-01T00:00:00.000Z",
2910
- )?,
2911
- ]
2912
- };
2913
- version_ctx.stage_canonical_ref_rows(&mut writes, &canonical_rows)?;
3052
+ let canonical_rows = vec![
3053
+ prepare_version_ref_row(
3054
+ "version-a",
3055
+ &init_receipt.initial_commit_id,
3056
+ "1970-01-01T00:00:00.000Z",
3057
+ )?,
3058
+ prepare_version_ref_row(
3059
+ "version-b",
3060
+ &init_receipt.initial_commit_id,
3061
+ "1970-01-01T00:00:00.000Z",
3062
+ )?,
3063
+ ];
3064
+ let rows = canonical_rows
3065
+ .into_iter()
3066
+ .map(|prepared| prepared.row)
3067
+ .collect::<Vec<_>>();
3068
+ version_ctx.stage_canonical_ref_rows(&mut writes, &rows)?;
2914
3069
  writes.apply(&mut transaction.as_mut()).await?;
2915
3070
  transaction.commit().await?;
2916
3071
  }
@@ -2919,7 +3074,6 @@ mod tests {
2919
3074
  let session_b = engine.open_session("version-b").await?;
2920
3075
  let schema_definition = json!({
2921
3076
  "x-lix-key": "test_state_schema",
2922
- "x-lix-version": "1",
2923
3077
  "type": "object",
2924
3078
  "properties": {
2925
3079
  "value": { "type": "string" }
@@ -2931,9 +3085,9 @@ mod tests {
2931
3085
  .execute(
2932
3086
  "INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
2933
3087
  VALUES (\
2934
- lix_json('{\"x-lix-key\":\"test_state_schema\",\"x-lix-version\":\"1\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
3088
+ lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
2935
3089
  false,\
2936
- true\
3090
+ false\
2937
3091
  )",
2938
3092
  &[],
2939
3093
  )
@@ -2942,9 +3096,9 @@ mod tests {
2942
3096
  .execute(
2943
3097
  "INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
2944
3098
  VALUES (\
2945
- lix_json('{\"x-lix-key\":\"test_state_schema\",\"x-lix-version\":\"1\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
3099
+ lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
2946
3100
  false,\
2947
- true\
3101
+ false\
2948
3102
  )",
2949
3103
  &[],
2950
3104
  )
@@ -2952,32 +3106,32 @@ mod tests {
2952
3106
  session_a
2953
3107
  .execute(
2954
3108
  "INSERT INTO lix_state (\
2955
- entity_id, schema_key, file_id, snapshot_content, schema_version, global, untracked\
2956
- ) VALUES (\
2957
- 'entity-a', 'test_state_schema', NULL, '{\"value\":\"A\"}', '1', false, false\
2958
- )",
3109
+ entity_id, schema_key, file_id, snapshot_content, global, untracked\
3110
+ ) VALUES (\
3111
+ lix_json('[\"entity-a\"]'), 'test_state_schema', NULL, '{\"value\":\"A\"}', false, false\
3112
+ )",
2959
3113
  &[],
2960
3114
  )
2961
3115
  .await?;
2962
3116
  session_b
2963
3117
  .execute(
2964
3118
  "INSERT INTO lix_state (\
2965
- entity_id, schema_key, file_id, snapshot_content, schema_version, global, untracked\
2966
- ) VALUES (\
2967
- 'entity-b', 'test_state_schema', NULL, '{\"value\":\"B\"}', '1', false, false\
2968
- )",
3119
+ entity_id, schema_key, file_id, snapshot_content, global, untracked\
3120
+ ) VALUES (\
3121
+ lix_json('[\"entity-b\"]'), 'test_state_schema', NULL, '{\"value\":\"B\"}', false, false\
3122
+ )",
2969
3123
  &[],
2970
3124
  )
2971
3125
  .await?;
2972
3126
  session_a
2973
- .execute(
2974
- "INSERT INTO lix_state (\
2975
- entity_id, schema_key, file_id, snapshot_content, schema_version, global, untracked\
2976
- ) VALUES (\
2977
- 'dir-docs', 'lix_directory_descriptor', NULL, '{\"id\":\"dir-docs\",\"parent_id\":null,\"name\":\"docs\",\"hidden\":false}', '1', false, false\
2978
- )",
2979
- &[],
2980
- )
3127
+ .execute(
3128
+ "INSERT INTO lix_state (\
3129
+ entity_id, schema_key, file_id, snapshot_content, global, untracked\
3130
+ ) VALUES (\
3131
+ lix_json('[\"dir-docs\"]'), 'lix_directory_descriptor', NULL, '{\"id\":\"dir-docs\",\"parent_id\":null,\"name\":\"docs\",\"hidden\":false}', false, false\
3132
+ )",
3133
+ &[],
3134
+ )
2981
3135
  .await?;
2982
3136
  session_a
2983
3137
  .execute(
@@ -2993,7 +3147,7 @@ mod tests {
2993
3147
  LiveStateContext::new(
2994
3148
  TrackedStateContext::new(),
2995
3149
  UntrackedStateContext::new(),
2996
- crate::commit_graph::CommitGraphContext::new(crate::changelog::ChangelogContext::new()),
3150
+ crate::commit_graph::CommitGraphContext::new(),
2997
3151
  )
2998
3152
  }
2999
3153
 
@@ -3050,7 +3204,7 @@ mod tests {
3050
3204
  vec!["entity_id", "version_id", "snapshot_content", "commit_id"]
3051
3205
  );
3052
3206
  assert_eq!(result.rows.len(), 1);
3053
- assert_eq!(result.rows[0][0], Value::Text("entity-b".to_string()));
3207
+ assert_eq!(result.rows[0][0], Value::Json(json!(["entity-b"])));
3054
3208
  assert_eq!(result.rows[0][1], Value::Text("version-b".to_string()));
3055
3209
  assert_eq!(result.rows[0][2], Value::Json(json!({"value": "B"})));
3056
3210
  match &result.rows[0][3] {
@@ -3090,11 +3244,11 @@ mod tests {
3090
3244
  .expect("broad by-version read should succeed");
3091
3245
 
3092
3246
  assert!(
3093
- result.rows.iter().any(|row| row[0] == Value::Text("entity-a".to_string()))
3094
- && result.rows.iter().any(|row| row[0] == Value::Text("entity-b".to_string())),
3095
- "expected broad by-version read to include rows from multiple visible versions: {:?}",
3096
- result.rows
3097
- );
3247
+ result.rows.iter().any(|row| row[0] == Value::Json(json!(["entity-a"])))
3248
+ && result.rows.iter().any(|row| row[0] == Value::Json(json!(["entity-b"]))),
3249
+ "expected broad by-version read to include rows from multiple visible versions: {:?}",
3250
+ result.rows
3251
+ );
3098
3252
  })
3099
3253
  });
3100
3254
  }
@@ -3131,7 +3285,7 @@ mod tests {
3131
3285
 
3132
3286
  assert_eq!(result.columns, vec!["entity_id", "snapshot_content"]);
3133
3287
  assert_eq!(result.rows.len(), 1);
3134
- assert_eq!(result.rows[0][0], Value::Text("entity-a".to_string()));
3288
+ assert_eq!(result.rows[0][0], Value::Json(json!(["entity-a"])));
3135
3289
  assert_eq!(result.rows[0][1], Value::Json(json!({"value": "A"})));
3136
3290
  })
3137
3291
  });
@@ -3169,7 +3323,7 @@ mod tests {
3169
3323
  assert_eq!(result.columns, vec!["value", "lixcol_entity_id"]);
3170
3324
  assert_eq!(result.rows.len(), 1);
3171
3325
  assert_eq!(result.rows[0][0], Value::Text("A".to_string()));
3172
- assert_eq!(result.rows[0][1], Value::Text("entity-a".to_string()));
3326
+ assert_eq!(result.rows[0][1], Value::Json(json!(["entity-a"])));
3173
3327
  })
3174
3328
  });
3175
3329
  }