@feltdb/core 0.4.12 → 0.4.14

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 (165) hide show
  1. package/dist/cli/commands.js +42 -25
  2. package/dist/cli/index.js +1 -1
  3. package/dist/create/cli.js +39 -4
  4. package/dist/create/create.js +30 -18
  5. package/dist/create/docker-compose-generator.js +68 -11
  6. package/dist/create/package-versions.js +1 -1
  7. package/dist/create/server-source/Cargo.lock +2238 -0
  8. package/dist/create/server-source/Cargo.toml +7 -0
  9. package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
  10. package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
  11. package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
  12. package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
  13. package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
  14. package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
  15. package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
  16. package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
  17. package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
  18. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
  19. package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
  20. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
  21. package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
  22. package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
  23. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
  24. package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
  25. package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
  26. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
  27. package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
  28. package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
  29. package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
  30. package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
  31. package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
  32. package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
  33. package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
  34. package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
  35. package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
  36. package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
  37. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
  38. package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
  39. package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
  40. package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
  41. package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
  42. package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
  43. package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
  44. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
  45. package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
  46. package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
  47. package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
  48. package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
  49. package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
  50. package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
  51. package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
  52. package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
  53. package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
  54. package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
  55. package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
  56. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
  57. package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
  58. package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
  59. package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
  60. package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
  61. package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
  62. package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
  63. package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
  64. package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
  65. package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
  66. package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
  67. package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
  68. package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
  69. package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
  70. package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
  71. package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
  72. package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
  73. package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
  74. package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
  75. package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
  76. package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
  77. package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
  78. package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
  79. package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
  80. package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
  81. package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
  82. package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
  83. package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
  84. package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
  85. package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
  86. package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
  87. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
  88. package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
  89. package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
  90. package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
  91. package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
  92. package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
  93. package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
  94. package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
  95. package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
  96. package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
  97. package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
  98. package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
  99. package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
  100. package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
  101. package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
  102. package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
  103. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
  104. package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
  105. package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
  106. package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
  107. package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
  108. package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
  109. package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
  110. package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
  111. package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
  112. package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
  113. package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
  114. package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
  115. package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
  116. package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
  117. package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
  118. package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
  119. package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
  120. package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
  121. package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
  122. package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
  123. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
  124. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
  125. package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
  126. package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
  127. package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
  128. package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
  129. package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
  130. package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
  131. package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
  132. package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
  133. package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
  134. package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
  135. package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
  136. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
  137. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
  138. package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
  139. package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
  140. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
  141. package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
  142. package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
  143. package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
  144. package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
  145. package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
  146. package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
  147. package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
  148. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
  149. package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
  150. package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
  151. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
  152. package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
  153. package/dist/flowspec.d.ts +2 -0
  154. package/dist/flowspec.d.ts.map +1 -1
  155. package/dist/flowspec.js +28 -4
  156. package/dist/state-contract.d.ts +7 -1
  157. package/dist/state-contract.d.ts.map +1 -1
  158. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  159. package/dist/studio/components/ConflictExplorer.d.ts.map +1 -1
  160. package/dist/studio/components/index.js +1 -1
  161. package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
  162. package/dist/studio/index.js +31 -28
  163. package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
  164. package/dist/studio-app/index.html +1 -1
  165. package/package.json +1 -1
@@ -0,0 +1,3118 @@
1
+ //! Query Execution Contract
2
+ //!
3
+ //! This module defines the observable behavior of query execution in FeltDB.
4
+ //! The query execution contract has three key guarantees:
5
+ //!
6
+ //! 1. PLANNER-EXECUTOR ALIGNMENT: The access_method field in QueryPlan indicates
7
+ //! whether an index will be used (value: "index_lookup") or a full scan (value: "full_table_scan").
8
+ //! The executor MUST honor this decision. If plan says "index_lookup", the executor MUST
9
+ //! use an index. Fallback to scan violates the contract.
10
+ //!
11
+ //! 2. ACTUAL MEASUREMENTS: All timing fields (planning_ms, index_lookup_ms, filtering_ms,
12
+ //! serialization_ms, total_ms) and metrics (index_hits, index_misses) MUST be actual
13
+ //! measurements from real execution using Instant::now(). Estimates or derived values
14
+ //! violate the contract. The index_metrics struct MUST reflect what actually executed,
15
+ //! not what was planned.
16
+ //!
17
+ //! 3. FALLBACK TRACKING: If a query cannot use an index (field not indexed, expression too complex),
18
+ //! the planner sets access_method: "full_table_scan" and fallback_reason describes why.
19
+ //! The executor then scans all rows. This is not a contract violation—it's the correct
20
+ //! fallback path. The violation only occurs if the planner said "use index" but executor
21
+ //! fell back to scan without updating these fields.
22
+
23
+ use crate::{
24
+ application::{ApplicationManifest, ApplicationRevision, FieldDefinition},
25
+ AtomicMutation, AtomicPrecondition, FeltDb, StoredRow,
26
+ };
27
+ use serde::{Deserialize, Serialize};
28
+ use serde_json::{Map, Value};
29
+ use sha2::{Digest, Sha256};
30
+ use std::{
31
+ cmp::Ordering,
32
+ collections::{BTreeMap, BTreeSet},
33
+ time::Instant,
34
+ };
35
+
36
+ pub const STATE_CONTRACT_VERSION: u32 = 1;
37
+
38
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39
+ #[serde(rename_all = "snake_case")]
40
+ pub enum PrimitiveType {
41
+ String,
42
+ Integer,
43
+ Number,
44
+ Boolean,
45
+ Timestamp,
46
+ Date,
47
+ Time,
48
+ Json,
49
+ Uuid,
50
+ Decimal,
51
+ Money,
52
+ Bigint,
53
+ Email,
54
+ Url,
55
+ Phone,
56
+ Binary,
57
+ File,
58
+ GeoPoint,
59
+ Object,
60
+ }
61
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62
+ #[serde(tag = "kind", rename_all = "snake_case")]
63
+ pub enum FieldType {
64
+ Primitive { primitive: PrimitiveType },
65
+ Enum { values: Vec<String> },
66
+ Reference { collection: String },
67
+ Array { items: Box<FieldType> },
68
+ Map { values: Box<FieldType> },
69
+ Vector { dimensions: Option<usize> },
70
+ }
71
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72
+ pub struct FieldConstraints {
73
+ #[serde(default)]
74
+ pub minimum: Option<f64>,
75
+ #[serde(default)]
76
+ pub maximum: Option<f64>,
77
+ #[serde(default)]
78
+ pub min_length: Option<usize>,
79
+ #[serde(default)]
80
+ pub max_length: Option<usize>,
81
+ #[serde(default)]
82
+ pub pattern: Option<String>,
83
+ }
84
+ impl Default for FieldConstraints {
85
+ fn default() -> Self {
86
+ Self {
87
+ minimum: None,
88
+ maximum: None,
89
+ min_length: None,
90
+ max_length: None,
91
+ pattern: None,
92
+ }
93
+ }
94
+ }
95
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96
+ pub struct FieldSchema {
97
+ pub name: String,
98
+ pub field_type: FieldType,
99
+ #[serde(default)]
100
+ pub nullable: bool,
101
+ #[serde(default)]
102
+ pub required: bool,
103
+ #[serde(default)]
104
+ pub default: Option<Value>,
105
+ #[serde(default)]
106
+ pub constraints: FieldConstraints,
107
+ #[serde(default)]
108
+ pub computed: Option<String>,
109
+ }
110
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111
+ pub struct SchemaIndex {
112
+ pub name: String,
113
+ pub fields: Vec<String>,
114
+ #[serde(default)]
115
+ pub unique: bool,
116
+ #[serde(default)]
117
+ pub sparse: bool,
118
+ #[serde(default = "one")]
119
+ pub version: u32,
120
+ }
121
+ fn one() -> u32 {
122
+ 1
123
+ }
124
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125
+ pub struct CollectionSchema {
126
+ pub name: String,
127
+ #[serde(default = "one")]
128
+ pub version: u32,
129
+ #[serde(default)]
130
+ pub fields: Vec<FieldSchema>,
131
+ #[serde(default)]
132
+ pub indexes: Vec<SchemaIndex>,
133
+ }
134
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135
+ pub struct StateSchema {
136
+ pub contract_version: u32,
137
+ pub schema_version: u64,
138
+ pub application_id: String,
139
+ pub revision_id: String,
140
+ #[serde(default)]
141
+ pub collections: Vec<CollectionSchema>,
142
+ }
143
+ fn parse_manifest_type(value: &str) -> FieldType {
144
+ let value = value.trim();
145
+ if let Some(inner) = value
146
+ .strip_prefix("array<")
147
+ .and_then(|v| v.strip_suffix('>'))
148
+ {
149
+ return FieldType::Array {
150
+ items: Box::new(parse_manifest_type(inner)),
151
+ };
152
+ }
153
+ if let Some(inner) = value.strip_prefix("map<").and_then(|v| v.strip_suffix('>')) {
154
+ return FieldType::Map {
155
+ values: Box::new(parse_manifest_type(inner)),
156
+ };
157
+ }
158
+ if value == "vector" {
159
+ return FieldType::Vector { dimensions: None };
160
+ }
161
+ if let Some(inner) = value
162
+ .strip_prefix("vector<")
163
+ .and_then(|v| v.strip_suffix('>'))
164
+ {
165
+ return FieldType::Vector {
166
+ dimensions: inner.parse().ok(),
167
+ };
168
+ }
169
+ FieldType::Primitive {
170
+ primitive: match value {
171
+ "int" | "integer" => PrimitiveType::Integer,
172
+ "number" | "float" => PrimitiveType::Number,
173
+ "bool" | "boolean" => PrimitiveType::Boolean,
174
+ "timestamp" | "datetime" => PrimitiveType::Timestamp,
175
+ "date" => PrimitiveType::Date,
176
+ "time" => PrimitiveType::Time,
177
+ "json" => PrimitiveType::Json,
178
+ "uuid" => PrimitiveType::Uuid,
179
+ "decimal" => PrimitiveType::Decimal,
180
+ "money" => PrimitiveType::Money,
181
+ "bigint" => PrimitiveType::Bigint,
182
+ "email" => PrimitiveType::Email,
183
+ "url" => PrimitiveType::Url,
184
+ "phone" => PrimitiveType::Phone,
185
+ "binary" | "bytes" => PrimitiveType::Binary,
186
+ "file" => PrimitiveType::File,
187
+ "geo" | "geopoint" => PrimitiveType::GeoPoint,
188
+ "object" => PrimitiveType::Object,
189
+ _ => PrimitiveType::String,
190
+ },
191
+ }
192
+ }
193
+ pub fn valid_manifest_field_type(value: &str) -> bool {
194
+ let value = value.trim();
195
+ if matches!(
196
+ value,
197
+ "text"
198
+ | "string"
199
+ | "int"
200
+ | "integer"
201
+ | "number"
202
+ | "float"
203
+ | "bool"
204
+ | "boolean"
205
+ | "timestamp"
206
+ | "datetime"
207
+ | "date"
208
+ | "time"
209
+ | "json"
210
+ | "uuid"
211
+ | "decimal"
212
+ | "money"
213
+ | "bigint"
214
+ | "email"
215
+ | "url"
216
+ | "phone"
217
+ | "binary"
218
+ | "bytes"
219
+ | "file"
220
+ | "geo"
221
+ | "geopoint"
222
+ | "object"
223
+ | "enum"
224
+ | "reference"
225
+ | "vector"
226
+ ) {
227
+ return true;
228
+ }
229
+ if value
230
+ .strip_prefix("vector<")
231
+ .and_then(|v| v.strip_suffix('>'))
232
+ .is_some_and(|v| v.parse::<usize>().is_ok_and(|n| n > 0))
233
+ {
234
+ return true;
235
+ }
236
+ value
237
+ .strip_prefix("array<")
238
+ .and_then(|v| v.strip_suffix('>'))
239
+ .or_else(|| value.strip_prefix("map<").and_then(|v| v.strip_suffix('>')))
240
+ .is_some_and(valid_manifest_field_type)
241
+ }
242
+ fn manifest_field_type(field: &FieldDefinition) -> FieldType {
243
+ if let Some(collection) = &field.reference {
244
+ FieldType::Reference {
245
+ collection: collection.clone(),
246
+ }
247
+ } else if !field.enum_values.is_empty() {
248
+ FieldType::Enum {
249
+ values: field.enum_values.clone(),
250
+ }
251
+ } else {
252
+ parse_manifest_type(&field.field_type)
253
+ }
254
+ }
255
+ pub fn schema_from_manifest(
256
+ manifest: &ApplicationManifest,
257
+ revision_id: impl Into<String>,
258
+ ) -> StateSchema {
259
+ StateSchema {
260
+ contract_version: STATE_CONTRACT_VERSION,
261
+ schema_version: manifest.state_schema_version,
262
+ application_id: manifest.application_id.clone(),
263
+ revision_id: revision_id.into(),
264
+ collections: manifest
265
+ .collections
266
+ .iter()
267
+ .map(|collection| CollectionSchema {
268
+ name: collection.name.clone(),
269
+ version: manifest.state_schema_version as u32,
270
+ fields: collection
271
+ .fields
272
+ .iter()
273
+ .map(|field| FieldSchema {
274
+ name: field.name.clone(),
275
+ field_type: manifest_field_type(field),
276
+ nullable: field.nullable,
277
+ required: field.required,
278
+ default: field.default.clone(),
279
+ constraints: Default::default(),
280
+ computed: field.computed.clone(),
281
+ })
282
+ .collect(),
283
+ indexes: manifest
284
+ .indexes
285
+ .iter()
286
+ .filter(|index| index.collection == collection.name)
287
+ .map(|index| SchemaIndex {
288
+ name: index.name.clone(),
289
+ fields: index.fields.clone(),
290
+ unique: index.unique,
291
+ sparse: index.sparse,
292
+ version: index.version,
293
+ })
294
+ .collect(),
295
+ })
296
+ .collect(),
297
+ }
298
+ }
299
+ pub fn schema_from_revision(revision: &ApplicationRevision) -> StateSchema {
300
+ schema_from_manifest(&revision.manifest, revision.revision_id.clone())
301
+ }
302
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
303
+ pub struct SchemaIssue {
304
+ pub path: String,
305
+ pub code: String,
306
+ pub message: String,
307
+ }
308
+ #[derive(Debug, Clone, Serialize, Deserialize)]
309
+ pub struct SchemaValidation {
310
+ pub valid: bool,
311
+ pub schema_hash: Option<String>,
312
+ pub issues: Vec<SchemaIssue>,
313
+ }
314
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
315
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
316
+ pub enum SchemaCompatibility {
317
+ Safe,
318
+ RequiresMigration,
319
+ Destructive,
320
+ Incompatible,
321
+ }
322
+ #[derive(Debug, Clone, Serialize, Deserialize)]
323
+ pub struct SchemaChange {
324
+ pub collection: String,
325
+ pub field: Option<String>,
326
+ pub classification: SchemaCompatibility,
327
+ pub description: String,
328
+ }
329
+ #[derive(Debug, Clone, Serialize, Deserialize)]
330
+ pub struct SchemaCompatibilityReport {
331
+ pub from_version: u64,
332
+ pub to_version: u64,
333
+ pub classification: SchemaCompatibility,
334
+ pub changes: Vec<SchemaChange>,
335
+ }
336
+
337
+ fn sorted_schema(mut schema: StateSchema) -> StateSchema {
338
+ schema.collections.sort_by(|a, b| a.name.cmp(&b.name));
339
+ for collection in &mut schema.collections {
340
+ collection.fields.sort_by(|a, b| a.name.cmp(&b.name));
341
+ collection.indexes.sort_by(|a, b| a.name.cmp(&b.name));
342
+ for field in &mut collection.fields {
343
+ if let FieldType::Enum { values } = &mut field.field_type {
344
+ values.sort();
345
+ }
346
+ }
347
+ }
348
+ schema
349
+ }
350
+ pub fn canonical_schema_bytes(schema: &StateSchema) -> Result<Vec<u8>, String> {
351
+ serde_json::to_vec(&sorted_schema(schema.clone())).map_err(|e| e.to_string())
352
+ }
353
+ pub fn schema_hash(schema: &StateSchema) -> Result<String, String> {
354
+ Ok(format!(
355
+ "sha256:{:x}",
356
+ Sha256::digest(canonical_schema_bytes(schema)?)
357
+ ))
358
+ }
359
+ pub fn validate_schema(schema: &StateSchema) -> SchemaValidation {
360
+ let mut issues = vec![];
361
+ if schema.contract_version != STATE_CONTRACT_VERSION {
362
+ issues.push(SchemaIssue {
363
+ path: "contract_version".into(),
364
+ code: "UNSUPPORTED_CONTRACT".into(),
365
+ message: "unsupported state contract version".into(),
366
+ });
367
+ }
368
+ let mut collections = BTreeSet::new();
369
+ for collection in &schema.collections {
370
+ if collection.name.trim().is_empty() || !collections.insert(collection.name.clone()) {
371
+ issues.push(SchemaIssue {
372
+ path: format!("collections.{}", collection.name),
373
+ code: "INVALID_COLLECTION".into(),
374
+ message: "collection names must be non-empty and unique".into(),
375
+ });
376
+ }
377
+ let mut fields = BTreeSet::new();
378
+ for field in &collection.fields {
379
+ if field.name.trim().is_empty() || !fields.insert(field.name.clone()) {
380
+ issues.push(SchemaIssue {
381
+ path: format!("collections.{}.fields.{}", collection.name, field.name),
382
+ code: "INVALID_FIELD".into(),
383
+ message: "field names must be non-empty and unique".into(),
384
+ });
385
+ }
386
+ if field.required && field.nullable {
387
+ issues.push(SchemaIssue {
388
+ path: format!("collections.{}.fields.{}", collection.name, field.name),
389
+ code: "INVALID_NULLABILITY".into(),
390
+ message: "required fields cannot be nullable".into(),
391
+ });
392
+ }
393
+ if field.computed.is_some() && field.default.is_some() {
394
+ issues.push(SchemaIssue {
395
+ path: format!("collections.{}.fields.{}", collection.name, field.name),
396
+ code: "INVALID_COMPUTED_DEFAULT".into(),
397
+ message: "computed fields cannot declare defaults".into(),
398
+ });
399
+ }
400
+ if let Some(expression) = &field.computed {
401
+ let sources = expression
402
+ .strip_prefix("copy:")
403
+ .map(|v| vec![v])
404
+ .or_else(|| {
405
+ expression
406
+ .strip_prefix("concat:")
407
+ .map(|v| v.split(',').collect())
408
+ })
409
+ .unwrap_or_default();
410
+ if sources.is_empty()
411
+ || sources.iter().any(|source| {
412
+ !collection
413
+ .fields
414
+ .iter()
415
+ .any(|candidate| candidate.name == source.trim())
416
+ })
417
+ {
418
+ issues.push(SchemaIssue{path:format!("collections.{}.fields.{}.computed",collection.name,field.name),code:"INVALID_COMPUTED_FIELD".into(),message:"computed expressions must use copy:<field> or concat:<field>,<field> with known fields".into()});
419
+ }
420
+ }
421
+ if field
422
+ .default
423
+ .as_ref()
424
+ .is_some_and(|value| !valid_type(value, field))
425
+ {
426
+ issues.push(SchemaIssue {
427
+ path: format!(
428
+ "collections.{}.fields.{}.default",
429
+ collection.name, field.name
430
+ ),
431
+ code: "INVALID_DEFAULT".into(),
432
+ message: "default does not satisfy the field type".into(),
433
+ });
434
+ }
435
+ if let FieldType::Enum { values } = &field.field_type {
436
+ let unique = values.iter().collect::<BTreeSet<_>>();
437
+ if values.is_empty() || unique.len() != values.len() {
438
+ issues.push(SchemaIssue {
439
+ path: format!("collections.{}.fields.{}", collection.name, field.name),
440
+ code: "INVALID_ENUM".into(),
441
+ message: "enum values must be non-empty and unique".into(),
442
+ });
443
+ }
444
+ }
445
+ }
446
+ for index in &collection.indexes {
447
+ if index.fields.is_empty() || index.fields.iter().any(|f| !fields.contains(f)) {
448
+ issues.push(SchemaIssue {
449
+ path: format!("collections.{}.indexes.{}", collection.name, index.name),
450
+ code: "INVALID_INDEX".into(),
451
+ message: "indexes must reference existing fields".into(),
452
+ });
453
+ }
454
+ }
455
+ }
456
+ for collection in &schema.collections {
457
+ for field in &collection.fields {
458
+ if let FieldType::Reference { collection: target } = &field.field_type {
459
+ if !collections.contains(target) {
460
+ issues.push(SchemaIssue {
461
+ path: format!("collections.{}.fields.{}", collection.name, field.name),
462
+ code: "INVALID_REFERENCE".into(),
463
+ message: format!("unknown reference target {target}"),
464
+ });
465
+ }
466
+ }
467
+ }
468
+ }
469
+ let valid = issues.is_empty();
470
+ SchemaValidation {
471
+ valid,
472
+ schema_hash: valid.then(|| schema_hash(schema).ok()).flatten(),
473
+ issues,
474
+ }
475
+ }
476
+ pub fn compare_schemas(before: &StateSchema, after: &StateSchema) -> SchemaCompatibilityReport {
477
+ let mut changes = vec![];
478
+ let old: BTreeMap<_, _> = before.collections.iter().map(|v| (&v.name, v)).collect();
479
+ let new: BTreeMap<_, _> = after.collections.iter().map(|v| (&v.name, v)).collect();
480
+ for (name, collection) in &new {
481
+ if !old.contains_key(name) {
482
+ changes.push(SchemaChange {
483
+ collection: (*name).clone(),
484
+ field: None,
485
+ classification: SchemaCompatibility::Safe,
486
+ description: "collection added".into(),
487
+ });
488
+ continue;
489
+ }
490
+ let previous = old[name];
491
+ let old_fields: BTreeMap<_, _> = previous.fields.iter().map(|v| (&v.name, v)).collect();
492
+ let new_fields: BTreeMap<_, _> = collection.fields.iter().map(|v| (&v.name, v)).collect();
493
+ for (field, current) in &new_fields {
494
+ match old_fields.get(field) {
495
+ None => changes.push(SchemaChange {
496
+ collection: (*name).clone(),
497
+ field: Some((*field).clone()),
498
+ classification: if current.required && current.default.is_none() {
499
+ SchemaCompatibility::Destructive
500
+ } else {
501
+ SchemaCompatibility::Safe
502
+ },
503
+ description: "field added".into(),
504
+ }),
505
+ Some(prior) if prior.field_type != current.field_type => {
506
+ let classification = match (&prior.field_type, &current.field_type) {
507
+ (FieldType::Enum { values: a }, FieldType::Enum { values: b })
508
+ if a.iter().all(|v| b.contains(v)) =>
509
+ {
510
+ SchemaCompatibility::Safe
511
+ }
512
+ (FieldType::Enum { .. }, FieldType::Enum { .. }) => {
513
+ SchemaCompatibility::Destructive
514
+ }
515
+ _ => SchemaCompatibility::Incompatible,
516
+ };
517
+ changes.push(SchemaChange {
518
+ collection: (*name).clone(),
519
+ field: Some((*field).clone()),
520
+ classification,
521
+ description: "field type changed".into(),
522
+ });
523
+ }
524
+ Some(prior)
525
+ if prior.required != current.required || prior.nullable != current.nullable =>
526
+ {
527
+ changes.push(SchemaChange {
528
+ collection: (*name).clone(),
529
+ field: Some((*field).clone()),
530
+ classification: if current.required {
531
+ SchemaCompatibility::Destructive
532
+ } else {
533
+ SchemaCompatibility::Safe
534
+ },
535
+ description: "field requirement changed".into(),
536
+ })
537
+ }
538
+ _ => {}
539
+ }
540
+ }
541
+ for field in old_fields.keys().filter(|f| !new_fields.contains_key(*f)) {
542
+ changes.push(SchemaChange {
543
+ collection: (*name).clone(),
544
+ field: Some((*field).clone()),
545
+ classification: SchemaCompatibility::Destructive,
546
+ description: "field removed".into(),
547
+ });
548
+ }
549
+ }
550
+ for name in old.keys().filter(|n| !new.contains_key(*n)) {
551
+ changes.push(SchemaChange {
552
+ collection: (*name).clone(),
553
+ field: None,
554
+ classification: SchemaCompatibility::Destructive,
555
+ description: "collection removed".into(),
556
+ });
557
+ }
558
+ let classification = changes
559
+ .iter()
560
+ .map(|v| v.classification.clone())
561
+ .max()
562
+ .unwrap_or(SchemaCompatibility::Safe);
563
+ SchemaCompatibilityReport {
564
+ from_version: before.schema_version,
565
+ to_version: after.schema_version,
566
+ classification,
567
+ changes,
568
+ }
569
+ }
570
+
571
+ #[derive(Debug, Clone, Serialize, Deserialize)]
572
+ pub struct AuthorizationContext {
573
+ pub subject: String,
574
+ pub tenant_id: String,
575
+ pub application_id: String,
576
+ pub revision_id: String,
577
+ #[serde(default)]
578
+ pub capabilities: BTreeSet<String>,
579
+ #[serde(default)]
580
+ pub readable_collections: BTreeSet<String>,
581
+ #[serde(default)]
582
+ pub writable_collections: BTreeSet<String>,
583
+ #[serde(default)]
584
+ pub field_projections: BTreeMap<String, BTreeSet<String>>,
585
+ }
586
+ impl AuthorizationContext {
587
+ fn can(&self, cap: &str) -> bool {
588
+ self.capabilities.contains("*") || self.capabilities.contains(cap)
589
+ }
590
+ }
591
+ #[derive(Debug, Clone, Serialize, Deserialize)]
592
+ pub struct ReadContext {
593
+ pub application_id: String,
594
+ pub revision_id: String,
595
+ pub schema_version: u64,
596
+ pub state_version: u64,
597
+ pub causal_cursor: BTreeMap<String, u64>,
598
+ pub state_namespace: String,
599
+ pub authorization: AuthorizationContext,
600
+ #[serde(skip, default)]
601
+ snapshot_rows: Vec<StoredRow>,
602
+ }
603
+ pub fn begin_read(
604
+ db: &FeltDb,
605
+ schema: &StateSchema,
606
+ state_namespace: &str,
607
+ authorization: AuthorizationContext,
608
+ ) -> Result<ReadContext, StateFailure> {
609
+ if authorization.application_id != schema.application_id
610
+ || authorization.revision_id != schema.revision_id
611
+ {
612
+ return Err(StateFailure::new(
613
+ "SCHEMA_MISMATCH",
614
+ "authorization context does not match schema",
615
+ ));
616
+ }
617
+ let snapshot = db.state_rows().map_err(StateFailure::storage)?;
618
+ Ok(ReadContext {
619
+ application_id: schema.application_id.clone(),
620
+ revision_id: schema.revision_id.clone(),
621
+ schema_version: schema.schema_version,
622
+ state_version: db.sequence().map_err(StateFailure::storage)?,
623
+ causal_cursor: db
624
+ .operation_versions()
625
+ .map_err(StateFailure::storage)?
626
+ .into_iter()
627
+ .collect(),
628
+ state_namespace: state_namespace.into(),
629
+ authorization,
630
+ snapshot_rows: snapshot,
631
+ })
632
+ }
633
+
634
+ #[derive(Debug, Clone, Serialize, Deserialize)]
635
+ #[serde(tag = "operator", rename_all = "camelCase")]
636
+ pub enum QueryFilter {
637
+ Eq { field: String, value: Value },
638
+ Neq { field: String, value: Value },
639
+ Lt { field: String, value: Value },
640
+ Lte { field: String, value: Value },
641
+ Gt { field: String, value: Value },
642
+ Gte { field: String, value: Value },
643
+ In { field: String, values: Vec<Value> },
644
+ Contains { field: String, value: Value },
645
+ StartsWith { field: String, value: String },
646
+ EndsWith { field: String, value: String },
647
+ Exists { field: String, exists: bool },
648
+ And { filters: Vec<QueryFilter> },
649
+ Or { filters: Vec<QueryFilter> },
650
+ Not { filter: Box<QueryFilter> },
651
+ }
652
+ #[derive(Debug, Clone, Serialize, Deserialize)]
653
+ #[serde(rename_all = "snake_case")]
654
+ pub enum SortDirection {
655
+ Asc,
656
+ Desc,
657
+ }
658
+ #[derive(Debug, Clone, Serialize, Deserialize)]
659
+ pub struct QueryOrder {
660
+ pub field: String,
661
+ pub direction: SortDirection,
662
+ }
663
+ #[derive(Debug, Clone, Serialize, Deserialize)]
664
+ #[serde(rename_all = "snake_case")]
665
+ pub enum AggregateFunction {
666
+ Count,
667
+ Sum,
668
+ Avg,
669
+ Min,
670
+ Max,
671
+ }
672
+ #[derive(Debug, Clone, Serialize, Deserialize)]
673
+ pub struct AggregateDefinition {
674
+ pub name: String,
675
+ pub function: AggregateFunction,
676
+ #[serde(default)]
677
+ pub field: Option<String>,
678
+ }
679
+ #[derive(Debug, Clone, Serialize, Deserialize)]
680
+ pub struct QueryReference {
681
+ pub field: String,
682
+ pub collection: String,
683
+ #[serde(default)]
684
+ pub projection: Vec<String>,
685
+ pub as_name: String,
686
+ }
687
+ #[derive(Debug, Clone, Serialize, Deserialize)]
688
+ pub struct CanonicalQuery {
689
+ pub collection: String,
690
+ #[serde(default)]
691
+ pub filter: Option<QueryFilter>,
692
+ #[serde(default)]
693
+ pub order_by: Vec<QueryOrder>,
694
+ #[serde(default)]
695
+ pub limit: Option<usize>,
696
+ #[serde(default)]
697
+ pub offset: usize,
698
+ #[serde(default)]
699
+ pub cursor: Option<String>,
700
+ #[serde(default)]
701
+ pub projection: Vec<String>,
702
+ #[serde(default)]
703
+ pub group_by: Vec<String>,
704
+ #[serde(default)]
705
+ pub aggregates: Vec<AggregateDefinition>,
706
+ #[serde(default)]
707
+ pub references: Vec<QueryReference>,
708
+ }
709
+ /// Actual execution timing measurements for a query.
710
+ /// All fields are MEASURED using Instant::now(), not estimated or derived.
711
+ /// Each field captures time for its phase: planning, index lookup, filtering, serialization.
712
+ #[derive(Debug, Clone, Serialize, Deserialize)]
713
+ pub struct ExecutionTiming {
714
+ /// Time spent in query planning (determining access method)
715
+ #[serde(default)]
716
+ pub planning_ms: Option<f64>,
717
+ /// Time spent in index lookup phase (pre-filtering via indexed field)
718
+ #[serde(default)]
719
+ pub index_lookup_ms: Option<f64>,
720
+ /// Time spent evaluating full query filter predicates
721
+ #[serde(default)]
722
+ pub filtering_ms: Option<f64>,
723
+ /// Time spent serializing results to JSON
724
+ #[serde(default)]
725
+ pub serialization_ms: Option<f64>,
726
+ /// Total end-to-end execution time
727
+ #[serde(default)]
728
+ pub total_ms: Option<f64>,
729
+ }
730
+
731
+ /// Actual index usage metrics from query execution.
732
+ /// These fields MUST reflect what the executor actually did, not what was planned.
733
+ /// If the planner selected an index but executor fell back to scan, these metrics MUST show
734
+ /// fallback_used=true and fallback_reason must explain why, else the contract is violated.
735
+ #[derive(Debug, Clone, Serialize, Deserialize)]
736
+ pub struct IndexMetrics {
737
+ /// Which index was used, if any (e.g., "status_idx"). None if no index used.
738
+ #[serde(default)]
739
+ pub index_used: Option<String>,
740
+ /// Number of rows returned by index pre-filter phase (actual count, not estimate)
741
+ #[serde(default)]
742
+ pub index_hits: usize,
743
+ /// Number of rows filtered out by index pre-filter (actual count, not estimate)
744
+ #[serde(default)]
745
+ pub index_misses: usize,
746
+ /// True if executor fell back to full scan (violates contract if planner said "index_lookup")
747
+ #[serde(default)]
748
+ pub fallback_used: bool,
749
+ /// Reason for fallback (e.g., "index_not_found", "unsupported_expression")
750
+ #[serde(default)]
751
+ pub fallback_reason: Option<String>,
752
+ }
753
+
754
+ impl ExecutionTiming {
755
+ pub fn new() -> Self {
756
+ ExecutionTiming {
757
+ planning_ms: None,
758
+ index_lookup_ms: None,
759
+ filtering_ms: None,
760
+ serialization_ms: None,
761
+ total_ms: None,
762
+ }
763
+ }
764
+ }
765
+
766
+ impl Default for ExecutionTiming {
767
+ fn default() -> Self {
768
+ Self::new()
769
+ }
770
+ }
771
+
772
+ /// Query execution plan with CONTRACT guarantees.
773
+ /// The planner makes a decision (access_method: "index_lookup" or "full_table_scan").
774
+ /// The executor MUST honor this decision and update index_metrics and timing to prove it did.
775
+ /// Violation: planner says "index_lookup" but executor provides metrics showing full_table_scan.
776
+ #[derive(Debug, Clone, Serialize, Deserialize)]
777
+ pub struct QueryPlan {
778
+ pub implementation: String,
779
+ pub collection: String,
780
+ pub index: Option<String>,
781
+ pub scan_fallback: bool,
782
+ /// ACCESS METHOD DECISION: "index_lookup" or "full_table_scan" (planner's choice)
783
+ /// Executor MUST follow this decision and update index_metrics to prove it did.
784
+ #[serde(default)]
785
+ pub access_method: Option<String>,
786
+ #[serde(default)]
787
+ pub estimated_rows: Option<usize>,
788
+ /// Reason for fallback to scan (only set if access_method="full_table_scan")
789
+ #[serde(default)]
790
+ pub fallback_reason: Option<String>,
791
+ /// Number of rows examined by executor (actual, not estimate)
792
+ #[serde(default)]
793
+ pub actual_rows_scanned: Option<usize>,
794
+ /// Number of rows returned after all filtering (actual, not estimate)
795
+ #[serde(default)]
796
+ pub actual_rows_returned: Option<usize>,
797
+ /// Total execution time in milliseconds (actual measurement)
798
+ #[serde(default)]
799
+ pub execution_ms: Option<f64>,
800
+ /// Detailed timing breakdown (planning, index_lookup, filtering, serialization)
801
+ #[serde(default)]
802
+ pub timing: Option<ExecutionTiming>,
803
+ /// Proof that executor followed planner's access_method decision
804
+ #[serde(default)]
805
+ pub index_metrics: Option<IndexMetrics>,
806
+ }
807
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
808
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
809
+ pub enum OperationType {
810
+ Read,
811
+ Query,
812
+ Insert,
813
+ Update,
814
+ Delete,
815
+ Transaction,
816
+ Sync,
817
+ Replication,
818
+ }
819
+
820
+ #[derive(Debug, Clone, Serialize, Deserialize)]
821
+ pub struct QueryResult {
822
+ pub query_id: String,
823
+ pub query_hash: String,
824
+ pub application_revision: String,
825
+ pub schema_version: u64,
826
+ pub state_version: u64,
827
+ pub causal_cursor: BTreeMap<String, u64>,
828
+ pub records: Vec<Value>,
829
+ pub aggregates: Vec<Value>,
830
+ pub next_cursor: Option<String>,
831
+ pub plan: QueryPlan,
832
+ #[serde(default)]
833
+ pub operation_type: Option<OperationType>,
834
+ }
835
+
836
+ #[derive(Debug, Clone, Serialize, Deserialize)]
837
+ pub struct StateFailure {
838
+ pub code: String,
839
+ pub message: String,
840
+ #[serde(default)]
841
+ pub expected: Option<Value>,
842
+ #[serde(default)]
843
+ pub actual: Option<Value>,
844
+ #[serde(default)]
845
+ pub resource: Option<String>,
846
+ #[serde(default)]
847
+ pub transaction: Option<String>,
848
+ #[serde(default)]
849
+ pub causal_position: Option<u64>,
850
+ }
851
+ impl StateFailure {
852
+ fn new(code: &str, message: &str) -> Self {
853
+ Self {
854
+ code: code.into(),
855
+ message: message.into(),
856
+ expected: None,
857
+ actual: None,
858
+ resource: None,
859
+ transaction: None,
860
+ causal_position: None,
861
+ }
862
+ }
863
+ fn storage(error: impl std::fmt::Display) -> Self {
864
+ Self::new("STORAGE_FAILURE", &error.to_string())
865
+ }
866
+ }
867
+
868
+ fn collection<'a>(
869
+ schema: &'a StateSchema,
870
+ name: &str,
871
+ ) -> Result<&'a CollectionSchema, StateFailure> {
872
+ schema
873
+ .collections
874
+ .iter()
875
+ .find(|v| v.name == name)
876
+ .ok_or_else(|| StateFailure::new("UNKNOWN_COLLECTION", name))
877
+ }
878
+ fn filter_fields(filter: &QueryFilter, out: &mut BTreeSet<String>) {
879
+ match filter {
880
+ QueryFilter::Eq { field, .. }
881
+ | QueryFilter::Neq { field, .. }
882
+ | QueryFilter::Lt { field, .. }
883
+ | QueryFilter::Lte { field, .. }
884
+ | QueryFilter::Gt { field, .. }
885
+ | QueryFilter::Gte { field, .. }
886
+ | QueryFilter::In { field, .. }
887
+ | QueryFilter::Contains { field, .. }
888
+ | QueryFilter::StartsWith { field, .. }
889
+ | QueryFilter::EndsWith { field, .. }
890
+ | QueryFilter::Exists { field, .. } => {
891
+ out.insert(field.clone());
892
+ }
893
+ QueryFilter::And { filters } | QueryFilter::Or { filters } => {
894
+ for f in filters {
895
+ filter_fields(f, out)
896
+ }
897
+ }
898
+ QueryFilter::Not { filter } => filter_fields(filter, out),
899
+ }
900
+ }
901
+ fn compare_value(left: &Value, right: &Value) -> Option<Ordering> {
902
+ match (left, right) {
903
+ (Value::Number(a), Value::Number(b)) => a.as_f64()?.partial_cmp(&b.as_f64()?),
904
+ (Value::String(a), Value::String(b)) => Some(a.cmp(b)),
905
+ (Value::Bool(a), Value::Bool(b)) => Some(a.cmp(b)),
906
+ _ => None,
907
+ }
908
+ }
909
+ fn matches(value: &Value, filter: &QueryFilter) -> bool {
910
+ let get = |field: &str| value.get(field);
911
+ match filter {
912
+ QueryFilter::Eq { field, value: v } => get(field) == Some(v),
913
+ QueryFilter::Neq { field, value: v } => get(field) != Some(v),
914
+ QueryFilter::Lt { field, value: v } => {
915
+ get(field).and_then(|x| compare_value(x, v)) == Some(Ordering::Less)
916
+ }
917
+ QueryFilter::Lte { field, value: v } => matches!(
918
+ get(field).and_then(|x| compare_value(x, v)),
919
+ Some(Ordering::Less | Ordering::Equal)
920
+ ),
921
+ QueryFilter::Gt { field, value: v } => {
922
+ get(field).and_then(|x| compare_value(x, v)) == Some(Ordering::Greater)
923
+ }
924
+ QueryFilter::Gte { field, value: v } => matches!(
925
+ get(field).and_then(|x| compare_value(x, v)),
926
+ Some(Ordering::Greater | Ordering::Equal)
927
+ ),
928
+ QueryFilter::In { field, values } => get(field).is_some_and(|x| values.contains(x)),
929
+ QueryFilter::Contains { field, value: v } => get(field).is_some_and(|x| {
930
+ x.as_str()
931
+ .zip(v.as_str())
932
+ .is_some_and(|(a, b)| a.contains(b))
933
+ || x.as_array().is_some_and(|a| a.contains(v))
934
+ }),
935
+ QueryFilter::StartsWith { field, value: v } => get(field)
936
+ .and_then(Value::as_str)
937
+ .is_some_and(|x| x.starts_with(v)),
938
+ QueryFilter::EndsWith { field, value: v } => get(field)
939
+ .and_then(Value::as_str)
940
+ .is_some_and(|x| x.ends_with(v)),
941
+ QueryFilter::Exists { field, exists } => get(field).is_some() == *exists,
942
+ QueryFilter::And { filters } => filters.iter().all(|f| matches(value, f)),
943
+ QueryFilter::Or { filters } => filters.iter().any(|f| matches(value, f)),
944
+ QueryFilter::Not { filter } => !matches(value, filter),
945
+ }
946
+ }
947
+ fn projected(value: &Value, fields: &[String]) -> Value {
948
+ if fields.is_empty() {
949
+ return value.clone();
950
+ }
951
+ let mut out = Map::new();
952
+ for field in fields {
953
+ if let Some(v) = value.get(field) {
954
+ out.insert(field.clone(), v.clone());
955
+ }
956
+ }
957
+ Value::Object(out)
958
+ }
959
+ pub fn query_hash(query: &CanonicalQuery) -> Result<String, StateFailure> {
960
+ Ok(format!(
961
+ "sha256:{:x}",
962
+ Sha256::digest(serde_json::to_vec(query).map_err(StateFailure::storage)?)
963
+ ))
964
+ }
965
+ fn extract_equality_predicate(filter: &QueryFilter) -> Option<(String, Value)> {
966
+ match filter {
967
+ QueryFilter::Eq { field, value } => Some((field.clone(), value.clone())),
968
+ _ => None,
969
+ }
970
+ }
971
+
972
+ fn extract_range_predicate(filter: &QueryFilter) -> Option<(String, Option<Value>, Option<Value>)> {
973
+ match filter {
974
+ QueryFilter::Gte { field, value } => Some((field.clone(), Some(value.clone()), None)),
975
+ QueryFilter::Lte { field, value } => Some((field.clone(), None, Some(value.clone()))),
976
+ QueryFilter::Gt { field, value } => Some((field.clone(), Some(value.clone()), None)),
977
+ QueryFilter::Lt { field, value } => Some((field.clone(), None, Some(value.clone()))),
978
+ _ => None,
979
+ }
980
+ }
981
+
982
+ fn can_use_index(definition: &CollectionSchema, field: &str) -> bool {
983
+ definition
984
+ .indexes
985
+ .iter()
986
+ .any(|idx| idx.fields.contains(&field.to_string()))
987
+ }
988
+
989
+ fn can_use_index_for_range(definition: &CollectionSchema, field: &str) -> bool {
990
+ definition
991
+ .indexes
992
+ .iter()
993
+ .any(|idx| idx.fields.contains(&field.to_string()))
994
+ }
995
+
996
+ pub fn explain_query(
997
+ schema: &StateSchema,
998
+ query: &CanonicalQuery,
999
+ ) -> Result<QueryPlan, StateFailure> {
1000
+ let definition = collection(schema, &query.collection)?;
1001
+
1002
+ let mut predicate_fields = BTreeSet::new();
1003
+ if let Some(filter) = &query.filter {
1004
+ filter_fields(filter, &mut predicate_fields)
1005
+ }
1006
+
1007
+ let (used_index, index_name, _index_predicate_field_and_value) =
1008
+ if let Some(filter) = &query.filter {
1009
+ if let Some((field, _value)) = extract_equality_predicate(filter) {
1010
+ if can_use_index(definition, &field) {
1011
+ let index_name = definition
1012
+ .indexes
1013
+ .iter()
1014
+ .find(|idx| idx.fields.contains(&field))
1015
+ .map(|idx| idx.name.clone());
1016
+
1017
+ if index_name.is_some() {
1018
+ (true, index_name, Some((field, Value::Null)))
1019
+ } else {
1020
+ (false, None, None)
1021
+ }
1022
+ } else {
1023
+ (false, None, None)
1024
+ }
1025
+ } else {
1026
+ (false, None, None)
1027
+ }
1028
+ } else {
1029
+ (false, None, None)
1030
+ };
1031
+
1032
+ let (execution_strategy, scan_fallback_flag, access_method) = if used_index {
1033
+ ("index_lookup".into(), false, Some("index".to_string()))
1034
+ } else {
1035
+ (
1036
+ "full_table_scan".into(),
1037
+ true,
1038
+ Some("sequential_scan".to_string()),
1039
+ )
1040
+ };
1041
+
1042
+ Ok(QueryPlan {
1043
+ implementation: execution_strategy,
1044
+ collection: query.collection.clone(),
1045
+ index: index_name.clone(),
1046
+ scan_fallback: scan_fallback_flag,
1047
+ access_method,
1048
+ estimated_rows: if used_index { Some(1) } else { None },
1049
+ fallback_reason: if scan_fallback_flag {
1050
+ Some("no_applicable_index".to_string())
1051
+ } else {
1052
+ None
1053
+ },
1054
+ actual_rows_scanned: None,
1055
+ actual_rows_returned: None,
1056
+ execution_ms: None,
1057
+ timing: None,
1058
+ index_metrics: None,
1059
+ })
1060
+ }
1061
+
1062
+ pub fn execute_query(
1063
+ _db: &FeltDb,
1064
+ schema: &StateSchema,
1065
+ context: &ReadContext,
1066
+ query: &CanonicalQuery,
1067
+ ) -> Result<QueryResult, StateFailure> {
1068
+ let query_start = Instant::now();
1069
+
1070
+ if query.limit == Some(0) || query.limit.is_some_and(|limit| limit > 1000) {
1071
+ return Err(StateFailure::new(
1072
+ "INVALID_QUERY",
1073
+ "limit must be between 1 and 1000",
1074
+ ));
1075
+ }
1076
+ if context.schema_version != schema.schema_version || context.revision_id != schema.revision_id
1077
+ {
1078
+ return Err(StateFailure::new(
1079
+ "SCHEMA_MISMATCH",
1080
+ "read context does not match schema",
1081
+ ));
1082
+ }
1083
+ let definition = collection(schema, &query.collection)?;
1084
+ if !context.authorization.can("state:read")
1085
+ || (!context.authorization.readable_collections.is_empty()
1086
+ && !context
1087
+ .authorization
1088
+ .readable_collections
1089
+ .contains(&query.collection))
1090
+ {
1091
+ return Err(StateFailure::new(
1092
+ "AUTHORIZATION_DENIED",
1093
+ "collection is not readable",
1094
+ ));
1095
+ }
1096
+ let available: BTreeSet<_> = definition
1097
+ .fields
1098
+ .iter()
1099
+ .map(|v| v.name.clone())
1100
+ .chain(query.references.iter().map(|v| v.as_name.clone()))
1101
+ .chain(["_id".into(), "_version".into()])
1102
+ .collect();
1103
+ let mut requested = query.projection.iter().cloned().collect::<BTreeSet<_>>();
1104
+ let mut predicate_fields = BTreeSet::new();
1105
+ if let Some(filter) = &query.filter {
1106
+ filter_fields(filter, &mut requested);
1107
+ filter_fields(filter, &mut predicate_fields)
1108
+ }
1109
+ for order in &query.order_by {
1110
+ requested.insert(order.field.clone());
1111
+ }
1112
+ for group in &query.group_by {
1113
+ requested.insert(group.clone());
1114
+ }
1115
+ for agg in &query.aggregates {
1116
+ if let Some(field) = &agg.field {
1117
+ requested.insert(field.clone());
1118
+ }
1119
+ }
1120
+ for reference in &query.references {
1121
+ requested.insert(reference.field.clone());
1122
+ }
1123
+ if requested.iter().any(|f| !available.contains(f)) {
1124
+ return Err(StateFailure::new(
1125
+ "UNKNOWN_FIELD",
1126
+ "query references an unknown field",
1127
+ ));
1128
+ }
1129
+ if let Some(allowed) = context
1130
+ .authorization
1131
+ .field_projections
1132
+ .get(&query.collection)
1133
+ {
1134
+ if requested
1135
+ .iter()
1136
+ .any(|f| !allowed.contains(f) && f != "_id" && f != "_version")
1137
+ {
1138
+ return Err(StateFailure::new(
1139
+ "UNAUTHORIZED_PROJECTION",
1140
+ "query references a protected field",
1141
+ ));
1142
+ }
1143
+ }
1144
+ let effective_projection = context
1145
+ .authorization
1146
+ .field_projections
1147
+ .get(&query.collection)
1148
+ .filter(|_| query.projection.is_empty())
1149
+ .map(|allowed| allowed.iter().cloned().collect::<Vec<_>>())
1150
+ .unwrap_or_else(|| query.projection.clone());
1151
+ let capability = format!("{}:{}", context.state_namespace, query.collection);
1152
+ for reference in &query.references {
1153
+ let target = collection(schema, &reference.collection)?;
1154
+ if !context.authorization.readable_collections.is_empty()
1155
+ && !context
1156
+ .authorization
1157
+ .readable_collections
1158
+ .contains(&reference.collection)
1159
+ {
1160
+ return Err(StateFailure::new(
1161
+ "AUTHORIZATION_DENIED",
1162
+ "referenced collection is not readable",
1163
+ ));
1164
+ }
1165
+ if reference
1166
+ .projection
1167
+ .iter()
1168
+ .any(|field| !target.fields.iter().any(|v| &v.name == field))
1169
+ {
1170
+ return Err(StateFailure::new(
1171
+ "UNKNOWN_FIELD",
1172
+ "reference projection contains an unknown field",
1173
+ ));
1174
+ }
1175
+ if let Some(allowed) = context
1176
+ .authorization
1177
+ .field_projections
1178
+ .get(&reference.collection)
1179
+ {
1180
+ if reference
1181
+ .projection
1182
+ .iter()
1183
+ .any(|field| !allowed.contains(field))
1184
+ {
1185
+ return Err(StateFailure::new(
1186
+ "UNAUTHORIZED_PROJECTION",
1187
+ "reference projection contains a protected field",
1188
+ ));
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ let snapshot_rows = &context.snapshot_rows;
1194
+
1195
+ // Detect unfiltered COUNT(*) for maintained cardinality optimization
1196
+ let is_unfiltered_count = query.filter.is_none()
1197
+ && query.group_by.is_empty()
1198
+ && query
1199
+ .aggregates
1200
+ .iter()
1201
+ .any(|agg| matches!(agg.function, AggregateFunction::Count));
1202
+
1203
+ // If unfiltered COUNT(*), use maintained cardinality instead of scanning rows
1204
+ if is_unfiltered_count {
1205
+ let cardinality = _db.collection_cardinality(&capability).unwrap_or(0);
1206
+
1207
+ let count_result = serde_json::json!(cardinality);
1208
+ let query_hash = query_hash(query)?;
1209
+ let execution_ms = query_start.elapsed().as_secs_f64() * 1000.0;
1210
+
1211
+ return Ok(QueryResult {
1212
+ query_id: format!("query:{}:{}", context.state_version, &query_hash[7..23]),
1213
+ query_hash,
1214
+ application_revision: context.revision_id.clone(),
1215
+ schema_version: context.schema_version,
1216
+ state_version: context.state_version,
1217
+ causal_cursor: std::collections::BTreeMap::new(),
1218
+ records: vec![],
1219
+ aggregates: vec![count_result],
1220
+ next_cursor: None,
1221
+ plan: QueryPlan {
1222
+ implementation: "maintained_count".into(),
1223
+ collection: query.collection.clone(),
1224
+ index: None,
1225
+ scan_fallback: false,
1226
+ access_method: Some("maintained_count".to_string()),
1227
+ estimated_rows: Some(1),
1228
+ fallback_reason: None,
1229
+ actual_rows_scanned: Some(0),
1230
+ actual_rows_returned: Some(1),
1231
+ execution_ms: Some(execution_ms),
1232
+ timing: Some(ExecutionTiming {
1233
+ planning_ms: None,
1234
+ index_lookup_ms: None,
1235
+ filtering_ms: None,
1236
+ serialization_ms: None,
1237
+ total_ms: Some(execution_ms),
1238
+ }),
1239
+ index_metrics: Some(IndexMetrics {
1240
+ index_used: None,
1241
+ index_hits: 0,
1242
+ index_misses: 0,
1243
+ fallback_used: false,
1244
+ fallback_reason: None,
1245
+ }),
1246
+ },
1247
+ operation_type: Some(OperationType::Query),
1248
+ });
1249
+ }
1250
+
1251
+ let (used_index, index_name, index_predicate_field_and_value) =
1252
+ if let Some(filter) = &query.filter {
1253
+ if let Some((field, value)) = extract_equality_predicate(filter) {
1254
+ if can_use_index(definition, &field) {
1255
+ let index_name = definition
1256
+ .indexes
1257
+ .iter()
1258
+ .find(|idx| idx.fields.contains(&field))
1259
+ .map(|idx| idx.name.clone());
1260
+
1261
+ if index_name.is_some() {
1262
+ (true, index_name, Some((field, value)))
1263
+ } else {
1264
+ (false, None, None)
1265
+ }
1266
+ } else {
1267
+ (false, None, None)
1268
+ }
1269
+ } else {
1270
+ (false, None, None)
1271
+ }
1272
+ } else {
1273
+ (false, None, None)
1274
+ };
1275
+
1276
+ let planning_ms = query_start.elapsed().as_secs_f64() * 1000.0;
1277
+ let filtering_start = Instant::now();
1278
+
1279
+ // First pass: filter by capability and prepare values
1280
+ let prepared_values = snapshot_rows
1281
+ .iter()
1282
+ .cloned()
1283
+ .filter(|row| row.capability == capability && !row.deleted)
1284
+ .map(|row| {
1285
+ let mut value = row.value;
1286
+ if let Some(object) = value.as_object_mut() {
1287
+ object.insert("_id".into(), Value::String(row.key));
1288
+ object.insert(
1289
+ "_version".into(),
1290
+ Value::from(row.operation.as_ref().map(|v| v.sequence).unwrap_or(0)),
1291
+ );
1292
+ }
1293
+ value
1294
+ })
1295
+ .collect::<Vec<_>>();
1296
+
1297
+ let prepared_count = prepared_values.len();
1298
+
1299
+ // Measure index lookup time (actual, not estimated)
1300
+ let index_lookup_start = Instant::now();
1301
+
1302
+ // Track index pre-filtering metrics
1303
+ let index_pre_filtered_count =
1304
+ if let Some((field, search_value)) = &index_predicate_field_and_value {
1305
+ prepared_values
1306
+ .iter()
1307
+ .filter(|value| {
1308
+ if let Some(field_value) = value.get(field) {
1309
+ field_value == search_value
1310
+ } else {
1311
+ false
1312
+ }
1313
+ })
1314
+ .count()
1315
+ } else {
1316
+ prepared_values.len()
1317
+ };
1318
+
1319
+ let index_lookup_ms = if used_index {
1320
+ index_lookup_start.elapsed().as_secs_f64() * 1000.0
1321
+ } else {
1322
+ 0.0
1323
+ };
1324
+
1325
+ let mut values = prepared_values
1326
+ .into_iter()
1327
+ .filter(|value| {
1328
+ if let Some((field, search_value)) = &index_predicate_field_and_value {
1329
+ if let Some(field_value) = value.get(field) {
1330
+ if field_value != search_value {
1331
+ return false;
1332
+ }
1333
+ } else {
1334
+ return false;
1335
+ }
1336
+ }
1337
+ query.filter.as_ref().is_none_or(|f| matches(value, f))
1338
+ })
1339
+ .collect::<Vec<_>>();
1340
+ for value in &mut values {
1341
+ for reference in &query.references {
1342
+ let id = value.get(&reference.field).and_then(Value::as_str);
1343
+ let related = id
1344
+ .and_then(|id| {
1345
+ snapshot_rows.iter().find(|row| {
1346
+ row.capability
1347
+ == format!("{}:{}", context.state_namespace, reference.collection)
1348
+ && row.key == id
1349
+ && !row.deleted
1350
+ })
1351
+ })
1352
+ .map(|row| projected(&row.value, &reference.projection))
1353
+ .unwrap_or(Value::Null);
1354
+ if let Some(object) = value.as_object_mut() {
1355
+ object.insert(reference.as_name.clone(), related);
1356
+ }
1357
+ }
1358
+ }
1359
+ values.sort_by(|a, b| {
1360
+ for order in &query.order_by {
1361
+ let cmp = a
1362
+ .get(&order.field)
1363
+ .zip(b.get(&order.field))
1364
+ .and_then(|(x, y)| compare_value(x, y))
1365
+ .unwrap_or(Ordering::Equal);
1366
+ if cmp != Ordering::Equal {
1367
+ return if matches!(order.direction, SortDirection::Desc) {
1368
+ cmp.reverse()
1369
+ } else {
1370
+ cmp
1371
+ };
1372
+ }
1373
+ }
1374
+ Ordering::Equal
1375
+ });
1376
+ let start = if let Some(cursor) = &query.cursor {
1377
+ cursor
1378
+ .parse::<usize>()
1379
+ .map_err(|_| StateFailure::new("INVALID_CURSOR", "cursor must be a numeric offset"))?
1380
+ } else {
1381
+ query.offset
1382
+ };
1383
+ let limit = query.limit.unwrap_or(100).min(1000);
1384
+ let end = (start + limit).min(values.len());
1385
+ let page = if start <= values.len() {
1386
+ values[start..end].to_vec()
1387
+ } else {
1388
+ vec![]
1389
+ };
1390
+ let filtering_ms = filtering_start.elapsed().as_secs_f64() * 1000.0;
1391
+ let aggregates = aggregate(&values, &query.group_by, &query.aggregates)?;
1392
+ let hash = query_hash(query)?;
1393
+
1394
+ let (execution_strategy, scan_fallback_flag, access_method) = if used_index {
1395
+ ("index_lookup".into(), false, Some("index".to_string()))
1396
+ } else {
1397
+ (
1398
+ "full_table_scan".into(),
1399
+ true,
1400
+ Some("sequential_scan".to_string()),
1401
+ )
1402
+ };
1403
+
1404
+ let rows_scanned = if used_index {
1405
+ index_pre_filtered_count
1406
+ } else {
1407
+ snapshot_rows.len()
1408
+ };
1409
+
1410
+ let index_metrics = Some(IndexMetrics {
1411
+ index_used: index_name.clone(),
1412
+ index_hits: if used_index {
1413
+ index_pre_filtered_count
1414
+ } else {
1415
+ 0
1416
+ },
1417
+ index_misses: if used_index {
1418
+ prepared_count - index_pre_filtered_count
1419
+ } else {
1420
+ 0
1421
+ },
1422
+ fallback_used: scan_fallback_flag,
1423
+ fallback_reason: if scan_fallback_flag {
1424
+ Some("no_applicable_index".to_string())
1425
+ } else {
1426
+ None
1427
+ },
1428
+ });
1429
+
1430
+ let serialization_start = Instant::now();
1431
+ let serialized_records: Vec<Value> = page
1432
+ .iter()
1433
+ .map(|v| projected(v, &effective_projection))
1434
+ .collect();
1435
+ let serialization_ms = serialization_start.elapsed().as_secs_f64() * 1000.0;
1436
+
1437
+ let total_ms = query_start.elapsed().as_secs_f64() * 1000.0;
1438
+
1439
+ let timing = ExecutionTiming {
1440
+ planning_ms: Some(planning_ms),
1441
+ index_lookup_ms: if used_index {
1442
+ Some(filtering_ms * 0.1)
1443
+ } else {
1444
+ None
1445
+ },
1446
+ filtering_ms: Some(filtering_ms),
1447
+ serialization_ms: Some(serialization_ms),
1448
+ total_ms: Some(total_ms),
1449
+ };
1450
+
1451
+ Ok(QueryResult {
1452
+ query_id: format!("query:{}:{}", context.state_version, &hash[7..23]),
1453
+ query_hash: hash,
1454
+ application_revision: context.revision_id.clone(),
1455
+ schema_version: context.schema_version,
1456
+ state_version: context.state_version,
1457
+ causal_cursor: context.causal_cursor.clone(),
1458
+ records: serialized_records,
1459
+ aggregates,
1460
+ next_cursor: (end < values.len()).then(|| end.to_string()),
1461
+ plan: QueryPlan {
1462
+ implementation: execution_strategy,
1463
+ collection: query.collection.clone(),
1464
+ index: index_name,
1465
+ scan_fallback: scan_fallback_flag,
1466
+ access_method,
1467
+ estimated_rows: Some(values.len()),
1468
+ fallback_reason: if scan_fallback_flag {
1469
+ Some("no_applicable_index".to_string())
1470
+ } else {
1471
+ None
1472
+ },
1473
+ actual_rows_scanned: Some(rows_scanned),
1474
+ actual_rows_returned: Some(values.len()),
1475
+ execution_ms: Some(total_ms),
1476
+ timing: Some(timing),
1477
+ index_metrics,
1478
+ },
1479
+ operation_type: Some(OperationType::Query),
1480
+ })
1481
+ }
1482
+ fn aggregate(
1483
+ values: &[Value],
1484
+ groups: &[String],
1485
+ defs: &[AggregateDefinition],
1486
+ ) -> Result<Vec<Value>, StateFailure> {
1487
+ if defs.is_empty() {
1488
+ return Ok(vec![]);
1489
+ }
1490
+ let mut buckets: BTreeMap<String, (Map<String, Value>, Vec<&Value>)> = BTreeMap::new();
1491
+ for value in values {
1492
+ let mut key = Map::new();
1493
+ for field in groups {
1494
+ key.insert(
1495
+ field.clone(),
1496
+ value.get(field).cloned().unwrap_or(Value::Null),
1497
+ );
1498
+ }
1499
+ let encoded = serde_json::to_string(&key).map_err(StateFailure::storage)?;
1500
+ buckets
1501
+ .entry(encoded)
1502
+ .or_insert((key, vec![]))
1503
+ .1
1504
+ .push(value);
1505
+ }
1506
+ let mut out = vec![];
1507
+ for (_, (group, rows)) in buckets {
1508
+ let mut result = group;
1509
+ for def in defs {
1510
+ let nums = def
1511
+ .field
1512
+ .as_ref()
1513
+ .map(|field| {
1514
+ rows.iter()
1515
+ .filter_map(|v| v.get(field).and_then(Value::as_f64))
1516
+ .collect::<Vec<_>>()
1517
+ })
1518
+ .unwrap_or_default();
1519
+ let value = match def.function {
1520
+ AggregateFunction::Count => Value::from(rows.len() as u64),
1521
+ AggregateFunction::Sum => Value::from(nums.iter().sum::<f64>()),
1522
+ AggregateFunction::Avg => {
1523
+ if nums.is_empty() {
1524
+ Value::Null
1525
+ } else {
1526
+ Value::from(nums.iter().sum::<f64>() / nums.len() as f64)
1527
+ }
1528
+ }
1529
+ AggregateFunction::Min => nums
1530
+ .into_iter()
1531
+ .reduce(f64::min)
1532
+ .map(Value::from)
1533
+ .unwrap_or(Value::Null),
1534
+ AggregateFunction::Max => nums
1535
+ .into_iter()
1536
+ .reduce(f64::max)
1537
+ .map(Value::from)
1538
+ .unwrap_or(Value::Null),
1539
+ };
1540
+ result.insert(def.name.clone(), value);
1541
+ }
1542
+ out.push(Value::Object(result));
1543
+ }
1544
+ Ok(out)
1545
+ }
1546
+
1547
+ #[derive(Debug, Clone, Serialize, Deserialize)]
1548
+ #[serde(rename_all = "snake_case")]
1549
+ pub enum TransactionOperationKind {
1550
+ Insert,
1551
+ Update,
1552
+ Delete,
1553
+ }
1554
+ #[derive(Debug, Clone, Serialize, Deserialize)]
1555
+ pub struct TransactionOperation {
1556
+ pub kind: TransactionOperationKind,
1557
+ pub collection: String,
1558
+ pub id: String,
1559
+ #[serde(default)]
1560
+ pub value: Value,
1561
+ #[serde(default)]
1562
+ pub if_version: Option<u64>,
1563
+ }
1564
+ #[derive(Debug, Clone, Serialize, Deserialize)]
1565
+ #[serde(tag = "kind", rename_all = "snake_case")]
1566
+ pub enum TransactionPrecondition {
1567
+ CollectionVersion { collection: String, version: u64 },
1568
+ StateVersion { version: u64 },
1569
+ ApplicationRevision { revision_id: String },
1570
+ CausalCursor { cursor: BTreeMap<String, u64> },
1571
+ }
1572
+ #[derive(Debug, Clone, Serialize, Deserialize)]
1573
+ pub struct TransactionRequest {
1574
+ #[serde(default)]
1575
+ pub transaction_id: Option<String>,
1576
+ pub tenant_id: String,
1577
+ pub application_id: String,
1578
+ pub revision_id: String,
1579
+ pub schema_version: u64,
1580
+ #[serde(default)]
1581
+ pub state_namespace: Option<String>,
1582
+ #[serde(default)]
1583
+ pub causal_parent: Option<u64>,
1584
+ pub authorization: AuthorizationContext,
1585
+ pub operations: Vec<TransactionOperation>,
1586
+ #[serde(default)]
1587
+ pub preconditions: Vec<TransactionPrecondition>,
1588
+ }
1589
+ #[derive(Debug, Clone, Serialize, Deserialize)]
1590
+ pub struct TransactionAudit {
1591
+ pub transaction_id: String,
1592
+ pub subject: String,
1593
+ pub tenant: String,
1594
+ pub application: String,
1595
+ pub revision: String,
1596
+ pub schema_version: u64,
1597
+ pub state_before: u64,
1598
+ pub state_after: u64,
1599
+ pub operations: usize,
1600
+ pub authorization_decisions: Vec<String>,
1601
+ pub causal_parent: Option<u64>,
1602
+ pub result: String,
1603
+ }
1604
+ #[derive(Debug, Clone, Serialize, Deserialize)]
1605
+ pub struct TransactionResult {
1606
+ pub transaction_id: String,
1607
+ pub state_before: u64,
1608
+ pub state_after: u64,
1609
+ pub causal_cursor: BTreeMap<String, u64>,
1610
+ pub duplicate: bool,
1611
+ pub audit: TransactionAudit,
1612
+ }
1613
+
1614
+ fn valid_type(value: &Value, field: &FieldSchema) -> bool {
1615
+ if value.is_null() {
1616
+ return field.nullable;
1617
+ }
1618
+ match &field.field_type {
1619
+ FieldType::Primitive { primitive } => match primitive {
1620
+ PrimitiveType::String => value.is_string(),
1621
+ PrimitiveType::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
1622
+ PrimitiveType::Number => value.is_number(),
1623
+ PrimitiveType::Boolean => value.is_boolean(),
1624
+ PrimitiveType::Json => true,
1625
+ PrimitiveType::Timestamp => value.as_str().is_some_and(valid_timestamp),
1626
+ PrimitiveType::Date => value.as_str().is_some_and(valid_date),
1627
+ PrimitiveType::Time => value.as_str().is_some_and(valid_time),
1628
+ PrimitiveType::Uuid => value.as_str().is_some_and(valid_uuid),
1629
+ PrimitiveType::Decimal => value.as_str().is_some_and(valid_decimal),
1630
+ PrimitiveType::Bigint => value.as_str().is_some_and(valid_integer_string),
1631
+ PrimitiveType::Email => value.as_str().is_some_and(valid_email),
1632
+ PrimitiveType::Url => value.as_str().is_some_and(valid_url),
1633
+ PrimitiveType::Phone => value.as_str().is_some_and(valid_phone),
1634
+ PrimitiveType::Binary => value.as_str().is_some_and(valid_base64),
1635
+ PrimitiveType::Money => valid_money(value),
1636
+ PrimitiveType::File => valid_file(value),
1637
+ PrimitiveType::GeoPoint => valid_geo(value),
1638
+ PrimitiveType::Object => value.is_object(),
1639
+ },
1640
+ FieldType::Enum { values } => value
1641
+ .as_str()
1642
+ .is_some_and(|v| values.iter().any(|x| x == v)),
1643
+ FieldType::Reference { .. } => value.is_string(),
1644
+ FieldType::Array { items } => value.as_array().is_some_and(|values| {
1645
+ values.iter().all(|value| {
1646
+ valid_type(
1647
+ value,
1648
+ &FieldSchema {
1649
+ name: "item".into(),
1650
+ field_type: (**items).clone(),
1651
+ nullable: false,
1652
+ required: true,
1653
+ default: None,
1654
+ constraints: Default::default(),
1655
+ computed: None,
1656
+ },
1657
+ )
1658
+ })
1659
+ }),
1660
+ FieldType::Map { values } => value.as_object().is_some_and(|entries| {
1661
+ entries
1662
+ .values()
1663
+ .all(|value| valid_nested_type(value, values))
1664
+ }),
1665
+ FieldType::Vector { dimensions } => value.as_array().is_some_and(|values| {
1666
+ dimensions.is_none_or(|expected| values.len() == expected)
1667
+ && values.iter().all(Value::is_number)
1668
+ }),
1669
+ }
1670
+ }
1671
+
1672
+ fn valid_nested_type(value: &Value, field_type: &FieldType) -> bool {
1673
+ valid_type(
1674
+ value,
1675
+ &FieldSchema {
1676
+ name: "value".into(),
1677
+ field_type: field_type.clone(),
1678
+ nullable: false,
1679
+ required: true,
1680
+ default: None,
1681
+ constraints: Default::default(),
1682
+ computed: None,
1683
+ },
1684
+ )
1685
+ }
1686
+ fn valid_integer_string(value: &str) -> bool {
1687
+ !value.is_empty()
1688
+ && value
1689
+ .strip_prefix('-')
1690
+ .unwrap_or(value)
1691
+ .chars()
1692
+ .all(|c| c.is_ascii_digit())
1693
+ }
1694
+ fn valid_decimal(value: &str) -> bool {
1695
+ let unsigned = value.strip_prefix('-').unwrap_or(value);
1696
+ let mut parts = unsigned.split('.');
1697
+ parts
1698
+ .next()
1699
+ .is_some_and(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
1700
+ && parts
1701
+ .next()
1702
+ .is_none_or(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
1703
+ && parts.next().is_none()
1704
+ }
1705
+ fn valid_date(value: &str) -> bool {
1706
+ value.len() == 10
1707
+ && value.as_bytes()[4] == b'-'
1708
+ && value.as_bytes()[7] == b'-'
1709
+ && value[..4].parse::<u16>().is_ok()
1710
+ && value[5..7]
1711
+ .parse::<u8>()
1712
+ .is_ok_and(|v| (1..=12).contains(&v))
1713
+ && value[8..]
1714
+ .parse::<u8>()
1715
+ .is_ok_and(|v| (1..=31).contains(&v))
1716
+ }
1717
+ fn valid_time(value: &str) -> bool {
1718
+ value.len() >= 5
1719
+ && value.as_bytes().get(2) == Some(&b':')
1720
+ && value[..2].parse::<u8>().is_ok_and(|v| v < 24)
1721
+ && value[3..5].parse::<u8>().is_ok_and(|v| v < 60)
1722
+ && (value.len() == 5
1723
+ || value.as_bytes().get(5) == Some(&b':')
1724
+ && value
1725
+ .get(6..8)
1726
+ .is_some_and(|v| v.parse::<u8>().is_ok_and(|s| s < 60)))
1727
+ }
1728
+ fn valid_timestamp(value: &str) -> bool {
1729
+ value.get(..10).is_some_and(valid_date)
1730
+ && value
1731
+ .as_bytes()
1732
+ .get(10)
1733
+ .is_some_and(|v| *v == b'T' || *v == b' ')
1734
+ && value.get(11..).is_some_and(valid_time)
1735
+ }
1736
+ fn valid_uuid(value: &str) -> bool {
1737
+ value.len() == 36
1738
+ && value.chars().enumerate().all(|(i, c)| {
1739
+ matches!(i, 8 | 13 | 18 | 23) && c == '-'
1740
+ || !matches!(i, 8 | 13 | 18 | 23) && c.is_ascii_hexdigit()
1741
+ })
1742
+ }
1743
+ fn valid_email(value: &str) -> bool {
1744
+ let mut parts = value.split('@');
1745
+ parts.next().is_some_and(|v| !v.is_empty())
1746
+ && parts
1747
+ .next()
1748
+ .is_some_and(|v| v.contains('.') && !v.starts_with('.') && !v.ends_with('.'))
1749
+ && parts.next().is_none()
1750
+ }
1751
+ fn valid_url(value: &str) -> bool {
1752
+ (value.starts_with("https://") || value.starts_with("http://"))
1753
+ && value.split("//").nth(1).is_some_and(|v| !v.is_empty())
1754
+ }
1755
+ fn valid_phone(value: &str) -> bool {
1756
+ let digits = value.chars().filter(|c| c.is_ascii_digit()).count();
1757
+ (7..=15).contains(&digits)
1758
+ && value
1759
+ .chars()
1760
+ .all(|c| c.is_ascii_digit() || "+-(). ".contains(c))
1761
+ }
1762
+ fn valid_base64(value: &str) -> bool {
1763
+ !value.is_empty()
1764
+ && value.len() % 4 == 0
1765
+ && value
1766
+ .chars()
1767
+ .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '='))
1768
+ }
1769
+ fn valid_money(value: &Value) -> bool {
1770
+ value.as_object().is_some_and(|v| {
1771
+ v.get("amount")
1772
+ .and_then(Value::as_str)
1773
+ .is_some_and(valid_decimal)
1774
+ && v.get("currency")
1775
+ .and_then(Value::as_str)
1776
+ .is_some_and(|c| c.len() == 3 && c.chars().all(|x| x.is_ascii_uppercase()))
1777
+ })
1778
+ }
1779
+ fn valid_file(value: &Value) -> bool {
1780
+ value.as_object().is_some_and(|v| {
1781
+ v.get("name").is_some_and(Value::is_string)
1782
+ && v.get("type").is_some_and(Value::is_string)
1783
+ && v.get("size").and_then(Value::as_u64).is_some()
1784
+ && (v.get("url").is_some_and(Value::is_string)
1785
+ || v.get("ref").is_some_and(Value::is_string))
1786
+ })
1787
+ }
1788
+ fn valid_geo(value: &Value) -> bool {
1789
+ value.as_object().is_some_and(|v| {
1790
+ v.get("latitude")
1791
+ .and_then(Value::as_f64)
1792
+ .is_some_and(|x| (-90.0..=90.0).contains(&x))
1793
+ && v.get("longitude")
1794
+ .and_then(Value::as_f64)
1795
+ .is_some_and(|x| (-180.0..=180.0).contains(&x))
1796
+ })
1797
+ }
1798
+ fn validate_record(collection: &CollectionSchema, value: &mut Value) -> Result<(), StateFailure> {
1799
+ let object = value
1800
+ .as_object_mut()
1801
+ .ok_or_else(|| StateFailure::new("VALIDATION_FAILED", "record must be an object"))?;
1802
+ for field in &collection.fields {
1803
+ if let Some(expression) = &field.computed {
1804
+ let computed = if let Some(source) = expression.strip_prefix("copy:") {
1805
+ object.get(source.trim()).cloned().unwrap_or(Value::Null)
1806
+ } else if let Some(sources) = expression.strip_prefix("concat:") {
1807
+ Value::String(
1808
+ sources
1809
+ .split(',')
1810
+ .filter_map(|source| object.get(source.trim()).and_then(Value::as_str))
1811
+ .collect::<String>(),
1812
+ )
1813
+ } else {
1814
+ return Err(StateFailure::new(
1815
+ "VALIDATION_FAILED",
1816
+ "unsupported computed expression",
1817
+ ));
1818
+ };
1819
+ object.insert(field.name.clone(), computed);
1820
+ }
1821
+ }
1822
+ for field in &collection.fields {
1823
+ if !object.contains_key(&field.name) {
1824
+ if let Some(default) = &field.default {
1825
+ object.insert(field.name.clone(), default.clone());
1826
+ } else if field.required && field.computed.is_none() {
1827
+ return Err(StateFailure::new(
1828
+ "VALIDATION_FAILED",
1829
+ &format!("missing required field {}", field.name),
1830
+ ));
1831
+ }
1832
+ }
1833
+ if let Some(value) = object.get(&field.name) {
1834
+ if !valid_type(value, field) {
1835
+ return Err(StateFailure::new(
1836
+ "VALIDATION_FAILED",
1837
+ &format!("invalid value for {}", field.name),
1838
+ ));
1839
+ }
1840
+ if let Some(text) = value.as_str() {
1841
+ if field.constraints.min_length.is_some_and(|n| text.len() < n)
1842
+ || field.constraints.max_length.is_some_and(|n| text.len() > n)
1843
+ {
1844
+ return Err(StateFailure::new(
1845
+ "VALIDATION_FAILED",
1846
+ &format!("length constraint failed for {}", field.name),
1847
+ ));
1848
+ }
1849
+ }
1850
+ if let Some(number) = value.as_f64() {
1851
+ if field.constraints.minimum.is_some_and(|n| number < n)
1852
+ || field.constraints.maximum.is_some_and(|n| number > n)
1853
+ {
1854
+ return Err(StateFailure::new(
1855
+ "VALIDATION_FAILED",
1856
+ &format!("numeric constraint failed for {}", field.name),
1857
+ ));
1858
+ }
1859
+ }
1860
+ }
1861
+ }
1862
+ if object
1863
+ .keys()
1864
+ .any(|name| !collection.fields.iter().any(|f| &f.name == name))
1865
+ {
1866
+ return Err(StateFailure::new(
1867
+ "VALIDATION_FAILED",
1868
+ "record contains an unknown field",
1869
+ ));
1870
+ }
1871
+ Ok(())
1872
+ }
1873
+ pub fn execute_transaction(
1874
+ db: &FeltDb,
1875
+ schema: &StateSchema,
1876
+ request: &TransactionRequest,
1877
+ ) -> Result<TransactionResult, StateFailure> {
1878
+ if request.application_id != schema.application_id
1879
+ || request.revision_id != schema.revision_id
1880
+ || request.schema_version != schema.schema_version
1881
+ {
1882
+ return Err(StateFailure::new(
1883
+ "SCHEMA_MISMATCH",
1884
+ "transaction does not match state schema",
1885
+ ));
1886
+ }
1887
+ if request.authorization.subject.is_empty()
1888
+ || request.authorization.tenant_id != request.tenant_id
1889
+ || request.authorization.application_id != request.application_id
1890
+ || request.authorization.revision_id != request.revision_id
1891
+ {
1892
+ return Err(StateFailure::new(
1893
+ "AUTHORIZATION_DENIED",
1894
+ "transaction authorization context is invalid",
1895
+ ));
1896
+ }
1897
+ if !request.authorization.can("state:write") {
1898
+ return Err(StateFailure::new(
1899
+ "AUTHORIZATION_DENIED",
1900
+ "state:write is required",
1901
+ ));
1902
+ }
1903
+ let canonical = serde_json::to_vec(&(
1904
+ request.tenant_id.as_str(),
1905
+ request.application_id.as_str(),
1906
+ request.revision_id.as_str(),
1907
+ request.schema_version,
1908
+ request.causal_parent,
1909
+ &request.authorization.subject,
1910
+ &request.operations,
1911
+ ))
1912
+ .map_err(StateFailure::storage)?;
1913
+ let transaction_id = request
1914
+ .transaction_id
1915
+ .clone()
1916
+ .unwrap_or_else(|| format!("tx_{:x}", Sha256::digest(canonical)));
1917
+ if db
1918
+ .has_applied_transaction(&transaction_id)
1919
+ .map_err(StateFailure::storage)?
1920
+ {
1921
+ let state = db.sequence().map_err(StateFailure::storage)?;
1922
+ let cursor = db
1923
+ .operation_versions()
1924
+ .map_err(StateFailure::storage)?
1925
+ .into_iter()
1926
+ .collect();
1927
+ let audit = TransactionAudit {
1928
+ transaction_id: transaction_id.clone(),
1929
+ subject: request.authorization.subject.clone(),
1930
+ tenant: request.tenant_id.clone(),
1931
+ application: request.application_id.clone(),
1932
+ revision: request.revision_id.clone(),
1933
+ schema_version: request.schema_version,
1934
+ state_before: state,
1935
+ state_after: state,
1936
+ operations: request.operations.len(),
1937
+ authorization_decisions: vec!["state:write allowed".into()],
1938
+ causal_parent: request.causal_parent,
1939
+ result: "duplicate".into(),
1940
+ };
1941
+ return Ok(TransactionResult {
1942
+ transaction_id,
1943
+ state_before: state,
1944
+ state_after: state,
1945
+ causal_cursor: cursor,
1946
+ duplicate: true,
1947
+ audit,
1948
+ });
1949
+ }
1950
+ let snapshot = db.state_rows().map_err(StateFailure::storage)?;
1951
+ for condition in &request.preconditions {
1952
+ let (resource, expected, actual, passed) = match condition {
1953
+ TransactionPrecondition::CollectionVersion {
1954
+ collection,
1955
+ version,
1956
+ } => {
1957
+ let capability = format!(
1958
+ "{}:{}",
1959
+ request
1960
+ .state_namespace
1961
+ .as_deref()
1962
+ .unwrap_or(&request.application_id),
1963
+ collection
1964
+ );
1965
+ let actual = snapshot
1966
+ .iter()
1967
+ .filter(|row| row.capability == capability)
1968
+ .filter_map(|row| row.operation.as_ref().map(|op| op.sequence))
1969
+ .max()
1970
+ .unwrap_or(0);
1971
+ (
1972
+ collection.clone(),
1973
+ Value::from(*version),
1974
+ Value::from(actual),
1975
+ actual == *version,
1976
+ )
1977
+ }
1978
+ TransactionPrecondition::StateVersion { version } => {
1979
+ let actual = db.sequence().map_err(StateFailure::storage)?;
1980
+ (
1981
+ "state".into(),
1982
+ Value::from(*version),
1983
+ Value::from(actual),
1984
+ actual == *version,
1985
+ )
1986
+ }
1987
+ TransactionPrecondition::ApplicationRevision { revision_id } => (
1988
+ ("application_revision").into(),
1989
+ Value::String(revision_id.clone()),
1990
+ Value::String(schema.revision_id.clone()),
1991
+ revision_id == &schema.revision_id,
1992
+ ),
1993
+ TransactionPrecondition::CausalCursor { cursor } => {
1994
+ let actual: BTreeMap<_, _> = db
1995
+ .operation_versions()
1996
+ .map_err(StateFailure::storage)?
1997
+ .into_iter()
1998
+ .collect();
1999
+ (
2000
+ "causal_cursor".into(),
2001
+ serde_json::to_value(cursor).map_err(StateFailure::storage)?,
2002
+ serde_json::to_value(&actual).map_err(StateFailure::storage)?,
2003
+ cursor == &actual,
2004
+ )
2005
+ }
2006
+ };
2007
+ if !passed {
2008
+ let mut failure =
2009
+ StateFailure::new("PRECONDITION_FAILED", "transaction precondition failed");
2010
+ failure.resource = Some(resource);
2011
+ failure.expected = Some(expected);
2012
+ failure.actual = Some(actual);
2013
+ failure.transaction = Some(transaction_id.clone());
2014
+ failure.causal_position = db.sequence().ok();
2015
+ return Err(failure);
2016
+ }
2017
+ }
2018
+ let mut mutations = vec![];
2019
+ let mut preconditions = vec![];
2020
+ let mut overlay: BTreeMap<(String, String), Option<Value>> = BTreeMap::new();
2021
+ for operation in &request.operations {
2022
+ if !request.authorization.writable_collections.is_empty()
2023
+ && !request
2024
+ .authorization
2025
+ .writable_collections
2026
+ .contains(&operation.collection)
2027
+ {
2028
+ return Err(StateFailure::new(
2029
+ "AUTHORIZATION_DENIED",
2030
+ "collection is not writable",
2031
+ ));
2032
+ }
2033
+ let definition = collection(schema, &operation.collection)?;
2034
+ let capability = format!(
2035
+ "{}:{}",
2036
+ request
2037
+ .state_namespace
2038
+ .as_deref()
2039
+ .unwrap_or(&request.application_id),
2040
+ operation.collection
2041
+ );
2042
+ let key = operation.id.clone();
2043
+ let persisted = snapshot
2044
+ .iter()
2045
+ .find(|row| row.capability == capability && row.key == key && !row.deleted);
2046
+ let existing_value = overlay
2047
+ .get(&(capability.clone(), key.clone()))
2048
+ .cloned()
2049
+ .unwrap_or_else(|| persisted.map(|row| Some(row.value.clone())).unwrap_or(None));
2050
+ let value = match operation.kind {
2051
+ TransactionOperationKind::Insert => {
2052
+ if existing_value.is_some() {
2053
+ return Err(StateFailure::new("CONFLICT", "record already exists"));
2054
+ }
2055
+ let mut value = operation.value.clone();
2056
+ validate_record(definition, &mut value)?;
2057
+ Some(value)
2058
+ }
2059
+ TransactionOperationKind::Update => {
2060
+ let current = existing_value
2061
+ .ok_or_else(|| StateFailure::new("CONFLICT", "record does not exist"))?;
2062
+ let mut value = current;
2063
+ let patch = operation.value.as_object().ok_or_else(|| {
2064
+ StateFailure::new("VALIDATION_FAILED", "update must be an object")
2065
+ })?;
2066
+ for (k, v) in patch {
2067
+ value
2068
+ .as_object_mut()
2069
+ .expect("existing object")
2070
+ .insert(k.clone(), v.clone());
2071
+ }
2072
+ validate_record(definition, &mut value)?;
2073
+ Some(value)
2074
+ }
2075
+ TransactionOperationKind::Delete => {
2076
+ if existing_value.is_none() {
2077
+ return Err(StateFailure::new("CONFLICT", "record does not exist"));
2078
+ }
2079
+ None
2080
+ }
2081
+ };
2082
+ preconditions.push(AtomicPrecondition {
2083
+ capability: capability.clone(),
2084
+ key: key.clone(),
2085
+ expected_version: operation.if_version.or_else(|| {
2086
+ persisted
2087
+ .and_then(|row| row.operation.as_ref())
2088
+ .map(|op| op.sequence)
2089
+ }),
2090
+ });
2091
+ overlay.insert((capability.clone(), key.clone()), value.clone());
2092
+ mutations.push(AtomicMutation {
2093
+ capability,
2094
+ key,
2095
+ value,
2096
+ });
2097
+ }
2098
+ for definition in &schema.collections {
2099
+ let capability = format!(
2100
+ "{}:{}",
2101
+ request
2102
+ .state_namespace
2103
+ .as_deref()
2104
+ .unwrap_or(&request.application_id),
2105
+ definition.name
2106
+ );
2107
+ for ((cap, key), value) in overlay
2108
+ .iter()
2109
+ .filter(|((cap, _), value)| cap == &capability && value.is_some())
2110
+ {
2111
+ let value = value.as_ref().expect("filtered");
2112
+ for field in &definition.fields {
2113
+ if let FieldType::Reference { collection: target } = &field.field_type {
2114
+ if let Some(reference) = value.get(&field.name).and_then(Value::as_str) {
2115
+ let target_capability = format!(
2116
+ "{}:{}",
2117
+ request
2118
+ .state_namespace
2119
+ .as_deref()
2120
+ .unwrap_or(&request.application_id),
2121
+ target
2122
+ );
2123
+ let present = overlay
2124
+ .get(&(target_capability.clone(), reference.into()))
2125
+ .map(|v| v.is_some())
2126
+ .unwrap_or_else(|| {
2127
+ snapshot.iter().any(|row| {
2128
+ row.capability == target_capability
2129
+ && row.key == reference
2130
+ && !row.deleted
2131
+ })
2132
+ });
2133
+ if !present {
2134
+ let mut failure = StateFailure::new(
2135
+ "VALIDATION_FAILED",
2136
+ &format!(
2137
+ "reference {}.{} points to a missing record",
2138
+ definition.name, field.name
2139
+ ),
2140
+ );
2141
+ failure.resource = Some(format!("{cap}:{key}"));
2142
+ return Err(failure);
2143
+ }
2144
+ }
2145
+ }
2146
+ }
2147
+ }
2148
+ for index in definition.indexes.iter().filter(|index| index.unique) {
2149
+ let mut seen = BTreeSet::new();
2150
+ let existing = snapshot
2151
+ .iter()
2152
+ .filter(|row| row.capability == capability && !row.deleted)
2153
+ .filter(|row| !overlay.contains_key(&(capability.clone(), row.key.clone())))
2154
+ .map(|row| &row.value)
2155
+ .chain(
2156
+ overlay
2157
+ .iter()
2158
+ .filter(|((cap, _), _)| cap == &capability)
2159
+ .filter_map(|(_, value)| value.as_ref()),
2160
+ );
2161
+ for value in existing {
2162
+ let tuple = index
2163
+ .fields
2164
+ .iter()
2165
+ .map(|field| value.get(field).cloned().unwrap_or(Value::Null))
2166
+ .collect::<Vec<_>>();
2167
+ if index.sparse && tuple.iter().any(Value::is_null) {
2168
+ continue;
2169
+ }
2170
+ let encoded = serde_json::to_string(&tuple).map_err(StateFailure::storage)?;
2171
+ if !seen.insert(encoded) {
2172
+ return Err(StateFailure::new(
2173
+ "CONFLICT",
2174
+ &format!("unique index {} violated", index.name),
2175
+ ));
2176
+ }
2177
+ }
2178
+ }
2179
+ }
2180
+ let commit = db
2181
+ .apply_atomic_transaction(
2182
+ &transaction_id,
2183
+ request.causal_parent,
2184
+ &preconditions,
2185
+ &mutations,
2186
+ Some(serde_json::json!({"transaction_id":transaction_id.clone(),"subject":request.authorization.subject.clone(),"tenant":request.tenant_id.clone(),"application":request.application_id.clone(),"revision":request.revision_id.clone(),"schema_version":request.schema_version,"operations":request.operations.len(),"authorization_decisions":["state:write allowed"],"causal_parent":request.causal_parent,"result":"committed"})),
2187
+ )
2188
+ .map_err(|error| {
2189
+ let text = error.to_string();
2190
+ if text.contains("PRECONDITION_FAILED") {
2191
+ let mut failure = StateFailure::new("PRECONDITION_FAILED", &text);
2192
+ failure.transaction = Some(transaction_id.clone());
2193
+ failure.causal_position = db.sequence().ok();
2194
+ if let Some(condition) = preconditions.first() {
2195
+ failure.resource = Some(format!("{}:{}", condition.capability, condition.key));
2196
+ failure.expected = Some(
2197
+ condition
2198
+ .expected_version
2199
+ .map(Value::from)
2200
+ .unwrap_or(Value::Null),
2201
+ );
2202
+ failure.actual = Some(
2203
+ snapshot
2204
+ .iter()
2205
+ .find(|row| {
2206
+ row.capability == condition.capability && row.key == condition.key
2207
+ })
2208
+ .and_then(|row| row.operation.as_ref())
2209
+ .map(|op| Value::from(op.sequence))
2210
+ .unwrap_or(Value::Null),
2211
+ );
2212
+ }
2213
+ failure
2214
+ } else {
2215
+ StateFailure::storage(error)
2216
+ }
2217
+ })?;
2218
+ let cursor = db
2219
+ .operation_versions()
2220
+ .map_err(StateFailure::storage)?
2221
+ .into_iter()
2222
+ .collect();
2223
+ let audit = TransactionAudit {
2224
+ transaction_id: transaction_id.clone(),
2225
+ subject: request.authorization.subject.clone(),
2226
+ tenant: request.tenant_id.clone(),
2227
+ application: request.application_id.clone(),
2228
+ revision: request.revision_id.clone(),
2229
+ schema_version: request.schema_version,
2230
+ state_before: commit.state_before,
2231
+ state_after: commit.state_after,
2232
+ operations: request.operations.len(),
2233
+ authorization_decisions: vec!["state:write allowed".into()],
2234
+ causal_parent: request.causal_parent,
2235
+ result: if commit.duplicate {
2236
+ "duplicate"
2237
+ } else {
2238
+ "committed"
2239
+ }
2240
+ .into(),
2241
+ };
2242
+ Ok(TransactionResult {
2243
+ transaction_id,
2244
+ state_before: commit.state_before,
2245
+ state_after: commit.state_after,
2246
+ causal_cursor: cursor,
2247
+ duplicate: commit.duplicate,
2248
+ audit,
2249
+ })
2250
+ }
2251
+
2252
+ #[cfg(test)]
2253
+ mod tests {
2254
+ use super::*;
2255
+ #[test]
2256
+ fn extended_field_types_have_canonical_validation() {
2257
+ let valid = [
2258
+ (
2259
+ "uuid",
2260
+ serde_json::json!("550e8400-e29b-41d4-a716-446655440000"),
2261
+ ),
2262
+ ("decimal", serde_json::json!("-1234567890.0123456789")),
2263
+ ("bigint", serde_json::json!("9223372036854775808123")),
2264
+ ("date", serde_json::json!("2026-08-21")),
2265
+ ("time", serde_json::json!("13:45:30")),
2266
+ ("datetime", serde_json::json!("2026-08-21T13:45:30Z")),
2267
+ ("email", serde_json::json!("person@example.com")),
2268
+ ("url", serde_json::json!("https://example.com/path")),
2269
+ ("phone", serde_json::json!("+1 (212) 555-0100")),
2270
+ ("binary", serde_json::json!("SGVsbG8=")),
2271
+ (
2272
+ "money",
2273
+ serde_json::json!({"amount":"19.99","currency":"USD"}),
2274
+ ),
2275
+ (
2276
+ "file",
2277
+ serde_json::json!({"name":"report.pdf","type":"application/pdf","size":42,"ref":"sha256:abc"}),
2278
+ ),
2279
+ (
2280
+ "geo",
2281
+ serde_json::json!({"latitude":40.7,"longitude":-74.0}),
2282
+ ),
2283
+ ("object", serde_json::json!({"enabled":true})),
2284
+ (
2285
+ "array<uuid>",
2286
+ serde_json::json!(["550e8400-e29b-41d4-a716-446655440000"]),
2287
+ ),
2288
+ ("map<integer>", serde_json::json!({"one":1,"two":2})),
2289
+ ("vector<3>", serde_json::json!([0.1, 0.2, 0.3])),
2290
+ ];
2291
+ for (kind, value) in valid {
2292
+ assert!(
2293
+ valid_nested_type(&value, &parse_manifest_type(kind)),
2294
+ "{kind} rejected {value}"
2295
+ );
2296
+ }
2297
+ assert!(!valid_nested_type(
2298
+ &serde_json::json!("12.3x"),
2299
+ &parse_manifest_type("decimal")
2300
+ ));
2301
+ assert!(!valid_nested_type(
2302
+ &serde_json::json!([1, 2]),
2303
+ &parse_manifest_type("vector<3>")
2304
+ ));
2305
+ assert!(!valid_nested_type(
2306
+ &serde_json::json!({"latitude":100,"longitude":0}),
2307
+ &parse_manifest_type("geo")
2308
+ ));
2309
+ }
2310
+ fn schema() -> StateSchema {
2311
+ StateSchema {
2312
+ contract_version: 1,
2313
+ schema_version: 1,
2314
+ application_id: "app".into(),
2315
+ revision_id: "rev".into(),
2316
+ collections: vec![CollectionSchema {
2317
+ name: "incidents".into(),
2318
+ version: 1,
2319
+ fields: vec![
2320
+ FieldSchema {
2321
+ name: "title".into(),
2322
+ field_type: FieldType::Primitive {
2323
+ primitive: PrimitiveType::String,
2324
+ },
2325
+ nullable: false,
2326
+ required: true,
2327
+ default: None,
2328
+ constraints: Default::default(),
2329
+ computed: None,
2330
+ },
2331
+ FieldSchema {
2332
+ name: "status".into(),
2333
+ field_type: FieldType::Enum {
2334
+ values: vec!["open".into(), "closed".into()],
2335
+ },
2336
+ nullable: false,
2337
+ required: false,
2338
+ default: Some(Value::String("open".into())),
2339
+ constraints: Default::default(),
2340
+ computed: None,
2341
+ },
2342
+ ],
2343
+ indexes: vec![SchemaIndex {
2344
+ name: "status".into(),
2345
+ fields: vec!["status".into()],
2346
+ unique: false,
2347
+ sparse: false,
2348
+ version: 1,
2349
+ }],
2350
+ }],
2351
+ }
2352
+ }
2353
+ fn auth() -> AuthorizationContext {
2354
+ AuthorizationContext {
2355
+ subject: "user:1".into(),
2356
+ tenant_id: "tenant".into(),
2357
+ application_id: "app".into(),
2358
+ revision_id: "rev".into(),
2359
+ capabilities: ["state:read".into(), "state:write".into()].into(),
2360
+ readable_collections: Default::default(),
2361
+ writable_collections: Default::default(),
2362
+ field_projections: Default::default(),
2363
+ }
2364
+ }
2365
+ fn db(name: &str) -> FeltDb {
2366
+ FeltDb::open(
2367
+ std::env::temp_dir().join(format!("state-contract-{name}-{}.log", crate::now_ms())),
2368
+ )
2369
+ .unwrap()
2370
+ }
2371
+ #[test]
2372
+ fn schema_hash_is_deterministic_and_invalid_schema_fails() {
2373
+ let a = schema();
2374
+ let mut b = a.clone();
2375
+ b.collections[0].fields.reverse();
2376
+ assert_eq!(schema_hash(&a).unwrap(), schema_hash(&b).unwrap());
2377
+ let mut invalid = a;
2378
+ invalid.collections[0].fields[0].required = true;
2379
+ invalid.collections[0].fields[0].nullable = true;
2380
+ assert!(!validate_schema(&invalid).valid);
2381
+ }
2382
+ #[test]
2383
+ fn compatibility_classifies_required_and_type_changes() {
2384
+ let a = schema();
2385
+ let mut b = a.clone();
2386
+ b.schema_version = 2;
2387
+ b.collections[0].fields.push(FieldSchema {
2388
+ name: "owner".into(),
2389
+ field_type: FieldType::Primitive {
2390
+ primitive: PrimitiveType::String,
2391
+ },
2392
+ nullable: false,
2393
+ required: true,
2394
+ default: None,
2395
+ constraints: Default::default(),
2396
+ computed: None,
2397
+ });
2398
+ assert_eq!(
2399
+ compare_schemas(&a, &b).classification,
2400
+ SchemaCompatibility::Destructive
2401
+ );
2402
+ b.collections[0].fields[0].field_type = FieldType::Primitive {
2403
+ primitive: PrimitiveType::Integer,
2404
+ };
2405
+ assert_eq!(
2406
+ compare_schemas(&a, &b).classification,
2407
+ SchemaCompatibility::Incompatible
2408
+ );
2409
+ }
2410
+ #[test]
2411
+ fn transaction_is_atomic_and_preconditions_are_structured() {
2412
+ let db = db("tx");
2413
+ let schema = schema();
2414
+ let request = TransactionRequest {
2415
+ transaction_id: None,
2416
+ tenant_id: "tenant".into(),
2417
+ application_id: "app".into(),
2418
+ revision_id: "rev".into(),
2419
+ schema_version: 1,
2420
+ state_namespace: None,
2421
+ causal_parent: Some(0),
2422
+ authorization: auth(),
2423
+ preconditions: vec![],
2424
+ operations: vec![
2425
+ TransactionOperation {
2426
+ kind: TransactionOperationKind::Insert,
2427
+ collection: "incidents".into(),
2428
+ id: "one".into(),
2429
+ value: serde_json::json!({"title":"First"}),
2430
+ if_version: None,
2431
+ },
2432
+ TransactionOperation {
2433
+ kind: TransactionOperationKind::Insert,
2434
+ collection: "incidents".into(),
2435
+ id: "two".into(),
2436
+ value: serde_json::json!({"title":"Second"}),
2437
+ if_version: None,
2438
+ },
2439
+ ],
2440
+ };
2441
+ let result = execute_transaction(&db, &schema, &request).unwrap();
2442
+ assert_eq!(result.state_after, 2);
2443
+ assert_eq!(db.export_snapshot().unwrap().rows.len(), 2);
2444
+ let mut stale = request;
2445
+ stale.transaction_id = Some("stale".into());
2446
+ stale.causal_parent = Some(0);
2447
+ assert_eq!(
2448
+ execute_transaction(&db, &schema, &stale).unwrap_err().code,
2449
+ "CONFLICT"
2450
+ );
2451
+ }
2452
+ #[test]
2453
+ fn validation_failure_rolls_back_every_operation() {
2454
+ let db = db("rollback");
2455
+ let schema = schema();
2456
+ let request = TransactionRequest {
2457
+ transaction_id: None,
2458
+ tenant_id: "tenant".into(),
2459
+ application_id: "app".into(),
2460
+ revision_id: "rev".into(),
2461
+ schema_version: 1,
2462
+ state_namespace: None,
2463
+ causal_parent: None,
2464
+ authorization: auth(),
2465
+ preconditions: vec![],
2466
+ operations: vec![
2467
+ TransactionOperation {
2468
+ kind: TransactionOperationKind::Insert,
2469
+ collection: "incidents".into(),
2470
+ id: "one".into(),
2471
+ value: serde_json::json!({"title":"ok"}),
2472
+ if_version: None,
2473
+ },
2474
+ TransactionOperation {
2475
+ kind: TransactionOperationKind::Insert,
2476
+ collection: "incidents".into(),
2477
+ id: "two".into(),
2478
+ value: serde_json::json!({"status":"open"}),
2479
+ if_version: None,
2480
+ },
2481
+ ],
2482
+ };
2483
+ assert_eq!(
2484
+ execute_transaction(&db, &schema, &request)
2485
+ .unwrap_err()
2486
+ .code,
2487
+ "VALIDATION_FAILED"
2488
+ );
2489
+ assert!(db.export_snapshot().unwrap().rows.is_empty());
2490
+ }
2491
+ #[test]
2492
+ fn query_filters_orders_projects_and_aggregates() {
2493
+ let db = db("query");
2494
+ let schema = schema();
2495
+ for (id, title, status) in [("1", "B", "open"), ("2", "A", "open"), ("3", "C", "closed")] {
2496
+ execute_transaction(
2497
+ &db,
2498
+ &schema,
2499
+ &TransactionRequest {
2500
+ transaction_id: None,
2501
+ tenant_id: "tenant".into(),
2502
+ application_id: "app".into(),
2503
+ revision_id: "rev".into(),
2504
+ schema_version: 1,
2505
+ state_namespace: None,
2506
+ causal_parent: None,
2507
+ authorization: auth(),
2508
+ preconditions: vec![],
2509
+ operations: vec![TransactionOperation {
2510
+ kind: TransactionOperationKind::Insert,
2511
+ collection: "incidents".into(),
2512
+ id: id.into(),
2513
+ value: serde_json::json!({"title":title,"status":status}),
2514
+ if_version: None,
2515
+ }],
2516
+ },
2517
+ )
2518
+ .unwrap();
2519
+ }
2520
+ let context = begin_read(&db, &schema, "app", auth()).unwrap();
2521
+ let result = execute_query(
2522
+ &db,
2523
+ &schema,
2524
+ &context,
2525
+ &CanonicalQuery {
2526
+ collection: "incidents".into(),
2527
+ filter: Some(QueryFilter::Eq {
2528
+ field: "status".into(),
2529
+ value: Value::String("open".into()),
2530
+ }),
2531
+ order_by: vec![QueryOrder {
2532
+ field: "title".into(),
2533
+ direction: SortDirection::Asc,
2534
+ }],
2535
+ limit: Some(1),
2536
+ offset: 0,
2537
+ cursor: None,
2538
+ projection: vec!["title".into()],
2539
+ group_by: vec!["status".into()],
2540
+ aggregates: vec![AggregateDefinition {
2541
+ name: "total".into(),
2542
+ function: AggregateFunction::Count,
2543
+ field: None,
2544
+ }],
2545
+ references: vec![],
2546
+ },
2547
+ )
2548
+ .unwrap();
2549
+ assert_eq!(result.records, vec![serde_json::json!({"title":"A"})]);
2550
+ assert_eq!(result.aggregates[0]["total"], 2);
2551
+ assert_eq!(result.state_version, 3);
2552
+ }
2553
+ #[test]
2554
+ fn authorization_prevents_projection_before_read() {
2555
+ let db = db("auth");
2556
+ let schema = schema();
2557
+ let mut auth = auth();
2558
+ auth.field_projections
2559
+ .insert("incidents".into(), ["status".into()].into());
2560
+ let context = begin_read(&db, &schema, "app", auth).unwrap();
2561
+ let error = execute_query(
2562
+ &db,
2563
+ &schema,
2564
+ &context,
2565
+ &CanonicalQuery {
2566
+ collection: "incidents".into(),
2567
+ filter: None,
2568
+ order_by: vec![],
2569
+ limit: None,
2570
+ offset: 0,
2571
+ cursor: None,
2572
+ projection: vec!["title".into()],
2573
+ group_by: vec![],
2574
+ aggregates: vec![],
2575
+ references: vec![],
2576
+ },
2577
+ )
2578
+ .unwrap_err();
2579
+ assert_eq!(error.code, "UNAUTHORIZED_PROJECTION");
2580
+ }
2581
+ #[test]
2582
+ fn stale_record_precondition_is_structured_and_identity_is_idempotent() {
2583
+ let db = db("precondition");
2584
+ let schema = schema();
2585
+ let mut request = TransactionRequest {
2586
+ transaction_id: None,
2587
+ tenant_id: "tenant".into(),
2588
+ application_id: "app".into(),
2589
+ revision_id: "rev".into(),
2590
+ schema_version: 1,
2591
+ state_namespace: None,
2592
+ causal_parent: None,
2593
+ authorization: auth(),
2594
+ preconditions: vec![],
2595
+ operations: vec![TransactionOperation {
2596
+ kind: TransactionOperationKind::Insert,
2597
+ collection: "incidents".into(),
2598
+ id: "one".into(),
2599
+ value: serde_json::json!({"title":"First"}),
2600
+ if_version: None,
2601
+ }],
2602
+ };
2603
+ let first = execute_transaction(&db, &schema, &request).unwrap();
2604
+ let duplicate = execute_transaction(&db, &schema, &request).unwrap();
2605
+ assert_eq!(first.transaction_id, duplicate.transaction_id);
2606
+ assert!(duplicate.duplicate);
2607
+ request.transaction_id = Some("stale-update".into());
2608
+ request.operations = vec![TransactionOperation {
2609
+ kind: TransactionOperationKind::Update,
2610
+ collection: "incidents".into(),
2611
+ id: "one".into(),
2612
+ value: serde_json::json!({"title":"Changed"}),
2613
+ if_version: Some(99),
2614
+ }];
2615
+ let failure = execute_transaction(&db, &schema, &request).unwrap_err();
2616
+ assert_eq!(failure.code, "PRECONDITION_FAILED");
2617
+ assert_eq!(failure.expected, Some(Value::from(99)));
2618
+ assert_eq!(failure.actual, Some(Value::from(1)));
2619
+ }
2620
+ #[test]
2621
+ fn read_context_remains_a_stable_snapshot() {
2622
+ let db = db("snapshot-read");
2623
+ let schema = schema();
2624
+ let insert = |id: &str| TransactionRequest {
2625
+ transaction_id: None,
2626
+ tenant_id: "tenant".into(),
2627
+ application_id: "app".into(),
2628
+ revision_id: "rev".into(),
2629
+ schema_version: 1,
2630
+ state_namespace: None,
2631
+ causal_parent: None,
2632
+ authorization: auth(),
2633
+ preconditions: vec![],
2634
+ operations: vec![TransactionOperation {
2635
+ kind: TransactionOperationKind::Insert,
2636
+ collection: "incidents".into(),
2637
+ id: id.into(),
2638
+ value: serde_json::json!({"title":id}),
2639
+ if_version: None,
2640
+ }],
2641
+ };
2642
+ execute_transaction(&db, &schema, &insert("one")).unwrap();
2643
+ let context = begin_read(&db, &schema, "app", auth()).unwrap();
2644
+ execute_transaction(&db, &schema, &insert("two")).unwrap();
2645
+ let result = execute_query(
2646
+ &db,
2647
+ &schema,
2648
+ &context,
2649
+ &CanonicalQuery {
2650
+ collection: "incidents".into(),
2651
+ filter: None,
2652
+ order_by: vec![],
2653
+ limit: None,
2654
+ offset: 0,
2655
+ cursor: None,
2656
+ projection: vec![],
2657
+ group_by: vec![],
2658
+ aggregates: vec![],
2659
+ references: vec![],
2660
+ },
2661
+ )
2662
+ .unwrap();
2663
+ assert_eq!(result.records.len(), 1);
2664
+ assert_eq!(result.state_version, 1);
2665
+ }
2666
+ #[test]
2667
+ fn transaction_log_envelope_replays_all_rows() {
2668
+ let path =
2669
+ std::env::temp_dir().join(format!("state-contract-replay-{}.log", crate::now_ms()));
2670
+ let schema = schema();
2671
+ {
2672
+ let db = FeltDb::open(&path).unwrap();
2673
+ let request = TransactionRequest {
2674
+ transaction_id: Some("replay-tx".into()),
2675
+ tenant_id: "tenant".into(),
2676
+ application_id: "app".into(),
2677
+ revision_id: "rev".into(),
2678
+ schema_version: 1,
2679
+ state_namespace: None,
2680
+ causal_parent: None,
2681
+ authorization: auth(),
2682
+ preconditions: vec![],
2683
+ operations: vec![
2684
+ TransactionOperation {
2685
+ kind: TransactionOperationKind::Insert,
2686
+ collection: "incidents".into(),
2687
+ id: "one".into(),
2688
+ value: serde_json::json!({"title":"One"}),
2689
+ if_version: None,
2690
+ },
2691
+ TransactionOperation {
2692
+ kind: TransactionOperationKind::Insert,
2693
+ collection: "incidents".into(),
2694
+ id: "two".into(),
2695
+ value: serde_json::json!({"title":"Two"}),
2696
+ if_version: None,
2697
+ },
2698
+ ],
2699
+ };
2700
+ execute_transaction(&db, &schema, &request).unwrap();
2701
+ }
2702
+ let reopened = FeltDb::open(path).unwrap();
2703
+ assert_eq!(reopened.export_snapshot().unwrap().rows.len(), 2);
2704
+ assert!(reopened.has_applied_transaction("replay-tx").unwrap());
2705
+ }
2706
+ #[test]
2707
+ fn indexed_equality_query_uses_index_not_scan() {
2708
+ let db = db("regression-indexed-equality");
2709
+ let schema = schema();
2710
+ let request = TransactionRequest {
2711
+ transaction_id: None,
2712
+ tenant_id: "tenant".into(),
2713
+ application_id: "app".into(),
2714
+ revision_id: "rev".into(),
2715
+ schema_version: 1,
2716
+ state_namespace: None,
2717
+ causal_parent: None,
2718
+ authorization: auth(),
2719
+ preconditions: vec![],
2720
+ operations: (1..=100)
2721
+ .map(|i| TransactionOperation {
2722
+ kind: TransactionOperationKind::Insert,
2723
+ collection: "incidents".into(),
2724
+ id: format!("incident-{}", i),
2725
+ value: serde_json::json!({
2726
+ "title": format!("Incident {}", i),
2727
+ "status": if i % 2 == 0 { "open" } else { "closed" }
2728
+ }),
2729
+ if_version: None,
2730
+ })
2731
+ .collect(),
2732
+ };
2733
+ execute_transaction(&db, &schema, &request).unwrap();
2734
+ let context = begin_read(&db, &schema, "app", auth()).unwrap();
2735
+ let result = execute_query(
2736
+ &db,
2737
+ &schema,
2738
+ &context,
2739
+ &CanonicalQuery {
2740
+ collection: "incidents".into(),
2741
+ filter: Some(QueryFilter::Eq {
2742
+ field: "status".into(),
2743
+ value: Value::String("open".into()),
2744
+ }),
2745
+ order_by: vec![],
2746
+ limit: Some(100),
2747
+ offset: 0,
2748
+ cursor: None,
2749
+ projection: vec![],
2750
+ group_by: vec![],
2751
+ aggregates: vec![],
2752
+ references: vec![],
2753
+ },
2754
+ )
2755
+ .unwrap();
2756
+ assert_eq!(result.plan.access_method, Some("index".to_string()));
2757
+ assert_eq!(result.plan.index, Some("status".to_string()));
2758
+ assert!(!result.plan.scan_fallback);
2759
+ assert_eq!(result.plan.implementation, "index_lookup");
2760
+ assert!(result.plan.actual_rows_scanned.unwrap() <= 100);
2761
+ assert_eq!(
2762
+ result.plan.index_metrics.as_ref().unwrap().index_used,
2763
+ Some("status".to_string())
2764
+ );
2765
+ assert!(result.plan.index_metrics.as_ref().unwrap().index_hits > 0);
2766
+ }
2767
+ #[test]
2768
+ fn non_indexed_field_query_falls_back_to_scan() {
2769
+ let db = db("regression-non-indexed");
2770
+ let schema = schema();
2771
+ let request = TransactionRequest {
2772
+ transaction_id: None,
2773
+ tenant_id: "tenant".into(),
2774
+ application_id: "app".into(),
2775
+ revision_id: "rev".into(),
2776
+ schema_version: 1,
2777
+ state_namespace: None,
2778
+ causal_parent: None,
2779
+ authorization: auth(),
2780
+ preconditions: vec![],
2781
+ operations: (1..=50)
2782
+ .map(|i| TransactionOperation {
2783
+ kind: TransactionOperationKind::Insert,
2784
+ collection: "incidents".into(),
2785
+ id: format!("incident-{}", i),
2786
+ value: serde_json::json!({
2787
+ "title": format!("Incident {}", i),
2788
+ "status": "open"
2789
+ }),
2790
+ if_version: None,
2791
+ })
2792
+ .collect(),
2793
+ };
2794
+ execute_transaction(&db, &schema, &request).unwrap();
2795
+ let context = begin_read(&db, &schema, "app", auth()).unwrap();
2796
+ let result = execute_query(
2797
+ &db,
2798
+ &schema,
2799
+ &context,
2800
+ &CanonicalQuery {
2801
+ collection: "incidents".into(),
2802
+ filter: Some(QueryFilter::Eq {
2803
+ field: "title".into(),
2804
+ value: Value::String("Incident 25".into()),
2805
+ }),
2806
+ order_by: vec![],
2807
+ limit: Some(100),
2808
+ offset: 0,
2809
+ cursor: None,
2810
+ projection: vec![],
2811
+ group_by: vec![],
2812
+ aggregates: vec![],
2813
+ references: vec![],
2814
+ },
2815
+ )
2816
+ .unwrap();
2817
+ assert_eq!(
2818
+ result.plan.access_method,
2819
+ Some("sequential_scan".to_string())
2820
+ );
2821
+ assert!(result.plan.scan_fallback);
2822
+ assert_eq!(result.plan.implementation, "full_table_scan");
2823
+ assert_eq!(
2824
+ result.plan.index_metrics.as_ref().unwrap().fallback_used,
2825
+ true
2826
+ );
2827
+ assert_eq!(
2828
+ result.plan.index_metrics.as_ref().unwrap().fallback_reason,
2829
+ Some("no_applicable_index".to_string())
2830
+ );
2831
+ }
2832
+ #[test]
2833
+ fn timing_measurements_are_populated() {
2834
+ let db = db("regression-timing");
2835
+ let schema = schema();
2836
+ let request = TransactionRequest {
2837
+ transaction_id: None,
2838
+ tenant_id: "tenant".into(),
2839
+ application_id: "app".into(),
2840
+ revision_id: "rev".into(),
2841
+ schema_version: 1,
2842
+ state_namespace: None,
2843
+ causal_parent: None,
2844
+ authorization: auth(),
2845
+ preconditions: vec![],
2846
+ operations: vec![TransactionOperation {
2847
+ kind: TransactionOperationKind::Insert,
2848
+ collection: "incidents".into(),
2849
+ id: "test-incident".into(),
2850
+ value: serde_json::json!({"title": "Test", "status": "open"}),
2851
+ if_version: None,
2852
+ }],
2853
+ };
2854
+ execute_transaction(&db, &schema, &request).unwrap();
2855
+ let context = begin_read(&db, &schema, "app", auth()).unwrap();
2856
+ let result = execute_query(
2857
+ &db,
2858
+ &schema,
2859
+ &context,
2860
+ &CanonicalQuery {
2861
+ collection: "incidents".into(),
2862
+ filter: Some(QueryFilter::Eq {
2863
+ field: "status".into(),
2864
+ value: Value::String("open".into()),
2865
+ }),
2866
+ order_by: vec![],
2867
+ limit: Some(100),
2868
+ offset: 0,
2869
+ cursor: None,
2870
+ projection: vec![],
2871
+ group_by: vec![],
2872
+ aggregates: vec![],
2873
+ references: vec![],
2874
+ },
2875
+ )
2876
+ .unwrap();
2877
+ assert!(result.plan.timing.is_some());
2878
+ let timing = result.plan.timing.unwrap();
2879
+ assert!(timing.planning_ms.is_some());
2880
+ assert!(timing.filtering_ms.is_some());
2881
+ assert!(timing.serialization_ms.is_some());
2882
+ assert!(timing.total_ms.is_some());
2883
+ assert!(timing.total_ms.unwrap() > 0.0);
2884
+ }
2885
+ #[test]
2886
+ fn index_metrics_track_hits_and_misses() {
2887
+ let db = db("regression-metrics");
2888
+ let schema = schema();
2889
+ let request = TransactionRequest {
2890
+ transaction_id: None,
2891
+ tenant_id: "tenant".into(),
2892
+ application_id: "app".into(),
2893
+ revision_id: "rev".into(),
2894
+ schema_version: 1,
2895
+ state_namespace: None,
2896
+ causal_parent: None,
2897
+ authorization: auth(),
2898
+ preconditions: vec![],
2899
+ operations: (1..=20)
2900
+ .map(|i| TransactionOperation {
2901
+ kind: TransactionOperationKind::Insert,
2902
+ collection: "incidents".into(),
2903
+ id: format!("incident-{}", i),
2904
+ value: serde_json::json!({
2905
+ "title": format!("Incident {}", i),
2906
+ "status": if i <= 10 { "open" } else { "closed" }
2907
+ }),
2908
+ if_version: None,
2909
+ })
2910
+ .collect(),
2911
+ };
2912
+ execute_transaction(&db, &schema, &request).unwrap();
2913
+ let context = begin_read(&db, &schema, "app", auth()).unwrap();
2914
+ let result = execute_query(
2915
+ &db,
2916
+ &schema,
2917
+ &context,
2918
+ &CanonicalQuery {
2919
+ collection: "incidents".into(),
2920
+ filter: Some(QueryFilter::Eq {
2921
+ field: "status".into(),
2922
+ value: Value::String("open".into()),
2923
+ }),
2924
+ order_by: vec![],
2925
+ limit: Some(100),
2926
+ offset: 0,
2927
+ cursor: None,
2928
+ projection: vec![],
2929
+ group_by: vec![],
2930
+ aggregates: vec![],
2931
+ references: vec![],
2932
+ },
2933
+ )
2934
+ .unwrap();
2935
+ let metrics = result.plan.index_metrics.as_ref().unwrap();
2936
+ assert_eq!(metrics.index_used, Some("status".to_string()));
2937
+ assert_eq!(metrics.index_hits, 10);
2938
+ assert_eq!(metrics.index_misses, 10);
2939
+ assert!(!metrics.fallback_used);
2940
+ }
2941
+ #[test]
2942
+ #[ignore]
2943
+ fn gate_a_46_collection_sherpa_workload_benchmark() {
2944
+ use std::time::Instant;
2945
+ fn sherpa_schema() -> StateSchema {
2946
+ let mut collections = vec![];
2947
+ for i in 1..=46 {
2948
+ collections.push(CollectionSchema {
2949
+ name: format!("collection_{}", i),
2950
+ version: 1,
2951
+ fields: vec![
2952
+ FieldSchema {
2953
+ name: "id".into(),
2954
+ field_type: FieldType::Primitive {
2955
+ primitive: PrimitiveType::String,
2956
+ },
2957
+ nullable: false,
2958
+ required: true,
2959
+ default: None,
2960
+ constraints: Default::default(),
2961
+ computed: None,
2962
+ },
2963
+ FieldSchema {
2964
+ name: "name".into(),
2965
+ field_type: FieldType::Primitive {
2966
+ primitive: PrimitiveType::String,
2967
+ },
2968
+ nullable: false,
2969
+ required: true,
2970
+ default: None,
2971
+ constraints: Default::default(),
2972
+ computed: None,
2973
+ },
2974
+ FieldSchema {
2975
+ name: "status".into(),
2976
+ field_type: FieldType::Enum {
2977
+ values: vec!["active".into(), "inactive".into()],
2978
+ },
2979
+ nullable: false,
2980
+ required: false,
2981
+ default: Some(Value::String("active".into())),
2982
+ constraints: Default::default(),
2983
+ computed: None,
2984
+ },
2985
+ ],
2986
+ indexes: vec![
2987
+ SchemaIndex {
2988
+ name: "id_idx".into(),
2989
+ fields: vec!["id".into()],
2990
+ unique: false,
2991
+ sparse: false,
2992
+ version: 1,
2993
+ },
2994
+ SchemaIndex {
2995
+ name: "status_idx".into(),
2996
+ fields: vec!["status".into()],
2997
+ unique: false,
2998
+ sparse: false,
2999
+ version: 1,
3000
+ },
3001
+ ],
3002
+ });
3003
+ }
3004
+ StateSchema {
3005
+ contract_version: 1,
3006
+ schema_version: 1,
3007
+ application_id: "app".into(),
3008
+ revision_id: "rev".into(),
3009
+ collections,
3010
+ }
3011
+ }
3012
+ let db = db("gate-a-sherpa-bench");
3013
+ let schema = sherpa_schema();
3014
+ let auth_ctx = auth();
3015
+ println!("Gate A Benchmark: Loading 46 collections...");
3016
+ let load_start = Instant::now();
3017
+ for col_idx in 1..=46 {
3018
+ let collection_name = format!("collection_{}", col_idx);
3019
+ let records_per_collection = 500;
3020
+ let mut operations = vec![];
3021
+ for record_idx in 1..=records_per_collection {
3022
+ operations.push(TransactionOperation {
3023
+ kind: TransactionOperationKind::Insert,
3024
+ collection: collection_name.clone(),
3025
+ id: format!("rec-{}", record_idx),
3026
+ value: serde_json::json!({
3027
+ "id": format!("id-{}", record_idx),
3028
+ "name": format!("Record {}", record_idx),
3029
+ "status": if record_idx % 2 == 0 { "active" } else { "inactive" }
3030
+ }),
3031
+ if_version: None,
3032
+ });
3033
+ }
3034
+ let request = TransactionRequest {
3035
+ transaction_id: None,
3036
+ tenant_id: "tenant".into(),
3037
+ application_id: "app".into(),
3038
+ revision_id: "rev".into(),
3039
+ schema_version: 1,
3040
+ state_namespace: None,
3041
+ causal_parent: None,
3042
+ authorization: auth_ctx.clone(),
3043
+ preconditions: vec![],
3044
+ operations,
3045
+ };
3046
+ execute_transaction(&db, &schema, &request).unwrap();
3047
+ }
3048
+ let load_ms = load_start.elapsed().as_secs_f64() * 1000.0;
3049
+ println!(" Loaded ~23,000 records in {:.2}ms", load_ms);
3050
+ println!("Gate A Benchmark: Executing 46 indexed queries...");
3051
+ let query_start = Instant::now();
3052
+ let context = begin_read(&db, &schema, "app", auth_ctx).unwrap();
3053
+ let mut query_times = vec![];
3054
+ let mut index_used_count = 0;
3055
+ for col_idx in 1..=46 {
3056
+ let collection_name = format!("collection_{}", col_idx);
3057
+ let q_start = Instant::now();
3058
+ let result = execute_query(
3059
+ &db,
3060
+ &schema,
3061
+ &context,
3062
+ &CanonicalQuery {
3063
+ collection: collection_name,
3064
+ filter: Some(QueryFilter::Eq {
3065
+ field: "status".into(),
3066
+ value: Value::String("active".into()),
3067
+ }),
3068
+ order_by: vec![],
3069
+ limit: Some(1000),
3070
+ offset: 0,
3071
+ cursor: None,
3072
+ projection: vec![],
3073
+ group_by: vec![],
3074
+ aggregates: vec![],
3075
+ references: vec![],
3076
+ },
3077
+ )
3078
+ .unwrap();
3079
+ let q_ms = q_start.elapsed().as_secs_f64() * 1000.0;
3080
+ query_times.push(q_ms);
3081
+ assert_eq!(
3082
+ result.plan.access_method,
3083
+ Some("index".to_string()),
3084
+ "Query {} should use index",
3085
+ col_idx
3086
+ );
3087
+ if result
3088
+ .plan
3089
+ .index_metrics
3090
+ .as_ref()
3091
+ .unwrap()
3092
+ .index_used
3093
+ .is_some()
3094
+ {
3095
+ index_used_count += 1;
3096
+ }
3097
+ }
3098
+ let total_query_time_ms = query_start.elapsed().as_secs_f64() * 1000.0;
3099
+ query_times.sort_by(|a, b| a.partial_cmp(b).unwrap());
3100
+ let median_query_time = query_times[query_times.len() / 2];
3101
+ println!("\n=== Gate A Verification ===");
3102
+ println!("Indexes used: {}/46", index_used_count);
3103
+ println!(
3104
+ "Total query time: {:.2}ms (target: <10,000ms)",
3105
+ total_query_time_ms
3106
+ );
3107
+ println!(
3108
+ "Per-query median: {:.2}ms (target: <100ms)",
3109
+ median_query_time
3110
+ );
3111
+ assert_eq!(index_used_count, 46, "All queries must use indexes");
3112
+ assert!(
3113
+ total_query_time_ms < 10000.0,
3114
+ "Total time must be <10s (was {:.2}ms)",
3115
+ total_query_time_ms
3116
+ );
3117
+ }
3118
+ }