@lix-js/sdk 0.6.0-preview.5 → 0.6.1

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 (274) 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 +38 -207
  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 +38 -71
  16. package/SKILL.md +0 -507
  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 -833
  23. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  24. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +0 -27
  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 -359
  29. package/dist-engine-src/README.md +0 -18
  30. package/dist-engine-src/src/backend/capabilities.rs +0 -67
  31. package/dist-engine-src/src/backend/conformance/baseline.rs +0 -1127
  32. package/dist-engine-src/src/backend/conformance/factory.rs +0 -93
  33. package/dist-engine-src/src/backend/conformance/failure_tests.rs +0 -608
  34. package/dist-engine-src/src/backend/conformance/fixtures.rs +0 -26
  35. package/dist-engine-src/src/backend/conformance/mod.rs +0 -75
  36. package/dist-engine-src/src/backend/conformance/model.rs +0 -28
  37. package/dist-engine-src/src/backend/conformance/model_based.rs +0 -257
  38. package/dist-engine-src/src/backend/conformance/persistence.rs +0 -204
  39. package/dist-engine-src/src/backend/conformance/projection.rs +0 -21
  40. package/dist-engine-src/src/backend/conformance/pushdown.rs +0 -24
  41. package/dist-engine-src/src/backend/conformance/runner.rs +0 -90
  42. package/dist-engine-src/src/backend/conformance/scan.rs +0 -24
  43. package/dist-engine-src/src/backend/conformance/write.rs +0 -16
  44. package/dist-engine-src/src/backend/error.rs +0 -94
  45. package/dist-engine-src/src/backend/in_memory.rs +0 -670
  46. package/dist-engine-src/src/backend/mod.rs +0 -39
  47. package/dist-engine-src/src/backend/predicate.rs +0 -80
  48. package/dist-engine-src/src/backend/traits.rs +0 -260
  49. package/dist-engine-src/src/backend/types.rs +0 -239
  50. package/dist-engine-src/src/binary_cas/chunking.rs +0 -31
  51. package/dist-engine-src/src/binary_cas/codec.rs +0 -346
  52. package/dist-engine-src/src/binary_cas/context.rs +0 -139
  53. package/dist-engine-src/src/binary_cas/kv.rs +0 -1038
  54. package/dist-engine-src/src/binary_cas/mod.rs +0 -11
  55. package/dist-engine-src/src/binary_cas/types.rs +0 -121
  56. package/dist-engine-src/src/branch/context.rs +0 -40
  57. package/dist-engine-src/src/branch/lifecycle.rs +0 -221
  58. package/dist-engine-src/src/branch/mod.rs +0 -13
  59. package/dist-engine-src/src/branch/refs.rs +0 -321
  60. package/dist-engine-src/src/branch/stage_rows.rs +0 -67
  61. package/dist-engine-src/src/branch/types.rs +0 -21
  62. package/dist-engine-src/src/catalog/context.rs +0 -412
  63. package/dist-engine-src/src/catalog/mod.rs +0 -10
  64. package/dist-engine-src/src/catalog/schema.rs +0 -4
  65. package/dist-engine-src/src/catalog/snapshot.rs +0 -1114
  66. package/dist-engine-src/src/cel/context.rs +0 -86
  67. package/dist-engine-src/src/cel/error.rs +0 -19
  68. package/dist-engine-src/src/cel/mod.rs +0 -8
  69. package/dist-engine-src/src/cel/provider.rs +0 -9
  70. package/dist-engine-src/src/cel/runtime.rs +0 -167
  71. package/dist-engine-src/src/cel/value.rs +0 -50
  72. package/dist-engine-src/src/changelog/bench_support.rs +0 -785
  73. package/dist-engine-src/src/changelog/change.rs +0 -1
  74. package/dist-engine-src/src/changelog/codec.rs +0 -497
  75. package/dist-engine-src/src/changelog/commit.rs +0 -1
  76. package/dist-engine-src/src/changelog/context.rs +0 -1614
  77. package/dist-engine-src/src/changelog/mod.rs +0 -29
  78. package/dist-engine-src/src/changelog/store.rs +0 -163
  79. package/dist-engine-src/src/changelog/test_support.rs +0 -54
  80. package/dist-engine-src/src/changelog/types.rs +0 -213
  81. package/dist-engine-src/src/commit_graph/context.rs +0 -944
  82. package/dist-engine-src/src/commit_graph/mod.rs +0 -9
  83. package/dist-engine-src/src/commit_graph/types.rs +0 -89
  84. package/dist-engine-src/src/commit_graph/walker.rs +0 -786
  85. package/dist-engine-src/src/common/error.rs +0 -347
  86. package/dist-engine-src/src/common/fingerprint.rs +0 -3
  87. package/dist-engine-src/src/common/fs_path.rs +0 -1336
  88. package/dist-engine-src/src/common/identity.rs +0 -145
  89. package/dist-engine-src/src/common/json_pointer.rs +0 -67
  90. package/dist-engine-src/src/common/metadata.rs +0 -40
  91. package/dist-engine-src/src/common/mod.rs +0 -23
  92. package/dist-engine-src/src/common/types.rs +0 -105
  93. package/dist-engine-src/src/common/wire.rs +0 -222
  94. package/dist-engine-src/src/domain.rs +0 -320
  95. package/dist-engine-src/src/engine.rs +0 -203
  96. package/dist-engine-src/src/entity_pk.rs +0 -402
  97. package/dist-engine-src/src/functions/context.rs +0 -296
  98. package/dist-engine-src/src/functions/deterministic.rs +0 -113
  99. package/dist-engine-src/src/functions/mod.rs +0 -18
  100. package/dist-engine-src/src/functions/provider.rs +0 -130
  101. package/dist-engine-src/src/functions/state.rs +0 -335
  102. package/dist-engine-src/src/functions/types.rs +0 -37
  103. package/dist-engine-src/src/init.rs +0 -692
  104. package/dist-engine-src/src/json_store/compression.rs +0 -77
  105. package/dist-engine-src/src/json_store/context.rs +0 -172
  106. package/dist-engine-src/src/json_store/encoded.rs +0 -15
  107. package/dist-engine-src/src/json_store/mod.rs +0 -38
  108. package/dist-engine-src/src/json_store/store.rs +0 -494
  109. package/dist-engine-src/src/json_store/types.rs +0 -212
  110. package/dist-engine-src/src/lib.rs +0 -92
  111. package/dist-engine-src/src/live_state/context.rs +0 -1883
  112. package/dist-engine-src/src/live_state/mod.rs +0 -21
  113. package/dist-engine-src/src/live_state/overlay.rs +0 -75
  114. package/dist-engine-src/src/live_state/reader.rs +0 -23
  115. package/dist-engine-src/src/live_state/types.rs +0 -231
  116. package/dist-engine-src/src/live_state/visibility.rs +0 -666
  117. package/dist-engine-src/src/plugin/archive.rs +0 -438
  118. package/dist-engine-src/src/plugin/component.rs +0 -183
  119. package/dist-engine-src/src/plugin/install.rs +0 -619
  120. package/dist-engine-src/src/plugin/manifest.rs +0 -516
  121. package/dist-engine-src/src/plugin/materializer.rs +0 -202
  122. package/dist-engine-src/src/plugin/mod.rs +0 -33
  123. package/dist-engine-src/src/plugin/plugin_manifest.json +0 -119
  124. package/dist-engine-src/src/plugin/storage.rs +0 -74
  125. package/dist-engine-src/src/schema/annotations/defaults.rs +0 -275
  126. package/dist-engine-src/src/schema/annotations/mod.rs +0 -1
  127. package/dist-engine-src/src/schema/builtin/lix_account.json +0 -21
  128. package/dist-engine-src/src/schema/builtin/lix_active_account.json +0 -29
  129. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +0 -29
  130. package/dist-engine-src/src/schema/builtin/lix_branch_descriptor.json +0 -34
  131. package/dist-engine-src/src/schema/builtin/lix_branch_ref.json +0 -48
  132. package/dist-engine-src/src/schema/builtin/lix_change.json +0 -63
  133. package/dist-engine-src/src/schema/builtin/lix_change_author.json +0 -45
  134. package/dist-engine-src/src/schema/builtin/lix_commit.json +0 -24
  135. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +0 -53
  136. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +0 -52
  137. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +0 -52
  138. package/dist-engine-src/src/schema/builtin/lix_key_value.json +0 -40
  139. package/dist-engine-src/src/schema/builtin/lix_label.json +0 -29
  140. package/dist-engine-src/src/schema/builtin/lix_label_assignment.json +0 -74
  141. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +0 -25
  142. package/dist-engine-src/src/schema/builtin/mod.rs +0 -220
  143. package/dist-engine-src/src/schema/compatibility.rs +0 -787
  144. package/dist-engine-src/src/schema/definition.json +0 -187
  145. package/dist-engine-src/src/schema/definition.rs +0 -742
  146. package/dist-engine-src/src/schema/key.rs +0 -138
  147. package/dist-engine-src/src/schema/mod.rs +0 -20
  148. package/dist-engine-src/src/schema/seed.rs +0 -14
  149. package/dist-engine-src/src/schema/tests.rs +0 -780
  150. package/dist-engine-src/src/session/context.rs +0 -1059
  151. package/dist-engine-src/src/session/create_branch.rs +0 -94
  152. package/dist-engine-src/src/session/execute.rs +0 -681
  153. package/dist-engine-src/src/session/merge/analysis.rs +0 -108
  154. package/dist-engine-src/src/session/merge/branch.rs +0 -417
  155. package/dist-engine-src/src/session/merge/conflicts.rs +0 -63
  156. package/dist-engine-src/src/session/merge/mod.rs +0 -10
  157. package/dist-engine-src/src/session/merge/stats.rs +0 -61
  158. package/dist-engine-src/src/session/mod.rs +0 -30
  159. package/dist-engine-src/src/session/switch_branch.rs +0 -113
  160. package/dist-engine-src/src/session/transaction.rs +0 -557
  161. package/dist-engine-src/src/sql2/bind/classify.rs +0 -102
  162. package/dist-engine-src/src/sql2/bind/error.rs +0 -5
  163. package/dist-engine-src/src/sql2/bind/expr.rs +0 -29
  164. package/dist-engine-src/src/sql2/bind/mod.rs +0 -12
  165. package/dist-engine-src/src/sql2/bind/public_udf.rs +0 -306
  166. package/dist-engine-src/src/sql2/bind/read.rs +0 -65
  167. package/dist-engine-src/src/sql2/bind/statement.rs +0 -2236
  168. package/dist-engine-src/src/sql2/bind/table.rs +0 -273
  169. package/dist-engine-src/src/sql2/bind/write.rs +0 -86
  170. package/dist-engine-src/src/sql2/branch_scope.rs +0 -436
  171. package/dist-engine-src/src/sql2/catalog/capability.rs +0 -20
  172. package/dist-engine-src/src/sql2/catalog/entity_surface.rs +0 -296
  173. package/dist-engine-src/src/sql2/catalog/mod.rs +0 -15
  174. package/dist-engine-src/src/sql2/catalog/registry.rs +0 -556
  175. package/dist-engine-src/src/sql2/catalog/schema.rs +0 -88
  176. package/dist-engine-src/src/sql2/catalog/surface.rs +0 -41
  177. package/dist-engine-src/src/sql2/change_materialization.rs +0 -122
  178. package/dist-engine-src/src/sql2/context.rs +0 -317
  179. package/dist-engine-src/src/sql2/dml.rs +0 -148
  180. package/dist-engine-src/src/sql2/error.rs +0 -215
  181. package/dist-engine-src/src/sql2/exec/bound_public_write.rs +0 -1593
  182. package/dist-engine-src/src/sql2/exec/datafusion.rs +0 -5266
  183. package/dist-engine-src/src/sql2/exec/fast_write.rs +0 -82
  184. package/dist-engine-src/src/sql2/exec/mod.rs +0 -24
  185. package/dist-engine-src/src/sql2/exec/write.rs +0 -661
  186. package/dist-engine-src/src/sql2/filesystem_planner.rs +0 -1485
  187. package/dist-engine-src/src/sql2/filesystem_predicates.rs +0 -159
  188. package/dist-engine-src/src/sql2/filesystem_visibility.rs +0 -383
  189. package/dist-engine-src/src/sql2/history_projection.rs +0 -56
  190. package/dist-engine-src/src/sql2/history_route.rs +0 -661
  191. package/dist-engine-src/src/sql2/mod.rs +0 -52
  192. package/dist-engine-src/src/sql2/optimize/datafusion.rs +0 -1
  193. package/dist-engine-src/src/sql2/optimize/mod.rs +0 -2
  194. package/dist-engine-src/src/sql2/optimize/simple_write.rs +0 -116
  195. package/dist-engine-src/src/sql2/parse/mod.rs +0 -69
  196. package/dist-engine-src/src/sql2/parse/normalize.rs +0 -1
  197. package/dist-engine-src/src/sql2/plan/branch_scope.rs +0 -24
  198. package/dist-engine-src/src/sql2/plan/mod.rs +0 -5
  199. package/dist-engine-src/src/sql2/plan/predicate.rs +0 -22
  200. package/dist-engine-src/src/sql2/plan/write.rs +0 -147
  201. package/dist-engine-src/src/sql2/predicate_typecheck.rs +0 -504
  202. package/dist-engine-src/src/sql2/providers/branch.rs +0 -1206
  203. package/dist-engine-src/src/sql2/providers/change.rs +0 -445
  204. package/dist-engine-src/src/sql2/providers/directory.rs +0 -2422
  205. package/dist-engine-src/src/sql2/providers/directory_history.rs +0 -645
  206. package/dist-engine-src/src/sql2/providers/entity.rs +0 -1484
  207. package/dist-engine-src/src/sql2/providers/entity_history.rs +0 -452
  208. package/dist-engine-src/src/sql2/providers/file.rs +0 -3686
  209. package/dist-engine-src/src/sql2/providers/file_history.rs +0 -924
  210. package/dist-engine-src/src/sql2/providers/history.rs +0 -426
  211. package/dist-engine-src/src/sql2/providers/lix_state.rs +0 -2542
  212. package/dist-engine-src/src/sql2/providers/mod.rs +0 -508
  213. package/dist-engine-src/src/sql2/read_only.rs +0 -63
  214. package/dist-engine-src/src/sql2/record_batch.rs +0 -17
  215. package/dist-engine-src/src/sql2/result_metadata.rs +0 -29
  216. package/dist-engine-src/src/sql2/runtime.rs +0 -60
  217. package/dist-engine-src/src/sql2/session.rs +0 -83
  218. package/dist-engine-src/src/sql2/storage/constraints.rs +0 -1
  219. package/dist-engine-src/src/sql2/storage/mod.rs +0 -1
  220. package/dist-engine-src/src/sql2/test_support/differential.rs +0 -712
  221. package/dist-engine-src/src/sql2/test_support/generators.rs +0 -354
  222. package/dist-engine-src/src/sql2/test_support/mod.rs +0 -2
  223. package/dist-engine-src/src/sql2/udfs/common.rs +0 -295
  224. package/dist-engine-src/src/sql2/udfs/lix_active_branch_commit_id.rs +0 -53
  225. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +0 -47
  226. package/dist-engine-src/src/sql2/udfs/lix_json.rs +0 -100
  227. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +0 -99
  228. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +0 -99
  229. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +0 -82
  230. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +0 -85
  231. package/dist-engine-src/src/sql2/udfs/lix_timestamp.rs +0 -76
  232. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +0 -76
  233. package/dist-engine-src/src/sql2/udfs/mod.rs +0 -86
  234. package/dist-engine-src/src/sql2/write_normalization.rs +0 -368
  235. package/dist-engine-src/src/storage/conformance.rs +0 -399
  236. package/dist-engine-src/src/storage/context.rs +0 -620
  237. package/dist-engine-src/src/storage/mod.rs +0 -52
  238. package/dist-engine-src/src/storage/point.rs +0 -440
  239. package/dist-engine-src/src/storage/read_scope.rs +0 -67
  240. package/dist-engine-src/src/storage/reader.rs +0 -867
  241. package/dist-engine-src/src/storage/scan.rs +0 -784
  242. package/dist-engine-src/src/storage/spaces.rs +0 -236
  243. package/dist-engine-src/src/storage/stats.rs +0 -80
  244. package/dist-engine-src/src/storage/write_set.rs +0 -962
  245. package/dist-engine-src/src/storage_bench.rs +0 -171
  246. package/dist-engine-src/src/test_support.rs +0 -450
  247. package/dist-engine-src/src/tracked_state/bench_support.rs +0 -394
  248. package/dist-engine-src/src/tracked_state/codec.rs +0 -1183
  249. package/dist-engine-src/src/tracked_state/commit_root_rebuild.rs +0 -358
  250. package/dist-engine-src/src/tracked_state/context.rs +0 -2801
  251. package/dist-engine-src/src/tracked_state/diff.rs +0 -2140
  252. package/dist-engine-src/src/tracked_state/merge.rs +0 -478
  253. package/dist-engine-src/src/tracked_state/mod.rs +0 -35
  254. package/dist-engine-src/src/tracked_state/row_materialization.rs +0 -275
  255. package/dist-engine-src/src/tracked_state/storage.rs +0 -427
  256. package/dist-engine-src/src/tracked_state/tree.rs +0 -3063
  257. package/dist-engine-src/src/tracked_state/types.rs +0 -238
  258. package/dist-engine-src/src/transaction/bench_support.rs +0 -407
  259. package/dist-engine-src/src/transaction/commit.rs +0 -1592
  260. package/dist-engine-src/src/transaction/context.rs +0 -1653
  261. package/dist-engine-src/src/transaction/mod.rs +0 -24
  262. package/dist-engine-src/src/transaction/normalization.rs +0 -877
  263. package/dist-engine-src/src/transaction/prep.rs +0 -37
  264. package/dist-engine-src/src/transaction/schema_resolver.rs +0 -163
  265. package/dist-engine-src/src/transaction/staging.rs +0 -1525
  266. package/dist-engine-src/src/transaction/types.rs +0 -403
  267. package/dist-engine-src/src/transaction/validation.rs +0 -5766
  268. package/dist-engine-src/src/untracked_state/codec.rs +0 -615
  269. package/dist-engine-src/src/untracked_state/context.rs +0 -98
  270. package/dist-engine-src/src/untracked_state/materialization.rs +0 -63
  271. package/dist-engine-src/src/untracked_state/mod.rs +0 -15
  272. package/dist-engine-src/src/untracked_state/storage.rs +0 -898
  273. package/dist-engine-src/src/untracked_state/types.rs +0 -146
  274. package/dist-engine-src/src/wasm/mod.rs +0 -60
@@ -1,1593 +0,0 @@
1
- use serde_json::Value as JsonValue;
2
-
3
- use crate::common::validate_row_metadata;
4
- use crate::entity_pk::EntityPk;
5
- use crate::live_state::{LiveStateFilter, LiveStateScanRequest};
6
- use crate::sql2::bind::expr::{BoundExpr, BoundLiteral};
7
- use crate::sql2::bind::write::{
8
- BoundInsertValues, BoundWriteInput, BoundWriteOp, BoundWriteTarget, EntityWriteSurface,
9
- };
10
- use crate::sql2::catalog::entity_surface::EntitySurfaceColumn;
11
- use crate::sql2::catalog::{
12
- derive_entity_surface_spec_from_schema, EntityColumnType, EntitySurfaceSpec,
13
- };
14
- use crate::sql2::plan::branch_scope::BranchScope;
15
- use crate::sql2::plan::predicate::FilterSet;
16
- use crate::sql2::plan::LogicalWritePlan;
17
- use crate::sql2::read_only::reject_read_only_entity_surface;
18
- use crate::sql2::SqlWriteExecutionContext;
19
- use crate::transaction::types::{
20
- TransactionJson, TransactionWrite, TransactionWriteMode, TransactionWriteRow,
21
- };
22
- use crate::{parse_row_metadata_value, LixError, Value};
23
-
24
- pub(crate) fn supports_bound_public_write(plan: &LogicalWritePlan) -> bool {
25
- matches!(plan.bound.target, BoundWriteTarget::Entity(_))
26
- && bound_public_write_shape_supported(plan)
27
- }
28
-
29
- pub(crate) async fn execute_bound_public_write(
30
- ctx: &mut dyn SqlWriteExecutionContext,
31
- plan: &LogicalWritePlan,
32
- params: &[Value],
33
- ) -> Result<u64, LixError> {
34
- match &plan.bound.target {
35
- BoundWriteTarget::Entity(surface) => execute_entity_write(ctx, plan, surface, params).await,
36
- _ => Err(LixError::new(
37
- LixError::CODE_UNSUPPORTED_SQL,
38
- "bound public write executor does not support this target yet",
39
- )),
40
- }
41
- }
42
-
43
- async fn execute_entity_write(
44
- ctx: &mut dyn SqlWriteExecutionContext,
45
- plan: &LogicalWritePlan,
46
- surface: &EntityWriteSurface,
47
- params: &[Value],
48
- ) -> Result<u64, LixError> {
49
- let schema_key = match surface {
50
- EntityWriteSurface::Base { schema_key } | EntityWriteSurface::ByBranch { schema_key } => {
51
- schema_key
52
- }
53
- };
54
- reject_read_only_entity_surface(schema_key, entity_action(&plan.bound.op))
55
- .map_err(crate::sql2::error::datafusion_error_to_lix_error)?;
56
-
57
- if schema_key == "lix_registered_schema" && plan.bound.op == BoundWriteOp::Delete {
58
- return Err(LixError::new(
59
- LixError::CODE_UNSUPPORTED_SQL,
60
- "delete lix_registered_schema is not supported",
61
- ));
62
- }
63
-
64
- let spec = entity_spec(ctx, schema_key)?;
65
- validate_bound_write_supported(plan, &spec)?;
66
- let active_branch_commit_id = load_active_branch_commit_id(ctx).await?;
67
- let no_op = matches!(plan.bound.branch_scope, BranchScope::Empty)
68
- || matches!(plan.filters.rows, FilterSet::None);
69
- match plan.bound.op {
70
- BoundWriteOp::Insert => {
71
- if no_op {
72
- entity_insert_rows(ctx, plan, &spec, params, active_branch_commit_id.as_deref())?;
73
- return Ok(0);
74
- }
75
- entity_insert(ctx, plan, &spec, params, active_branch_commit_id.as_deref()).await
76
- }
77
- BoundWriteOp::Update => {
78
- if no_op {
79
- return Ok(0);
80
- }
81
- entity_update(ctx, plan, &spec, params, active_branch_commit_id.as_deref()).await
82
- }
83
- BoundWriteOp::Delete => {
84
- if no_op {
85
- return Ok(0);
86
- }
87
- entity_delete(ctx, plan, &spec, params, active_branch_commit_id.as_deref()).await
88
- }
89
- }
90
- }
91
-
92
- async fn load_active_branch_commit_id(
93
- ctx: &mut dyn SqlWriteExecutionContext,
94
- ) -> Result<Option<String>, LixError> {
95
- let active_branch_id = ctx.active_branch_id().to_string();
96
- ctx.load_branch_head(&active_branch_id)
97
- .await?
98
- .map(Some)
99
- .ok_or_else(|| {
100
- LixError::branch_not_found(
101
- active_branch_id,
102
- "execute bound public write",
103
- "active branch",
104
- )
105
- })
106
- }
107
-
108
- async fn entity_insert(
109
- ctx: &mut dyn SqlWriteExecutionContext,
110
- plan: &LogicalWritePlan,
111
- spec: &EntitySurfaceSpec,
112
- params: &[Value],
113
- active_branch_commit_id: Option<&str>,
114
- ) -> Result<u64, LixError> {
115
- let write_rows = entity_insert_rows(ctx, plan, spec, params, active_branch_commit_id)?;
116
- stage_rows(ctx, TransactionWriteMode::Insert, write_rows).await
117
- }
118
-
119
- fn entity_insert_rows(
120
- ctx: &mut dyn SqlWriteExecutionContext,
121
- plan: &LogicalWritePlan,
122
- spec: &EntitySurfaceSpec,
123
- params: &[Value],
124
- active_branch_commit_id: Option<&str>,
125
- ) -> Result<Vec<TransactionWriteRow>, LixError> {
126
- let BoundWriteInput::Values(values) = &plan.bound.input else {
127
- return Err(LixError::new(
128
- LixError::CODE_UNSUPPORTED_SQL,
129
- "bound entity INSERT supports VALUES only",
130
- ));
131
- };
132
-
133
- let layout = InsertRowLayout::from_values(spec, values)?;
134
- let mut write_rows = Vec::with_capacity(values.rows.len());
135
- for row in &values.rows {
136
- write_rows.push(entity_insert_row(
137
- ctx,
138
- plan,
139
- &layout,
140
- row,
141
- params,
142
- active_branch_commit_id,
143
- )?);
144
- }
145
- Ok(write_rows)
146
- }
147
-
148
- async fn entity_update(
149
- ctx: &mut dyn SqlWriteExecutionContext,
150
- plan: &LogicalWritePlan,
151
- spec: &EntitySurfaceSpec,
152
- params: &[Value],
153
- active_branch_commit_id: Option<&str>,
154
- ) -> Result<u64, LixError> {
155
- let candidates = scan_entity_candidates(ctx, plan, spec).await?;
156
- let mut write_rows = Vec::new();
157
- for candidate in candidates {
158
- let Some(snapshot) = candidate_snapshot(&candidate)? else {
159
- continue;
160
- };
161
- let original_context = EntityEvalContext::live(&snapshot, &candidate, spec);
162
- if !predicate_matches(
163
- &plan.bound.predicate,
164
- &original_context,
165
- spec,
166
- ctx,
167
- params,
168
- active_branch_commit_id,
169
- )? {
170
- continue;
171
- }
172
- reject_projected_global_write(plan, &candidate, "UPDATE")?;
173
- let mut updated = snapshot.clone();
174
- let mut visible_assignments = Vec::new();
175
- for assignment in &plan.bound.assignments {
176
- if let Some(column) = spec.visible_column(&assignment.column.name) {
177
- reject_direct_blob_json_value(&assignment.value, column.column_type, params)?;
178
- let value = eval_expr_value(
179
- &assignment.value,
180
- &original_context,
181
- ctx,
182
- params,
183
- active_branch_commit_id,
184
- )?;
185
- visible_assignments.push((
186
- column.name.clone(),
187
- entity_json_value(value, column.column_type)?,
188
- ));
189
- } else if assignment.column.name == "lixcol_metadata" {
190
- // handled below from the assignment list
191
- } else {
192
- return Err(LixError::new(
193
- LixError::CODE_UNSUPPORTED_SQL,
194
- format!(
195
- "bound entity UPDATE does not support assignment to '{}'",
196
- assignment.column.name
197
- ),
198
- ));
199
- }
200
- }
201
- for (column_name, value) in visible_assignments {
202
- updated[&column_name] = value;
203
- }
204
- write_rows.push(entity_replace_row_from_live(
205
- ctx,
206
- spec,
207
- &candidate,
208
- Some(updated),
209
- plan,
210
- params,
211
- active_branch_commit_id,
212
- )?);
213
- }
214
- stage_rows(ctx, TransactionWriteMode::Replace, write_rows).await
215
- }
216
-
217
- async fn entity_delete(
218
- ctx: &mut dyn SqlWriteExecutionContext,
219
- plan: &LogicalWritePlan,
220
- spec: &EntitySurfaceSpec,
221
- params: &[Value],
222
- active_branch_commit_id: Option<&str>,
223
- ) -> Result<u64, LixError> {
224
- let candidates = scan_entity_candidates(ctx, plan, spec).await?;
225
- let mut write_rows = Vec::new();
226
- for candidate in candidates {
227
- let Some(snapshot) = candidate_snapshot(&candidate)? else {
228
- continue;
229
- };
230
- let context = EntityEvalContext::live(&snapshot, &candidate, spec);
231
- if predicate_matches(
232
- &plan.bound.predicate,
233
- &context,
234
- spec,
235
- ctx,
236
- params,
237
- active_branch_commit_id,
238
- )? {
239
- reject_projected_global_write(plan, &candidate, "DELETE")?;
240
- write_rows.push(entity_replace_row_from_live(
241
- ctx,
242
- spec,
243
- &candidate,
244
- None,
245
- plan,
246
- params,
247
- active_branch_commit_id,
248
- )?);
249
- }
250
- }
251
- stage_rows(ctx, TransactionWriteMode::Replace, write_rows).await
252
- }
253
-
254
- async fn stage_rows(
255
- ctx: &mut dyn SqlWriteExecutionContext,
256
- mode: TransactionWriteMode,
257
- rows: Vec<TransactionWriteRow>,
258
- ) -> Result<u64, LixError> {
259
- if rows.is_empty() {
260
- return Ok(0);
261
- }
262
- let outcome = ctx
263
- .stage_write(TransactionWrite::Rows { mode, rows })
264
- .await?;
265
- Ok(outcome.count)
266
- }
267
-
268
- async fn scan_entity_candidates(
269
- ctx: &mut dyn SqlWriteExecutionContext,
270
- plan: &LogicalWritePlan,
271
- spec: &EntitySurfaceSpec,
272
- ) -> Result<Vec<crate::live_state::MaterializedLiveStateRow>, LixError> {
273
- let branch_ids = scan_branch_ids(&plan.bound.branch_scope)?;
274
- let request = LiveStateScanRequest {
275
- filter: LiveStateFilter {
276
- schema_keys: vec![spec.schema_key.clone()],
277
- branch_ids,
278
- include_tombstones: false,
279
- ..LiveStateFilter::default()
280
- },
281
- ..LiveStateScanRequest::default()
282
- };
283
- ctx.scan_live_state(&request).await
284
- }
285
-
286
- struct InsertRowLayout {
287
- schema_key: String,
288
- visible_columns: Vec<EntitySurfaceColumn>,
289
- snapshot_context: String,
290
- snapshot_capacity: usize,
291
- columns: Vec<InsertColumnTarget>,
292
- }
293
-
294
- #[derive(Clone)]
295
- enum InsertColumnTarget {
296
- Visible {
297
- name: String,
298
- column_type: EntityColumnType,
299
- },
300
- EntityPk,
301
- FileId,
302
- Metadata,
303
- Global,
304
- Untracked,
305
- BranchId,
306
- }
307
-
308
- impl InsertRowLayout {
309
- fn from_values(spec: &EntitySurfaceSpec, values: &BoundInsertValues) -> Result<Self, LixError> {
310
- let mut snapshot_capacity = 0;
311
- let mut seen_columns = std::collections::BTreeSet::new();
312
- let columns = values
313
- .columns
314
- .iter()
315
- .map(|column| {
316
- if !seen_columns.insert(column.name.clone()) {
317
- return Err(LixError::new(
318
- LixError::CODE_UNSUPPORTED_SQL,
319
- format!("duplicate entity INSERT column '{}'", column.name),
320
- ));
321
- }
322
- if let Some(surface_column) = spec.visible_column(&column.name) {
323
- snapshot_capacity += 1;
324
- return Ok(InsertColumnTarget::Visible {
325
- name: surface_column.name.clone(),
326
- column_type: surface_column.column_type,
327
- });
328
- }
329
- Ok(match column.name.as_str() {
330
- "lixcol_entity_pk" => InsertColumnTarget::EntityPk,
331
- "lixcol_file_id" => InsertColumnTarget::FileId,
332
- "lixcol_metadata" => InsertColumnTarget::Metadata,
333
- "lixcol_global" => InsertColumnTarget::Global,
334
- "lixcol_untracked" => InsertColumnTarget::Untracked,
335
- "lixcol_branch_id" => InsertColumnTarget::BranchId,
336
- _ => {
337
- return Err(LixError::new(
338
- LixError::CODE_UNSUPPORTED_SQL,
339
- format!(
340
- "bound entity INSERT does not support column '{}'",
341
- column.name
342
- ),
343
- ));
344
- }
345
- })
346
- })
347
- .collect::<Result<Vec<_>, LixError>>()?;
348
- Ok(Self {
349
- schema_key: spec.schema_key.clone(),
350
- visible_columns: spec.columns.clone(),
351
- snapshot_context: format!("{} insert snapshot_content", spec.schema_key),
352
- snapshot_capacity,
353
- columns,
354
- })
355
- }
356
- }
357
-
358
- fn entity_insert_row(
359
- ctx: &mut dyn SqlWriteExecutionContext,
360
- plan: &LogicalWritePlan,
361
- layout: &InsertRowLayout,
362
- row: &[BoundExpr],
363
- params: &[Value],
364
- active_branch_commit_id: Option<&str>,
365
- ) -> Result<TransactionWriteRow, LixError> {
366
- if row.len() != layout.columns.len() {
367
- return Err(LixError::new(
368
- LixError::CODE_UNSUPPORTED_SQL,
369
- "entity INSERT rows must have a consistent column layout",
370
- ));
371
- }
372
-
373
- let mut snapshot = serde_json::Map::with_capacity(layout.snapshot_capacity);
374
- let mut entity_pk = None;
375
- let mut file_id = None;
376
- let mut metadata = None;
377
- let mut global = None;
378
- let mut untracked = None;
379
- let mut explicit_branch_id = None;
380
- let context = EntityEvalContext::insert(&JsonValue::Null, &layout.visible_columns);
381
-
382
- for (expr, target) in row.iter().zip(layout.columns.iter()) {
383
- if let InsertColumnTarget::Visible { column_type, .. } = target {
384
- reject_direct_blob_json_value(expr, *column_type, params)?;
385
- }
386
- let eval_value = eval_expr_value(expr, &context, ctx, params, active_branch_commit_id)?;
387
- if matches!(target, InsertColumnTarget::Metadata) {
388
- metadata = optional_metadata_from_eval_value(
389
- eval_value,
390
- "lixcol_metadata",
391
- &layout.schema_key,
392
- )?;
393
- continue;
394
- }
395
- match target {
396
- InsertColumnTarget::Visible { name, column_type } => {
397
- snapshot.insert(name.clone(), entity_json_value(eval_value, *column_type)?);
398
- continue;
399
- }
400
- _ => {}
401
- }
402
- let value = eval_value.into_json();
403
- match target {
404
- InsertColumnTarget::Visible { .. } => unreachable!("visible columns handled above"),
405
- InsertColumnTarget::EntityPk => {
406
- entity_pk = Some(entity_pk_from_value(&value, "lixcol_entity_pk")?);
407
- }
408
- InsertColumnTarget::FileId => {
409
- file_id = text_value(value, "lixcol_file_id")?;
410
- }
411
- InsertColumnTarget::Metadata => {
412
- unreachable!("metadata handled before JSON value coercion")
413
- }
414
- InsertColumnTarget::Global => {
415
- global = bool_value(value, "lixcol_global")?;
416
- }
417
- InsertColumnTarget::Untracked => {
418
- untracked = bool_value(value, "lixcol_untracked")?;
419
- }
420
- InsertColumnTarget::BranchId => {
421
- explicit_branch_id = text_value(value, "lixcol_branch_id")?;
422
- }
423
- }
424
- }
425
-
426
- let snapshot = JsonValue::Object(snapshot);
427
- let global = global.unwrap_or(false);
428
- let branch_id = entity_row_branch_id(plan, explicit_branch_id, global)?;
429
- Ok(TransactionWriteRow {
430
- entity_pk,
431
- schema_key: layout.schema_key.clone(),
432
- file_id,
433
- snapshot: Some(TransactionJson::from_value(
434
- snapshot,
435
- &layout.snapshot_context,
436
- )?),
437
- metadata,
438
- origin: None,
439
- created_at: None,
440
- updated_at: None,
441
- global,
442
- change_id: None,
443
- commit_id: None,
444
- untracked: untracked.unwrap_or(false),
445
- branch_id,
446
- })
447
- }
448
-
449
- fn reject_projected_global_write(
450
- plan: &LogicalWritePlan,
451
- row: &crate::live_state::MaterializedLiveStateRow,
452
- action: &str,
453
- ) -> Result<(), LixError> {
454
- let target_is_by_branch = matches!(
455
- &plan.bound.target,
456
- BoundWriteTarget::Entity(EntityWriteSurface::ByBranch { .. })
457
- );
458
- if target_is_by_branch && row.global && row.branch_id != crate::GLOBAL_BRANCH_ID {
459
- return Err(LixError::new(
460
- LixError::CODE_UNSUPPORTED_SQL,
461
- format!(
462
- "{action} through an entity by-branch surface cannot mutate a projected global row"
463
- ),
464
- ));
465
- }
466
- Ok(())
467
- }
468
-
469
- fn entity_replace_row_from_live(
470
- ctx: &mut dyn SqlWriteExecutionContext,
471
- spec: &EntitySurfaceSpec,
472
- row: &crate::live_state::MaterializedLiveStateRow,
473
- snapshot: Option<JsonValue>,
474
- plan: &LogicalWritePlan,
475
- params: &[Value],
476
- active_branch_commit_id: Option<&str>,
477
- ) -> Result<TransactionWriteRow, LixError> {
478
- let metadata = if let Some(expr) = assignment_value(plan, "lixcol_metadata") {
479
- let snapshot_for_eval = candidate_snapshot(row)?.unwrap_or(JsonValue::Null);
480
- let context = EntityEvalContext::live(&snapshot_for_eval, row, spec);
481
- let value = eval_expr_value(expr, &context, ctx, params, active_branch_commit_id)?;
482
- optional_metadata_from_eval_value(value, "lixcol_metadata", &spec.schema_key)?
483
- } else {
484
- inherited_metadata(row, spec)?
485
- };
486
-
487
- Ok(TransactionWriteRow {
488
- entity_pk: Some(row.entity_pk.clone()),
489
- schema_key: spec.schema_key.clone(),
490
- file_id: row.file_id.clone(),
491
- snapshot: snapshot
492
- .map(|snapshot| {
493
- TransactionJson::from_value(
494
- snapshot,
495
- &format!("{} update snapshot_content", spec.schema_key),
496
- )
497
- })
498
- .transpose()?,
499
- metadata,
500
- origin: None,
501
- created_at: None,
502
- updated_at: None,
503
- global: row.global,
504
- change_id: None,
505
- commit_id: None,
506
- untracked: row.untracked,
507
- branch_id: if row.global {
508
- crate::GLOBAL_BRANCH_ID.to_string()
509
- } else {
510
- row.branch_id.clone()
511
- },
512
- })
513
- }
514
-
515
- fn inherited_metadata(
516
- row: &crate::live_state::MaterializedLiveStateRow,
517
- spec: &EntitySurfaceSpec,
518
- ) -> Result<Option<TransactionJson>, LixError> {
519
- row.metadata
520
- .as_ref()
521
- .map(|metadata| {
522
- let metadata = parse_row_metadata_value(metadata, &spec.schema_key)?;
523
- TransactionJson::from_value(metadata, &format!("{} metadata", spec.schema_key))
524
- })
525
- .transpose()
526
- }
527
-
528
- struct EntityEvalContext<'a> {
529
- snapshot: &'a JsonValue,
530
- row: Option<&'a crate::live_state::MaterializedLiveStateRow>,
531
- visible_columns: &'a [EntitySurfaceColumn],
532
- }
533
-
534
- impl<'a> EntityEvalContext<'a> {
535
- fn insert(snapshot: &'a JsonValue, visible_columns: &'a [EntitySurfaceColumn]) -> Self {
536
- Self {
537
- snapshot,
538
- row: None,
539
- visible_columns,
540
- }
541
- }
542
-
543
- fn live(
544
- snapshot: &'a JsonValue,
545
- row: &'a crate::live_state::MaterializedLiveStateRow,
546
- spec: &'a EntitySurfaceSpec,
547
- ) -> Self {
548
- Self {
549
- snapshot,
550
- row: Some(row),
551
- visible_columns: &spec.columns,
552
- }
553
- }
554
- }
555
-
556
- fn entity_spec(
557
- ctx: &dyn SqlWriteExecutionContext,
558
- schema_key: &str,
559
- ) -> Result<EntitySurfaceSpec, LixError> {
560
- ctx.list_visible_schemas()?
561
- .into_iter()
562
- .filter_map(|schema| derive_entity_surface_spec_from_schema(&schema).ok())
563
- .find(|spec| spec.schema_key == schema_key)
564
- .ok_or_else(|| {
565
- LixError::new(
566
- LixError::CODE_SCHEMA_DEFINITION,
567
- format!("entity surface '{schema_key}' is not visible"),
568
- )
569
- })
570
- }
571
-
572
- #[derive(Clone, Debug)]
573
- enum EntityEvalValue {
574
- SqlNull,
575
- SqlText(String),
576
- Json(JsonValue),
577
- }
578
-
579
- impl EntityEvalValue {
580
- fn into_json(self) -> JsonValue {
581
- match self {
582
- Self::SqlNull => JsonValue::Null,
583
- Self::SqlText(value) => JsonValue::String(value),
584
- Self::Json(value) => value,
585
- }
586
- }
587
- }
588
-
589
- fn eval_expr(
590
- expr: &BoundExpr,
591
- context: &EntityEvalContext<'_>,
592
- ctx: &mut dyn SqlWriteExecutionContext,
593
- params: &[Value],
594
- active_branch_commit_id: Option<&str>,
595
- ) -> Result<JsonValue, LixError> {
596
- eval_expr_value(expr, context, ctx, params, active_branch_commit_id)
597
- .map(EntityEvalValue::into_json)
598
- }
599
-
600
- fn eval_expr_value(
601
- expr: &BoundExpr,
602
- context: &EntityEvalContext<'_>,
603
- ctx: &mut dyn SqlWriteExecutionContext,
604
- params: &[Value],
605
- active_branch_commit_id: Option<&str>,
606
- ) -> Result<EntityEvalValue, LixError> {
607
- match expr {
608
- BoundExpr::Literal(BoundLiteral::Null) => Ok(EntityEvalValue::SqlNull),
609
- BoundExpr::Literal(BoundLiteral::Text(value)) => {
610
- Ok(EntityEvalValue::SqlText(value.clone()))
611
- }
612
- BoundExpr::Literal(literal) => Ok(EntityEvalValue::Json(literal_json(literal))),
613
- BoundExpr::Param(param) => params
614
- .get(param.index.saturating_sub(1))
615
- .map(value_eval)
616
- .ok_or_else(|| {
617
- LixError::new(
618
- LixError::CODE_INVALID_PARAM,
619
- format!("missing SQL parameter ${}", param.index),
620
- )
621
- }),
622
- BoundExpr::Column(column) => column_eval_value(context, &column.name),
623
- BoundExpr::Function { name, args } if name == "lix_json" && args.len() == 1 => {
624
- let raw = eval_expr_value(&args[0], context, ctx, params, active_branch_commit_id)?;
625
- let raw = match raw {
626
- EntityEvalValue::SqlNull => return Ok(EntityEvalValue::Json(JsonValue::Null)),
627
- EntityEvalValue::SqlText(value) => JsonValue::String(value),
628
- EntityEvalValue::Json(value) => value,
629
- };
630
- let JsonValue::String(raw) = raw else {
631
- return Err(LixError::new(
632
- LixError::CODE_TYPE_MISMATCH,
633
- "lix_json expects a text argument",
634
- ));
635
- };
636
- serde_json::from_str(&raw)
637
- .map_err(|error| {
638
- LixError::new(
639
- LixError::CODE_TYPE_MISMATCH,
640
- format!("lix_json argument is not valid JSON: {error}"),
641
- )
642
- })
643
- .map(EntityEvalValue::Json)
644
- }
645
- BoundExpr::Function { name, args } if name == "lix_uuid_v7" && args.is_empty() => {
646
- Ok(EntityEvalValue::SqlText(ctx.functions().call_uuid_v7()))
647
- }
648
- BoundExpr::Function { name, args } if name == "lix_timestamp" && args.is_empty() => {
649
- Ok(EntityEvalValue::SqlText(ctx.functions().call_timestamp()))
650
- }
651
- BoundExpr::Function { name, args } if name == "lix_empty_blob" && args.is_empty() => {
652
- Ok(EntityEvalValue::Json(JsonValue::Array(Vec::new())))
653
- }
654
- BoundExpr::Function { name, args }
655
- if name == "lix_active_branch_commit_id" && args.is_empty() =>
656
- {
657
- Ok(active_branch_commit_id
658
- .map(|commit_id| EntityEvalValue::SqlText(commit_id.to_string()))
659
- .unwrap_or(EntityEvalValue::SqlNull))
660
- }
661
- BoundExpr::Function { name, args }
662
- if (name == "lix_json_get" || name == "lix_json_get_text") && args.len() >= 2 =>
663
- {
664
- let root = eval_expr_value(&args[0], context, ctx, params, active_branch_commit_id)?;
665
- let mut current = match root {
666
- EntityEvalValue::SqlNull => return Ok(EntityEvalValue::SqlNull),
667
- EntityEvalValue::SqlText(raw) => {
668
- serde_json::from_str::<JsonValue>(&raw).map_err(|error| {
669
- LixError::new(
670
- LixError::CODE_TYPE_MISMATCH,
671
- format!(
672
- "{name} expected valid JSON text in its first argument: {error}"
673
- ),
674
- )
675
- })?
676
- }
677
- EntityEvalValue::Json(root) => match root {
678
- JsonValue::Null => return Ok(EntityEvalValue::SqlNull),
679
- value => value,
680
- },
681
- };
682
- for arg in &args[1..] {
683
- let segment = eval_expr(arg, context, ctx, params, active_branch_commit_id)?;
684
- let Some(next) = json_path_get(&current, &segment, name)? else {
685
- return Ok(EntityEvalValue::SqlNull);
686
- };
687
- current = next;
688
- }
689
- if name == "lix_json_get_text" {
690
- if current.is_null() {
691
- return Ok(EntityEvalValue::SqlNull);
692
- }
693
- Ok(EntityEvalValue::SqlText(json_text_value(&current)?))
694
- } else {
695
- Ok(EntityEvalValue::Json(current))
696
- }
697
- }
698
- BoundExpr::Function { name, args }
699
- if (name == "lix_text_encode" || name == "lix_text_decode")
700
- && (1..=2).contains(&args.len()) =>
701
- {
702
- if args.len() == 2 {
703
- validate_utf8_encoding(
704
- eval_expr(&args[1], context, ctx, params, active_branch_commit_id)?,
705
- name,
706
- )?;
707
- }
708
- let value = eval_expr(&args[0], context, ctx, params, active_branch_commit_id)?;
709
- if name == "lix_text_encode" {
710
- Ok(EntityEvalValue::Json(JsonValue::Array(
711
- text_like_bytes(&value, name)?
712
- .into_iter()
713
- .map(JsonValue::from)
714
- .collect(),
715
- )))
716
- } else {
717
- let bytes = binary_like_bytes(&value, name)?;
718
- String::from_utf8(bytes)
719
- .map_err(|error| {
720
- LixError::new(
721
- LixError::CODE_TYPE_MISMATCH,
722
- format!("lix_text_decode() expected valid UTF8 bytes: {error}"),
723
- )
724
- })
725
- .map(EntityEvalValue::SqlText)
726
- }
727
- }
728
- BoundExpr::Function { name, .. } => Err(LixError::new(
729
- LixError::CODE_UNSUPPORTED_SQL,
730
- format!("bound entity write does not support function '{name}' yet"),
731
- )),
732
- }
733
- }
734
-
735
- fn predicate_matches(
736
- predicate: &crate::sql2::plan::predicate::BoundPredicate,
737
- context: &EntityEvalContext<'_>,
738
- spec: &EntitySurfaceSpec,
739
- ctx: &mut dyn SqlWriteExecutionContext,
740
- params: &[Value],
741
- active_branch_commit_id: Option<&str>,
742
- ) -> Result<bool, LixError> {
743
- use crate::sql2::plan::predicate::BoundPredicate;
744
- match predicate {
745
- BoundPredicate::True => Ok(true),
746
- BoundPredicate::False => Ok(false),
747
- BoundPredicate::And(predicates) => {
748
- for predicate in predicates {
749
- if !predicate_matches(
750
- predicate,
751
- context,
752
- spec,
753
- ctx,
754
- params,
755
- active_branch_commit_id,
756
- )? {
757
- return Ok(false);
758
- }
759
- }
760
- Ok(true)
761
- }
762
- BoundPredicate::Or(predicates) => {
763
- for predicate in predicates {
764
- if predicate_matches(
765
- predicate,
766
- context,
767
- spec,
768
- ctx,
769
- params,
770
- active_branch_commit_id,
771
- )? {
772
- return Ok(true);
773
- }
774
- }
775
- Ok(false)
776
- }
777
- BoundPredicate::Eq(left, right) => {
778
- let (left, right) = eval_comparison_operands(
779
- left,
780
- right,
781
- context,
782
- spec,
783
- ctx,
784
- params,
785
- active_branch_commit_id,
786
- )?;
787
- Ok(!left.is_null() && !right.is_null() && left == right)
788
- }
789
- BoundPredicate::IsNull(expr) => {
790
- let value = eval_expr(expr, context, ctx, params, active_branch_commit_id)?;
791
- Ok(value.is_null())
792
- }
793
- BoundPredicate::IsNotNull(expr) => {
794
- let value = eval_expr(expr, context, ctx, params, active_branch_commit_id)?;
795
- Ok(!value.is_null())
796
- }
797
- BoundPredicate::In { expr, values } => {
798
- let candidate = eval_expr(expr, context, ctx, params, active_branch_commit_id)?;
799
- if candidate.is_null() {
800
- return Ok(false);
801
- }
802
- for value_expr in values {
803
- let value = eval_expr(value_expr, context, ctx, params, active_branch_commit_id)?;
804
- let (candidate, value) = normalize_comparison_operands(
805
- expr,
806
- candidate.clone(),
807
- value_expr,
808
- value,
809
- spec,
810
- )?;
811
- if !value.is_null() && candidate == value {
812
- return Ok(true);
813
- }
814
- }
815
- Ok(false)
816
- }
817
- }
818
- }
819
-
820
- fn eval_comparison_operands(
821
- left: &BoundExpr,
822
- right: &BoundExpr,
823
- context: &EntityEvalContext<'_>,
824
- spec: &EntitySurfaceSpec,
825
- ctx: &mut dyn SqlWriteExecutionContext,
826
- params: &[Value],
827
- active_branch_commit_id: Option<&str>,
828
- ) -> Result<(JsonValue, JsonValue), LixError> {
829
- let left_value = eval_expr(left, context, ctx, params, active_branch_commit_id)?;
830
- let right_value = eval_expr(right, context, ctx, params, active_branch_commit_id)?;
831
- normalize_comparison_operands(left, left_value, right, right_value, spec)
832
- }
833
-
834
- fn normalize_comparison_operands(
835
- left_expr: &BoundExpr,
836
- left_value: JsonValue,
837
- right_expr: &BoundExpr,
838
- right_value: JsonValue,
839
- spec: &EntitySurfaceSpec,
840
- ) -> Result<(JsonValue, JsonValue), LixError> {
841
- let left_is_json = bound_expr_is_json(left_expr, spec);
842
- let right_is_json = bound_expr_is_json(right_expr, spec);
843
- Ok((
844
- normalize_json_comparison_value(
845
- left_expr,
846
- left_value,
847
- right_is_json,
848
- is_identity_json_expr(right_expr),
849
- )?,
850
- normalize_json_comparison_value(
851
- right_expr,
852
- right_value,
853
- left_is_json,
854
- is_identity_json_expr(left_expr),
855
- )?,
856
- ))
857
- }
858
-
859
- fn normalize_json_comparison_value(
860
- expr: &BoundExpr,
861
- value: JsonValue,
862
- other_side_is_json: bool,
863
- other_side_is_identity_json: bool,
864
- ) -> Result<JsonValue, LixError> {
865
- if !other_side_is_json {
866
- return Ok(value);
867
- }
868
- let should_parse = matches!(expr, BoundExpr::Param(_))
869
- || (other_side_is_identity_json
870
- && matches!(expr, BoundExpr::Literal(BoundLiteral::Text(_))));
871
- if !should_parse {
872
- return Ok(value);
873
- }
874
- let JsonValue::String(raw) = value else {
875
- return Ok(value);
876
- };
877
- serde_json::from_str(&raw).map_err(|error| {
878
- LixError::new(
879
- LixError::CODE_TYPE_MISMATCH,
880
- format!("JSON comparison parameter is not valid JSON: {error}"),
881
- )
882
- })
883
- }
884
-
885
- fn validate_bound_write_supported(
886
- plan: &LogicalWritePlan,
887
- spec: &EntitySurfaceSpec,
888
- ) -> Result<(), LixError> {
889
- validate_predicate_supported(&plan.bound.predicate)?;
890
- validate_json_predicate_types(&plan.bound.predicate, spec)?;
891
- match &plan.bound.input {
892
- BoundWriteInput::Values(values) => {
893
- for row in &values.rows {
894
- for expr in row {
895
- validate_expr_supported(expr)?;
896
- }
897
- }
898
- }
899
- BoundWriteInput::Query { .. } | BoundWriteInput::None => {}
900
- }
901
- for assignment in &plan.bound.assignments {
902
- validate_expr_supported(&assignment.value)?;
903
- }
904
- Ok(())
905
- }
906
-
907
- fn bound_public_write_shape_supported(plan: &LogicalWritePlan) -> bool {
908
- let input_supported = match (&plan.bound.op, &plan.bound.input) {
909
- (BoundWriteOp::Insert, BoundWriteInput::Values(values)) => values
910
- .rows
911
- .iter()
912
- .flatten()
913
- .all(|expr| validate_expr_supported(expr).is_ok()),
914
- (BoundWriteOp::Update | BoundWriteOp::Delete, BoundWriteInput::None) => true,
915
- _ => false,
916
- };
917
- input_supported
918
- && validate_predicate_supported(&plan.bound.predicate).is_ok()
919
- && plan
920
- .bound
921
- .assignments
922
- .iter()
923
- .all(|assignment| validate_expr_supported(&assignment.value).is_ok())
924
- }
925
-
926
- fn validate_predicate_supported(
927
- predicate: &crate::sql2::plan::predicate::BoundPredicate,
928
- ) -> Result<(), LixError> {
929
- use crate::sql2::plan::predicate::BoundPredicate;
930
- match predicate {
931
- BoundPredicate::True | BoundPredicate::False => Ok(()),
932
- BoundPredicate::And(predicates) => {
933
- for predicate in predicates {
934
- validate_predicate_supported(predicate)?;
935
- }
936
- Ok(())
937
- }
938
- BoundPredicate::Or(predicates) => {
939
- for predicate in predicates {
940
- validate_predicate_supported(predicate)?;
941
- }
942
- Ok(())
943
- }
944
- BoundPredicate::Eq(left, right) => {
945
- validate_expr_supported(left)?;
946
- validate_expr_supported(right)
947
- }
948
- BoundPredicate::IsNull(expr) | BoundPredicate::IsNotNull(expr) => {
949
- validate_expr_supported(expr)
950
- }
951
- BoundPredicate::In { expr, values } => {
952
- validate_expr_supported(expr)?;
953
- for value in values {
954
- validate_expr_supported(value)?;
955
- }
956
- Ok(())
957
- }
958
- }
959
- }
960
-
961
- fn validate_json_predicate_types(
962
- predicate: &crate::sql2::plan::predicate::BoundPredicate,
963
- spec: &EntitySurfaceSpec,
964
- ) -> Result<(), LixError> {
965
- use crate::sql2::plan::predicate::BoundPredicate;
966
- match predicate {
967
- BoundPredicate::True | BoundPredicate::False => Ok(()),
968
- BoundPredicate::And(predicates) => {
969
- for predicate in predicates {
970
- validate_json_predicate_types(predicate, spec)?;
971
- }
972
- Ok(())
973
- }
974
- BoundPredicate::Or(predicates) => {
975
- for predicate in predicates {
976
- validate_json_predicate_types(predicate, spec)?;
977
- }
978
- Ok(())
979
- }
980
- BoundPredicate::Eq(left, right) => validate_json_comparison_operands(left, right, spec),
981
- BoundPredicate::IsNull(_) | BoundPredicate::IsNotNull(_) => Ok(()),
982
- BoundPredicate::In { expr, values } => {
983
- if bound_expr_is_json(expr, spec) {
984
- for value in values {
985
- if is_identity_json_expr(expr) && is_parseable_json_text_literal(value) {
986
- continue;
987
- }
988
- require_json_comparison_operand(value, spec)?;
989
- }
990
- }
991
- for value in values {
992
- if bound_expr_is_json(value, spec) {
993
- if is_identity_json_expr(value) && is_parseable_json_text_literal(expr) {
994
- continue;
995
- }
996
- require_json_comparison_operand(expr, spec)?;
997
- }
998
- }
999
- Ok(())
1000
- }
1001
- }
1002
- }
1003
-
1004
- fn validate_json_comparison_operands(
1005
- left: &BoundExpr,
1006
- right: &BoundExpr,
1007
- spec: &EntitySurfaceSpec,
1008
- ) -> Result<(), LixError> {
1009
- if bound_expr_is_json(left, spec) {
1010
- if is_identity_json_expr(left) && is_parseable_json_text_literal(right) {
1011
- return Ok(());
1012
- }
1013
- require_json_comparison_operand(right, spec)?;
1014
- }
1015
- if bound_expr_is_json(right, spec) {
1016
- if is_identity_json_expr(right) && is_parseable_json_text_literal(left) {
1017
- return Ok(());
1018
- }
1019
- require_json_comparison_operand(left, spec)?;
1020
- }
1021
- Ok(())
1022
- }
1023
-
1024
- fn require_json_comparison_operand(
1025
- expr: &BoundExpr,
1026
- spec: &EntitySurfaceSpec,
1027
- ) -> Result<(), LixError> {
1028
- if bound_expr_is_json(expr, spec)
1029
- || matches!(expr, BoundExpr::Param(_))
1030
- || matches!(expr, BoundExpr::Literal(BoundLiteral::Null))
1031
- {
1032
- return Ok(());
1033
- }
1034
- Err(LixError::new(
1035
- LixError::CODE_TYPE_MISMATCH,
1036
- "JSON columns can only be compared with JSON expressions",
1037
- )
1038
- .with_hint("Wrap JSON text with lix_json(...), use lix_json_get(...) for JSON values, or use IS NULL for null checks."))
1039
- }
1040
-
1041
- fn is_identity_json_expr(expr: &BoundExpr) -> bool {
1042
- matches!(
1043
- expr,
1044
- BoundExpr::Column(column)
1045
- if matches!(column.name.as_str(), "entity_pk" | "lixcol_entity_pk")
1046
- )
1047
- }
1048
-
1049
- fn is_parseable_json_text_literal(expr: &BoundExpr) -> bool {
1050
- match expr {
1051
- BoundExpr::Literal(BoundLiteral::Text(value)) => {
1052
- serde_json::from_str::<JsonValue>(value).is_ok()
1053
- }
1054
- _ => false,
1055
- }
1056
- }
1057
-
1058
- fn bound_expr_is_json(expr: &BoundExpr, spec: &EntitySurfaceSpec) -> bool {
1059
- match expr {
1060
- BoundExpr::Column(column) => {
1061
- spec.visible_column(&column.name)
1062
- .is_some_and(|column| column.column_type == EntityColumnType::Json)
1063
- || matches!(
1064
- column.name.as_str(),
1065
- "lixcol_entity_pk" | "lixcol_metadata" | "lixcol_snapshot_content"
1066
- )
1067
- }
1068
- BoundExpr::Literal(BoundLiteral::Json(_)) => true,
1069
- BoundExpr::Function { name, .. } => matches!(name.as_str(), "lix_json" | "lix_json_get"),
1070
- _ => false,
1071
- }
1072
- }
1073
-
1074
- fn validate_expr_supported(expr: &BoundExpr) -> Result<(), LixError> {
1075
- match expr {
1076
- BoundExpr::Column(_) | BoundExpr::Param(_) | BoundExpr::Literal(_) => Ok(()),
1077
- BoundExpr::Function { name, args } => {
1078
- match name.as_str() {
1079
- "lix_json" if args.len() == 1 => {}
1080
- "lix_empty_blob"
1081
- | "lix_uuid_v7"
1082
- | "lix_timestamp"
1083
- | "lix_active_branch_commit_id"
1084
- if args.is_empty() => {}
1085
- "lix_json_get" | "lix_json_get_text" if args.len() >= 2 => {}
1086
- "lix_text_encode" | "lix_text_decode" if (1..=2).contains(&args.len()) => {}
1087
- _ => {
1088
- return Err(LixError::new(
1089
- LixError::CODE_UNSUPPORTED_SQL,
1090
- format!("bound entity write does not support function '{name}' yet"),
1091
- ));
1092
- }
1093
- }
1094
- for arg in args {
1095
- validate_expr_supported(arg)?;
1096
- }
1097
- Ok(())
1098
- }
1099
- }
1100
- }
1101
-
1102
- fn candidate_snapshot(
1103
- row: &crate::live_state::MaterializedLiveStateRow,
1104
- ) -> Result<Option<JsonValue>, LixError> {
1105
- row.snapshot_content
1106
- .as_deref()
1107
- .map(|snapshot| {
1108
- serde_json::from_str(snapshot).map_err(|error| {
1109
- LixError::new(
1110
- LixError::CODE_TYPE_MISMATCH,
1111
- format!("entity row snapshot_content is not valid JSON: {error}"),
1112
- )
1113
- })
1114
- })
1115
- .transpose()
1116
- }
1117
-
1118
- fn entity_json_value(
1119
- value: EntityEvalValue,
1120
- column_type: EntityColumnType,
1121
- ) -> Result<JsonValue, LixError> {
1122
- Ok(match (value, column_type) {
1123
- (EntityEvalValue::SqlNull, _) => JsonValue::Null,
1124
- (EntityEvalValue::SqlText(value), EntityColumnType::Json) => {
1125
- serde_json::from_str(&value).unwrap_or(JsonValue::String(value))
1126
- }
1127
- (EntityEvalValue::SqlText(value), _) => JsonValue::String(value),
1128
- (EntityEvalValue::Json(value), EntityColumnType::Json) => value,
1129
- (EntityEvalValue::Json(JsonValue::String(value)), EntityColumnType::String) => {
1130
- JsonValue::String(value)
1131
- }
1132
- (EntityEvalValue::Json(JsonValue::Number(value)), EntityColumnType::Integer)
1133
- if value.is_i64() =>
1134
- {
1135
- JsonValue::Number(value)
1136
- }
1137
- (
1138
- EntityEvalValue::Json(JsonValue::Number(value)),
1139
- EntityColumnType::Number | EntityColumnType::Integer,
1140
- ) => JsonValue::Number(value),
1141
- (EntityEvalValue::Json(JsonValue::Bool(value)), EntityColumnType::Boolean) => {
1142
- JsonValue::Bool(value)
1143
- }
1144
- (EntityEvalValue::Json(value), _) => value,
1145
- })
1146
- }
1147
-
1148
- fn reject_direct_blob_json_value(
1149
- expr: &BoundExpr,
1150
- column_type: EntityColumnType,
1151
- params: &[Value],
1152
- ) -> Result<(), LixError> {
1153
- if column_type != EntityColumnType::Json {
1154
- return Ok(());
1155
- }
1156
- let is_blob = match expr {
1157
- BoundExpr::Literal(BoundLiteral::Blob(_)) => true,
1158
- BoundExpr::Param(param) => params
1159
- .get(param.index.saturating_sub(1))
1160
- .is_some_and(|value| matches!(value, Value::Blob(_))),
1161
- _ => false,
1162
- };
1163
- if is_blob {
1164
- return Err(LixError::new(
1165
- LixError::CODE_INVALID_PARAM,
1166
- "cannot store blob values directly in JSON entity columns",
1167
- ));
1168
- }
1169
- Ok(())
1170
- }
1171
-
1172
- fn literal_json(literal: &BoundLiteral) -> JsonValue {
1173
- match literal {
1174
- BoundLiteral::Null => JsonValue::Null,
1175
- BoundLiteral::Bool(value) => JsonValue::Bool(*value),
1176
- BoundLiteral::Integer(value) => JsonValue::from(*value),
1177
- BoundLiteral::Text(value) => JsonValue::String(value.clone()),
1178
- BoundLiteral::Json(value) => value.clone(),
1179
- BoundLiteral::Blob(value) => {
1180
- JsonValue::Array(value.iter().copied().map(JsonValue::from).collect())
1181
- }
1182
- }
1183
- }
1184
-
1185
- fn value_eval(value: &Value) -> EntityEvalValue {
1186
- match value {
1187
- Value::Null => EntityEvalValue::SqlNull,
1188
- Value::Text(value) => EntityEvalValue::SqlText(value.clone()),
1189
- _ => EntityEvalValue::Json(value_json(value)),
1190
- }
1191
- }
1192
-
1193
- fn value_json(value: &Value) -> JsonValue {
1194
- match value {
1195
- Value::Null => JsonValue::Null,
1196
- Value::Boolean(value) => JsonValue::Bool(*value),
1197
- Value::Integer(value) => JsonValue::from(*value),
1198
- Value::Real(value) => serde_json::Number::from_f64(*value)
1199
- .map(JsonValue::Number)
1200
- .unwrap_or(JsonValue::Null),
1201
- Value::Text(value) => JsonValue::String(value.clone()),
1202
- Value::Json(value) => value.clone(),
1203
- Value::Blob(value) => {
1204
- JsonValue::Array(value.iter().copied().map(JsonValue::from).collect())
1205
- }
1206
- }
1207
- }
1208
-
1209
- fn json_path_get(
1210
- value: &JsonValue,
1211
- segment: &JsonValue,
1212
- fn_name: &str,
1213
- ) -> Result<Option<JsonValue>, LixError> {
1214
- match segment {
1215
- JsonValue::String(key) => {
1216
- if key == "$" || key.starts_with("$.") || key.starts_with("$[") || key.starts_with('/')
1217
- {
1218
- return Err(LixError::new(
1219
- LixError::CODE_TYPE_MISMATCH,
1220
- format!(
1221
- "{fn_name}() uses variadic path segments, not JSONPath or JSON Pointer; got '{key}'"
1222
- ),
1223
- ));
1224
- }
1225
- Ok(value.get(key).cloned())
1226
- }
1227
- JsonValue::Number(number) => {
1228
- let Some(index) = number
1229
- .as_u64()
1230
- .and_then(|value| usize::try_from(value).ok())
1231
- else {
1232
- return Err(LixError::new(
1233
- LixError::CODE_TYPE_MISMATCH,
1234
- format!("{fn_name}() path indexes must be non-negative integers"),
1235
- ));
1236
- };
1237
- Ok(value
1238
- .as_array()
1239
- .and_then(|values| values.get(index))
1240
- .cloned())
1241
- }
1242
- JsonValue::Null => Ok(None),
1243
- other => Err(LixError::new(
1244
- LixError::CODE_TYPE_MISMATCH,
1245
- format!(
1246
- "{fn_name}() path arguments must be strings or non-negative integers, got {other}"
1247
- ),
1248
- )),
1249
- }
1250
- }
1251
-
1252
- fn json_text_value(value: &JsonValue) -> Result<String, LixError> {
1253
- match value {
1254
- JsonValue::String(text) => Ok(text.clone()),
1255
- JsonValue::Number(number) => Ok(number.to_string()),
1256
- JsonValue::Bool(boolean) => Ok(boolean.to_string()),
1257
- JsonValue::Array(_) | JsonValue::Object(_) => {
1258
- serde_json::to_string(value).map_err(|error| {
1259
- LixError::new(
1260
- LixError::CODE_TYPE_MISMATCH,
1261
- format!("lix_json_get_text() could not render JSON value: {error}"),
1262
- )
1263
- })
1264
- }
1265
- JsonValue::Null => Ok("null".to_string()),
1266
- }
1267
- }
1268
-
1269
- fn validate_utf8_encoding(value: JsonValue, fn_name: &str) -> Result<(), LixError> {
1270
- let value = json_text_value(&value)?;
1271
- let normalized = value.trim().to_ascii_uppercase().replace('-', "");
1272
- if normalized == "UTF8" {
1273
- Ok(())
1274
- } else {
1275
- Err(LixError::new(
1276
- LixError::CODE_TYPE_MISMATCH,
1277
- format!("{fn_name}() only supports UTF8 encoding, got '{value}'"),
1278
- ))
1279
- }
1280
- }
1281
-
1282
- fn text_like_bytes(value: &JsonValue, fn_name: &str) -> Result<Vec<u8>, LixError> {
1283
- Ok(match value {
1284
- JsonValue::String(value) => value.as_bytes().to_vec(),
1285
- JsonValue::Number(value) => value.to_string().into_bytes(),
1286
- JsonValue::Bool(value) => value.to_string().into_bytes(),
1287
- JsonValue::Array(values) => values
1288
- .iter()
1289
- .map(byte_from_json_value)
1290
- .collect::<Result<Vec<_>, _>>()?,
1291
- JsonValue::Null => Vec::new(),
1292
- other => {
1293
- return Err(LixError::new(
1294
- LixError::CODE_TYPE_MISMATCH,
1295
- format!("{fn_name}() expected text or binary-compatible input, got {other}"),
1296
- ));
1297
- }
1298
- })
1299
- }
1300
-
1301
- fn binary_like_bytes(value: &JsonValue, fn_name: &str) -> Result<Vec<u8>, LixError> {
1302
- match value {
1303
- JsonValue::Array(values) => values.iter().map(byte_from_json_value).collect(),
1304
- JsonValue::String(value) => Ok(value.as_bytes().to_vec()),
1305
- JsonValue::Null => Ok(Vec::new()),
1306
- other => Err(LixError::new(
1307
- LixError::CODE_TYPE_MISMATCH,
1308
- format!("{fn_name}() expected binary or text-compatible input, got {other}"),
1309
- )),
1310
- }
1311
- }
1312
-
1313
- fn byte_from_json_value(value: &JsonValue) -> Result<u8, LixError> {
1314
- value
1315
- .as_u64()
1316
- .and_then(|value| u8::try_from(value).ok())
1317
- .ok_or_else(|| {
1318
- LixError::new(
1319
- LixError::CODE_TYPE_MISMATCH,
1320
- format!("binary value must contain integer bytes, got {value}"),
1321
- )
1322
- })
1323
- }
1324
-
1325
- fn column_eval_value(
1326
- context: &EntityEvalContext<'_>,
1327
- column_name: &str,
1328
- ) -> Result<EntityEvalValue, LixError> {
1329
- if let Some(value) = context.snapshot.get(column_name) {
1330
- return Ok(visible_column_eval_value(
1331
- context
1332
- .visible_columns
1333
- .iter()
1334
- .find(|column| column.name == column_name),
1335
- value,
1336
- ));
1337
- }
1338
- let Some(row) = context.row else {
1339
- return Ok(EntityEvalValue::SqlNull);
1340
- };
1341
- match column_name {
1342
- "lixcol_entity_pk" => row
1343
- .entity_pk
1344
- .as_json_array_value()
1345
- .map(EntityEvalValue::Json),
1346
- "lixcol_schema_key" => Ok(EntityEvalValue::Json(JsonValue::String(
1347
- row.schema_key.clone(),
1348
- ))),
1349
- "lixcol_file_id" => Ok(row
1350
- .file_id
1351
- .as_ref()
1352
- .map(|value| EntityEvalValue::Json(JsonValue::String(value.clone())))
1353
- .unwrap_or(EntityEvalValue::SqlNull)),
1354
- "lixcol_metadata" => row
1355
- .metadata
1356
- .as_ref()
1357
- .map(|metadata| parse_row_metadata_value(metadata, &row.schema_key))
1358
- .transpose()
1359
- .map(|metadata| {
1360
- metadata
1361
- .map(EntityEvalValue::Json)
1362
- .unwrap_or(EntityEvalValue::SqlNull)
1363
- }),
1364
- "lixcol_change_id" => Ok(row
1365
- .change_id
1366
- .as_ref()
1367
- .map(|value| EntityEvalValue::Json(JsonValue::String(value.clone())))
1368
- .unwrap_or(EntityEvalValue::SqlNull)),
1369
- "lixcol_created_at" => Ok(EntityEvalValue::Json(JsonValue::String(
1370
- row.created_at.clone(),
1371
- ))),
1372
- "lixcol_updated_at" => Ok(EntityEvalValue::Json(JsonValue::String(
1373
- row.updated_at.clone(),
1374
- ))),
1375
- "lixcol_commit_id" => Ok(row
1376
- .commit_id
1377
- .as_ref()
1378
- .map(|value| EntityEvalValue::Json(JsonValue::String(value.clone())))
1379
- .unwrap_or(EntityEvalValue::SqlNull)),
1380
- "lixcol_global" => Ok(EntityEvalValue::Json(JsonValue::Bool(row.global))),
1381
- "lixcol_untracked" => Ok(EntityEvalValue::Json(JsonValue::Bool(row.untracked))),
1382
- "lixcol_branch_id" => Ok(EntityEvalValue::Json(JsonValue::String(
1383
- row.branch_id.clone(),
1384
- ))),
1385
- _ => Ok(EntityEvalValue::SqlNull),
1386
- }
1387
- }
1388
-
1389
- fn visible_column_eval_value(
1390
- column: Option<&EntitySurfaceColumn>,
1391
- value: &JsonValue,
1392
- ) -> EntityEvalValue {
1393
- match (column.map(|column| column.column_type), value) {
1394
- (Some(EntityColumnType::String), JsonValue::String(value)) => {
1395
- EntityEvalValue::SqlText(value.clone())
1396
- }
1397
- _ => EntityEvalValue::Json(value.clone()),
1398
- }
1399
- }
1400
-
1401
- fn scan_branch_ids(scope: &BranchScope) -> Result<Vec<String>, LixError> {
1402
- Ok(match scope {
1403
- BranchScope::Active { branch_id } => vec![branch_id.clone()],
1404
- BranchScope::Explicit { branch_ids } | BranchScope::ExplicitRequired { branch_ids } => {
1405
- branch_ids.iter().cloned().collect()
1406
- }
1407
- BranchScope::ExplicitDynamic { .. } | BranchScope::ExplicitRequiredDynamic { .. } => {
1408
- return Err(LixError::new(
1409
- LixError::CODE_INVALID_PARAM,
1410
- "parameterized branch scope was not resolved before write execution",
1411
- ));
1412
- }
1413
- BranchScope::Global => vec![crate::GLOBAL_BRANCH_ID.to_string()],
1414
- BranchScope::Empty => Vec::new(),
1415
- })
1416
- }
1417
-
1418
- fn entity_row_branch_id(
1419
- plan: &LogicalWritePlan,
1420
- explicit_branch_id: Option<String>,
1421
- global: bool,
1422
- ) -> Result<String, LixError> {
1423
- if global {
1424
- let target_branch_ids = insert_target_branch_ids(&plan.bound.branch_scope);
1425
- let target_is_by_branch = matches!(
1426
- &plan.bound.target,
1427
- BoundWriteTarget::Entity(EntityWriteSurface::ByBranch { .. })
1428
- );
1429
- if explicit_branch_id
1430
- .as_deref()
1431
- .is_some_and(|branch_id| branch_id != crate::GLOBAL_BRANCH_ID)
1432
- {
1433
- return Err(LixError::new(
1434
- LixError::CODE_TYPE_MISMATCH,
1435
- "entity INSERT cannot combine lixcol_global = true with a non-global lixcol_branch_id",
1436
- ));
1437
- }
1438
- if target_is_by_branch
1439
- && target_branch_ids.iter().any(|branch_ids| {
1440
- !branch_ids
1441
- .iter()
1442
- .any(|branch_id| branch_id == crate::GLOBAL_BRANCH_ID)
1443
- })
1444
- {
1445
- return Err(LixError::new(
1446
- LixError::CODE_TYPE_MISMATCH,
1447
- "entity INSERT cannot combine lixcol_global = true with a non-global target branch",
1448
- ));
1449
- }
1450
- return Ok(crate::GLOBAL_BRANCH_ID.to_string());
1451
- }
1452
- if explicit_branch_id.as_deref() == Some(crate::GLOBAL_BRANCH_ID) {
1453
- return Err(LixError::new(
1454
- LixError::CODE_TYPE_MISMATCH,
1455
- "entity INSERT with lixcol_branch_id = 'global' must also set lixcol_global = true",
1456
- ));
1457
- }
1458
- let target_is_by_branch = matches!(
1459
- &plan.bound.target,
1460
- BoundWriteTarget::Entity(EntityWriteSurface::ByBranch { .. })
1461
- );
1462
- if target_is_by_branch && matches!(plan.bound.branch_scope, BranchScope::Global) {
1463
- return Err(LixError::new(
1464
- LixError::CODE_TYPE_MISMATCH,
1465
- "entity INSERT into the global scope must set lixcol_global = true",
1466
- ));
1467
- }
1468
- if let Some(branch_id) = explicit_branch_id {
1469
- if target_is_by_branch {
1470
- let target_branch_ids = insert_target_branch_ids(&plan.bound.branch_scope);
1471
- if let Some(target_branch_ids) = &target_branch_ids {
1472
- if !target_branch_ids.contains(&branch_id) {
1473
- return Err(LixError::new(
1474
- LixError::CODE_TYPE_MISMATCH,
1475
- format!(
1476
- "entity INSERT lixcol_branch_id '{branch_id}' does not match the target branch scope"
1477
- ),
1478
- ));
1479
- }
1480
- } else {
1481
- return Err(LixError::new(
1482
- LixError::CODE_TYPE_MISMATCH,
1483
- "entity INSERT has no target branch scope",
1484
- ));
1485
- }
1486
- }
1487
- return Ok(branch_id);
1488
- }
1489
- match &plan.bound.branch_scope {
1490
- BranchScope::Active { branch_id } => Ok(branch_id.clone()),
1491
- BranchScope::ExplicitRequired { branch_ids } if branch_ids.len() == 1 => {
1492
- Ok(branch_ids.iter().next().expect("len checked").clone())
1493
- }
1494
- BranchScope::Explicit { branch_ids } if branch_ids.len() == 1 => {
1495
- Ok(branch_ids.iter().next().expect("len checked").clone())
1496
- }
1497
- BranchScope::ExplicitDynamic { .. } | BranchScope::ExplicitRequiredDynamic { .. } => {
1498
- Err(LixError::new(
1499
- LixError::CODE_INVALID_PARAM,
1500
- "parameterized branch scope was not resolved before write execution",
1501
- ))
1502
- }
1503
- BranchScope::Global => Ok(crate::GLOBAL_BRANCH_ID.to_string()),
1504
- BranchScope::Empty => Ok(crate::GLOBAL_BRANCH_ID.to_string()),
1505
- _ => Err(LixError::new(
1506
- LixError::CODE_UNSUPPORTED_SQL,
1507
- "entity write requires exactly one target branch",
1508
- )),
1509
- }
1510
- }
1511
-
1512
- fn insert_target_branch_ids(scope: &BranchScope) -> Option<Vec<String>> {
1513
- match scope {
1514
- BranchScope::Active { branch_id } => Some(vec![branch_id.clone()]),
1515
- BranchScope::Explicit { branch_ids } | BranchScope::ExplicitRequired { branch_ids } => {
1516
- Some(branch_ids.iter().cloned().collect())
1517
- }
1518
- BranchScope::ExplicitDynamic { .. } | BranchScope::ExplicitRequiredDynamic { .. } => None,
1519
- BranchScope::Global => Some(vec![crate::GLOBAL_BRANCH_ID.to_string()]),
1520
- BranchScope::Empty => Some(Vec::new()),
1521
- }
1522
- }
1523
-
1524
- fn assignment_value<'a>(plan: &'a LogicalWritePlan, column_name: &str) -> Option<&'a BoundExpr> {
1525
- plan.bound
1526
- .assignments
1527
- .iter()
1528
- .find(|assignment| assignment.column.name == column_name)
1529
- .map(|assignment| &assignment.value)
1530
- }
1531
-
1532
- fn optional_metadata_from_eval_value(
1533
- value: EntityEvalValue,
1534
- column_name: &str,
1535
- context: &str,
1536
- ) -> Result<Option<TransactionJson>, LixError> {
1537
- let metadata = match value {
1538
- EntityEvalValue::SqlNull => return Ok(None),
1539
- EntityEvalValue::SqlText(value) => parse_row_metadata_value(&value, context)?,
1540
- EntityEvalValue::Json(value) => {
1541
- validate_row_metadata(&value, context)?;
1542
- value
1543
- }
1544
- };
1545
- TransactionJson::from_value(metadata, &format!("{context} {column_name}")).map(Some)
1546
- }
1547
-
1548
- fn text_value(value: JsonValue, column_name: &str) -> Result<Option<String>, LixError> {
1549
- match value {
1550
- JsonValue::Null => Ok(None),
1551
- JsonValue::String(value) => Ok(Some(value)),
1552
- other => Err(LixError::new(
1553
- LixError::CODE_TYPE_MISMATCH,
1554
- format!("entity write expected text-compatible column '{column_name}', got {other}"),
1555
- )),
1556
- }
1557
- }
1558
-
1559
- fn bool_value(value: JsonValue, column_name: &str) -> Result<Option<bool>, LixError> {
1560
- match value {
1561
- JsonValue::Null => Ok(None),
1562
- JsonValue::Bool(value) => Ok(Some(value)),
1563
- other => Err(LixError::new(
1564
- LixError::CODE_TYPE_MISMATCH,
1565
- format!("entity write expected boolean column '{column_name}', got {other}"),
1566
- )),
1567
- }
1568
- }
1569
-
1570
- fn entity_pk_from_value(value: &JsonValue, column_name: &str) -> Result<EntityPk, LixError> {
1571
- match value {
1572
- JsonValue::String(value) => EntityPk::from_json_array_text(value).map_err(|error| {
1573
- LixError::new(
1574
- LixError::CODE_TYPE_MISMATCH,
1575
- format!("entity write has invalid {column_name}: {error}"),
1576
- )
1577
- }),
1578
- value => EntityPk::from_json_array_value(value).map_err(|error| {
1579
- LixError::new(
1580
- LixError::CODE_TYPE_MISMATCH,
1581
- format!("entity write has invalid {column_name}: {error}"),
1582
- )
1583
- }),
1584
- }
1585
- }
1586
-
1587
- fn entity_action(op: &BoundWriteOp) -> &'static str {
1588
- match op {
1589
- BoundWriteOp::Insert => "INSERT into entity surface",
1590
- BoundWriteOp::Update => "UPDATE entity surface",
1591
- BoundWriteOp::Delete => "DELETE from entity surface",
1592
- }
1593
- }