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

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 (191) hide show
  1. package/SKILL.md +305 -320
  2. package/dist/engine-wasm/wasm/lix_engine.d.ts +5 -0
  3. package/dist/engine-wasm/wasm/lix_engine.js +9 -13
  4. package/dist/engine-wasm/wasm/lix_engine.wasm +0 -0
  5. package/dist/engine-wasm/wasm/lix_engine.wasm.d.ts +1 -0
  6. package/dist/open-lix.d.ts +103 -14
  7. package/dist/open-lix.js +3 -0
  8. package/dist/sqlite/index.js +99 -22
  9. package/dist-engine-src/README.md +18 -0
  10. package/dist-engine-src/src/backend/kv.rs +358 -0
  11. package/dist-engine-src/src/backend/mod.rs +12 -0
  12. package/dist-engine-src/src/backend/testing.rs +658 -0
  13. package/dist-engine-src/src/backend/types.rs +96 -0
  14. package/dist-engine-src/src/binary_cas/chunking.rs +31 -0
  15. package/dist-engine-src/src/binary_cas/codec.rs +346 -0
  16. package/dist-engine-src/src/binary_cas/context.rs +139 -0
  17. package/dist-engine-src/src/binary_cas/kv.rs +1063 -0
  18. package/dist-engine-src/src/binary_cas/mod.rs +11 -0
  19. package/dist-engine-src/src/binary_cas/types.rs +127 -0
  20. package/dist-engine-src/src/cel/context.rs +86 -0
  21. package/dist-engine-src/src/cel/error.rs +19 -0
  22. package/dist-engine-src/src/cel/mod.rs +8 -0
  23. package/dist-engine-src/src/cel/provider.rs +9 -0
  24. package/dist-engine-src/src/cel/runtime.rs +167 -0
  25. package/dist-engine-src/src/cel/value.rs +50 -0
  26. package/dist-engine-src/src/changelog/codec.rs +321 -0
  27. package/dist-engine-src/src/changelog/context.rs +92 -0
  28. package/dist-engine-src/src/changelog/materialization.rs +121 -0
  29. package/dist-engine-src/src/changelog/mod.rs +13 -0
  30. package/dist-engine-src/src/changelog/reader.rs +20 -0
  31. package/dist-engine-src/src/changelog/storage.rs +220 -0
  32. package/dist-engine-src/src/changelog/types.rs +38 -0
  33. package/dist-engine-src/src/commit_graph/context.rs +1588 -0
  34. package/dist-engine-src/src/commit_graph/mod.rs +12 -0
  35. package/dist-engine-src/src/commit_graph/types.rs +145 -0
  36. package/dist-engine-src/src/commit_graph/walker.rs +780 -0
  37. package/dist-engine-src/src/common/error.rs +313 -0
  38. package/dist-engine-src/src/common/fingerprint.rs +3 -0
  39. package/dist-engine-src/src/common/fs_path.rs +1336 -0
  40. package/dist-engine-src/src/common/identity.rs +135 -0
  41. package/dist-engine-src/src/common/metadata.rs +35 -0
  42. package/dist-engine-src/src/common/mod.rs +23 -0
  43. package/dist-engine-src/src/common/types.rs +105 -0
  44. package/dist-engine-src/src/common/wire.rs +222 -0
  45. package/dist-engine-src/src/engine.rs +239 -0
  46. package/dist-engine-src/src/entity_identity.rs +285 -0
  47. package/dist-engine-src/src/functions/context.rs +327 -0
  48. package/dist-engine-src/src/functions/deterministic.rs +113 -0
  49. package/dist-engine-src/src/functions/mod.rs +18 -0
  50. package/dist-engine-src/src/functions/provider.rs +130 -0
  51. package/dist-engine-src/src/functions/state.rs +363 -0
  52. package/dist-engine-src/src/functions/types.rs +37 -0
  53. package/dist-engine-src/src/init.rs +505 -0
  54. package/dist-engine-src/src/json_store/compression.rs +77 -0
  55. package/dist-engine-src/src/json_store/context.rs +129 -0
  56. package/dist-engine-src/src/json_store/encoded.rs +15 -0
  57. package/dist-engine-src/src/json_store/mod.rs +9 -0
  58. package/dist-engine-src/src/json_store/store.rs +236 -0
  59. package/dist-engine-src/src/json_store/types.rs +52 -0
  60. package/dist-engine-src/src/lib.rs +61 -0
  61. package/dist-engine-src/src/live_state/context.rs +2241 -0
  62. package/dist-engine-src/src/live_state/mod.rs +15 -0
  63. package/dist-engine-src/src/live_state/overlay.rs +75 -0
  64. package/dist-engine-src/src/live_state/reader.rs +23 -0
  65. package/dist-engine-src/src/live_state/types.rs +239 -0
  66. package/dist-engine-src/src/live_state/visibility.rs +218 -0
  67. package/dist-engine-src/src/plugin/archive.rs +441 -0
  68. package/dist-engine-src/src/plugin/component.rs +183 -0
  69. package/dist-engine-src/src/plugin/install.rs +637 -0
  70. package/dist-engine-src/src/plugin/manifest.rs +516 -0
  71. package/dist-engine-src/src/plugin/materializer.rs +477 -0
  72. package/dist-engine-src/src/plugin/mod.rs +33 -0
  73. package/dist-engine-src/src/plugin/plugin_manifest.json +119 -0
  74. package/dist-engine-src/src/plugin/storage.rs +74 -0
  75. package/dist-engine-src/src/schema/annotations/defaults.rs +280 -0
  76. package/dist-engine-src/src/schema/annotations/mod.rs +1 -0
  77. package/dist-engine-src/src/schema/builtin/lix_account.json +22 -0
  78. package/dist-engine-src/src/schema/builtin/lix_active_account.json +30 -0
  79. package/dist-engine-src/src/schema/builtin/lix_binary_blob_ref.json +30 -0
  80. package/dist-engine-src/src/schema/builtin/lix_change.json +62 -0
  81. package/dist-engine-src/src/schema/builtin/lix_change_author.json +46 -0
  82. package/dist-engine-src/src/schema/builtin/lix_change_set.json +18 -0
  83. package/dist-engine-src/src/schema/builtin/lix_change_set_element.json +75 -0
  84. package/dist-engine-src/src/schema/builtin/lix_commit.json +62 -0
  85. package/dist-engine-src/src/schema/builtin/lix_commit_edge.json +46 -0
  86. package/dist-engine-src/src/schema/builtin/lix_directory_descriptor.json +53 -0
  87. package/dist-engine-src/src/schema/builtin/lix_entity_label.json +63 -0
  88. package/dist-engine-src/src/schema/builtin/lix_file_descriptor.json +53 -0
  89. package/dist-engine-src/src/schema/builtin/lix_key_value.json +41 -0
  90. package/dist-engine-src/src/schema/builtin/lix_label.json +22 -0
  91. package/dist-engine-src/src/schema/builtin/lix_registered_schema.json +31 -0
  92. package/dist-engine-src/src/schema/builtin/lix_version_descriptor.json +35 -0
  93. package/dist-engine-src/src/schema/builtin/lix_version_ref.json +49 -0
  94. package/dist-engine-src/src/schema/builtin/mod.rs +271 -0
  95. package/dist-engine-src/src/schema/definition.json +157 -0
  96. package/dist-engine-src/src/schema/definition.rs +636 -0
  97. package/dist-engine-src/src/schema/key.rs +206 -0
  98. package/dist-engine-src/src/schema/mod.rs +20 -0
  99. package/dist-engine-src/src/schema/seed.rs +14 -0
  100. package/dist-engine-src/src/schema/tests.rs +739 -0
  101. package/dist-engine-src/src/schema_registry.rs +294 -0
  102. package/dist-engine-src/src/session/context.rs +366 -0
  103. package/dist-engine-src/src/session/create_version.rs +80 -0
  104. package/dist-engine-src/src/session/execute.rs +447 -0
  105. package/dist-engine-src/src/session/merge/analysis.rs +102 -0
  106. package/dist-engine-src/src/session/merge/apply.rs +23 -0
  107. package/dist-engine-src/src/session/merge/conflicts.rs +62 -0
  108. package/dist-engine-src/src/session/merge/mod.rs +11 -0
  109. package/dist-engine-src/src/session/merge/stats.rs +65 -0
  110. package/dist-engine-src/src/session/merge/version.rs +437 -0
  111. package/dist-engine-src/src/session/mod.rs +25 -0
  112. package/dist-engine-src/src/session/switch_version.rs +121 -0
  113. package/dist-engine-src/src/sql2/change_provider.rs +337 -0
  114. package/dist-engine-src/src/sql2/classify.rs +147 -0
  115. package/dist-engine-src/src/sql2/commit_derived_provider.rs +591 -0
  116. package/dist-engine-src/src/sql2/context.rs +307 -0
  117. package/dist-engine-src/src/sql2/directory_history_provider.rs +623 -0
  118. package/dist-engine-src/src/sql2/directory_provider.rs +2405 -0
  119. package/dist-engine-src/src/sql2/dml.rs +148 -0
  120. package/dist-engine-src/src/sql2/entity_history_provider.rs +444 -0
  121. package/dist-engine-src/src/sql2/entity_provider.rs +2700 -0
  122. package/dist-engine-src/src/sql2/error.rs +196 -0
  123. package/dist-engine-src/src/sql2/execute.rs +3379 -0
  124. package/dist-engine-src/src/sql2/file_history_provider.rs +902 -0
  125. package/dist-engine-src/src/sql2/file_provider.rs +3254 -0
  126. package/dist-engine-src/src/sql2/filesystem_planner.rs +1526 -0
  127. package/dist-engine-src/src/sql2/filesystem_predicates.rs +159 -0
  128. package/dist-engine-src/src/sql2/filesystem_visibility.rs +369 -0
  129. package/dist-engine-src/src/sql2/history_projection.rs +80 -0
  130. package/dist-engine-src/src/sql2/history_provider.rs +418 -0
  131. package/dist-engine-src/src/sql2/history_route.rs +643 -0
  132. package/dist-engine-src/src/sql2/lix_state_provider.rs +2430 -0
  133. package/dist-engine-src/src/sql2/mod.rs +43 -0
  134. package/dist-engine-src/src/sql2/read_only.rs +65 -0
  135. package/dist-engine-src/src/sql2/record_batch.rs +17 -0
  136. package/dist-engine-src/src/sql2/result_metadata.rs +29 -0
  137. package/dist-engine-src/src/sql2/runtime.rs +60 -0
  138. package/dist-engine-src/src/sql2/session.rs +135 -0
  139. package/dist-engine-src/src/sql2/udfs/common.rs +295 -0
  140. package/dist-engine-src/src/sql2/udfs/lix_active_version_commit_id.rs +53 -0
  141. package/dist-engine-src/src/sql2/udfs/lix_empty_blob.rs +47 -0
  142. package/dist-engine-src/src/sql2/udfs/lix_json.rs +100 -0
  143. package/dist-engine-src/src/sql2/udfs/lix_json_get.rs +99 -0
  144. package/dist-engine-src/src/sql2/udfs/lix_json_get_text.rs +99 -0
  145. package/dist-engine-src/src/sql2/udfs/lix_text_decode.rs +82 -0
  146. package/dist-engine-src/src/sql2/udfs/lix_text_encode.rs +85 -0
  147. package/dist-engine-src/src/sql2/udfs/lix_uuid_v7.rs +76 -0
  148. package/dist-engine-src/src/sql2/udfs/mod.rs +82 -0
  149. package/dist-engine-src/src/sql2/version_provider.rs +1187 -0
  150. package/dist-engine-src/src/sql2/version_scope.rs +394 -0
  151. package/dist-engine-src/src/sql2/write_normalization.rs +345 -0
  152. package/dist-engine-src/src/storage/context.rs +356 -0
  153. package/dist-engine-src/src/storage/mod.rs +14 -0
  154. package/dist-engine-src/src/storage/read_scope.rs +88 -0
  155. package/dist-engine-src/src/storage/types.rs +501 -0
  156. package/dist-engine-src/src/storage_bench.rs +3406 -0
  157. package/dist-engine-src/src/test_support.rs +81 -0
  158. package/dist-engine-src/src/tracked_state/by_file_index.rs +102 -0
  159. package/dist-engine-src/src/tracked_state/codec.rs +747 -0
  160. package/dist-engine-src/src/tracked_state/context.rs +983 -0
  161. package/dist-engine-src/src/tracked_state/diff.rs +494 -0
  162. package/dist-engine-src/src/tracked_state/materialization.rs +141 -0
  163. package/dist-engine-src/src/tracked_state/merge.rs +474 -0
  164. package/dist-engine-src/src/tracked_state/mod.rs +31 -0
  165. package/dist-engine-src/src/tracked_state/rebuild.rs +771 -0
  166. package/dist-engine-src/src/tracked_state/storage.rs +243 -0
  167. package/dist-engine-src/src/tracked_state/tree.rs +2744 -0
  168. package/dist-engine-src/src/tracked_state/tree_types.rs +176 -0
  169. package/dist-engine-src/src/tracked_state/types.rs +61 -0
  170. package/dist-engine-src/src/transaction/commit.rs +1224 -0
  171. package/dist-engine-src/src/transaction/context.rs +1307 -0
  172. package/dist-engine-src/src/transaction/live_state_overlay.rs +34 -0
  173. package/dist-engine-src/src/transaction/mod.rs +11 -0
  174. package/dist-engine-src/src/transaction/normalization.rs +1026 -0
  175. package/dist-engine-src/src/transaction/schema_resolver.rs +127 -0
  176. package/dist-engine-src/src/transaction/staging.rs +1436 -0
  177. package/dist-engine-src/src/transaction/types.rs +351 -0
  178. package/dist-engine-src/src/transaction/validation.rs +4811 -0
  179. package/dist-engine-src/src/untracked_state/codec.rs +363 -0
  180. package/dist-engine-src/src/untracked_state/context.rs +82 -0
  181. package/dist-engine-src/src/untracked_state/materialization.rs +157 -0
  182. package/dist-engine-src/src/untracked_state/mod.rs +17 -0
  183. package/dist-engine-src/src/untracked_state/storage.rs +348 -0
  184. package/dist-engine-src/src/untracked_state/types.rs +96 -0
  185. package/dist-engine-src/src/version/context.rs +52 -0
  186. package/dist-engine-src/src/version/mod.rs +12 -0
  187. package/dist-engine-src/src/version/refs.rs +421 -0
  188. package/dist-engine-src/src/version/stage_rows.rs +71 -0
  189. package/dist-engine-src/src/version/types.rs +21 -0
  190. package/dist-engine-src/src/wasm/mod.rs +60 -0
  191. package/package.json +68 -64
@@ -0,0 +1,80 @@
1
+ use crate::transaction::types::{StageWrite, StageWriteMode};
2
+ use crate::version::{version_descriptor_stage_row, version_ref_stage_row, VersionRefReader};
3
+ use crate::LixError;
4
+
5
+ use super::context::SessionContext;
6
+
7
+ /// Options for creating a new version from the session's active version.
8
+ #[derive(Debug, Clone, PartialEq, Eq)]
9
+ pub struct CreateVersionOptions {
10
+ /// Optional caller-provided version id. If omitted, engine2 generates one.
11
+ pub id: Option<String>,
12
+ /// User-facing version name.
13
+ pub name: String,
14
+ /// Optional commit id for the new version head. If omitted, the current
15
+ /// active version head is used.
16
+ pub from_commit_id: Option<String>,
17
+ }
18
+
19
+ /// Receipt returned after creating a version.
20
+ #[derive(Debug, Clone, PartialEq, Eq)]
21
+ pub struct CreateVersionReceipt {
22
+ pub id: String,
23
+ pub name: String,
24
+ pub hidden: bool,
25
+ pub commit_id: String,
26
+ }
27
+
28
+ impl SessionContext {
29
+ /// Creates a new version from this session's current version head.
30
+ ///
31
+ /// Version descriptors are tracked global facts so every version agrees on
32
+ /// which versions exist. Version refs are untracked global moving pointers,
33
+ /// so creating a ref does not add another changelog fact.
34
+ pub async fn create_version(
35
+ &self,
36
+ options: CreateVersionOptions,
37
+ ) -> Result<CreateVersionReceipt, LixError> {
38
+ self.with_write_transaction(|transaction| {
39
+ Box::pin(async move {
40
+ let version_id = options
41
+ .id
42
+ .unwrap_or_else(|| transaction.functions().call_uuid_v7());
43
+ let source_head = if let Some(from_commit_id) = options.from_commit_id {
44
+ from_commit_id
45
+ } else {
46
+ let active_version_id = transaction.active_version_id().to_string();
47
+ let reader = transaction.version_ref_reader();
48
+ reader
49
+ .load_head_commit_id(&active_version_id)
50
+ .await?
51
+ .ok_or_else(|| {
52
+ LixError::version_not_found(
53
+ active_version_id.clone(),
54
+ "create_version",
55
+ "source",
56
+ )
57
+ })?
58
+ };
59
+
60
+ transaction
61
+ .stage_write(StageWrite::Rows {
62
+ mode: StageWriteMode::Insert,
63
+ rows: vec![
64
+ version_descriptor_stage_row(&version_id, &options.name, false),
65
+ version_ref_stage_row(&version_id, &source_head),
66
+ ],
67
+ })
68
+ .await?;
69
+
70
+ Ok(CreateVersionReceipt {
71
+ id: version_id,
72
+ name: options.name,
73
+ hidden: false,
74
+ commit_id: source_head,
75
+ })
76
+ })
77
+ })
78
+ .await
79
+ }
80
+ }
@@ -0,0 +1,447 @@
1
+ use std::sync::Arc;
2
+
3
+ use crate::functions::FunctionContext;
4
+ use crate::json_store::JsonStoreContext;
5
+ use crate::sql2;
6
+ use crate::storage::{StorageReadScope, StorageWriteSet};
7
+ use crate::{LixError, LixNotice, SqlQueryResult, Value};
8
+
9
+ use super::context::{SessionContext, SessionSqlExecutionContext};
10
+
11
+ /// Result of executing one SQL statement through engine2.
12
+ ///
13
+ /// Column names live once at the result-set level. Individual rows only own
14
+ /// values, which keeps the public API row-oriented without copying schema
15
+ /// metadata into every row.
16
+ #[derive(Debug, Clone, PartialEq)]
17
+ pub struct ExecuteResult {
18
+ columns: Vec<String>,
19
+ rows: Vec<Row>,
20
+ rows_affected: u64,
21
+ notices: Vec<LixNotice>,
22
+ }
23
+
24
+ impl ExecuteResult {
25
+ fn from_sql_query_result(result: SqlQueryResult) -> Self {
26
+ Self {
27
+ columns: result.columns,
28
+ rows: Vec::new(),
29
+ rows_affected: 0,
30
+ notices: result.notices,
31
+ }
32
+ .with_rows(result.rows)
33
+ }
34
+
35
+ pub fn from_rows_affected(rows_affected: u64) -> Self {
36
+ Self {
37
+ columns: Vec::new(),
38
+ rows: Vec::new(),
39
+ rows_affected,
40
+ notices: Vec::new(),
41
+ }
42
+ }
43
+
44
+ pub fn from_rows(columns: Vec<String>, rows: Vec<Vec<Value>>) -> Self {
45
+ Self {
46
+ columns,
47
+ rows: Vec::new(),
48
+ rows_affected: 0,
49
+ notices: Vec::new(),
50
+ }
51
+ .with_rows(rows)
52
+ }
53
+
54
+ fn with_rows(mut self, rows: Vec<Vec<Value>>) -> Self {
55
+ let columns = Arc::<[String]>::from(self.columns.clone().into_boxed_slice());
56
+ self.rows = rows
57
+ .into_iter()
58
+ .map(|values| Row {
59
+ columns: Arc::clone(&columns),
60
+ values,
61
+ })
62
+ .collect();
63
+ self
64
+ }
65
+
66
+ /// Returns the result-set column names in row value order.
67
+ pub fn columns(&self) -> &[String] {
68
+ &self.columns
69
+ }
70
+
71
+ /// Returns the owned rows. Use `iter()` for name-based access.
72
+ pub fn rows(&self) -> &[Row] {
73
+ &self.rows
74
+ }
75
+
76
+ /// Iterates rows with borrowed access to the shared column metadata.
77
+ pub fn iter(&self) -> impl Iterator<Item = RowRef<'_>> {
78
+ self.rows.iter().map(|row| RowRef {
79
+ columns: self.columns.as_slice(),
80
+ values: row.values.as_slice(),
81
+ })
82
+ }
83
+
84
+ /// Returns the number of rows in this result set.
85
+ pub fn len(&self) -> usize {
86
+ self.rows.len()
87
+ }
88
+
89
+ /// Returns true when this result set has no rows.
90
+ pub fn is_empty(&self) -> bool {
91
+ self.rows.is_empty()
92
+ }
93
+
94
+ /// Returns the number of rows affected by a mutation statement.
95
+ pub fn rows_affected(&self) -> u64 {
96
+ self.rows_affected
97
+ }
98
+
99
+ /// Returns non-fatal diagnostics produced while executing the statement.
100
+ pub fn notices(&self) -> &[LixNotice] {
101
+ &self.notices
102
+ }
103
+
104
+ /// Looks up the value for `column_name` on an owned row from this set.
105
+ pub fn get<'a>(&self, row: &'a Row, column_name: &str) -> Option<&'a Value> {
106
+ let index = self.column_index(column_name)?;
107
+ row.get_index(index)
108
+ }
109
+
110
+ /// Returns the index for a column name.
111
+ pub fn column_index(&self, column_name: &str) -> Option<usize> {
112
+ self.columns.iter().position(|column| column == column_name)
113
+ }
114
+ }
115
+
116
+ /// One owned row returned by a query.
117
+ #[derive(Debug, Clone, PartialEq)]
118
+ pub struct Row {
119
+ columns: Arc<[String]>,
120
+ values: Vec<Value>,
121
+ }
122
+
123
+ impl Row {
124
+ /// Returns the values in result-set column order.
125
+ pub fn values(&self) -> &[Value] {
126
+ &self.values
127
+ }
128
+
129
+ /// Returns the value at `index`.
130
+ pub fn get_index(&self, index: usize) -> Option<&Value> {
131
+ self.values.get(index)
132
+ }
133
+
134
+ /// Returns the raw value for `column_name`, or an error when the column is absent.
135
+ pub fn value(&self, column_name: &str) -> Result<&Value, LixError> {
136
+ let index = self.column_index(column_name)?;
137
+ self.values.get(index).ok_or_else(|| {
138
+ LixError::new(
139
+ LixError::CODE_COLUMN_NOT_FOUND,
140
+ format!(
141
+ "column '{}' points past row width {}; available columns: {}",
142
+ column_name,
143
+ self.values.len(),
144
+ self.available_columns()
145
+ ),
146
+ )
147
+ })
148
+ }
149
+
150
+ /// Converts the named column to a native Rust value.
151
+ pub fn get<T>(&self, column_name: &str) -> Result<T, LixError>
152
+ where
153
+ T: TryFromValue,
154
+ {
155
+ T::try_from_value(self.value(column_name)?)
156
+ }
157
+
158
+ fn column_index(&self, column_name: &str) -> Result<usize, LixError> {
159
+ self.columns
160
+ .iter()
161
+ .position(|column| column == column_name)
162
+ .ok_or_else(|| {
163
+ LixError::new(
164
+ LixError::CODE_COLUMN_NOT_FOUND,
165
+ format!(
166
+ "column '{}' does not exist; available columns: {}",
167
+ column_name,
168
+ self.available_columns()
169
+ ),
170
+ )
171
+ })
172
+ }
173
+
174
+ fn available_columns(&self) -> String {
175
+ if self.columns.is_empty() {
176
+ "<none>".to_string()
177
+ } else {
178
+ self.columns.join(", ")
179
+ }
180
+ }
181
+ }
182
+
183
+ pub trait TryFromValue: Sized {
184
+ fn try_from_value(value: &Value) -> Result<Self, LixError>;
185
+ }
186
+
187
+ impl TryFromValue for Value {
188
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
189
+ Ok(value.clone())
190
+ }
191
+ }
192
+
193
+ impl TryFromValue for String {
194
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
195
+ match value {
196
+ Value::Text(value) => Ok(value.clone()),
197
+ other => Err(value_type_error("text", other)),
198
+ }
199
+ }
200
+ }
201
+
202
+ impl TryFromValue for bool {
203
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
204
+ match value {
205
+ Value::Boolean(value) => Ok(*value),
206
+ other => Err(value_type_error("boolean", other)),
207
+ }
208
+ }
209
+ }
210
+
211
+ impl TryFromValue for i64 {
212
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
213
+ match value {
214
+ Value::Integer(value) => Ok(*value),
215
+ other => Err(value_type_error("integer", other)),
216
+ }
217
+ }
218
+ }
219
+
220
+ impl TryFromValue for f64 {
221
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
222
+ match value {
223
+ Value::Real(value) => Ok(*value),
224
+ other => Err(value_type_error("real", other)),
225
+ }
226
+ }
227
+ }
228
+
229
+ impl TryFromValue for serde_json::Value {
230
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
231
+ match value {
232
+ Value::Json(value) => Ok(value.clone()),
233
+ other => Err(value_type_error("json", other)),
234
+ }
235
+ }
236
+ }
237
+
238
+ impl TryFromValue for Vec<u8> {
239
+ fn try_from_value(value: &Value) -> Result<Self, LixError> {
240
+ match value {
241
+ Value::Blob(value) => Ok(value.clone()),
242
+ other => Err(value_type_error("blob", other)),
243
+ }
244
+ }
245
+ }
246
+
247
+ fn value_type_error(expected: &str, actual: &Value) -> LixError {
248
+ LixError::new(
249
+ "LIX_ERROR_VALUE_TYPE",
250
+ format!("expected {expected} value, got {actual:?}"),
251
+ )
252
+ }
253
+
254
+ /// Borrowed row view with access to the result-set column names.
255
+ ///
256
+ /// This is the ergonomic path for callers that want `row.get("column")`
257
+ /// without storing column metadata on every owned row.
258
+ #[derive(Debug, Clone, Copy)]
259
+ pub struct RowRef<'a> {
260
+ columns: &'a [String],
261
+ values: &'a [Value],
262
+ }
263
+
264
+ impl RowRef<'_> {
265
+ /// Returns the result-set column names in row value order.
266
+ pub fn columns(&self) -> &[String] {
267
+ self.columns
268
+ }
269
+
270
+ /// Returns the row values in result-set column order.
271
+ pub fn values(&self) -> &[Value] {
272
+ self.values
273
+ }
274
+
275
+ /// Returns the value for `column_name`.
276
+ pub fn get(&self, column_name: &str) -> Option<&Value> {
277
+ let index = self
278
+ .columns
279
+ .iter()
280
+ .position(|column| column == column_name)?;
281
+ self.values.get(index)
282
+ }
283
+
284
+ /// Returns the value at `index`.
285
+ pub fn get_index(&self, index: usize) -> Option<&Value> {
286
+ self.values.get(index)
287
+ }
288
+ }
289
+
290
+ impl SessionContext {
291
+ /// Executes one DataFusion SQL statement against this Lix session.
292
+ ///
293
+ /// The SQL dialect is DataFusion SQL, not SQLite SQL. Positional
294
+ /// placeholders use `$1`, `$2`, and so on. SQLite-specific catalog tables
295
+ /// and transaction statements such as `sqlite_master`, `BEGIN`, and
296
+ /// `COMMIT` are not part of this contract; use `information_schema` for
297
+ /// catalog inspection. Lix owns transaction boundaries for each statement.
298
+ pub async fn execute(&self, sql: &str, params: &[Value]) -> Result<ExecuteResult, LixError> {
299
+ self.ensure_open()?;
300
+ let kind = sql2::classify_statement(sql)?;
301
+ if kind == sql2::SqlStatementKind::Write {
302
+ let sql = sql.to_string();
303
+ let params = params.to_vec();
304
+ return self
305
+ .with_write_transaction(|transaction| {
306
+ Box::pin(async move {
307
+ // Re-plan against the transaction-backed write
308
+ // session so provider hooks read and stage through the
309
+ // transaction-owned SQL write context.
310
+ let tx_plan = sql2::create_write_logical_plan(transaction, &sql).await?;
311
+ let result = sql2::execute_logical_plan(tx_plan, &params).await?;
312
+ let affected_rows = affected_rows_from_query_result(result)?;
313
+ Ok(ExecuteResult::from_rows_affected(affected_rows))
314
+ })
315
+ })
316
+ .await;
317
+ }
318
+
319
+ let read_scope = StorageReadScope::new(self.storage.begin_read_transaction().await?);
320
+ let read_result = async {
321
+ let mut read_store = read_scope.store();
322
+ let live_state: Arc<dyn crate::live_state::LiveStateReader> =
323
+ Arc::new(self.live_state.reader(read_store.clone()));
324
+ let runtime_functions = FunctionContext::prepare(live_state.as_ref()).await?;
325
+ let functions = runtime_functions.provider();
326
+ let active_version_id = self.active_version_id_from_reader(&mut read_store).await?;
327
+ let visible_schemas = self
328
+ .schema_registry
329
+ .visible_schemas(live_state.as_ref(), &active_version_id)
330
+ .await?;
331
+ let ctx = SessionSqlExecutionContext {
332
+ active_version_id: &active_version_id,
333
+ read_store,
334
+ live_state: Arc::clone(&self.live_state),
335
+ binary_cas: Arc::clone(&self.binary_cas),
336
+ changelog: Arc::clone(&self.changelog),
337
+ version_ctx: Arc::clone(&self.version_ctx),
338
+ visible_schemas,
339
+ functions: functions.clone(),
340
+ };
341
+
342
+ let plan = sql2::create_logical_plan(&ctx, sql).await?;
343
+ let result = sql2::execute_logical_plan(plan, params).await?;
344
+ drop(ctx);
345
+ drop(live_state);
346
+ Ok::<_, LixError>((runtime_functions, result))
347
+ };
348
+ let (runtime_functions, result) = match read_result.await {
349
+ Ok(result) => {
350
+ read_scope.rollback().await?;
351
+ result
352
+ }
353
+ Err(error) => {
354
+ let _ = read_scope.rollback().await;
355
+ return Err(error);
356
+ }
357
+ };
358
+ self.persist_runtime_functions_if_needed(&runtime_functions)
359
+ .await?;
360
+ Ok(ExecuteResult::from_sql_query_result(result))
361
+ }
362
+
363
+ /// Persists execution-scoped runtime function state after a successful read.
364
+ ///
365
+ /// Reads do not otherwise own a write transaction, but SQL functions such as
366
+ /// `lix_uuid_v7()` can still advance runtime state. Persisting happens only
367
+ /// after successful execution so failed reads do not consume durable
368
+ /// sequence state.
369
+ async fn persist_runtime_functions_if_needed(
370
+ &self,
371
+ runtime_functions: &FunctionContext,
372
+ ) -> Result<(), LixError> {
373
+ let mut transaction = self.storage.begin_write_transaction().await?;
374
+ let mut writes = StorageWriteSet::new();
375
+ let mut json_writer = JsonStoreContext::new().writer();
376
+ runtime_functions
377
+ .stage_persist_if_needed(
378
+ &mut self.live_state.writer(transaction.as_mut()),
379
+ &mut writes,
380
+ &mut json_writer,
381
+ )
382
+ .await?;
383
+ if !writes.is_empty() {
384
+ writes.apply(&mut transaction.as_mut()).await?;
385
+ }
386
+ transaction.commit().await
387
+ }
388
+ }
389
+
390
+ fn affected_rows_from_query_result(result: SqlQueryResult) -> Result<u64, LixError> {
391
+ let Some(first_row) = result.rows.first() else {
392
+ return Ok(0);
393
+ };
394
+ let Some(first_value) = first_row.first() else {
395
+ return Ok(0);
396
+ };
397
+ match first_value {
398
+ Value::Integer(value) if *value >= 0 => Ok(*value as u64),
399
+ Value::Text(value) => value.parse::<u64>().map_err(|error| {
400
+ LixError::new(
401
+ "LIX_ERROR_UNKNOWN",
402
+ format!("failed to parse affected row count from SQL result: {error}"),
403
+ )
404
+ }),
405
+ other => Err(LixError::new(
406
+ "LIX_ERROR_UNKNOWN",
407
+ format!("expected affected row count, got {other:?}"),
408
+ )),
409
+ }
410
+ }
411
+
412
+ #[cfg(test)]
413
+ mod tests {
414
+ use super::*;
415
+
416
+ #[test]
417
+ fn row_get_converts_native_values_and_value_keeps_wrapper() {
418
+ let result = ExecuteResult::from_rows(
419
+ vec!["title".to_string(), "done".to_string()],
420
+ vec![vec![Value::Text("Hello".to_string()), Value::Boolean(true)]],
421
+ );
422
+ let row = &result.rows()[0];
423
+
424
+ assert_eq!(row.get::<String>("title").unwrap(), "Hello");
425
+ assert!(row.get::<bool>("done").unwrap());
426
+ assert_eq!(
427
+ row.value("title").unwrap(),
428
+ &Value::Text("Hello".to_string())
429
+ );
430
+ }
431
+
432
+ #[test]
433
+ fn row_get_errors_on_missing_column_and_wrong_type() {
434
+ let result = ExecuteResult::from_rows(
435
+ vec!["title".to_string()],
436
+ vec![vec![Value::Text("Hello".to_string())]],
437
+ );
438
+ let row = &result.rows()[0];
439
+
440
+ let missing = row.get::<String>("missing").unwrap_err();
441
+ assert_eq!(missing.code, LixError::CODE_COLUMN_NOT_FOUND);
442
+ assert!(missing.message.contains("available columns: title"));
443
+
444
+ let wrong_type = row.get::<bool>("title").unwrap_err();
445
+ assert_eq!(wrong_type.code, "LIX_ERROR_VALUE_TYPE");
446
+ }
447
+ }
@@ -0,0 +1,102 @@
1
+ use crate::storage::StorageReader;
2
+ use crate::tracked_state::{
3
+ plan_merge, TrackedStateDiff, TrackedStateDiffRequest, TrackedStateMergePlan,
4
+ TrackedStateStoreReader,
5
+ };
6
+ use crate::LixError;
7
+
8
+ use super::conflicts::{conflicts_from_plan, MergeConflict};
9
+ use super::stats::{stats_from_diff, stats_from_plan, MergeStats};
10
+
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12
+ pub(crate) enum MergeOutcome {
13
+ AlreadyUpToDate,
14
+ FastForward,
15
+ MergeCommitted,
16
+ }
17
+
18
+ #[derive(Debug, Clone, PartialEq, Eq)]
19
+ pub(crate) struct MergeCommits {
20
+ pub(crate) base_commit_id: String,
21
+ pub(crate) target_commit_id: String,
22
+ pub(crate) source_commit_id: String,
23
+ }
24
+
25
+ #[derive(Debug, Clone, PartialEq, Eq)]
26
+ pub(crate) struct MergeAnalysis {
27
+ pub(crate) outcome: MergeOutcome,
28
+ pub(crate) commits: MergeCommits,
29
+ pub(crate) source_diff: TrackedStateDiff,
30
+ pub(crate) target_diff: TrackedStateDiff,
31
+ pub(crate) stats: MergeStats,
32
+ pub(crate) conflicts: Vec<MergeConflict>,
33
+ pub(crate) merge_plan: Option<TrackedStateMergePlan>,
34
+ }
35
+
36
+ impl MergeAnalysis {
37
+ pub(crate) fn merge_plan(&self) -> Option<&TrackedStateMergePlan> {
38
+ self.merge_plan.as_ref()
39
+ }
40
+ }
41
+
42
+ pub(crate) async fn analyze<S>(
43
+ reader: &mut TrackedStateStoreReader<S>,
44
+ commits: MergeCommits,
45
+ ) -> Result<MergeAnalysis, LixError>
46
+ where
47
+ S: StorageReader,
48
+ {
49
+ let request = TrackedStateDiffRequest::default();
50
+ let source_diff = reader
51
+ .diff_commits(&commits.base_commit_id, &commits.source_commit_id, &request)
52
+ .await?;
53
+ let target_diff = if commits.base_commit_id == commits.source_commit_id
54
+ || commits.base_commit_id == commits.target_commit_id
55
+ {
56
+ TrackedStateDiff::default()
57
+ } else {
58
+ reader
59
+ .diff_commits(&commits.base_commit_id, &commits.target_commit_id, &request)
60
+ .await?
61
+ };
62
+
63
+ let outcome = if commits.base_commit_id == commits.source_commit_id {
64
+ MergeOutcome::AlreadyUpToDate
65
+ } else if commits.base_commit_id == commits.target_commit_id {
66
+ MergeOutcome::FastForward
67
+ } else {
68
+ MergeOutcome::MergeCommitted
69
+ };
70
+
71
+ let merge_plan = if outcome == MergeOutcome::MergeCommitted {
72
+ Some(plan_merge(&target_diff, &source_diff)?)
73
+ } else {
74
+ None
75
+ };
76
+
77
+ let stats = match outcome {
78
+ MergeOutcome::AlreadyUpToDate => MergeStats::default(),
79
+ MergeOutcome::FastForward => stats_from_diff(&source_diff),
80
+ MergeOutcome::MergeCommitted => merge_plan
81
+ .as_ref()
82
+ .map(|plan| stats_from_plan(plan, &source_diff))
83
+ .transpose()?
84
+ .unwrap_or_default(),
85
+ };
86
+
87
+ let conflicts = merge_plan
88
+ .as_ref()
89
+ .map(conflicts_from_plan)
90
+ .transpose()?
91
+ .unwrap_or_default();
92
+
93
+ Ok(MergeAnalysis {
94
+ outcome,
95
+ commits,
96
+ source_diff,
97
+ target_diff,
98
+ stats,
99
+ conflicts,
100
+ merge_plan,
101
+ })
102
+ }
@@ -0,0 +1,23 @@
1
+ use crate::tracked_state::TrackedStateMergePlan;
2
+ use crate::transaction::types::StageAdoptedChange;
3
+
4
+ pub(crate) fn adopted_changes_from_merge_plan(
5
+ plan: &TrackedStateMergePlan,
6
+ target_version_id: &str,
7
+ ) -> Vec<StageAdoptedChange> {
8
+ plan.patches
9
+ .iter()
10
+ .map(|patch| stage_adopted_change_from_patch(patch, target_version_id))
11
+ .collect()
12
+ }
13
+
14
+ fn stage_adopted_change_from_patch(
15
+ patch: &crate::tracked_state::TrackedStateMergePatch,
16
+ target_version_id: &str,
17
+ ) -> StageAdoptedChange {
18
+ StageAdoptedChange {
19
+ version_id: target_version_id.to_string(),
20
+ change_id: patch.change_id().to_string(),
21
+ projected_row: patch.projected_row().clone(),
22
+ }
23
+ }