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

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 (223) hide show
  1. package/README.md +76 -4
  2. package/dist/errors.d.ts +7 -0
  3. package/dist/errors.js +19 -0
  4. package/dist/index.d.ts +4 -5
  5. package/dist/index.js +3 -3
  6. package/dist/native.d.ts +1 -0
  7. package/dist/native.js +47 -0
  8. package/dist/open-lix.d.ts +39 -201
  9. package/dist/open-lix.js +59 -284
  10. package/dist/result.d.ts +18 -0
  11. package/dist/result.js +48 -0
  12. package/dist/types.d.ts +114 -1
  13. package/dist/value.d.ts +28 -0
  14. package/dist/value.js +245 -0
  15. package/package.json +20 -50
  16. package/SKILL.md +0 -506
  17. package/dist/builtin-schemas.d.ts +0 -1
  18. package/dist/builtin-schemas.js +0 -1
  19. package/dist/engine-wasm/index.d.ts +0 -87
  20. package/dist/engine-wasm/index.js +0 -339
  21. package/dist/engine-wasm/wasm/lix_engine.d.ts +0 -79
  22. package/dist/engine-wasm/wasm/lix_engine.js +0 -821
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -26
  25. package/dist/generated/builtin-schemas.d.ts +0 -427
  26. package/dist/generated/builtin-schemas.js +0 -643
  27. package/dist/sqlite/index.d.ts +0 -12
  28. package/dist/sqlite/index.js +0 -303
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/kv.rs +0 -358
  31. package/dist-engine-src/src/backend/mod.rs +0 -12
  32. package/dist-engine-src/src/backend/testing.rs +0 -658
  33. package/dist-engine-src/src/backend/types.rs +0 -96
  34. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  35. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  36. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  37. package/dist-engine-src/src/binary_cas/kv.rs +0 -1063
  38. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  39. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  40. package/dist-engine-src/src/catalog/context.rs +0 -412
  41. package/dist-engine-src/src/catalog/mod.rs +0 -10
  42. package/dist-engine-src/src/catalog/schema.rs +0 -4
  43. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  44. package/dist-engine-src/src/cel/context.rs +0 -86
  45. package/dist-engine-src/src/cel/error.rs +0 -19
  46. package/dist-engine-src/src/cel/mod.rs +0 -8
  47. package/dist-engine-src/src/cel/provider.rs +0 -9
  48. package/dist-engine-src/src/cel/runtime.rs +0 -167
  49. package/dist-engine-src/src/cel/value.rs +0 -50
  50. package/dist-engine-src/src/commit_graph/context.rs +0 -901
  51. package/dist-engine-src/src/commit_graph/mod.rs +0 -11
  52. package/dist-engine-src/src/commit_graph/types.rs +0 -109
  53. package/dist-engine-src/src/commit_graph/walker.rs +0 -756
  54. package/dist-engine-src/src/commit_store/codec.rs +0 -887
  55. package/dist-engine-src/src/commit_store/context.rs +0 -944
  56. package/dist-engine-src/src/commit_store/materialization.rs +0 -84
  57. package/dist-engine-src/src/commit_store/mod.rs +0 -16
  58. package/dist-engine-src/src/commit_store/storage.rs +0 -600
  59. package/dist-engine-src/src/commit_store/types.rs +0 -215
  60. package/dist-engine-src/src/common/error.rs +0 -313
  61. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  62. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  63. package/dist-engine-src/src/common/identity.rs +0 -145
  64. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  65. package/dist-engine-src/src/common/metadata.rs +0 -40
  66. package/dist-engine-src/src/common/mod.rs +0 -23
  67. package/dist-engine-src/src/common/types.rs +0 -105
  68. package/dist-engine-src/src/common/wire.rs +0 -222
  69. package/dist-engine-src/src/domain.rs +0 -324
  70. package/dist-engine-src/src/engine.rs +0 -225
  71. package/dist-engine-src/src/entity_identity.rs +0 -405
  72. package/dist-engine-src/src/functions/context.rs +0 -292
  73. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  74. package/dist-engine-src/src/functions/mod.rs +0 -18
  75. package/dist-engine-src/src/functions/provider.rs +0 -130
  76. package/dist-engine-src/src/functions/state.rs +0 -336
  77. package/dist-engine-src/src/functions/types.rs +0 -37
  78. package/dist-engine-src/src/init.rs +0 -558
  79. package/dist-engine-src/src/json_store/compression.rs +0 -77
  80. package/dist-engine-src/src/json_store/context.rs +0 -423
  81. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  82. package/dist-engine-src/src/json_store/mod.rs +0 -12
  83. package/dist-engine-src/src/json_store/store.rs +0 -1109
  84. package/dist-engine-src/src/json_store/types.rs +0 -217
  85. package/dist-engine-src/src/lib.rs +0 -62
  86. package/dist-engine-src/src/live_state/context.rs +0 -2019
  87. package/dist-engine-src/src/live_state/mod.rs +0 -15
  88. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  89. package/dist-engine-src/src/live_state/reader.rs +0 -23
  90. package/dist-engine-src/src/live_state/types.rs +0 -222
  91. package/dist-engine-src/src/live_state/visibility.rs +0 -223
  92. package/dist-engine-src/src/plugin/archive.rs +0 -438
  93. package/dist-engine-src/src/plugin/component.rs +0 -183
  94. package/dist-engine-src/src/plugin/install.rs +0 -619
  95. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  96. package/dist-engine-src/src/plugin/materializer.rs +0 -477
  97. package/dist-engine-src/src/plugin/mod.rs +0 -33
  98. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -118
  99. package/dist-engine-src/src/plugin/storage.rs +0 -74
  100. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  101. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  102. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  103. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  104. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  105. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  106. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  107. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  108. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  109. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  110. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  111. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  112. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  113. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  114. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  115. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +0 -34
  116. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +0 -48
  117. package/dist-engine-src/src/schema/builtin/mod.rs +0 -222
  118. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  119. package/dist-engine-src/src/schema/definition.json +0 -187
  120. package/dist-engine-src/src/schema/definition.rs +0 -742
  121. package/dist-engine-src/src/schema/key.rs +0 -138
  122. package/dist-engine-src/src/schema/mod.rs +0 -20
  123. package/dist-engine-src/src/schema/seed.rs +0 -14
  124. package/dist-engine-src/src/schema/tests.rs +0 -780
  125. package/dist-engine-src/src/session/context.rs +0 -404
  126. package/dist-engine-src/src/session/create_version.rs +0 -88
  127. package/dist-engine-src/src/session/execute.rs +0 -541
  128. package/dist-engine-src/src/session/merge/analysis.rs +0 -102
  129. package/dist-engine-src/src/session/merge/apply.rs +0 -23
  130. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  131. package/dist-engine-src/src/session/merge/mod.rs +0 -11
  132. package/dist-engine-src/src/session/merge/stats.rs +0 -65
  133. package/dist-engine-src/src/session/merge/version.rs +0 -427
  134. package/dist-engine-src/src/session/mod.rs +0 -27
  135. package/dist-engine-src/src/session/optimization9_sql2_bench.rs +0 -100
  136. package/dist-engine-src/src/session/switch_version.rs +0 -110
  137. package/dist-engine-src/src/session/transaction.rs +0 -76
  138. package/dist-engine-src/src/sql2/change_provider.rs +0 -331
  139. package/dist-engine-src/src/sql2/classify.rs +0 -174
  140. package/dist-engine-src/src/sql2/context.rs +0 -311
  141. package/dist-engine-src/src/sql2/directory_history_provider.rs +0 -631
  142. package/dist-engine-src/src/sql2/directory_provider.rs +0 -2453
  143. package/dist-engine-src/src/sql2/dml.rs +0 -148
  144. package/dist-engine-src/src/sql2/entity_history_provider.rs +0 -440
  145. package/dist-engine-src/src/sql2/entity_provider.rs +0 -3211
  146. package/dist-engine-src/src/sql2/error.rs +0 -215
  147. package/dist-engine-src/src/sql2/execute.rs +0 -3533
  148. package/dist-engine-src/src/sql2/file_history_provider.rs +0 -910
  149. package/dist-engine-src/src/sql2/file_provider.rs +0 -3679
  150. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1490
  151. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  152. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  153. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  154. package/dist-engine-src/src/sql2/history_provider.rs +0 -412
  155. package/dist-engine-src/src/sql2/history_route.rs +0 -657
  156. package/dist-engine-src/src/sql2/lix_state_provider.rs +0 -2512
  157. package/dist-engine-src/src/sql2/mod.rs +0 -47
  158. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -246
  159. package/dist-engine-src/src/sql2/public_bind/assignment.rs +0 -46
  160. package/dist-engine-src/src/sql2/public_bind/capability.rs +0 -41
  161. package/dist-engine-src/src/sql2/public_bind/dml.rs +0 -172
  162. package/dist-engine-src/src/sql2/public_bind/mod.rs +0 -26
  163. package/dist-engine-src/src/sql2/public_bind/table.rs +0 -168
  164. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  165. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  166. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  167. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  168. package/dist-engine-src/src/sql2/session.rs +0 -132
  169. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  170. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +0 -53
  171. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  172. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  173. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  174. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  175. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  176. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  177. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  178. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  179. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -89
  180. package/dist-engine-src/src/sql2/udfs/public_call.rs +0 -238
  181. package/dist-engine-src/src/sql2/version_provider.rs +0 -1202
  182. package/dist-engine-src/src/sql2/version_scope.rs +0 -394
  183. package/dist-engine-src/src/sql2/write_normalization.rs +0 -345
  184. package/dist-engine-src/src/storage/context.rs +0 -356
  185. package/dist-engine-src/src/storage/mod.rs +0 -14
  186. package/dist-engine-src/src/storage/read_scope.rs +0 -88
  187. package/dist-engine-src/src/storage/types.rs +0 -501
  188. package/dist-engine-src/src/storage_bench.rs +0 -4863
  189. package/dist-engine-src/src/test_support.rs +0 -228
  190. package/dist-engine-src/src/tracked_state/by_file_index.rs +0 -98
  191. package/dist-engine-src/src/tracked_state/codec.rs +0 -2085
  192. package/dist-engine-src/src/tracked_state/context.rs +0 -1867
  193. package/dist-engine-src/src/tracked_state/diff.rs +0 -686
  194. package/dist-engine-src/src/tracked_state/materialization.rs +0 -403
  195. package/dist-engine-src/src/tracked_state/materializer.rs +0 -488
  196. package/dist-engine-src/src/tracked_state/merge.rs +0 -492
  197. package/dist-engine-src/src/tracked_state/mod.rs +0 -32
  198. package/dist-engine-src/src/tracked_state/storage.rs +0 -375
  199. package/dist-engine-src/src/tracked_state/tree.rs +0 -3187
  200. package/dist-engine-src/src/tracked_state/types.rs +0 -231
  201. package/dist-engine-src/src/transaction/commit.rs +0 -1484
  202. package/dist-engine-src/src/transaction/context.rs +0 -1548
  203. package/dist-engine-src/src/transaction/live_state_overlay.rs +0 -35
  204. package/dist-engine-src/src/transaction/mod.rs +0 -13
  205. package/dist-engine-src/src/transaction/normalization.rs +0 -890
  206. package/dist-engine-src/src/transaction/prep.rs +0 -37
  207. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -149
  208. package/dist-engine-src/src/transaction/staging.rs +0 -1731
  209. package/dist-engine-src/src/transaction/types.rs +0 -460
  210. package/dist-engine-src/src/transaction/validation.rs +0 -5830
  211. package/dist-engine-src/src/untracked_state/codec.rs +0 -307
  212. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  213. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  214. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  215. package/dist-engine-src/src/untracked_state/storage.rs +0 -396
  216. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  217. package/dist-engine-src/src/version/context.rs +0 -40
  218. package/dist-engine-src/src/version/lifecycle.rs +0 -221
  219. package/dist-engine-src/src/version/mod.rs +0 -13
  220. package/dist-engine-src/src/version/refs.rs +0 -330
  221. package/dist-engine-src/src/version/stage_rows.rs +0 -67
  222. package/dist-engine-src/src/version/types.rs +0 -21
  223. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,3533 +0,0 @@
1
- use datafusion::arrow::datatypes::Field;
2
- use datafusion::arrow::record_batch::RecordBatch;
3
- use datafusion::common::metadata::{FieldMetadata, ScalarAndMetadata};
4
- use datafusion::common::{ParamValues, ScalarValue};
5
- use datafusion::logical_expr::{Expr, LogicalPlan, WriteOp};
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};
10
- use serde_json::{json, Value as JsonValue};
11
- use std::collections::{BTreeMap, BTreeSet, HashSet};
12
-
13
- use crate::schema::schema_key_from_definition;
14
- use crate::{LixError, LixNotice, SqlQueryResult, Value};
15
-
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};
18
- use super::session::{build_read_session, build_write_session};
19
- use super::write_normalization::{
20
- is_binary_type, lix_file_data_type_lix_error, logical_expr_is_binary_or_null,
21
- };
22
- use super::{SqlExecutionContext, SqlStatementKind, SqlWriteExecutionContext};
23
-
24
- #[allow(dead_code)]
25
- pub(crate) struct SqlLogicalPlan {
26
- session: SessionContext,
27
- plan: LogicalPlan,
28
- kind: SqlStatementKind,
29
- notices: Vec<LixNotice>,
30
- strict_binary_params: BTreeSet<usize>,
31
- }
32
-
33
- impl SqlLogicalPlan {
34
- #[allow(dead_code)]
35
- pub(crate) fn kind(&self) -> SqlStatementKind {
36
- self.kind
37
- }
38
-
39
- #[allow(dead_code)]
40
- pub(crate) fn is_write(&self) -> bool {
41
- self.kind == SqlStatementKind::Write
42
- }
43
- }
44
-
45
- /// Minimal top-level sql2 entrypoint.
46
- ///
47
- /// The final implementation will build the DataFusion session from the
48
- /// execution context and source rows from `live_state()`.
49
- ///
50
- /// `catalog()` is intentionally omitted from the MVP boundary for now.
51
- #[allow(dead_code)]
52
- pub(crate) async fn execute_sql(
53
- ctx: &dyn SqlExecutionContext,
54
- sql: &str,
55
- params: &[Value],
56
- ) -> Result<SqlQueryResult, LixError> {
57
- let plan = create_logical_plan(ctx, sql).await?;
58
- execute_logical_plan(plan, params).await
59
- }
60
-
61
- pub(crate) async fn create_logical_plan(
62
- ctx: &dyn SqlExecutionContext,
63
- sql: &str,
64
- ) -> Result<SqlLogicalPlan, LixError> {
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)?;
81
- let session = build_read_session(ctx).await?;
82
- let plan = create_logical_plan_from_statement(&session, statement).await?;
83
- validate_supported_logical_plan(&plan)?;
84
- validate_json_predicates_in_logical_plan(&plan)?;
85
- let kind = classify_logical_plan(&plan);
86
- let notices = history_filter_notices(&plan);
87
-
88
- Ok(SqlLogicalPlan {
89
- session,
90
- plan,
91
- kind,
92
- notices,
93
- strict_binary_params: BTreeSet::new(),
94
- })
95
- }
96
-
97
- #[allow(dead_code)]
98
- pub(crate) async fn create_write_logical_plan(
99
- ctx: &mut dyn SqlWriteExecutionContext,
100
- sql: &str,
101
- ) -> Result<SqlLogicalPlan, LixError> {
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)?;
115
- let session = build_write_session(ctx).await?;
116
- let plan = create_logical_plan_from_statement(&session, statement).await?;
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)?;
120
- let strict_binary_params = validate_strict_lix_file_data_writes(&plan)?;
121
- let kind = classify_logical_plan(&plan);
122
-
123
- Ok(SqlLogicalPlan {
124
- session,
125
- plan,
126
- kind,
127
- notices: Vec::new(),
128
- strict_binary_params,
129
- })
130
- }
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
-
267
- fn validate_strict_lix_file_data_writes(plan: &LogicalPlan) -> Result<BTreeSet<usize>, LixError> {
268
- let mut strict_binary_params = BTreeSet::new();
269
- let LogicalPlan::Dml(dml) = plan else {
270
- return Ok(strict_binary_params);
271
- };
272
- if dml.table_name.table() != "lix_file"
273
- || !matches!(dml.op, WriteOp::Insert(_) | WriteOp::Update)
274
- {
275
- return Ok(strict_binary_params);
276
- }
277
-
278
- reject_non_binary_lix_file_data_write(&dml.input, &mut strict_binary_params)?;
279
- Ok(strict_binary_params)
280
- }
281
-
282
- fn reject_non_binary_lix_file_data_write(
283
- input: &LogicalPlan,
284
- strict_binary_params: &mut BTreeSet<usize>,
285
- ) -> Result<(), LixError> {
286
- let LogicalPlan::Projection(projection) = input else {
287
- return Ok(());
288
- };
289
-
290
- let Some(data_expr) = projection.expr.iter().find_map(|expr| match expr {
291
- Expr::Alias(alias) if alias.name == "data" => Some(alias.expr.as_ref()),
292
- _ => None,
293
- }) else {
294
- return Ok(());
295
- };
296
-
297
- validate_lix_file_data_expr(data_expr, strict_binary_params)?;
298
-
299
- let Expr::Column(column) = data_expr else {
300
- return Ok(());
301
- };
302
- let LogicalPlan::Values(values) = projection.input.as_ref() else {
303
- return Ok(());
304
- };
305
- let Ok(column_index) = values.schema.index_of_column(column) else {
306
- return Ok(());
307
- };
308
-
309
- for row in &values.values {
310
- if let Some(value_expr) = row.get(column_index) {
311
- validate_lix_file_data_expr(value_expr, strict_binary_params)?;
312
- }
313
- }
314
-
315
- Ok(())
316
- }
317
-
318
- fn validate_lix_file_data_expr(
319
- expr: &Expr,
320
- strict_binary_params: &mut BTreeSet<usize>,
321
- ) -> Result<(), LixError> {
322
- match expr {
323
- Expr::Cast(cast) if is_binary_type(&cast.data_type) => {
324
- if collect_placeholder_param(&cast.expr, strict_binary_params)? {
325
- return Ok(());
326
- }
327
- if !logical_expr_is_binary_or_null(&cast.expr) {
328
- return Err(lix_file_data_type_lix_error());
329
- }
330
- }
331
- Expr::Placeholder(_) => {
332
- collect_placeholder_param(expr, strict_binary_params)?;
333
- }
334
- Expr::Alias(alias) => validate_lix_file_data_expr(&alias.expr, strict_binary_params)?,
335
- _ => {}
336
- }
337
- Ok(())
338
- }
339
-
340
- fn collect_placeholder_param(
341
- expr: &Expr,
342
- strict_binary_params: &mut BTreeSet<usize>,
343
- ) -> Result<bool, LixError> {
344
- match expr {
345
- Expr::Placeholder(placeholder) => {
346
- let index = placeholder_index(&placeholder.id)?;
347
- strict_binary_params.insert(index);
348
- Ok(true)
349
- }
350
- Expr::Alias(alias) => collect_placeholder_param(&alias.expr, strict_binary_params),
351
- _ => Ok(false),
352
- }
353
- }
354
-
355
- fn placeholder_index(id: &str) -> Result<usize, LixError> {
356
- id.strip_prefix('$')
357
- .and_then(|raw| raw.parse::<usize>().ok())
358
- .filter(|index| *index > 0)
359
- .ok_or_else(|| {
360
- LixError::new(
361
- LixError::CODE_PARSE_ERROR,
362
- format!("unsupported SQL parameter placeholder '{id}'"),
363
- )
364
- .with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
365
- })
366
- }
367
-
368
- pub(crate) async fn execute_logical_plan(
369
- plan: SqlLogicalPlan,
370
- params: &[Value],
371
- ) -> Result<SqlQueryResult, LixError> {
372
- let SqlLogicalPlan {
373
- session,
374
- plan,
375
- kind: _,
376
- notices,
377
- strict_binary_params,
378
- } = plan;
379
- validate_parameter_count(&plan, params.len())?;
380
- validate_strict_binary_params(&strict_binary_params, params)?;
381
-
382
- let mut dataframe = session
383
- .execute_logical_plan(plan)
384
- .await
385
- .map_err(datafusion_error_to_lix_error)?;
386
- if !params.is_empty() {
387
- dataframe = dataframe
388
- .with_param_values(ParamValues::List(
389
- params.iter().map(scalar_value_from_lix_value).collect(),
390
- ))
391
- .map_err(datafusion_error_to_lix_error)?;
392
- }
393
-
394
- let result_fields = dataframe
395
- .schema()
396
- .fields()
397
- .iter()
398
- .map(|field| field.as_ref().clone())
399
- .collect::<Vec<_>>();
400
- let batches = super::runtime::collect_dataframe(dataframe)
401
- .await
402
- .map_err(datafusion_error_to_lix_error)?;
403
- let mut result = query_result_from_batches(&result_fields, &batches)?;
404
- result.notices = notices;
405
- Ok(result)
406
- }
407
-
408
- fn validate_strict_binary_params(
409
- strict_binary_params: &BTreeSet<usize>,
410
- params: &[Value],
411
- ) -> Result<(), LixError> {
412
- for index in strict_binary_params {
413
- let Some(value) = params.get(index - 1) else {
414
- continue;
415
- };
416
- if !matches!(value, Value::Blob(_)) {
417
- return Err(lix_file_data_type_lix_error());
418
- }
419
- }
420
- Ok(())
421
- }
422
-
423
- fn validate_parameter_count(plan: &LogicalPlan, param_count: usize) -> Result<(), LixError> {
424
- let parameter_names = plan
425
- .get_parameter_names()
426
- .map_err(datafusion_error_to_lix_error)?;
427
- let expected_count = expected_positional_parameter_count(&parameter_names)?;
428
- if param_count == expected_count {
429
- return Ok(());
430
- }
431
-
432
- Err(LixError::new(
433
- LixError::CODE_INVALID_PARAM,
434
- format!(
435
- "SQL expected {expected_count} parameter(s), but {param_count} parameter(s) were provided"
436
- ),
437
- )
438
- .with_details(json!({
439
- "operation": "execute",
440
- "expected_param_count": expected_count,
441
- "provided_param_count": param_count,
442
- "placeholders": sorted_parameter_names(&parameter_names),
443
- })))
444
- }
445
-
446
- fn expected_positional_parameter_count(
447
- parameter_names: &HashSet<String>,
448
- ) -> Result<usize, LixError> {
449
- let mut max_index = 0usize;
450
- for name in parameter_names {
451
- let Some(index) = name
452
- .strip_prefix('$')
453
- .and_then(|raw| raw.parse::<usize>().ok())
454
- else {
455
- return Err(LixError::new(
456
- LixError::CODE_PARSE_ERROR,
457
- format!("unsupported SQL parameter placeholder '{name}'"),
458
- )
459
- .with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
460
- .with_details(json!({
461
- "operation": "execute",
462
- "placeholder": name,
463
- })));
464
- };
465
- if index == 0 {
466
- return Err(LixError::new(
467
- LixError::CODE_PARSE_ERROR,
468
- "SQL parameter placeholders are 1-indexed",
469
- )
470
- .with_hint("Use placeholders like ?, ? or numbered placeholders like $1, $2, ...")
471
- .with_details(json!({
472
- "operation": "execute",
473
- "placeholder": name,
474
- })));
475
- }
476
- max_index = max_index.max(index);
477
- }
478
- Ok(max_index)
479
- }
480
-
481
- fn sorted_parameter_names(parameter_names: &HashSet<String>) -> Vec<String> {
482
- let mut names = parameter_names.iter().cloned().collect::<Vec<_>>();
483
- names.sort();
484
- names
485
- }
486
-
487
- fn reject_read_only_history_view_dml_from_statement(
488
- statement: &DataFusionStatement,
489
- visible_schemas: &[JsonValue],
490
- ) -> Result<(), LixError> {
491
- let target_names = super::datafusion_statement_dml_target_table_names(statement);
492
- for target_name in target_names {
493
- if is_history_view_name(&target_name, visible_schemas)? {
494
- return Err(read_only_history_view_error(&target_name));
495
- }
496
- }
497
- Ok(())
498
- }
499
-
500
- fn is_history_view_name(table_name: &str, visible_schemas: &[JsonValue]) -> Result<bool, LixError> {
501
- if matches!(
502
- table_name,
503
- "lix_state_history" | "lix_file_history" | "lix_directory_history"
504
- ) {
505
- return Ok(true);
506
- }
507
-
508
- for schema in visible_schemas {
509
- let schema_key = schema_key_from_definition(schema)?;
510
- if table_name == format!("{}_history", schema_key.schema_key) {
511
- return Ok(true);
512
- }
513
- }
514
-
515
- Ok(false)
516
- }
517
-
518
- fn read_only_history_view_error(view_name: &str) -> LixError {
519
- LixError::new(
520
- LixError::CODE_READ_ONLY,
521
- format!("DML cannot write read-only history view '{view_name}'"),
522
- )
523
- .with_hint(
524
- "History views are query-only; write to the live surface such as lix_state, lix_file, lix_directory, or the typed entity table.",
525
- )
526
- }
527
-
528
- fn classify_logical_plan(plan: &LogicalPlan) -> SqlStatementKind {
529
- match plan {
530
- LogicalPlan::Dml(_) => SqlStatementKind::Write,
531
- LogicalPlan::Ddl(_) | LogicalPlan::Statement(_) | LogicalPlan::Copy(_) => {
532
- SqlStatementKind::Other
533
- }
534
- _ => SqlStatementKind::Read,
535
- }
536
- }
537
-
538
- fn validate_supported_logical_plan(plan: &LogicalPlan) -> Result<(), LixError> {
539
- match plan {
540
- LogicalPlan::Ddl(_) => {
541
- return Err(LixError::new(
542
- LixError::CODE_UNSUPPORTED_SQL,
543
- "DDL statements are not supported by Lix SQL",
544
- )
545
- .with_hint(
546
- "Use Lix entity surfaces such as lix_registered_schema, lix_version, lix_file, and lix_key_value instead of CREATE/DROP statements.",
547
- ));
548
- }
549
- LogicalPlan::Statement(_) => {
550
- return Err(LixError::new(
551
- LixError::CODE_UNSUPPORTED_SQL,
552
- "SQL utility statements are not supported by Lix SQL",
553
- ));
554
- }
555
- LogicalPlan::Copy(_) => {
556
- return Err(LixError::new(
557
- LixError::CODE_UNSUPPORTED_SQL,
558
- "COPY statements are not supported by Lix SQL",
559
- ));
560
- }
561
- LogicalPlan::RecursiveQuery(_) => {
562
- return Err(LixError::new(
563
- LixError::CODE_UNSUPPORTED_SQL,
564
- "recursive CTEs are not supported by Lix SQL",
565
- )
566
- .with_hint(
567
- "Use explicit commit graph surfaces such as lix_commit, lix_commit_edge, and lix_state_history instead of WITH RECURSIVE.",
568
- ));
569
- }
570
- _ => {}
571
- }
572
-
573
- for input in plan.inputs() {
574
- validate_supported_logical_plan(input)?;
575
- }
576
-
577
- Ok(())
578
- }
579
-
580
- fn scalar_value_from_lix_value(value: &Value) -> ScalarAndMetadata {
581
- match value {
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(),
592
- }
593
- }
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
-
602
- fn datafusion_error_to_lix_error(error: datafusion::error::DataFusionError) -> LixError {
603
- super::error::datafusion_error_to_lix_error(error)
604
- }
605
-
606
- fn query_result_from_batches(
607
- result_fields: &[Field],
608
- batches: &[RecordBatch],
609
- ) -> Result<SqlQueryResult, LixError> {
610
- let result_columns = result_fields
611
- .iter()
612
- .map(|field| field.name().to_string())
613
- .collect::<Vec<_>>();
614
- let mut rows = Vec::<Vec<Value>>::new();
615
- for batch in batches {
616
- for row_index in 0..batch.num_rows() {
617
- let mut row = Vec::<Value>::with_capacity(batch.num_columns());
618
- for (column_index, array) in batch.columns().iter().enumerate() {
619
- let scalar = ScalarValue::try_from_array(array.as_ref(), row_index)
620
- .map_err(datafusion_error_to_lix_error)?;
621
- let field = result_fields.get(column_index);
622
- row.push(scalar_value_to_lix_value(&scalar, field)?);
623
- }
624
- rows.push(row);
625
- }
626
- }
627
-
628
- Ok(SqlQueryResult {
629
- rows,
630
- columns: result_columns.to_vec(),
631
- notices: Vec::new(),
632
- })
633
- }
634
-
635
- fn history_filter_notices(plan: &LogicalPlan) -> Vec<LixNotice> {
636
- let mut observations = Vec::new();
637
- collect_notice_observations(plan, &Vec::new(), &mut observations);
638
-
639
- let mut notices = Vec::new();
640
- let mut emitted_codes = HashSet::<String>::new();
641
- for observation in observations {
642
- for rule in HISTORY_NOTICE_RULES {
643
- if observation.table_name != rule.table_name {
644
- continue;
645
- }
646
- if !observation.references_any(rule.payload_columns)
647
- || observation.references_any(rule.identity_columns)
648
- {
649
- continue;
650
- }
651
-
652
- let code = format!("LIX_HISTORY_NON_IDENTITY_FILTER:{}", rule.table_name);
653
- if emitted_codes.insert(code) {
654
- notices.push(history_non_identity_filter_notice(rule.table_name));
655
- }
656
- }
657
- }
658
- notices
659
- }
660
-
661
- #[derive(Debug)]
662
- struct NoticeObservation {
663
- table_name: String,
664
- filter_columns: HashSet<String>,
665
- }
666
-
667
- impl NoticeObservation {
668
- fn references_any(&self, columns: &[&str]) -> bool {
669
- columns
670
- .iter()
671
- .any(|column| self.filter_columns.contains(*column))
672
- }
673
- }
674
-
675
- struct HistoryNoticeRule {
676
- table_name: &'static str,
677
- payload_columns: &'static [&'static str],
678
- identity_columns: &'static [&'static str],
679
- }
680
-
681
- const HISTORY_NOTICE_RULES: &[HistoryNoticeRule] = &[
682
- HistoryNoticeRule {
683
- table_name: "lix_file_history",
684
- payload_columns: &["path", "directory_id", "name", "hidden", "data"],
685
- identity_columns: &["id", "lixcol_entity_id"],
686
- },
687
- HistoryNoticeRule {
688
- table_name: "lix_directory_history",
689
- payload_columns: &["path", "parent_id", "name", "hidden"],
690
- identity_columns: &["id", "lixcol_entity_id"],
691
- },
692
- ];
693
-
694
- fn collect_notice_observations(
695
- plan: &LogicalPlan,
696
- active_filter_columns: &Vec<HashSet<String>>,
697
- observations: &mut Vec<NoticeObservation>,
698
- ) {
699
- match plan {
700
- LogicalPlan::Filter(filter) => {
701
- let mut next_filters = active_filter_columns.clone();
702
- next_filters.push(expr_column_names(&filter.predicate));
703
- collect_notice_observations(&filter.input, &next_filters, observations);
704
- }
705
- LogicalPlan::TableScan(scan) => {
706
- let mut filter_columns = HashSet::new();
707
- for columns in active_filter_columns {
708
- filter_columns.extend(columns.iter().cloned());
709
- }
710
- for filter in &scan.filters {
711
- filter_columns.extend(expr_column_names(filter));
712
- }
713
- if !filter_columns.is_empty() {
714
- observations.push(NoticeObservation {
715
- table_name: table_reference_name(&scan.table_name),
716
- filter_columns,
717
- });
718
- }
719
- }
720
- other => {
721
- for input in other.inputs() {
722
- collect_notice_observations(input, active_filter_columns, observations);
723
- }
724
- }
725
- }
726
- }
727
-
728
- fn expr_column_names(expr: &Expr) -> HashSet<String> {
729
- expr.column_refs()
730
- .iter()
731
- .map(|column| column.name.clone())
732
- .collect()
733
- }
734
-
735
- fn table_reference_name(table: &datafusion::common::TableReference) -> String {
736
- match table {
737
- datafusion::common::TableReference::Bare { table } => table.to_string(),
738
- datafusion::common::TableReference::Partial { table, .. } => table.to_string(),
739
- datafusion::common::TableReference::Full { table, .. } => table.to_string(),
740
- }
741
- }
742
-
743
- fn history_non_identity_filter_notice(view_name: &str) -> LixNotice {
744
- LixNotice {
745
- code: "LIX_HISTORY_NON_IDENTITY_FILTER".to_string(),
746
- message: format!("{view_name} was filtered without an identity predicate."),
747
- hint: Some(
748
- "Filter by id or lixcol_entity_id to include tombstones and renamed history."
749
- .to_string(),
750
- ),
751
- }
752
- }
753
-
754
- fn scalar_value_to_lix_value(
755
- value: &ScalarValue,
756
- field: Option<&Field>,
757
- ) -> Result<Value, LixError> {
758
- match value {
759
- ScalarValue::Null => Ok(Value::Null),
760
- ScalarValue::Boolean(Some(value)) => Ok(Value::Boolean(*value)),
761
- ScalarValue::Boolean(None) => Ok(Value::Null),
762
- ScalarValue::Int8(Some(value)) => Ok(Value::Integer(i64::from(*value))),
763
- ScalarValue::Int8(None) => Ok(Value::Null),
764
- ScalarValue::Int16(Some(value)) => Ok(Value::Integer(i64::from(*value))),
765
- ScalarValue::Int16(None) => Ok(Value::Null),
766
- ScalarValue::Int32(Some(value)) => Ok(Value::Integer(i64::from(*value))),
767
- ScalarValue::Int32(None) => Ok(Value::Null),
768
- ScalarValue::Int64(Some(value)) => Ok(Value::Integer(*value)),
769
- ScalarValue::Int64(None) => Ok(Value::Null),
770
- ScalarValue::UInt8(Some(value)) => Ok(Value::Integer(i64::from(*value))),
771
- ScalarValue::UInt8(None) => Ok(Value::Null),
772
- ScalarValue::UInt16(Some(value)) => Ok(Value::Integer(i64::from(*value))),
773
- ScalarValue::UInt16(None) => Ok(Value::Null),
774
- ScalarValue::UInt32(Some(value)) => Ok(Value::Integer(i64::from(*value))),
775
- ScalarValue::UInt32(None) => Ok(Value::Null),
776
- ScalarValue::UInt64(Some(value)) => match i64::try_from(*value) {
777
- Ok(value) => Ok(Value::Integer(value)),
778
- Err(_) => Ok(Value::Text(value.to_string())),
779
- },
780
- ScalarValue::UInt64(None) => Ok(Value::Null),
781
- ScalarValue::Float32(Some(value)) => Ok(Value::Real(f64::from(*value))),
782
- ScalarValue::Float32(None) => Ok(Value::Null),
783
- ScalarValue::Float64(Some(value)) => Ok(Value::Real(*value)),
784
- ScalarValue::Float64(None) => Ok(Value::Null),
785
- ScalarValue::Utf8(Some(value))
786
- | ScalarValue::Utf8View(Some(value))
787
- | ScalarValue::LargeUtf8(Some(value)) => string_scalar_to_lix_value(value, field),
788
- ScalarValue::Utf8(None) | ScalarValue::Utf8View(None) | ScalarValue::LargeUtf8(None) => {
789
- Ok(Value::Null)
790
- }
791
- ScalarValue::Binary(Some(value)) | ScalarValue::LargeBinary(Some(value)) => {
792
- Ok(Value::Blob(value.clone()))
793
- }
794
- ScalarValue::Binary(None) | ScalarValue::LargeBinary(None) => Ok(Value::Null),
795
- other => Ok(Value::Text(other.to_string())),
796
- }
797
- }
798
-
799
- fn string_scalar_to_lix_value(value: &str, field: Option<&Field>) -> Result<Value, LixError> {
800
- if field.is_some_and(field_is_json) {
801
- return serde_json::from_str::<serde_json::Value>(value)
802
- .map(Value::Json)
803
- .map_err(|error| {
804
- LixError::new(
805
- "LIX_ERROR_INVALID_JSON",
806
- format!(
807
- "column '{}' is marked as JSON but contains invalid JSON: {error}",
808
- field
809
- .map(|field| field.name().as_str())
810
- .unwrap_or("<unknown>")
811
- ),
812
- )
813
- });
814
- }
815
- Ok(Value::Text(value.to_string()))
816
- }
817
-
818
- #[cfg(test)]
819
- mod tests {
820
- use std::sync::{Arc, Mutex};
821
-
822
- use async_trait::async_trait;
823
- use serde_json::json;
824
- use serde_json::Value as JsonValue;
825
-
826
- use super::{
827
- create_write_logical_plan, execute_logical_plan, execute_sql, SqlExecutionContext,
828
- SqlWriteExecutionContext,
829
- };
830
- use crate::binary_cas::BlobDataReader;
831
- use crate::commit_graph::{
832
- CommitGraphChangeHistoryEntry, CommitGraphChangeHistoryRequest, CommitGraphCommit,
833
- CommitGraphEdge, CommitGraphReader, ReachableCommitGraphCommit,
834
- };
835
- use crate::commit_store::CommitStoreContext;
836
- use crate::functions::{
837
- FunctionProvider, FunctionProviderHandle, SharedFunctionProvider, SystemFunctionProvider,
838
- };
839
- use crate::json_store::JsonStoreContext;
840
- use crate::live_state::{
841
- LiveStateContext, LiveStateReader, LiveStateRowRequest, LiveStateScanRequest,
842
- MaterializedLiveStateRow,
843
- };
844
- use crate::sql2::{CommitStoreQuerySource, SqlCommitStoreQuerySource};
845
- use crate::storage::{
846
- KvEntryPage, KvExistsBatch, KvGetRequest, KvKeyPage, KvScanRequest, KvValueBatch,
847
- KvValuePage, StorageContext, StorageReadScope, StorageReadTransaction, StorageReader,
848
- StorageWriteSet,
849
- };
850
- use crate::tracked_state::TrackedStateContext;
851
- use crate::transaction::prepare_version_ref_row;
852
- use crate::transaction::types::{
853
- TransactionWrite, TransactionWriteOutcome, TransactionWriteRow,
854
- };
855
- use crate::untracked_state::UntrackedStateContext;
856
- use crate::version::VersionRefReader;
857
- use crate::{Engine, ExecuteResult, SessionContext};
858
- use crate::{LixError, Value};
859
-
860
- struct DummyBlobReader;
861
- struct DummyLiveStateReader;
862
- struct RowsLiveStateReader {
863
- rows: Vec<MaterializedLiveStateRow>,
864
- }
865
- struct BackendBlobReader(StorageContext);
866
- struct DummyCommitGraphReader;
867
- struct DummyVersionRefReader;
868
- struct TestReadTransaction(StorageContext);
869
-
870
- fn test_read_scope(
871
- storage: StorageContext,
872
- ) -> StorageReadScope<Box<dyn StorageReadTransaction + Send + Sync + 'static>> {
873
- StorageReadScope::new(Box::new(TestReadTransaction(storage)))
874
- }
875
-
876
- #[async_trait]
877
- impl StorageReader for TestReadTransaction {
878
- async fn get_values(&mut self, request: KvGetRequest) -> Result<KvValueBatch, LixError> {
879
- self.0.get_values(request).await
880
- }
881
-
882
- async fn exists_many(&mut self, request: KvGetRequest) -> Result<KvExistsBatch, LixError> {
883
- self.0.exists_many(request).await
884
- }
885
-
886
- async fn scan_keys(&mut self, request: KvScanRequest) -> Result<KvKeyPage, LixError> {
887
- self.0.scan_keys(request).await
888
- }
889
-
890
- async fn scan_values(&mut self, request: KvScanRequest) -> Result<KvValuePage, LixError> {
891
- self.0.scan_values(request).await
892
- }
893
-
894
- async fn scan_entries(&mut self, request: KvScanRequest) -> Result<KvEntryPage, LixError> {
895
- self.0.scan_entries(request).await
896
- }
897
- }
898
-
899
- #[async_trait]
900
- impl StorageReadTransaction for TestReadTransaction {
901
- async fn rollback(self: Box<Self>) -> Result<(), LixError> {
902
- Ok(())
903
- }
904
- }
905
-
906
- #[allow(dead_code)]
907
- fn test_functions() -> FunctionProviderHandle {
908
- SharedFunctionProvider::new(
909
- Box::new(SystemFunctionProvider) as Box<dyn FunctionProvider + Send>
910
- )
911
- }
912
-
913
- #[derive(Default)]
914
- struct CapturingStagedWrites {
915
- deltas: Vec<CapturedStageWrite>,
916
- }
917
-
918
- #[derive(Clone)]
919
- struct CapturedStageWrite {
920
- rows: Vec<TransactionWriteRow>,
921
- }
922
-
923
- impl CapturedStageWrite {
924
- fn pending_write_overlay(&self) -> Result<CapturedStageOverlay, LixError> {
925
- Ok(CapturedStageOverlay {
926
- rows: self.rows.clone(),
927
- })
928
- }
929
- }
930
-
931
- struct CapturedStageOverlay {
932
- rows: Vec<TransactionWriteRow>,
933
- }
934
-
935
- impl CapturedStageOverlay {
936
- fn visible_semantic_rows(
937
- &self,
938
- include_tombstones: bool,
939
- schema_key: &str,
940
- ) -> Vec<CapturedStageRow> {
941
- self.visible_all_semantic_rows()
942
- .into_iter()
943
- .filter(|row| row.schema_key == schema_key)
944
- .filter(|row| include_tombstones || !row.tombstone)
945
- .collect()
946
- }
947
-
948
- fn visible_all_semantic_rows(&self) -> Vec<CapturedStageRow> {
949
- self.rows
950
- .iter()
951
- .cloned()
952
- .map(CapturedStageRow::from)
953
- .collect()
954
- }
955
- }
956
-
957
- struct CapturedStageRow {
958
- entity_id: String,
959
- schema_key: String,
960
- version_id: String,
961
- file_id: Option<String>,
962
- snapshot_content: Option<String>,
963
- metadata: Option<String>,
964
- global: bool,
965
- untracked: bool,
966
- tombstone: bool,
967
- }
968
-
969
- impl From<TransactionWriteRow> for CapturedStageRow {
970
- fn from(row: TransactionWriteRow) -> Self {
971
- Self {
972
- entity_id: row
973
- .entity_id
974
- .expect("captured staged row should carry entity_id")
975
- .as_json_array_text()
976
- .expect("captured staged row should project entity_id"),
977
- schema_key: row.schema_key,
978
- version_id: row.version_id,
979
- file_id: row.file_id,
980
- global: row.global,
981
- untracked: row.untracked,
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()),
985
- }
986
- }
987
- }
988
-
989
- struct DummySqlExecutionContext<'a> {
990
- active_version_id: &'a str,
991
- blob_reader: Arc<dyn BlobDataReader>,
992
- live_state: Arc<dyn LiveStateReader>,
993
- schema_definitions: Vec<JsonValue>,
994
- }
995
-
996
- impl<'a> SqlExecutionContext for DummySqlExecutionContext<'a> {
997
- fn active_version_id(&self) -> &str {
998
- self.active_version_id
999
- }
1000
-
1001
- fn live_state(&self) -> Arc<dyn LiveStateReader> {
1002
- Arc::clone(&self.live_state)
1003
- }
1004
-
1005
- fn functions(&self) -> FunctionProviderHandle {
1006
- test_functions()
1007
- }
1008
-
1009
- fn blob_reader(&self) -> Arc<dyn BlobDataReader> {
1010
- Arc::clone(&self.blob_reader)
1011
- }
1012
-
1013
- fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
1014
- let base_scope = test_read_scope(StorageContext::new(Arc::new(
1015
- crate::backend::testing::UnitTestBackend::new(),
1016
- )));
1017
- let read_scope = StorageReadScope::new(base_scope.store());
1018
- CommitStoreQuerySource {
1019
- commit_store_reader: Arc::new(CommitStoreContext::new().reader(read_scope.store())),
1020
- json_reader: JsonStoreContext::new().reader(read_scope.store()),
1021
- }
1022
- }
1023
-
1024
- fn commit_graph(&self) -> Box<dyn CommitGraphReader> {
1025
- Box::new(DummyCommitGraphReader)
1026
- }
1027
-
1028
- fn version_ref(&self) -> Arc<dyn VersionRefReader> {
1029
- Arc::new(DummyVersionRefReader)
1030
- }
1031
-
1032
- fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
1033
- Ok(self.schema_definitions.clone())
1034
- }
1035
- }
1036
-
1037
- struct DummySqlWriteExecutionContext<'a> {
1038
- active_version_id: &'a str,
1039
- blob_reader: Arc<dyn BlobDataReader>,
1040
- live_state: Arc<dyn LiveStateReader>,
1041
- staged_writes: Arc<Mutex<CapturingStagedWrites>>,
1042
- schema_definitions: Vec<JsonValue>,
1043
- }
1044
-
1045
- #[async_trait]
1046
- impl SqlWriteExecutionContext for DummySqlWriteExecutionContext<'_> {
1047
- fn active_version_id(&self) -> &str {
1048
- self.active_version_id
1049
- }
1050
-
1051
- fn functions(&self) -> FunctionProviderHandle {
1052
- test_functions()
1053
- }
1054
-
1055
- fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
1056
- Ok(self.schema_definitions.clone())
1057
- }
1058
-
1059
- async fn load_bytes_many(
1060
- &mut self,
1061
- hashes: &[crate::binary_cas::BlobHash],
1062
- ) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
1063
- self.blob_reader.load_bytes_many(hashes).await
1064
- }
1065
-
1066
- async fn scan_live_state(
1067
- &mut self,
1068
- request: &LiveStateScanRequest,
1069
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
1070
- self.live_state.scan_rows(request).await
1071
- }
1072
-
1073
- async fn load_version_head(
1074
- &mut self,
1075
- version_id: &str,
1076
- ) -> Result<Option<String>, LixError> {
1077
- Ok(Some(format!("commit-{version_id}")))
1078
- }
1079
-
1080
- async fn stage_write(
1081
- &mut self,
1082
- write: TransactionWrite,
1083
- ) -> Result<TransactionWriteOutcome, LixError> {
1084
- let count = match &write {
1085
- TransactionWrite::Rows { rows, .. } => rows.len() as u64,
1086
- TransactionWrite::RowsWithFileData { count, .. } => *count,
1087
- TransactionWrite::AdoptedChanges { changes } => changes.len() as u64,
1088
- };
1089
- let rows = match write {
1090
- TransactionWrite::Rows { rows, .. } => rows,
1091
- TransactionWrite::RowsWithFileData { rows, .. } => rows,
1092
- TransactionWrite::AdoptedChanges { .. } => Vec::new(),
1093
- };
1094
- self.staged_writes
1095
- .lock()
1096
- .expect("staged writes lock")
1097
- .deltas
1098
- .push(CapturedStageWrite { rows });
1099
- Ok(TransactionWriteOutcome { count })
1100
- }
1101
- }
1102
-
1103
- async fn execute_write_sql(
1104
- ctx: &mut dyn SqlWriteExecutionContext,
1105
- sql: &str,
1106
- params: &[Value],
1107
- ) -> Result<crate::SqlQueryResult, LixError> {
1108
- let plan = create_write_logical_plan(ctx, sql).await?;
1109
- execute_logical_plan(plan, params).await
1110
- }
1111
-
1112
- #[async_trait]
1113
- impl VersionRefReader for DummyVersionRefReader {
1114
- async fn load_head(
1115
- &self,
1116
- _version_id: &str,
1117
- ) -> Result<Option<crate::version::VersionHead>, LixError> {
1118
- Ok(None)
1119
- }
1120
-
1121
- async fn scan_heads(&self) -> Result<Vec<crate::version::VersionHead>, LixError> {
1122
- Ok(Vec::new())
1123
- }
1124
- }
1125
-
1126
- #[async_trait]
1127
- impl CommitGraphReader for DummyCommitGraphReader {
1128
- async fn load_commit(
1129
- &mut self,
1130
- _commit_id: &str,
1131
- ) -> Result<Option<CommitGraphCommit>, LixError> {
1132
- Ok(None)
1133
- }
1134
-
1135
- async fn all_commits(&mut self) -> Result<Vec<CommitGraphCommit>, LixError> {
1136
- Ok(Vec::new())
1137
- }
1138
-
1139
- async fn reachable_commits(
1140
- &mut self,
1141
- _head_commit_id: &str,
1142
- ) -> Result<Vec<ReachableCommitGraphCommit>, LixError> {
1143
- Ok(Vec::new())
1144
- }
1145
-
1146
- async fn best_common_ancestors(
1147
- &mut self,
1148
- _left_commit_id: &str,
1149
- _right_commit_id: &str,
1150
- ) -> Result<Vec<CommitGraphCommit>, LixError> {
1151
- Ok(Vec::new())
1152
- }
1153
-
1154
- async fn merge_base(
1155
- &mut self,
1156
- _left_commit_id: &str,
1157
- _right_commit_id: &str,
1158
- ) -> Result<CommitGraphCommit, LixError> {
1159
- Err(LixError::new(
1160
- "LIX_ERROR_UNKNOWN",
1161
- "dummy commit graph reader cannot resolve merge base",
1162
- ))
1163
- }
1164
-
1165
- fn commit_edges(&self, _commits: &[CommitGraphCommit]) -> Vec<CommitGraphEdge> {
1166
- Vec::new()
1167
- }
1168
-
1169
- async fn change_history_from_commit(
1170
- &mut self,
1171
- _start_commit_id: &str,
1172
- _request: &CommitGraphChangeHistoryRequest,
1173
- ) -> Result<Vec<CommitGraphChangeHistoryEntry>, LixError> {
1174
- Ok(Vec::new())
1175
- }
1176
- }
1177
-
1178
- #[async_trait]
1179
- impl LiveStateReader for DummyLiveStateReader {
1180
- async fn scan_rows(
1181
- &self,
1182
- _request: &LiveStateScanRequest,
1183
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
1184
- Ok(vec![])
1185
- }
1186
-
1187
- async fn load_row(
1188
- &self,
1189
- _request: &LiveStateRowRequest,
1190
- ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
1191
- Ok(None)
1192
- }
1193
- }
1194
-
1195
- #[async_trait]
1196
- impl LiveStateReader for RowsLiveStateReader {
1197
- async fn scan_rows(
1198
- &self,
1199
- _request: &LiveStateScanRequest,
1200
- ) -> Result<Vec<MaterializedLiveStateRow>, LixError> {
1201
- Ok(self.rows.clone())
1202
- }
1203
-
1204
- async fn load_row(
1205
- &self,
1206
- _request: &LiveStateRowRequest,
1207
- ) -> Result<Option<MaterializedLiveStateRow>, LixError> {
1208
- Ok(None)
1209
- }
1210
- }
1211
-
1212
- #[async_trait]
1213
- impl BlobDataReader for DummyBlobReader {
1214
- async fn load_bytes_many(
1215
- &self,
1216
- hashes: &[crate::binary_cas::BlobHash],
1217
- ) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
1218
- Ok(crate::binary_cas::BlobBytesBatch::new(vec![
1219
- None;
1220
- hashes.len()
1221
- ]))
1222
- }
1223
- }
1224
-
1225
- #[async_trait]
1226
- impl BlobDataReader for BackendBlobReader {
1227
- async fn load_bytes_many(
1228
- &self,
1229
- hashes: &[crate::binary_cas::BlobHash],
1230
- ) -> Result<crate::binary_cas::BlobBytesBatch, LixError> {
1231
- let binary_cas = crate::binary_cas::BinaryCasContext::new();
1232
- let reader = binary_cas.reader(self.0.clone());
1233
- reader.load_bytes_many(hashes).await
1234
- }
1235
- }
1236
-
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),
1240
- schema_key: "lix_key_value".to_string(),
1241
- file_id: None,
1242
- snapshot_content: Some("{\"key\":\"hello\",\"value\":\"world\"}".to_string()),
1243
- metadata: metadata.map(str::to_string),
1244
- deleted: false,
1245
- version_id: "version-a".to_string(),
1246
- change_id: Some(format!("change-{entity_id}")),
1247
- commit_id: Some(format!("commit-{entity_id}")),
1248
- global: false,
1249
- untracked: false,
1250
- created_at: "2026-04-23T00:00:00Z".to_string(),
1251
- updated_at: "2026-04-23T01:00:00Z".to_string(),
1252
- }
1253
- }
1254
-
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),
1258
- schema_key: "test_state_schema".to_string(),
1259
- file_id: None,
1260
- snapshot_content: Some(format!("{{\"value\":\"{value}\"}}")),
1261
- metadata: Some(json!({ "source": entity_id }).to_string()),
1262
- deleted: false,
1263
- version_id: version_id.to_string(),
1264
- change_id: Some(format!("change-{entity_id}")),
1265
- commit_id: Some(format!("commit-{entity_id}")),
1266
- global: false,
1267
- untracked: false,
1268
- created_at: "2026-04-23T00:00:00Z".to_string(),
1269
- updated_at: "2026-04-23T01:00:00Z".to_string(),
1270
- }
1271
- }
1272
-
1273
- fn live_directory_row(
1274
- entity_id: &str,
1275
- version_id: &str,
1276
- parent_id: Option<&str>,
1277
- name: &str,
1278
- hidden: bool,
1279
- ) -> MaterializedLiveStateRow {
1280
- MaterializedLiveStateRow {
1281
- entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1282
- schema_key: "lix_directory_descriptor".to_string(),
1283
- file_id: None,
1284
- snapshot_content: Some(
1285
- json!({
1286
- "id": entity_id,
1287
- "parent_id": parent_id,
1288
- "name": name,
1289
- "hidden": hidden
1290
- })
1291
- .to_string(),
1292
- ),
1293
- metadata: Some(json!({ "source": entity_id }).to_string()),
1294
- deleted: false,
1295
- version_id: version_id.to_string(),
1296
- change_id: Some(format!("change-{entity_id}")),
1297
- commit_id: Some(format!("commit-{entity_id}")),
1298
- global: false,
1299
- untracked: false,
1300
- created_at: "2026-04-23T00:00:00Z".to_string(),
1301
- updated_at: "2026-04-23T01:00:00Z".to_string(),
1302
- }
1303
- }
1304
-
1305
- fn live_file_row(
1306
- entity_id: &str,
1307
- version_id: &str,
1308
- directory_id: Option<&str>,
1309
- name: &str,
1310
- hidden: bool,
1311
- ) -> MaterializedLiveStateRow {
1312
- MaterializedLiveStateRow {
1313
- entity_id: crate::entity_identity::EntityIdentity::single(entity_id),
1314
- schema_key: "lix_file_descriptor".to_string(),
1315
- file_id: None,
1316
- snapshot_content: Some(
1317
- json!({
1318
- "id": entity_id,
1319
- "directory_id": directory_id,
1320
- "name": name,
1321
- "hidden": hidden
1322
- })
1323
- .to_string(),
1324
- ),
1325
- metadata: Some(json!({ "source": entity_id }).to_string()),
1326
- deleted: false,
1327
- version_id: version_id.to_string(),
1328
- change_id: Some(format!("change-{entity_id}")),
1329
- commit_id: Some(format!("commit-{entity_id}")),
1330
- global: false,
1331
- untracked: false,
1332
- created_at: "2026-04-23T00:00:00Z".to_string(),
1333
- updated_at: "2026-04-23T01:00:00Z".to_string(),
1334
- }
1335
- }
1336
-
1337
- #[tokio::test]
1338
- async fn sql_execution_context_exposes_live_state_and_blob_reader() {
1339
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1340
- let live_state = Arc::new(DummyLiveStateReader);
1341
- let ctx = DummySqlExecutionContext {
1342
- active_version_id: "version-a",
1343
- blob_reader: Arc::clone(&blob_reader),
1344
- live_state: Arc::clone(&live_state) as Arc<dyn LiveStateReader>,
1345
- schema_definitions: vec![],
1346
- };
1347
-
1348
- let actual = ctx.live_state();
1349
- let expected = live_state as Arc<dyn LiveStateReader>;
1350
- assert_eq!(ctx.active_version_id(), "version-a");
1351
- assert!(Arc::ptr_eq(&actual, &expected));
1352
- assert!(Arc::ptr_eq(&ctx.blob_reader(), &blob_reader));
1353
- }
1354
-
1355
- #[tokio::test]
1356
- async fn execute_sql_uses_execution_context_boundary() {
1357
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1358
- let live_state = Arc::new(DummyLiveStateReader);
1359
- let ctx = DummySqlExecutionContext {
1360
- active_version_id: "version-a",
1361
- blob_reader,
1362
- live_state,
1363
- schema_definitions: vec![],
1364
- };
1365
-
1366
- let result = execute_sql(&ctx, "SELECT 1", &[])
1367
- .await
1368
- .expect("sql2 execute should support literal-only queries");
1369
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
1370
- }
1371
-
1372
- #[tokio::test]
1373
- async fn execute_sql_collects_union_all_partitions() {
1374
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1375
- let live_state = Arc::new(DummyLiveStateReader);
1376
- let ctx = DummySqlExecutionContext {
1377
- active_version_id: "version-a",
1378
- blob_reader,
1379
- live_state,
1380
- schema_definitions: vec![],
1381
- };
1382
-
1383
- let result = execute_sql(&ctx, "SELECT 1 UNION ALL SELECT 2", &[])
1384
- .await
1385
- .expect("sql2 execute should collect UNION ALL partitions");
1386
- assert_eq!(
1387
- result.rows,
1388
- vec![vec![Value::Integer(1)], vec![Value::Integer(2)]]
1389
- );
1390
- }
1391
-
1392
- #[tokio::test]
1393
- async fn execute_sql_rejects_extra_parameters() {
1394
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1395
- let live_state = Arc::new(DummyLiveStateReader);
1396
- let ctx = DummySqlExecutionContext {
1397
- active_version_id: "version-a",
1398
- blob_reader,
1399
- live_state,
1400
- schema_definitions: vec![],
1401
- };
1402
-
1403
- let error = execute_sql(
1404
- &ctx,
1405
- "SELECT $1 AS value",
1406
- &[Value::Integer(1), Value::Integer(2)],
1407
- )
1408
- .await
1409
- .expect_err("extra params should fail instead of being ignored");
1410
-
1411
- assert_eq!(error.code, LixError::CODE_INVALID_PARAM);
1412
- assert_eq!(
1413
- error.message,
1414
- "SQL expected 1 parameter(s), but 2 parameter(s) were provided"
1415
- );
1416
- assert_eq!(
1417
- error.details,
1418
- Some(json!({
1419
- "operation": "execute",
1420
- "expected_param_count": 1,
1421
- "provided_param_count": 2,
1422
- "placeholders": ["$1"],
1423
- }))
1424
- );
1425
- }
1426
-
1427
- #[tokio::test]
1428
- async fn execute_sql_exposes_datafusion_information_schema() {
1429
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1430
- let live_state = Arc::new(DummyLiveStateReader);
1431
- let ctx = DummySqlExecutionContext {
1432
- active_version_id: "version-a",
1433
- blob_reader,
1434
- live_state,
1435
- schema_definitions: vec![],
1436
- };
1437
-
1438
- let information_schema_result = execute_sql(
1439
- &ctx,
1440
- "SELECT table_name FROM information_schema.tables WHERE table_name = 'lix_state'",
1441
- &[],
1442
- )
1443
- .await
1444
- .expect("information_schema.tables should be enabled");
1445
- assert_eq!(
1446
- information_schema_result.rows,
1447
- vec![vec![Value::Text("lix_state".to_string())]]
1448
- );
1449
-
1450
- let tables_result = execute_sql(
1451
- &ctx,
1452
- "SELECT table_name FROM information_schema.tables",
1453
- &[],
1454
- )
1455
- .await
1456
- .expect("information_schema.tables should list registered tables");
1457
- assert!(tables_result.rows.iter().any(|row| {
1458
- row.iter()
1459
- .any(|value| matches!(value, Value::Text(value) if value == "lix_state"))
1460
- }));
1461
- }
1462
-
1463
- async fn setup_engine_history_fixture() -> Result<(SessionContext, String), LixError> {
1464
- let backend = crate::backend::testing::UnitTestBackend::new();
1465
- let init_receipt = Engine::initialize(Box::new(backend.clone())).await?;
1466
- let engine = Engine::new(Box::new(backend)).await?;
1467
- let session = engine.open_session(init_receipt.main_version_id).await?;
1468
-
1469
- session
1470
- .execute(
1471
- "INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
1472
- VALUES (\
1473
- lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"},\"count\":{\"type\":\"integer\"}},\"required\":[\"value\",\"count\"],\"additionalProperties\":false}'),\
1474
- false,\
1475
- false\
1476
- )",
1477
- &[],
1478
- )
1479
- .await?;
1480
- session
1481
- .execute(
1482
- "INSERT INTO test_state_schema \
1483
- (lixcol_entity_id, value, count, lixcol_metadata, lixcol_untracked) \
1484
- VALUES (lix_json('[\"entity-history\"]'), 'A', 7, '{\"source\":\"history\"}', false)",
1485
- &[],
1486
- )
1487
- .await?;
1488
- session
1489
- .execute(
1490
- "INSERT INTO lix_directory (id, path, hidden) \
1491
- VALUES ('dir-docs', '/docs/', false)",
1492
- &[],
1493
- )
1494
- .await?;
1495
- session
1496
- .execute(
1497
- "INSERT INTO lix_file (id, path, data, hidden) \
1498
- VALUES ('file-a', '/docs/readme.md', X'68656C6C6F', false)",
1499
- &[],
1500
- )
1501
- .await?;
1502
-
1503
- let active_version_id = session.active_version_id().await?;
1504
- let head_commit_id = engine
1505
- .load_version_head_commit_id(&active_version_id)
1506
- .await?
1507
- .ok_or_else(|| {
1508
- LixError::new(
1509
- "LIX_ERROR_UNKNOWN",
1510
- "history fixture expected the session version to have a head commit",
1511
- )
1512
- })?;
1513
- Ok((session, head_commit_id))
1514
- }
1515
-
1516
- #[tokio::test]
1517
- async fn lix_file_path_predicates_canonicalize_bound_values_like_writes() {
1518
- let backend = crate::backend::testing::UnitTestBackend::new();
1519
- let init_receipt = Engine::initialize(Box::new(backend.clone()))
1520
- .await
1521
- .expect("engine should initialize");
1522
- let engine = Engine::new(Box::new(backend))
1523
- .await
1524
- .expect("engine should open");
1525
- let session = engine
1526
- .open_session(init_receipt.main_version_id)
1527
- .await
1528
- .expect("session should open");
1529
-
1530
- session
1531
- .execute(
1532
- "INSERT INTO lix_file (id, path, data) VALUES ('file-nfc', $1, X'41')",
1533
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1534
- )
1535
- .await
1536
- .expect("NFD path insert should canonicalize");
1537
-
1538
- let nfd_result = session
1539
- .execute(
1540
- "SELECT id FROM lix_file WHERE path = $1",
1541
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1542
- )
1543
- .await
1544
- .expect("NFD path predicate should canonicalize");
1545
- assert_eq!(
1546
- rows_from_execute_result(nfd_result).1,
1547
- vec![vec![Value::Text("file-nfc".to_string())]]
1548
- );
1549
-
1550
- let percent_result = session
1551
- .execute(
1552
- "SELECT id FROM lix_file WHERE path = '/%43afe%CC%81.txt'",
1553
- &[],
1554
- )
1555
- .await
1556
- .expect("percent-encoded path predicate should canonicalize");
1557
- assert_eq!(
1558
- rows_from_execute_result(percent_result).1,
1559
- vec![vec![Value::Text("file-nfc".to_string())]]
1560
- );
1561
-
1562
- let reversed_result = session
1563
- .execute(
1564
- "SELECT id FROM lix_file WHERE $1 = path",
1565
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1566
- )
1567
- .await
1568
- .expect("reversed path predicate should canonicalize");
1569
- assert_eq!(
1570
- rows_from_execute_result(reversed_result).1,
1571
- vec![vec![Value::Text("file-nfc".to_string())]]
1572
- );
1573
-
1574
- let or_result = session
1575
- .execute(
1576
- "SELECT id FROM lix_file WHERE path = $1 OR id = 'missing'",
1577
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1578
- )
1579
- .await
1580
- .expect("OR path predicate should canonicalize");
1581
- assert_eq!(
1582
- rows_from_execute_result(or_result).1,
1583
- vec![vec![Value::Text("file-nfc".to_string())]]
1584
- );
1585
-
1586
- let not_result = session
1587
- .execute(
1588
- "SELECT id FROM lix_file WHERE NOT (path = $1)",
1589
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1590
- )
1591
- .await
1592
- .expect("NOT path predicate should canonicalize");
1593
- assert!(rows_from_execute_result(not_result).1.is_empty());
1594
-
1595
- let not_in_result = session
1596
- .execute(
1597
- "SELECT id FROM lix_file WHERE path NOT IN ($1)",
1598
- &[Value::Text("/%43afe%CC%81.txt".to_string())],
1599
- )
1600
- .await
1601
- .expect("NOT IN path predicate should canonicalize");
1602
- assert!(rows_from_execute_result(not_in_result).1.is_empty());
1603
-
1604
- let update_result = session
1605
- .execute(
1606
- "UPDATE lix_file SET hidden = true WHERE path = $1 OR id = 'missing'",
1607
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1608
- )
1609
- .await
1610
- .expect("update predicate should canonicalize through OR");
1611
- assert_eq!(update_result.rows_affected(), 1);
1612
-
1613
- let delete_result = session
1614
- .execute(
1615
- "DELETE FROM lix_file WHERE path = $1",
1616
- &[Value::Text("/%43afe%CC%81.txt".to_string())],
1617
- )
1618
- .await
1619
- .expect("delete predicate should canonicalize");
1620
- assert_eq!(delete_result.rows_affected(), 1);
1621
- }
1622
-
1623
- #[tokio::test]
1624
- async fn lix_file_path_predicates_reject_non_literal_path_values() {
1625
- let backend = crate::backend::testing::UnitTestBackend::new();
1626
- let init_receipt = Engine::initialize(Box::new(backend.clone()))
1627
- .await
1628
- .expect("engine should initialize");
1629
- let engine = Engine::new(Box::new(backend))
1630
- .await
1631
- .expect("engine should open");
1632
- let session = engine
1633
- .open_session(init_receipt.main_version_id)
1634
- .await
1635
- .expect("session should open");
1636
-
1637
- session
1638
- .execute(
1639
- "INSERT INTO lix_file (id, path, data) VALUES ('file-nfc', $1, X'41')",
1640
- &[Value::Text("/Cafe\u{301}.txt".to_string())],
1641
- )
1642
- .await
1643
- .expect("NFD path insert should canonicalize");
1644
-
1645
- let error = session
1646
- .execute("SELECT id FROM lix_file WHERE path = id", &[])
1647
- .await
1648
- .expect_err("computed path predicate values should be rejected");
1649
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1650
- assert!(
1651
- error
1652
- .message
1653
- .contains("filesystem path predicates only support literal path values"),
1654
- "{error:?}"
1655
- );
1656
- }
1657
-
1658
- #[tokio::test]
1659
- async fn lix_directory_path_predicates_canonicalize_bound_values_like_writes() {
1660
- let backend = crate::backend::testing::UnitTestBackend::new();
1661
- let init_receipt = Engine::initialize(Box::new(backend.clone()))
1662
- .await
1663
- .expect("engine should initialize");
1664
- let engine = Engine::new(Box::new(backend))
1665
- .await
1666
- .expect("engine should open");
1667
- let session = engine
1668
- .open_session(init_receipt.main_version_id)
1669
- .await
1670
- .expect("session should open");
1671
-
1672
- session
1673
- .execute(
1674
- "INSERT INTO lix_directory (id, path) VALUES ('dir-nfc', $1)",
1675
- &[Value::Text("/Cafe\u{301}/".to_string())],
1676
- )
1677
- .await
1678
- .expect("NFD directory path insert should canonicalize");
1679
-
1680
- let result = session
1681
- .execute(
1682
- "SELECT id FROM lix_directory WHERE path IN ($1)",
1683
- &[Value::Text("/%43afe%CC%81/".to_string())],
1684
- )
1685
- .await
1686
- .expect("directory path predicate should canonicalize");
1687
- assert_eq!(
1688
- rows_from_execute_result(result).1,
1689
- vec![vec![Value::Text("dir-nfc".to_string())]]
1690
- );
1691
-
1692
- let or_result = session
1693
- .execute(
1694
- "SELECT id FROM lix_directory WHERE id = 'missing' OR path = $1",
1695
- &[Value::Text("/Cafe\u{301}/".to_string())],
1696
- )
1697
- .await
1698
- .expect("directory OR path predicate should canonicalize");
1699
- assert_eq!(
1700
- rows_from_execute_result(or_result).1,
1701
- vec![vec![Value::Text("dir-nfc".to_string())]]
1702
- );
1703
-
1704
- let not_in_result = session
1705
- .execute(
1706
- "SELECT id FROM lix_directory WHERE path NOT IN ($1)",
1707
- &[Value::Text("/%43afe%CC%81/".to_string())],
1708
- )
1709
- .await
1710
- .expect("directory NOT IN path predicate should canonicalize");
1711
- assert!(rows_from_execute_result(not_in_result).1.is_empty());
1712
- }
1713
-
1714
- #[tokio::test]
1715
- async fn lix_directory_path_predicates_reject_non_literal_path_values() {
1716
- let backend = crate::backend::testing::UnitTestBackend::new();
1717
- let init_receipt = Engine::initialize(Box::new(backend.clone()))
1718
- .await
1719
- .expect("engine should initialize");
1720
- let engine = Engine::new(Box::new(backend))
1721
- .await
1722
- .expect("engine should open");
1723
- let session = engine
1724
- .open_session(init_receipt.main_version_id)
1725
- .await
1726
- .expect("session should open");
1727
-
1728
- session
1729
- .execute(
1730
- "INSERT INTO lix_directory (id, path) VALUES ('dir-nfc', $1)",
1731
- &[Value::Text("/Cafe\u{301}/".to_string())],
1732
- )
1733
- .await
1734
- .expect("NFD directory path insert should canonicalize");
1735
-
1736
- let error = session
1737
- .execute("SELECT id FROM lix_directory WHERE path IN (id)", &[])
1738
- .await
1739
- .expect_err("computed directory path predicate values should be rejected");
1740
- assert_eq!(error.code, LixError::CODE_UNSUPPORTED_SQL);
1741
- assert!(
1742
- error
1743
- .message
1744
- .contains("filesystem path predicates only support literal path values"),
1745
- "{error:?}"
1746
- );
1747
- }
1748
-
1749
- fn rows_from_execute_result(result: ExecuteResult) -> (Vec<String>, Vec<Vec<Value>>) {
1750
- let rows = result;
1751
- (
1752
- rows.columns().to_vec(),
1753
- rows.rows()
1754
- .iter()
1755
- .map(|row| row.values().to_vec())
1756
- .collect(),
1757
- )
1758
- }
1759
-
1760
- #[tokio::test]
1761
- async fn execute_sql_reads_lix_state_history_from_history_context() {
1762
- let (session, head_commit_id) = setup_engine_history_fixture()
1763
- .await
1764
- .expect("history fixture should initialize");
1765
- let result = session
1766
- .execute(
1767
- &format!(
1768
- "SELECT entity_id, snapshot_content, metadata, depth, start_commit_id \
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"
1774
- ),
1775
- &[],
1776
- )
1777
- .await
1778
- .expect("sql2 execute should read lix_state_history through real engine context");
1779
- let (columns, rows) = rows_from_execute_result(result);
1780
-
1781
- assert_eq!(
1782
- columns,
1783
- vec![
1784
- "entity_id",
1785
- "snapshot_content",
1786
- "metadata",
1787
- "depth",
1788
- "start_commit_id"
1789
- ]
1790
- );
1791
- assert_eq!(rows.len(), 1);
1792
- assert_eq!(rows[0][0], Value::Json(json!(["entity-history"])));
1793
- assert_eq!(rows[0][1], Value::Json(json!({"count": 7, "value": "A"})));
1794
- assert_eq!(rows[0][2], Value::Json(json!({"source": "history"})));
1795
- assert!(matches!(rows[0][3], Value::Integer(_)));
1796
- assert_eq!(rows[0][4], Value::Text(head_commit_id.clone()));
1797
- }
1798
-
1799
- #[tokio::test]
1800
- async fn execute_sql_reads_entity_history_view_from_history_context() {
1801
- let (session, head_commit_id) = setup_engine_history_fixture()
1802
- .await
1803
- .expect("history fixture should initialize");
1804
- let result = session
1805
- .execute(
1806
- &format!(
1807
- "SELECT value, count, lixcol_entity_id, lixcol_start_commit_id, lixcol_depth \
1808
- FROM test_state_schema_history \
1809
- WHERE lixcol_start_commit_id = '{head_commit_id}' \
1810
- AND lixcol_entity_id = lix_json('[\"entity-history\"]')"
1811
- ),
1812
- &[],
1813
- )
1814
- .await
1815
- .expect("sql2 execute should read entity history through real engine context");
1816
- let (columns, rows) = rows_from_execute_result(result);
1817
-
1818
- assert_eq!(
1819
- columns,
1820
- vec![
1821
- "value",
1822
- "count",
1823
- "lixcol_entity_id",
1824
- "lixcol_start_commit_id",
1825
- "lixcol_depth",
1826
- ]
1827
- );
1828
- assert_eq!(rows.len(), 1);
1829
- assert_eq!(rows[0][0], Value::Text("A".to_string()));
1830
- assert_eq!(rows[0][1], Value::Integer(7));
1831
- assert_eq!(rows[0][2], Value::Json(json!(["entity-history"])));
1832
- assert_eq!(rows[0][3], Value::Text(head_commit_id));
1833
- assert!(matches!(rows[0][4], Value::Integer(_)));
1834
- }
1835
-
1836
- #[tokio::test]
1837
- async fn execute_sql_reads_directory_history_view_from_history_context() {
1838
- let (session, head_commit_id) = setup_engine_history_fixture()
1839
- .await
1840
- .expect("history fixture should initialize");
1841
- let result = session
1842
- .execute(
1843
- &format!(
1844
- "SELECT id, parent_id, name, path, hidden, lixcol_start_commit_id, lixcol_depth \
1845
- FROM lix_directory_history \
1846
- WHERE id = 'dir-docs' AND lixcol_start_commit_id = '{head_commit_id}'"
1847
- ),
1848
- &[],
1849
- )
1850
- .await
1851
- .expect("sql2 execute should read directory history through real engine context");
1852
- assert!(
1853
- result.notices().is_empty(),
1854
- "identity-filtered directory history should not emit soft notices"
1855
- );
1856
- let (columns, rows) = rows_from_execute_result(result);
1857
-
1858
- assert_eq!(
1859
- columns,
1860
- vec![
1861
- "id",
1862
- "parent_id",
1863
- "name",
1864
- "path",
1865
- "hidden",
1866
- "lixcol_start_commit_id",
1867
- "lixcol_depth",
1868
- ]
1869
- );
1870
- assert_eq!(rows.len(), 1);
1871
- assert_eq!(rows[0][0], Value::Text("dir-docs".to_string()));
1872
- assert_eq!(rows[0][1], Value::Null);
1873
- assert_eq!(rows[0][2], Value::Text("docs".to_string()));
1874
- assert_eq!(rows[0][3], Value::Text("/docs/".to_string()));
1875
- assert_eq!(rows[0][4], Value::Boolean(false));
1876
- assert_eq!(rows[0][5], Value::Text(head_commit_id.clone()));
1877
- assert!(matches!(rows[0][6], Value::Integer(_)));
1878
-
1879
- let name_filtered_result = session
1880
- .execute(
1881
- &format!(
1882
- "SELECT id \
1883
- FROM lix_directory_history \
1884
- WHERE name = 'docs' \
1885
- AND lixcol_start_commit_id = '{head_commit_id}'"
1886
- ),
1887
- &[],
1888
- )
1889
- .await
1890
- .expect("sql2 execute should attach notices to name-filtered directory history reads");
1891
- assert_eq!(name_filtered_result.notices().len(), 1);
1892
- assert_eq!(
1893
- name_filtered_result.notices()[0].code,
1894
- "LIX_HISTORY_NON_IDENTITY_FILTER"
1895
- );
1896
- }
1897
-
1898
- #[tokio::test]
1899
- async fn execute_sql_reads_file_history_view_from_history_context() {
1900
- let (session, head_commit_id) = setup_engine_history_fixture()
1901
- .await
1902
- .expect("history fixture should initialize");
1903
- let result = session
1904
- .execute(
1905
- &format!(
1906
- "SELECT id, path, data, hidden, lixcol_start_commit_id, lixcol_depth \
1907
- FROM lix_file_history \
1908
- WHERE id = 'file-a' \
1909
- AND lixcol_start_commit_id = '{head_commit_id}' \
1910
- AND data IS NOT NULL \
1911
- ORDER BY lixcol_depth",
1912
- ),
1913
- &[],
1914
- )
1915
- .await
1916
- .expect("sql2 execute should read file history through real engine context");
1917
- assert!(
1918
- result.notices().is_empty(),
1919
- "identity-filtered file history should not emit soft notices"
1920
- );
1921
- let (columns, rows) = rows_from_execute_result(result);
1922
-
1923
- assert_eq!(
1924
- columns,
1925
- vec![
1926
- "id",
1927
- "path",
1928
- "data",
1929
- "hidden",
1930
- "lixcol_start_commit_id",
1931
- "lixcol_depth",
1932
- ]
1933
- );
1934
- assert_eq!(rows.len(), 1);
1935
- assert_eq!(rows[0][0], Value::Text("file-a".to_string()));
1936
- assert_eq!(rows[0][1], Value::Text("/docs/readme.md".to_string()));
1937
- assert_eq!(rows[0][2], Value::Blob(b"hello".to_vec()));
1938
- assert_eq!(rows[0][3], Value::Boolean(false));
1939
- assert_eq!(rows[0][4], Value::Text(head_commit_id.clone()));
1940
- assert!(matches!(rows[0][5], Value::Integer(_)));
1941
-
1942
- let path_filtered_result = session
1943
- .execute(
1944
- &format!(
1945
- "SELECT id \
1946
- FROM lix_file_history \
1947
- WHERE path = '/docs/readme.md' \
1948
- AND lixcol_start_commit_id = '{head_commit_id}'"
1949
- ),
1950
- &[],
1951
- )
1952
- .await
1953
- .expect("sql2 execute should attach notices to path-filtered file history reads");
1954
- assert_eq!(path_filtered_result.notices().len(), 1);
1955
- assert_eq!(
1956
- path_filtered_result.notices()[0].code,
1957
- "LIX_HISTORY_NON_IDENTITY_FILTER"
1958
- );
1959
- }
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
-
1991
- #[tokio::test]
1992
- async fn execute_sql_insert_into_lix_state_values_stages_write() {
1993
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
1994
- let live_state = Arc::new(DummyLiveStateReader);
1995
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
1996
- let mut ctx = DummySqlWriteExecutionContext {
1997
- active_version_id: "version-a",
1998
- blob_reader,
1999
- live_state,
2000
- staged_writes: Arc::clone(&staged_writes),
2001
- schema_definitions: vec![],
2002
- };
2003
-
2004
- let result = execute_write_sql(
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
- )
2013
- .await
2014
- .expect("INSERT INTO lix_state VALUES should stage write");
2015
-
2016
- assert_eq!(result.columns, vec!["count"]);
2017
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2018
-
2019
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2020
- assert_eq!(staged_writes.deltas.len(), 1);
2021
- let overlay = staged_writes.deltas[0]
2022
- .pending_write_overlay()
2023
- .expect("staged delta should expose pending overlay");
2024
- let rows = overlay.visible_semantic_rows(false, "lix_key_value");
2025
- assert_eq!(rows.len(), 1);
2026
- assert_eq!(rows[0].entity_id, "[\"entity-1\"]");
2027
- assert_eq!(rows[0].version_id, "version-a");
2028
- assert!(!rows[0].global);
2029
- assert!(!rows[0].untracked);
2030
- assert_eq!(
2031
- rows[0].snapshot_content.as_deref(),
2032
- Some("{\"key\":\"hello\",\"value\":\"world\"}")
2033
- );
2034
- assert_eq!(rows[0].metadata.as_deref(), Some("{\"source\":\"sql\"}"));
2035
- }
2036
-
2037
- #[tokio::test]
2038
- async fn execute_sql_insert_into_lix_state_defaults_global_and_untracked_to_false() {
2039
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2040
- let live_state = Arc::new(DummyLiveStateReader);
2041
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2042
- let mut ctx = DummySqlWriteExecutionContext {
2043
- active_version_id: "version-a",
2044
- blob_reader,
2045
- live_state,
2046
- staged_writes: Arc::clone(&staged_writes),
2047
- schema_definitions: vec![],
2048
- };
2049
-
2050
- let result = execute_write_sql(
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
- )
2059
- .await
2060
- .expect("INSERT INTO lix_state should default bookkeeping flags");
2061
-
2062
- assert_eq!(result.columns, vec!["count"]);
2063
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2064
-
2065
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2066
- assert_eq!(staged_writes.deltas.len(), 1);
2067
- let overlay = staged_writes.deltas[0]
2068
- .pending_write_overlay()
2069
- .expect("staged delta should expose pending overlay");
2070
- let rows = overlay.visible_semantic_rows(false, "lix_key_value");
2071
- assert_eq!(rows.len(), 1);
2072
- assert_eq!(rows[0].entity_id, "[\"entity-defaults\"]");
2073
- assert_eq!(rows[0].version_id, "version-a");
2074
- assert!(!rows[0].global);
2075
- assert!(!rows[0].untracked);
2076
- }
2077
-
2078
- #[tokio::test]
2079
- async fn execute_sql_insert_into_lix_state_select_stages_write() {
2080
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2081
- let live_state = Arc::new(DummyLiveStateReader);
2082
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2083
- let mut ctx = DummySqlWriteExecutionContext {
2084
- active_version_id: "version-a",
2085
- blob_reader,
2086
- live_state,
2087
- staged_writes: Arc::clone(&staged_writes),
2088
- schema_definitions: vec![],
2089
- };
2090
-
2091
- let result = execute_write_sql(
2092
- &mut ctx,
2093
- "INSERT INTO lix_state (\
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, \
2100
- '{\"key\":\"hello\",\"value\":\"from-select\"}' AS snapshot_content, \
2101
- '{\"source\":\"select\"}' AS metadata, \
2102
- false AS global, \
2103
- false AS untracked",
2104
- &[],
2105
- )
2106
- .await
2107
- .expect("INSERT INTO lix_state SELECT should stage write");
2108
-
2109
- assert_eq!(result.columns, vec!["count"]);
2110
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2111
-
2112
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2113
- assert_eq!(staged_writes.deltas.len(), 1);
2114
- let overlay = staged_writes.deltas[0]
2115
- .pending_write_overlay()
2116
- .expect("staged delta should expose pending overlay");
2117
- let rows = overlay.visible_semantic_rows(false, "lix_key_value");
2118
- assert_eq!(rows.len(), 1);
2119
- assert_eq!(rows[0].entity_id, "[\"entity-from-select\"]");
2120
- assert_eq!(rows[0].version_id, "version-a");
2121
- assert_eq!(
2122
- rows[0].snapshot_content.as_deref(),
2123
- Some("{\"key\":\"hello\",\"value\":\"from-select\"}")
2124
- );
2125
- assert_eq!(rows[0].metadata.as_deref(), Some("{\"source\":\"select\"}"));
2126
- }
2127
-
2128
- #[tokio::test]
2129
- async fn execute_sql_insert_into_entity_by_version_stages_write() {
2130
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2131
- let live_state = Arc::new(DummyLiveStateReader);
2132
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2133
- let mut ctx = DummySqlWriteExecutionContext {
2134
- active_version_id: "version-a",
2135
- blob_reader,
2136
- live_state,
2137
- staged_writes: Arc::clone(&staged_writes),
2138
- schema_definitions: vec![json!({
2139
- "x-lix-key": "test_state_schema",
2140
- "type": "object",
2141
- "properties": {
2142
- "value": { "type": "string" }
2143
- }
2144
- })],
2145
- };
2146
-
2147
- let result = execute_write_sql(
2148
- &mut ctx,
2149
- "INSERT INTO test_state_schema_by_version (\
2150
- lixcol_entity_id, lixcol_version_id, value\
2151
- ) VALUES (lix_json('[\"entity-c\"]'), 'version-b', 'C')",
2152
- &[],
2153
- )
2154
- .await
2155
- .expect("INSERT INTO entity by-version surface should stage write");
2156
-
2157
- assert_eq!(result.columns, vec!["count"]);
2158
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2159
-
2160
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2161
- assert_eq!(staged_writes.deltas.len(), 1);
2162
- let overlay = staged_writes.deltas[0]
2163
- .pending_write_overlay()
2164
- .expect("staged delta should expose pending overlay");
2165
- let rows = overlay.visible_semantic_rows(false, "test_state_schema");
2166
- assert_eq!(rows.len(), 1);
2167
- assert_eq!(rows[0].entity_id, "[\"entity-c\"]");
2168
- assert_eq!(rows[0].version_id, "version-b");
2169
- assert!(!rows[0].global);
2170
- assert!(!rows[0].untracked);
2171
- assert_eq!(
2172
- rows[0].snapshot_content.as_deref(),
2173
- Some("{\"value\":\"C\"}")
2174
- );
2175
- }
2176
-
2177
- #[tokio::test]
2178
- async fn execute_sql_insert_into_active_entity_defaults_active_version() {
2179
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2180
- let live_state = Arc::new(DummyLiveStateReader);
2181
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2182
- let mut ctx = DummySqlWriteExecutionContext {
2183
- active_version_id: "version-a",
2184
- blob_reader,
2185
- live_state,
2186
- staged_writes: Arc::clone(&staged_writes),
2187
- schema_definitions: vec![json!({
2188
- "x-lix-key": "test_state_schema",
2189
- "type": "object",
2190
- "properties": {
2191
- "value": { "type": "string" }
2192
- }
2193
- })],
2194
- };
2195
-
2196
- let result = execute_write_sql(
2197
- &mut ctx,
2198
- "INSERT INTO test_state_schema (lixcol_entity_id, value) \
2199
- VALUES (lix_json('[\"entity-c\"]'), 'C')",
2200
- &[],
2201
- )
2202
- .await
2203
- .expect("INSERT INTO active entity surface should stage write");
2204
-
2205
- assert_eq!(result.columns, vec!["count"]);
2206
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2207
-
2208
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2209
- assert_eq!(staged_writes.deltas.len(), 1);
2210
- let overlay = staged_writes.deltas[0]
2211
- .pending_write_overlay()
2212
- .expect("staged delta should expose pending overlay");
2213
- let rows = overlay.visible_semantic_rows(false, "test_state_schema");
2214
- assert_eq!(rows.len(), 1);
2215
- assert_eq!(rows[0].entity_id, "[\"entity-c\"]");
2216
- assert_eq!(rows[0].version_id, "version-a");
2217
- assert!(!rows[0].global);
2218
- assert!(!rows[0].untracked);
2219
- assert_eq!(
2220
- rows[0].snapshot_content.as_deref(),
2221
- Some("{\"value\":\"C\"}")
2222
- );
2223
- }
2224
-
2225
- #[tokio::test]
2226
- async fn execute_sql_insert_into_directory_by_version_stages_write() {
2227
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2228
- let live_state = Arc::new(DummyLiveStateReader);
2229
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2230
- let mut ctx = DummySqlWriteExecutionContext {
2231
- active_version_id: "version-a",
2232
- blob_reader,
2233
- live_state,
2234
- staged_writes: Arc::clone(&staged_writes),
2235
- schema_definitions: vec![],
2236
- };
2237
-
2238
- let result = execute_write_sql(
2239
- &mut ctx,
2240
- "INSERT INTO lix_directory_by_version (\
2241
- id, parent_id, name, hidden, lixcol_version_id\
2242
- ) VALUES ('dir-docs', NULL, 'docs', false, 'version-b')",
2243
- &[],
2244
- )
2245
- .await
2246
- .expect("INSERT INTO lix_directory_by_version should stage write");
2247
-
2248
- assert_eq!(result.columns, vec!["count"]);
2249
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2250
-
2251
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2252
- assert_eq!(staged_writes.deltas.len(), 1);
2253
- let overlay = staged_writes.deltas[0]
2254
- .pending_write_overlay()
2255
- .expect("staged delta should expose pending overlay");
2256
- let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
2257
- assert_eq!(rows.len(), 1);
2258
- assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
2259
- assert_eq!(rows[0].version_id, "version-b");
2260
- assert!(!rows[0].global);
2261
- assert!(!rows[0].untracked);
2262
- assert_eq!(
2263
- rows[0].snapshot_content.as_deref(),
2264
- Some("{\"hidden\":false,\"id\":\"dir-docs\",\"name\":\"docs\",\"parent_id\":null}")
2265
- );
2266
- }
2267
-
2268
- #[tokio::test]
2269
- async fn execute_sql_insert_into_active_directory_defaults_active_version() {
2270
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2271
- let live_state = Arc::new(DummyLiveStateReader);
2272
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2273
- let mut ctx = DummySqlWriteExecutionContext {
2274
- active_version_id: "version-a",
2275
- blob_reader,
2276
- live_state,
2277
- staged_writes: Arc::clone(&staged_writes),
2278
- schema_definitions: vec![],
2279
- };
2280
-
2281
- let result = execute_write_sql(
2282
- &mut ctx,
2283
- "INSERT INTO lix_directory (id, parent_id, name, hidden) \
2284
- VALUES ('dir-docs', NULL, 'docs', false)",
2285
- &[],
2286
- )
2287
- .await
2288
- .expect("INSERT INTO lix_directory should stage write");
2289
-
2290
- assert_eq!(result.columns, vec!["count"]);
2291
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2292
-
2293
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2294
- assert_eq!(staged_writes.deltas.len(), 1);
2295
- let overlay = staged_writes.deltas[0]
2296
- .pending_write_overlay()
2297
- .expect("staged delta should expose pending overlay");
2298
- let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
2299
- assert_eq!(rows.len(), 1);
2300
- assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
2301
- assert_eq!(rows[0].version_id, "version-a");
2302
- assert!(!rows[0].global);
2303
- assert!(!rows[0].untracked);
2304
- }
2305
-
2306
- #[tokio::test]
2307
- async fn execute_sql_update_directory_stages_rewritten_descriptor() {
2308
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2309
- let live_state = Arc::new(RowsLiveStateReader {
2310
- rows: vec![
2311
- live_directory_row("dir-docs", "version-a", None, "docs", false),
2312
- live_directory_row("dir-guides", "version-a", Some("dir-docs"), "guides", false),
2313
- ],
2314
- });
2315
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2316
- let mut ctx = DummySqlWriteExecutionContext {
2317
- active_version_id: "version-a",
2318
- blob_reader,
2319
- live_state,
2320
- staged_writes: Arc::clone(&staged_writes),
2321
- schema_definitions: vec![],
2322
- };
2323
-
2324
- let result = execute_write_sql(
2325
- &mut ctx,
2326
- "UPDATE lix_directory \
2327
- SET hidden = true, lixcol_metadata = '{\"source\":\"directory-update\"}' \
2328
- WHERE id = 'dir-docs'",
2329
- &[],
2330
- )
2331
- .await
2332
- .expect("UPDATE lix_directory should stage rewritten descriptor");
2333
-
2334
- assert_eq!(result.columns, vec!["count"]);
2335
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2336
-
2337
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2338
- assert_eq!(staged_writes.deltas.len(), 1);
2339
- let overlay = staged_writes.deltas[0]
2340
- .pending_write_overlay()
2341
- .expect("staged delta should expose pending overlay");
2342
- let rows = overlay.visible_semantic_rows(false, "lix_directory_descriptor");
2343
- assert_eq!(rows.len(), 1);
2344
- assert_eq!(rows[0].entity_id, "[\"dir-docs\"]");
2345
- assert_eq!(rows[0].version_id, "version-a");
2346
- assert_eq!(
2347
- rows[0].snapshot_content.as_deref(),
2348
- Some("{\"hidden\":true,\"id\":\"dir-docs\",\"name\":\"docs\",\"parent_id\":null}")
2349
- );
2350
- assert_eq!(
2351
- rows[0].metadata.as_deref(),
2352
- Some("{\"source\":\"directory-update\"}")
2353
- );
2354
- }
2355
-
2356
- #[tokio::test]
2357
- async fn execute_sql_update_directory_rejects_path_assignment() {
2358
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2359
- let live_state = Arc::new(RowsLiveStateReader {
2360
- rows: vec![live_directory_row(
2361
- "dir-docs",
2362
- "version-a",
2363
- None,
2364
- "docs",
2365
- false,
2366
- )],
2367
- });
2368
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2369
- let mut ctx = DummySqlWriteExecutionContext {
2370
- active_version_id: "version-a",
2371
- blob_reader,
2372
- live_state,
2373
- staged_writes: Arc::clone(&staged_writes),
2374
- schema_definitions: vec![],
2375
- };
2376
-
2377
- let error = execute_write_sql(
2378
- &mut ctx,
2379
- "UPDATE lix_directory SET path = '/renamed/' WHERE id = 'dir-docs'",
2380
- &[],
2381
- )
2382
- .await
2383
- .expect_err("path should remain read-only");
2384
-
2385
- assert!(
2386
- error.message.contains("read-only column 'path'"),
2387
- "unexpected error: {error:?}"
2388
- );
2389
- assert!(staged_writes
2390
- .lock()
2391
- .expect("staged writes lock")
2392
- .deltas
2393
- .is_empty());
2394
- }
2395
-
2396
- #[tokio::test]
2397
- async fn execute_sql_delete_directory_by_version_stages_tombstone() {
2398
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2399
- let live_state = Arc::new(RowsLiveStateReader {
2400
- rows: vec![
2401
- live_directory_row("dir-docs", "version-a", None, "docs", false),
2402
- live_directory_row("dir-guides", "version-b", Some("dir-docs"), "guides", false),
2403
- ],
2404
- });
2405
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2406
- let mut ctx = DummySqlWriteExecutionContext {
2407
- active_version_id: "version-a",
2408
- blob_reader,
2409
- live_state,
2410
- staged_writes: Arc::clone(&staged_writes),
2411
- schema_definitions: vec![],
2412
- };
2413
-
2414
- let result = execute_write_sql(
2415
- &mut ctx,
2416
- "DELETE FROM lix_directory_by_version \
2417
- WHERE id = 'dir-guides' AND lixcol_version_id = 'version-b'",
2418
- &[],
2419
- )
2420
- .await
2421
- .expect("DELETE lix_directory_by_version should stage tombstone");
2422
-
2423
- assert_eq!(result.columns, vec!["count"]);
2424
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2425
-
2426
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2427
- assert_eq!(staged_writes.deltas.len(), 1);
2428
- let overlay = staged_writes.deltas[0]
2429
- .pending_write_overlay()
2430
- .expect("staged delta should expose pending overlay");
2431
- let rows = overlay.visible_all_semantic_rows();
2432
- assert_eq!(rows.len(), 1);
2433
- assert_eq!(rows[0].entity_id, "[\"dir-guides\"]");
2434
- assert_eq!(rows[0].version_id, "version-b");
2435
- assert!(rows[0].tombstone);
2436
- assert_eq!(rows[0].snapshot_content, None);
2437
- }
2438
-
2439
- #[tokio::test]
2440
- async fn execute_sql_insert_into_file_by_version_stages_descriptor_write() {
2441
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2442
- let live_state = Arc::new(DummyLiveStateReader);
2443
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2444
- let mut ctx = DummySqlWriteExecutionContext {
2445
- active_version_id: "version-a",
2446
- blob_reader,
2447
- live_state,
2448
- staged_writes: Arc::clone(&staged_writes),
2449
- schema_definitions: vec![],
2450
- };
2451
-
2452
- let result = execute_write_sql(
2453
- &mut ctx,
2454
- "INSERT INTO lix_file_by_version (\
2455
- id, directory_id, name, hidden, lixcol_version_id\
2456
- ) VALUES ('file-readme', 'dir-docs', 'readme.md', false, 'version-b')",
2457
- &[],
2458
- )
2459
- .await
2460
- .expect("INSERT INTO lix_file_by_version should stage descriptor write");
2461
-
2462
- assert_eq!(result.columns, vec!["count"]);
2463
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2464
-
2465
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2466
- assert_eq!(staged_writes.deltas.len(), 1);
2467
- let overlay = staged_writes.deltas[0]
2468
- .pending_write_overlay()
2469
- .expect("staged delta should expose pending overlay");
2470
- let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2471
- assert_eq!(rows.len(), 1);
2472
- assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
2473
- assert_eq!(rows[0].version_id, "version-b");
2474
- assert!(!rows[0].global);
2475
- assert!(!rows[0].untracked);
2476
- let snapshot: JsonValue =
2477
- serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
2478
- .expect("descriptor snapshot JSON");
2479
- assert_eq!(snapshot["id"], "file-readme");
2480
- assert_eq!(snapshot["directory_id"], "dir-docs");
2481
- assert_eq!(snapshot["name"], "readme.md");
2482
- assert_eq!(snapshot["hidden"], false);
2483
- }
2484
-
2485
- #[tokio::test]
2486
- async fn execute_sql_insert_into_active_file_defaults_active_version() {
2487
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2488
- let live_state = Arc::new(DummyLiveStateReader);
2489
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2490
- let mut ctx = DummySqlWriteExecutionContext {
2491
- active_version_id: "version-a",
2492
- blob_reader,
2493
- live_state,
2494
- staged_writes: Arc::clone(&staged_writes),
2495
- schema_definitions: vec![],
2496
- };
2497
-
2498
- let result = execute_write_sql(
2499
- &mut ctx,
2500
- "INSERT INTO lix_file (id, directory_id, name, hidden) \
2501
- VALUES ('file-readme', 'dir-docs', 'readme.md', false)",
2502
- &[],
2503
- )
2504
- .await
2505
- .expect("INSERT INTO lix_file should stage descriptor write");
2506
-
2507
- assert_eq!(result.columns, vec!["count"]);
2508
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2509
-
2510
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2511
- assert_eq!(staged_writes.deltas.len(), 1);
2512
- let overlay = staged_writes.deltas[0]
2513
- .pending_write_overlay()
2514
- .expect("staged delta should expose pending overlay");
2515
- let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2516
- assert_eq!(rows.len(), 1);
2517
- assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
2518
- assert_eq!(rows[0].version_id, "version-a");
2519
- assert!(!rows[0].global);
2520
- assert!(!rows[0].untracked);
2521
- }
2522
-
2523
- #[tokio::test]
2524
- async fn execute_sql_insert_into_file_with_data_stages_blob_ref() {
2525
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2526
- let live_state = Arc::new(DummyLiveStateReader);
2527
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2528
- let mut ctx = DummySqlWriteExecutionContext {
2529
- active_version_id: "version-a",
2530
- blob_reader,
2531
- live_state,
2532
- staged_writes: Arc::clone(&staged_writes),
2533
- schema_definitions: vec![],
2534
- };
2535
-
2536
- let result = execute_write_sql(
2537
- &mut ctx,
2538
- "INSERT INTO lix_file_by_version (\
2539
- id, directory_id, name, hidden, data, lixcol_version_id\
2540
- ) VALUES ('file-readme', 'dir-docs', 'readme.md', false, X'4142', 'version-b')",
2541
- &[],
2542
- )
2543
- .await
2544
- .expect("INSERT INTO lix_file_by_version should stage descriptor and data writes");
2545
-
2546
- assert_eq!(result.columns, vec!["count"]);
2547
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2548
-
2549
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2550
- assert_eq!(staged_writes.deltas.len(), 1);
2551
- let overlay = staged_writes.deltas[0]
2552
- .pending_write_overlay()
2553
- .expect("staged delta should expose pending overlay");
2554
- let descriptor_rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2555
- assert_eq!(descriptor_rows.len(), 1);
2556
- assert_eq!(descriptor_rows[0].entity_id, "[\"file-readme\"]");
2557
- let blob_ref_rows = overlay.visible_semantic_rows(false, "lix_binary_blob_ref");
2558
- assert_eq!(blob_ref_rows.len(), 1);
2559
- assert_eq!(blob_ref_rows[0].entity_id, "[\"file-readme\"]");
2560
- assert_eq!(blob_ref_rows[0].file_id.as_deref(), Some("file-readme"));
2561
- assert_eq!(blob_ref_rows[0].version_id, "version-b");
2562
- let snapshot: JsonValue =
2563
- serde_json::from_str(blob_ref_rows[0].snapshot_content.as_deref().unwrap())
2564
- .expect("blob ref snapshot JSON");
2565
- assert_eq!(snapshot["id"], "file-readme");
2566
- assert_eq!(snapshot["size_bytes"], 2);
2567
- assert!(snapshot["blob_hash"]
2568
- .as_str()
2569
- .is_some_and(|value| !value.is_empty()));
2570
- }
2571
-
2572
- #[tokio::test]
2573
- async fn execute_sql_update_file_stages_rewritten_descriptor() {
2574
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2575
- let live_state = Arc::new(RowsLiveStateReader {
2576
- rows: vec![
2577
- live_directory_row("dir-docs", "version-a", None, "docs", false),
2578
- live_file_row(
2579
- "file-readme",
2580
- "version-a",
2581
- Some("dir-docs"),
2582
- "readme.md",
2583
- false,
2584
- ),
2585
- live_file_row(
2586
- "file-guide",
2587
- "version-a",
2588
- Some("dir-docs"),
2589
- "guide.md",
2590
- false,
2591
- ),
2592
- ],
2593
- });
2594
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2595
- let mut ctx = DummySqlWriteExecutionContext {
2596
- active_version_id: "version-a",
2597
- blob_reader,
2598
- live_state,
2599
- staged_writes: Arc::clone(&staged_writes),
2600
- schema_definitions: vec![],
2601
- };
2602
-
2603
- let result = execute_write_sql(
2604
- &mut ctx,
2605
- "UPDATE lix_file \
2606
- SET name = 'readme-updated.txt', hidden = true, lixcol_metadata = '{\"source\":\"file-update\"}' \
2607
- WHERE id = 'file-readme'",
2608
- &[],
2609
- )
2610
- .await
2611
- .expect("UPDATE lix_file should stage rewritten descriptor");
2612
-
2613
- assert_eq!(result.columns, vec!["count"]);
2614
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2615
-
2616
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2617
- assert_eq!(staged_writes.deltas.len(), 1);
2618
- let overlay = staged_writes.deltas[0]
2619
- .pending_write_overlay()
2620
- .expect("staged delta should expose pending overlay");
2621
- let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2622
- assert_eq!(rows.len(), 1);
2623
- assert_eq!(rows[0].entity_id, "[\"file-readme\"]");
2624
- assert_eq!(rows[0].version_id, "version-a");
2625
- let snapshot: JsonValue =
2626
- serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
2627
- .expect("descriptor snapshot JSON");
2628
- assert_eq!(snapshot["id"], "file-readme");
2629
- assert_eq!(snapshot["directory_id"], "dir-docs");
2630
- assert_eq!(snapshot["name"], "readme-updated.txt");
2631
- assert_eq!(snapshot["hidden"], true);
2632
- assert_eq!(
2633
- rows[0].metadata.as_deref(),
2634
- Some("{\"source\":\"file-update\"}")
2635
- );
2636
- }
2637
-
2638
- #[tokio::test]
2639
- async fn execute_sql_update_file_stages_data_blob_ref() {
2640
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2641
- let live_state = Arc::new(RowsLiveStateReader {
2642
- rows: vec![
2643
- live_directory_row("dir-docs", "version-a", None, "docs", false),
2644
- live_file_row(
2645
- "file-readme",
2646
- "version-a",
2647
- Some("dir-docs"),
2648
- "readme.md",
2649
- false,
2650
- ),
2651
- ],
2652
- });
2653
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2654
- let mut ctx = DummySqlWriteExecutionContext {
2655
- active_version_id: "version-a",
2656
- blob_reader,
2657
- live_state,
2658
- staged_writes: Arc::clone(&staged_writes),
2659
- schema_definitions: vec![],
2660
- };
2661
-
2662
- let result = execute_write_sql(
2663
- &mut ctx,
2664
- "UPDATE lix_file SET data = X'4142' WHERE id = 'file-readme'",
2665
- &[],
2666
- )
2667
- .await
2668
- .expect("UPDATE lix_file should stage data write");
2669
-
2670
- assert_eq!(result.columns, vec!["count"]);
2671
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2672
-
2673
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2674
- assert_eq!(staged_writes.deltas.len(), 1);
2675
- let overlay = staged_writes.deltas[0]
2676
- .pending_write_overlay()
2677
- .expect("staged delta should expose pending overlay");
2678
- assert!(overlay
2679
- .visible_semantic_rows(false, "lix_file_descriptor")
2680
- .is_empty());
2681
- let blob_ref_rows = overlay.visible_semantic_rows(false, "lix_binary_blob_ref");
2682
- assert_eq!(blob_ref_rows.len(), 1);
2683
- assert_eq!(blob_ref_rows[0].entity_id, "[\"file-readme\"]");
2684
- let snapshot: JsonValue =
2685
- serde_json::from_str(blob_ref_rows[0].snapshot_content.as_deref().unwrap())
2686
- .expect("blob ref snapshot JSON");
2687
- assert_eq!(snapshot["id"], "file-readme");
2688
- assert_eq!(snapshot["size_bytes"], 2);
2689
- }
2690
-
2691
- #[tokio::test]
2692
- async fn execute_sql_update_file_stages_path_assignment() {
2693
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2694
- let live_state = Arc::new(RowsLiveStateReader {
2695
- rows: vec![
2696
- live_directory_row("dir-docs", "version-a", None, "docs", false),
2697
- live_file_row(
2698
- "file-readme",
2699
- "version-a",
2700
- Some("dir-docs"),
2701
- "readme.md",
2702
- false,
2703
- ),
2704
- ],
2705
- });
2706
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2707
- let mut ctx = DummySqlWriteExecutionContext {
2708
- active_version_id: "version-a",
2709
- blob_reader,
2710
- live_state,
2711
- staged_writes: Arc::clone(&staged_writes),
2712
- schema_definitions: vec![],
2713
- };
2714
-
2715
- let result = execute_write_sql(
2716
- &mut ctx,
2717
- "UPDATE lix_file SET path = '/docs/renamed.md' WHERE id = 'file-readme'",
2718
- &[],
2719
- )
2720
- .await
2721
- .expect("path update should stage descriptor rewrite");
2722
-
2723
- assert_eq!(result.columns, vec!["count"]);
2724
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2725
-
2726
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2727
- assert_eq!(staged_writes.deltas.len(), 1);
2728
- let overlay = staged_writes.deltas[0]
2729
- .pending_write_overlay()
2730
- .expect("staged delta should expose pending overlay");
2731
- let rows = overlay.visible_semantic_rows(false, "lix_file_descriptor");
2732
- assert_eq!(rows.len(), 1);
2733
- let snapshot: JsonValue =
2734
- serde_json::from_str(rows[0].snapshot_content.as_deref().unwrap())
2735
- .expect("descriptor snapshot JSON");
2736
- assert_eq!(snapshot["directory_id"], "dir-docs");
2737
- assert_eq!(snapshot["name"], "renamed.md");
2738
- }
2739
-
2740
- #[tokio::test]
2741
- async fn execute_sql_delete_file_by_version_stages_descriptor_tombstone() {
2742
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2743
- let live_state = Arc::new(RowsLiveStateReader {
2744
- rows: vec![
2745
- live_directory_row("dir-docs", "version-a", None, "docs", false),
2746
- live_directory_row("dir-docs", "version-b", None, "docs", false),
2747
- live_file_row(
2748
- "file-readme",
2749
- "version-a",
2750
- Some("dir-docs"),
2751
- "readme.md",
2752
- false,
2753
- ),
2754
- live_file_row(
2755
- "file-guide",
2756
- "version-b",
2757
- Some("dir-docs"),
2758
- "guide.md",
2759
- false,
2760
- ),
2761
- ],
2762
- });
2763
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2764
- let mut ctx = DummySqlWriteExecutionContext {
2765
- active_version_id: "version-a",
2766
- blob_reader,
2767
- live_state,
2768
- staged_writes: Arc::clone(&staged_writes),
2769
- schema_definitions: vec![],
2770
- };
2771
-
2772
- let result = execute_write_sql(
2773
- &mut ctx,
2774
- "DELETE FROM lix_file_by_version \
2775
- WHERE id = 'file-guide' AND lixcol_version_id = 'version-b'",
2776
- &[],
2777
- )
2778
- .await
2779
- .expect("DELETE lix_file_by_version should stage descriptor tombstone");
2780
-
2781
- assert_eq!(result.columns, vec!["count"]);
2782
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2783
-
2784
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2785
- assert_eq!(staged_writes.deltas.len(), 1);
2786
- let overlay = staged_writes.deltas[0]
2787
- .pending_write_overlay()
2788
- .expect("staged delta should expose pending overlay");
2789
- let rows = overlay.visible_all_semantic_rows();
2790
- assert_eq!(rows.len(), 1);
2791
- assert_eq!(rows[0].entity_id, "[\"file-guide\"]");
2792
- assert_eq!(rows[0].version_id, "version-b");
2793
- assert!(rows[0].tombstone);
2794
- assert_eq!(rows[0].snapshot_content, None);
2795
- }
2796
-
2797
- #[tokio::test]
2798
- async fn execute_sql_update_entity_surface_stages_rewritten_snapshot() {
2799
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2800
- let live_state = Arc::new(RowsLiveStateReader {
2801
- rows: vec![
2802
- live_entity_row("entity-a", "version-a", "A"),
2803
- live_entity_row("entity-b", "version-a", "B"),
2804
- ],
2805
- });
2806
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2807
- let mut ctx = DummySqlWriteExecutionContext {
2808
- active_version_id: "version-a",
2809
- blob_reader,
2810
- live_state,
2811
- staged_writes: Arc::clone(&staged_writes),
2812
- schema_definitions: vec![json!({
2813
- "x-lix-key": "test_state_schema",
2814
- "type": "object",
2815
- "properties": {
2816
- "value": { "type": "string" }
2817
- }
2818
- })],
2819
- };
2820
-
2821
- let result = execute_write_sql(
2822
- &mut ctx,
2823
- "UPDATE test_state_schema \
2824
- SET value = 'updated', lixcol_metadata = '{\"source\":\"entity-update\"}' \
2825
- WHERE value = 'A'",
2826
- &[],
2827
- )
2828
- .await
2829
- .expect("UPDATE entity surface should stage rewritten row");
2830
-
2831
- assert_eq!(result.columns, vec!["count"]);
2832
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2833
-
2834
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2835
- assert_eq!(staged_writes.deltas.len(), 1);
2836
- let overlay = staged_writes.deltas[0]
2837
- .pending_write_overlay()
2838
- .expect("staged delta should expose pending overlay");
2839
- let rows = overlay.visible_semantic_rows(false, "test_state_schema");
2840
- assert_eq!(rows.len(), 1);
2841
- assert_eq!(rows[0].entity_id, "[\"entity-a\"]");
2842
- assert_eq!(rows[0].version_id, "version-a");
2843
- assert_eq!(
2844
- rows[0].snapshot_content.as_deref(),
2845
- Some("{\"value\":\"updated\"}")
2846
- );
2847
- assert_eq!(
2848
- rows[0].metadata.as_deref(),
2849
- Some("{\"source\":\"entity-update\"}")
2850
- );
2851
- }
2852
-
2853
- #[tokio::test]
2854
- async fn execute_sql_delete_entity_by_version_stages_tombstone() {
2855
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2856
- let live_state = Arc::new(RowsLiveStateReader {
2857
- rows: vec![
2858
- live_entity_row("entity-a", "version-a", "A"),
2859
- live_entity_row("entity-b", "version-b", "B"),
2860
- ],
2861
- });
2862
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2863
- let mut ctx = DummySqlWriteExecutionContext {
2864
- active_version_id: "version-a",
2865
- blob_reader,
2866
- live_state,
2867
- staged_writes: Arc::clone(&staged_writes),
2868
- schema_definitions: vec![json!({
2869
- "x-lix-key": "test_state_schema",
2870
- "type": "object",
2871
- "properties": {
2872
- "value": { "type": "string" }
2873
- }
2874
- })],
2875
- };
2876
-
2877
- let result = execute_write_sql(
2878
- &mut ctx,
2879
- "DELETE FROM test_state_schema_by_version \
2880
- WHERE lixcol_version_id = 'version-b'",
2881
- &[],
2882
- )
2883
- .await
2884
- .expect("DELETE entity by-version surface should stage tombstone");
2885
-
2886
- assert_eq!(result.columns, vec!["count"]);
2887
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2888
-
2889
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2890
- assert_eq!(staged_writes.deltas.len(), 1);
2891
- let overlay = staged_writes.deltas[0]
2892
- .pending_write_overlay()
2893
- .expect("staged delta should expose pending overlay");
2894
- let rows = overlay.visible_all_semantic_rows();
2895
- assert_eq!(rows.len(), 1);
2896
- assert_eq!(rows[0].entity_id, "[\"entity-b\"]");
2897
- assert_eq!(rows[0].version_id, "version-b");
2898
- assert!(rows[0].tombstone);
2899
- assert_eq!(rows[0].snapshot_content, None);
2900
- }
2901
-
2902
- #[tokio::test]
2903
- async fn execute_sql_update_lix_state_stages_rewritten_rows() {
2904
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2905
- let live_state = Arc::new(RowsLiveStateReader {
2906
- rows: vec![
2907
- live_lix_state_row("entity-1", Some("{\"source\":\"match\"}")),
2908
- live_lix_state_row("entity-2", Some("{\"source\":\"skip\"}")),
2909
- ],
2910
- });
2911
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2912
- let mut ctx = DummySqlWriteExecutionContext {
2913
- active_version_id: "version-a",
2914
- blob_reader,
2915
- live_state,
2916
- staged_writes: Arc::clone(&staged_writes),
2917
- schema_definitions: vec![],
2918
- };
2919
-
2920
- let result = execute_write_sql(
2921
- &mut ctx,
2922
- "UPDATE lix_state \
2923
- SET snapshot_content = '{\"key\":\"hello\",\"value\":\"updated\"}', \
2924
- metadata = '{\"schema_key\":\"lix_key_value\"}' \
2925
- WHERE metadata = lix_json('{\"source\":\"match\"}')",
2926
- &[],
2927
- )
2928
- .await
2929
- .expect("UPDATE lix_state should stage rewritten rows");
2930
-
2931
- assert_eq!(result.columns, vec!["count"]);
2932
- assert_eq!(result.rows, vec![vec![Value::Integer(1)]]);
2933
-
2934
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2935
- assert_eq!(staged_writes.deltas.len(), 1);
2936
- let overlay = staged_writes.deltas[0]
2937
- .pending_write_overlay()
2938
- .expect("staged delta should expose pending overlay");
2939
- let rows = overlay.visible_semantic_rows(false, "lix_key_value");
2940
- assert_eq!(rows.len(), 1);
2941
- assert_eq!(rows[0].entity_id, "[\"entity-1\"]");
2942
- assert_eq!(rows[0].version_id, "version-a");
2943
- assert_eq!(
2944
- rows[0].snapshot_content.as_deref(),
2945
- Some("{\"key\":\"hello\",\"value\":\"updated\"}")
2946
- );
2947
- assert_eq!(
2948
- rows[0].metadata.as_deref(),
2949
- Some("{\"schema_key\":\"lix_key_value\"}")
2950
- );
2951
- }
2952
-
2953
- #[tokio::test]
2954
- async fn execute_sql_delete_lix_state_without_where_stages_all_rows() {
2955
- let blob_reader: Arc<dyn BlobDataReader> = Arc::new(DummyBlobReader);
2956
- let live_state = Arc::new(RowsLiveStateReader {
2957
- rows: vec![
2958
- live_lix_state_row("entity-1", Some("{\"source\":\"one\"}")),
2959
- live_lix_state_row("entity-2", Some("{\"source\":\"two\"}")),
2960
- ],
2961
- });
2962
- let staged_writes = Arc::new(Mutex::new(CapturingStagedWrites::default()));
2963
- let mut ctx = DummySqlWriteExecutionContext {
2964
- active_version_id: "version-a",
2965
- blob_reader,
2966
- live_state,
2967
- staged_writes: Arc::clone(&staged_writes),
2968
- schema_definitions: vec![],
2969
- };
2970
-
2971
- let result = execute_write_sql(&mut ctx, "DELETE FROM lix_state", &[])
2972
- .await
2973
- .expect("DELETE FROM lix_state should follow DataFusion delete-all semantics");
2974
-
2975
- assert_eq!(result.columns, vec!["count"]);
2976
- assert_eq!(result.rows, vec![vec![Value::Integer(2)]]);
2977
-
2978
- let staged_writes = staged_writes.lock().expect("staged writes lock");
2979
- assert_eq!(staged_writes.deltas.len(), 1);
2980
- let overlay = staged_writes.deltas[0]
2981
- .pending_write_overlay()
2982
- .expect("staged delta should expose pending overlay");
2983
- let rows = overlay.visible_all_semantic_rows();
2984
- assert_eq!(rows.len(), 2);
2985
- assert!(rows.iter().all(|row| row.tombstone));
2986
- assert!(rows.iter().all(|row| row.snapshot_content.is_none()));
2987
- assert!(rows.iter().any(|row| row.entity_id == "[\"entity-1\"]"));
2988
- assert!(rows.iter().any(|row| row.entity_id == "[\"entity-2\"]"));
2989
- }
2990
-
2991
- struct BackendSqlExecutionContext<'a> {
2992
- active_version_id: &'a str,
2993
- storage: StorageContext,
2994
- blob_reader: Arc<dyn BlobDataReader>,
2995
- live_state: Arc<dyn LiveStateReader>,
2996
- schema_definitions: Vec<JsonValue>,
2997
- }
2998
-
2999
- impl SqlExecutionContext for BackendSqlExecutionContext<'_> {
3000
- fn active_version_id(&self) -> &str {
3001
- self.active_version_id
3002
- }
3003
-
3004
- fn live_state(&self) -> Arc<dyn LiveStateReader> {
3005
- Arc::clone(&self.live_state)
3006
- }
3007
-
3008
- fn functions(&self) -> FunctionProviderHandle {
3009
- test_functions()
3010
- }
3011
-
3012
- fn blob_reader(&self) -> Arc<dyn BlobDataReader> {
3013
- Arc::clone(&self.blob_reader)
3014
- }
3015
-
3016
- fn commit_store_query_source(&self) -> SqlCommitStoreQuerySource {
3017
- let base_scope = test_read_scope(self.storage.clone());
3018
- let read_scope = StorageReadScope::new(base_scope.store());
3019
- CommitStoreQuerySource {
3020
- commit_store_reader: Arc::new(CommitStoreContext::new().reader(read_scope.store())),
3021
- json_reader: JsonStoreContext::new().reader(read_scope.store()),
3022
- }
3023
- }
3024
-
3025
- fn commit_graph(&self) -> Box<dyn CommitGraphReader> {
3026
- Box::new(DummyCommitGraphReader)
3027
- }
3028
-
3029
- fn version_ref(&self) -> Arc<dyn VersionRefReader> {
3030
- Arc::new(
3031
- crate::version::VersionContext::new(Arc::new(UntrackedStateContext::new()))
3032
- .ref_reader(self.storage.clone()),
3033
- )
3034
- }
3035
-
3036
- fn list_visible_schemas(&self) -> Result<Vec<JsonValue>, LixError> {
3037
- Ok(self.schema_definitions.clone())
3038
- }
3039
- }
3040
-
3041
- async fn setup_sql2_state_fixture(
3042
- ) -> Result<(crate::backend::testing::UnitTestBackend, JsonValue), crate::LixError> {
3043
- let backend = crate::backend::testing::UnitTestBackend::new();
3044
- let init_receipt = Engine::initialize(Box::new(backend.clone())).await?;
3045
- let storage = crate::storage::StorageContext::new(std::sync::Arc::new(backend.clone()));
3046
- {
3047
- let mut transaction = storage.begin_write_transaction().await?;
3048
- let version_ctx = crate::version::VersionContext::new(Arc::new(
3049
- crate::untracked_state::UntrackedStateContext::new(),
3050
- ));
3051
- let mut writes = StorageWriteSet::new();
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)?;
3069
- writes.apply(&mut transaction.as_mut()).await?;
3070
- transaction.commit().await?;
3071
- }
3072
- let engine = Engine::new(Box::new(backend.clone())).await?;
3073
- let session_a = engine.open_session("version-a").await?;
3074
- let session_b = engine.open_session("version-b").await?;
3075
- let schema_definition = json!({
3076
- "x-lix-key": "test_state_schema",
3077
- "type": "object",
3078
- "properties": {
3079
- "value": { "type": "string" }
3080
- },
3081
- "required": ["value"],
3082
- "additionalProperties": false
3083
- });
3084
- session_a
3085
- .execute(
3086
- "INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
3087
- VALUES (\
3088
- lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
3089
- false,\
3090
- false\
3091
- )",
3092
- &[],
3093
- )
3094
- .await?;
3095
- session_b
3096
- .execute(
3097
- "INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
3098
- VALUES (\
3099
- lix_json('{\"x-lix-key\":\"test_state_schema\",\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"string\"}},\"required\":[\"value\"],\"additionalProperties\":false}'),\
3100
- false,\
3101
- false\
3102
- )",
3103
- &[],
3104
- )
3105
- .await?;
3106
- session_a
3107
- .execute(
3108
- "INSERT INTO lix_state (\
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
- )",
3113
- &[],
3114
- )
3115
- .await?;
3116
- session_b
3117
- .execute(
3118
- "INSERT INTO lix_state (\
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
- )",
3123
- &[],
3124
- )
3125
- .await?;
3126
- session_a
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
- )
3135
- .await?;
3136
- session_a
3137
- .execute(
3138
- "INSERT INTO lix_file (id, path, data) \
3139
- VALUES ('file-a', '/docs/readme.md', X'4142')",
3140
- &[],
3141
- )
3142
- .await?;
3143
- Ok((backend, schema_definition))
3144
- }
3145
-
3146
- fn test_live_state_context() -> LiveStateContext {
3147
- LiveStateContext::new(
3148
- TrackedStateContext::new(),
3149
- UntrackedStateContext::new(),
3150
- crate::commit_graph::CommitGraphContext::new(),
3151
- )
3152
- }
3153
-
3154
- fn run_async_test_with_large_stack(
3155
- test: impl FnOnce() -> futures_util::future::LocalBoxFuture<'static, ()> + Send + 'static,
3156
- ) {
3157
- std::thread::Builder::new()
3158
- .name("sql2-execute-test".to_string())
3159
- .stack_size(32 * 1024 * 1024)
3160
- .spawn(move || {
3161
- tokio::runtime::Builder::new_current_thread()
3162
- .enable_all()
3163
- .build()
3164
- .expect("test runtime should build")
3165
- .block_on(test());
3166
- })
3167
- .expect("test thread should spawn")
3168
- .join()
3169
- .expect("test thread should join");
3170
- }
3171
-
3172
- #[test]
3173
- fn execute_sql_reads_lix_state_by_version() {
3174
- run_async_test_with_large_stack(|| {
3175
- Box::pin(async move {
3176
- let (backend, schema_definition) = setup_sql2_state_fixture()
3177
- .await
3178
- .expect("fixture should initialize");
3179
- let backend = Arc::new(backend);
3180
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3181
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3182
- let blob_reader: Arc<dyn BlobDataReader> =
3183
- Arc::new(BackendBlobReader(storage.clone()));
3184
- let ctx = BackendSqlExecutionContext {
3185
- active_version_id: "version-a",
3186
- storage: storage.clone(),
3187
- blob_reader: Arc::clone(&blob_reader),
3188
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3189
- schema_definitions: vec![schema_definition],
3190
- };
3191
-
3192
- let result = execute_sql(
3193
- &ctx,
3194
- "SELECT entity_id, version_id, snapshot_content, commit_id \
3195
- FROM lix_state_by_version \
3196
- WHERE version_id = 'version-b' AND schema_key = 'test_state_schema'",
3197
- &[],
3198
- )
3199
- .await
3200
- .expect("sql2 execute should read lix_state_by_version");
3201
-
3202
- assert_eq!(
3203
- result.columns,
3204
- vec!["entity_id", "version_id", "snapshot_content", "commit_id"]
3205
- );
3206
- assert_eq!(result.rows.len(), 1);
3207
- assert_eq!(result.rows[0][0], Value::Json(json!(["entity-b"])));
3208
- assert_eq!(result.rows[0][1], Value::Text("version-b".to_string()));
3209
- assert_eq!(result.rows[0][2], Value::Json(json!({"value": "B"})));
3210
- match &result.rows[0][3] {
3211
- Value::Text(commit_id) => assert!(!commit_id.is_empty()),
3212
- other => panic!("expected non-null commit_id text, got {other:?}"),
3213
- }
3214
- })
3215
- });
3216
- }
3217
-
3218
- #[test]
3219
- fn execute_sql_supports_broad_lix_state_by_version_reads() {
3220
- run_async_test_with_large_stack(|| {
3221
- Box::pin(async move {
3222
- let (backend, schema_definition) = setup_sql2_state_fixture()
3223
- .await
3224
- .expect("fixture should initialize");
3225
- let backend = Arc::new(backend);
3226
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3227
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3228
- let blob_reader: Arc<dyn BlobDataReader> =
3229
- Arc::new(BackendBlobReader(storage.clone()));
3230
- let ctx = BackendSqlExecutionContext {
3231
- active_version_id: "version-a",
3232
- storage: storage.clone(),
3233
- blob_reader: Arc::clone(&blob_reader),
3234
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3235
- schema_definitions: vec![schema_definition],
3236
- };
3237
-
3238
- let result = execute_sql(
3239
- &ctx,
3240
- "SELECT entity_id FROM lix_state_by_version WHERE schema_key = 'test_state_schema'",
3241
- &[],
3242
- )
3243
- .await
3244
- .expect("broad by-version read should succeed");
3245
-
3246
- assert!(
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
- );
3252
- })
3253
- });
3254
- }
3255
-
3256
- #[test]
3257
- fn execute_sql_reads_lix_state_from_active_version() {
3258
- run_async_test_with_large_stack(|| {
3259
- Box::pin(async move {
3260
- let (backend, schema_definition) = setup_sql2_state_fixture()
3261
- .await
3262
- .expect("fixture should initialize");
3263
- let backend = Arc::new(backend);
3264
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3265
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3266
- let blob_reader: Arc<dyn BlobDataReader> =
3267
- Arc::new(BackendBlobReader(storage.clone()));
3268
- let ctx = BackendSqlExecutionContext {
3269
- active_version_id: "version-a",
3270
- storage: storage.clone(),
3271
- blob_reader: Arc::clone(&blob_reader),
3272
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3273
- schema_definitions: vec![schema_definition],
3274
- };
3275
-
3276
- let result = execute_sql(
3277
- &ctx,
3278
- "SELECT entity_id, snapshot_content \
3279
- FROM lix_state \
3280
- WHERE schema_key = 'test_state_schema'",
3281
- &[],
3282
- )
3283
- .await
3284
- .expect("sql2 execute should read lix_state");
3285
-
3286
- assert_eq!(result.columns, vec!["entity_id", "snapshot_content"]);
3287
- assert_eq!(result.rows.len(), 1);
3288
- assert_eq!(result.rows[0][0], Value::Json(json!(["entity-a"])));
3289
- assert_eq!(result.rows[0][1], Value::Json(json!({"value": "A"})));
3290
- })
3291
- });
3292
- }
3293
-
3294
- #[test]
3295
- fn execute_sql_reads_entity_view_from_active_version() {
3296
- run_async_test_with_large_stack(|| {
3297
- Box::pin(async move {
3298
- let (backend, schema_definition) = setup_sql2_state_fixture()
3299
- .await
3300
- .expect("fixture should initialize");
3301
- let backend = Arc::new(backend);
3302
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3303
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3304
- let blob_reader: Arc<dyn BlobDataReader> =
3305
- Arc::new(BackendBlobReader(storage.clone()));
3306
- let ctx = BackendSqlExecutionContext {
3307
- active_version_id: "version-a",
3308
- storage: storage.clone(),
3309
- blob_reader: Arc::clone(&blob_reader),
3310
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3311
- schema_definitions: vec![schema_definition],
3312
- };
3313
-
3314
- let result = execute_sql(
3315
- &ctx,
3316
- "SELECT value, lixcol_entity_id \
3317
- FROM test_state_schema",
3318
- &[],
3319
- )
3320
- .await
3321
- .expect("sql2 execute should read entity view");
3322
-
3323
- assert_eq!(result.columns, vec!["value", "lixcol_entity_id"]);
3324
- assert_eq!(result.rows.len(), 1);
3325
- assert_eq!(result.rows[0][0], Value::Text("A".to_string()));
3326
- assert_eq!(result.rows[0][1], Value::Json(json!(["entity-a"])));
3327
- })
3328
- });
3329
- }
3330
-
3331
- #[test]
3332
- fn execute_sql_reads_entity_by_version_view() {
3333
- run_async_test_with_large_stack(|| {
3334
- Box::pin(async move {
3335
- let (backend, schema_definition) = setup_sql2_state_fixture()
3336
- .await
3337
- .expect("fixture should initialize");
3338
- let backend = Arc::new(backend);
3339
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3340
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3341
- let blob_reader: Arc<dyn BlobDataReader> =
3342
- Arc::new(BackendBlobReader(storage.clone()));
3343
- let ctx = BackendSqlExecutionContext {
3344
- active_version_id: "version-a",
3345
- storage: storage.clone(),
3346
- blob_reader: Arc::clone(&blob_reader),
3347
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3348
- schema_definitions: vec![schema_definition],
3349
- };
3350
-
3351
- let result = execute_sql(
3352
- &ctx,
3353
- "SELECT value, lixcol_version_id \
3354
- FROM test_state_schema_by_version \
3355
- WHERE lixcol_version_id = 'version-b'",
3356
- &[],
3357
- )
3358
- .await
3359
- .expect("sql2 execute should read entity by-version view");
3360
-
3361
- assert_eq!(result.columns, vec!["value", "lixcol_version_id"]);
3362
- assert_eq!(result.rows.len(), 1);
3363
- assert_eq!(result.rows[0][0], Value::Text("B".to_string()));
3364
- assert_eq!(result.rows[0][1], Value::Text("version-b".to_string()));
3365
- })
3366
- });
3367
- }
3368
-
3369
- #[test]
3370
- fn execute_sql_reads_lix_directory_by_version_view() {
3371
- run_async_test_with_large_stack(|| {
3372
- Box::pin(async move {
3373
- let (backend, schema_definition) = setup_sql2_state_fixture()
3374
- .await
3375
- .expect("fixture should initialize");
3376
- let backend = Arc::new(backend);
3377
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3378
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3379
- let blob_reader: Arc<dyn BlobDataReader> =
3380
- Arc::new(BackendBlobReader(storage.clone()));
3381
- let ctx = BackendSqlExecutionContext {
3382
- active_version_id: "version-a",
3383
- storage: storage.clone(),
3384
- blob_reader: Arc::clone(&blob_reader),
3385
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3386
- schema_definitions: vec![schema_definition],
3387
- };
3388
-
3389
- let result = execute_sql(
3390
- &ctx,
3391
- "SELECT path, name, lixcol_version_id \
3392
- FROM lix_directory_by_version \
3393
- WHERE id = 'dir-docs' AND lixcol_version_id = 'version-a'",
3394
- &[],
3395
- )
3396
- .await
3397
- .expect("sql2 execute should read lix_directory_by_version");
3398
-
3399
- assert_eq!(result.columns, vec!["path", "name", "lixcol_version_id"]);
3400
- assert_eq!(result.rows.len(), 1);
3401
- assert_eq!(result.rows[0][0], Value::Text("/docs/".to_string()));
3402
- assert_eq!(result.rows[0][1], Value::Text("docs".to_string()));
3403
- assert_eq!(result.rows[0][2], Value::Text("version-a".to_string()));
3404
- })
3405
- });
3406
- }
3407
-
3408
- #[test]
3409
- fn execute_sql_reads_lix_directory_from_active_version() {
3410
- run_async_test_with_large_stack(|| {
3411
- Box::pin(async move {
3412
- let (backend, schema_definition) = setup_sql2_state_fixture()
3413
- .await
3414
- .expect("fixture should initialize");
3415
- let backend = Arc::new(backend);
3416
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3417
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3418
- let blob_reader: Arc<dyn BlobDataReader> =
3419
- Arc::new(BackendBlobReader(storage.clone()));
3420
- let ctx = BackendSqlExecutionContext {
3421
- active_version_id: "version-a",
3422
- storage: storage.clone(),
3423
- blob_reader: Arc::clone(&blob_reader),
3424
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3425
- schema_definitions: vec![schema_definition],
3426
- };
3427
-
3428
- let result = execute_sql(
3429
- &ctx,
3430
- "SELECT path, name \
3431
- FROM lix_directory \
3432
- WHERE id = 'dir-docs'",
3433
- &[],
3434
- )
3435
- .await
3436
- .expect("sql2 execute should read lix_directory");
3437
-
3438
- assert_eq!(result.columns, vec!["path", "name"]);
3439
- assert_eq!(result.rows.len(), 1);
3440
- assert_eq!(result.rows[0][0], Value::Text("/docs/".to_string()));
3441
- assert_eq!(result.rows[0][1], Value::Text("docs".to_string()));
3442
- })
3443
- });
3444
- }
3445
-
3446
- #[test]
3447
- fn execute_sql_reads_lix_file_by_version_view() {
3448
- run_async_test_with_large_stack(|| {
3449
- Box::pin(async move {
3450
- let (backend, schema_definition) = setup_sql2_state_fixture()
3451
- .await
3452
- .expect("fixture should initialize");
3453
- let backend = Arc::new(backend);
3454
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3455
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3456
- let blob_reader: Arc<dyn BlobDataReader> =
3457
- Arc::new(BackendBlobReader(storage.clone()));
3458
- let ctx = BackendSqlExecutionContext {
3459
- active_version_id: "version-a",
3460
- storage: storage.clone(),
3461
- blob_reader: Arc::clone(&blob_reader),
3462
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3463
- schema_definitions: vec![schema_definition],
3464
- };
3465
-
3466
- let result = execute_sql(
3467
- &ctx,
3468
- "SELECT path, name, data, lixcol_version_id \
3469
- FROM lix_file_by_version \
3470
- WHERE id = 'file-a' AND lixcol_version_id = 'version-a'",
3471
- &[],
3472
- )
3473
- .await
3474
- .expect("sql2 execute should read lix_file_by_version");
3475
-
3476
- assert_eq!(
3477
- result.columns,
3478
- vec!["path", "name", "data", "lixcol_version_id"]
3479
- );
3480
- assert_eq!(result.rows.len(), 1);
3481
- assert_eq!(
3482
- result.rows[0][0],
3483
- Value::Text("/docs/readme.md".to_string())
3484
- );
3485
- assert_eq!(result.rows[0][1], Value::Text("readme.md".to_string()));
3486
- assert_eq!(result.rows[0][2], Value::Blob(vec![0x41, 0x42]));
3487
- assert_eq!(result.rows[0][3], Value::Text("version-a".to_string()));
3488
- })
3489
- });
3490
- }
3491
-
3492
- #[test]
3493
- fn execute_sql_reads_lix_file_from_active_version() {
3494
- run_async_test_with_large_stack(|| {
3495
- Box::pin(async move {
3496
- let (backend, schema_definition) = setup_sql2_state_fixture()
3497
- .await
3498
- .expect("fixture should initialize");
3499
- let backend = Arc::new(backend);
3500
- let backend_ref: Arc<dyn crate::Backend + Send + Sync> = backend;
3501
- let storage = StorageContext::new(Arc::clone(&backend_ref));
3502
- let blob_reader: Arc<dyn BlobDataReader> =
3503
- Arc::new(BackendBlobReader(storage.clone()));
3504
- let ctx = BackendSqlExecutionContext {
3505
- active_version_id: "version-a",
3506
- storage: storage.clone(),
3507
- blob_reader: Arc::clone(&blob_reader),
3508
- live_state: Arc::new(test_live_state_context().reader(storage.clone())),
3509
- schema_definitions: vec![schema_definition],
3510
- };
3511
-
3512
- let result = execute_sql(
3513
- &ctx,
3514
- "SELECT path, name, data \
3515
- FROM lix_file \
3516
- WHERE id = 'file-a'",
3517
- &[],
3518
- )
3519
- .await
3520
- .expect("sql2 execute should read lix_file");
3521
-
3522
- assert_eq!(result.columns, vec!["path", "name", "data"]);
3523
- assert_eq!(result.rows.len(), 1);
3524
- assert_eq!(
3525
- result.rows[0][0],
3526
- Value::Text("/docs/readme.md".to_string())
3527
- );
3528
- assert_eq!(result.rows[0][1], Value::Text("readme.md".to_string()));
3529
- assert_eq!(result.rows[0][2], Value::Blob(vec![0x41, 0x42]));
3530
- })
3531
- });
3532
- }
3533
- }