1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::future::Future;
3use std::pin::Pin;
4use std::str::FromStr;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone};
8use rusqlite::types::{Value as SqliteValue, ValueRef};
9use rusqlite::{
10 Connection, OptionalExtension, Row, functions::FunctionFlags, params, params_from_iter,
11};
12use rust_decimal::Decimal;
13use teaql_core::{
14 CompactRow, DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, SelectQuery,
15 UpdateCommand, Value,
16};
17use teaql_runtime::{
18 GraphNode, InternalIdGenerator, RawAuditEvent, RuntimeError, SchemaProvider, UserContext,
19 canonical_id_space_entity,
20};
21use teaql_sql::{
22 CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
23 quote_identifier_if_needed,
24};
25
26pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
27pub const DEFAULT_PREPARED_STATEMENT_CACHE_CAPACITY: usize = 64;
28pub const DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY: usize = 64;
29
30#[derive(Debug, Default, Clone, Copy)]
31pub struct SqliteDialect;
32
33impl SqlDialect for SqliteDialect {
34 fn kind(&self) -> DatabaseKind {
35 DatabaseKind::Sqlite
36 }
37
38 fn quote_ident(&self, ident: &str) -> String {
39 quote_ident(ident)
40 }
41
42 fn placeholder(&self, _index: usize) -> String {
43 "?".to_owned()
44 }
45
46 fn prefers_small_parent_relation_probes(&self) -> bool {
47 true
48 }
49
50 fn schema_type_sql(
51 &self,
52 data_type: DataType,
53 property: &PropertyDescriptor,
54 ) -> Result<&'static str, SqlCompileError> {
55 match data_type {
56 DataType::Bool => Ok("BOOLEAN"),
57 DataType::I64 | DataType::U64 if property.is_id => Ok("INTEGER"),
58 DataType::I64 | DataType::U64 => Ok("INTEGER"),
59 DataType::F64 => Ok("REAL"),
60 DataType::Decimal => Ok("NUMERIC"),
61 DataType::Text => Ok("VARCHAR(255)"),
62 DataType::LargeText => Ok("TEXT"),
63 DataType::Json => Ok("JSON"),
64 DataType::Date => Ok("DATE"),
65 DataType::Timestamp => Ok("TIMESTAMP"),
66 }
67 }
68
69 fn compile_add_column(
70 &self,
71 entity: &EntityDescriptor,
72 property: &PropertyDescriptor,
73 ) -> Result<String, SqlCompileError> {
74 let def = self.column_definition_sql(property)?;
78 let def_without_not_null = def.replace(" NOT NULL", "");
79
80 Ok(format!(
81 "ALTER TABLE {} ADD COLUMN {}",
82 self.quote_ident(&entity.table_name),
83 def_without_not_null
84 ))
85 }
86}
87
88#[derive(Debug)]
89pub enum MutationExecutorError {
90 Sqlite(rusqlite::Error),
91 SqlCompile(SqlCompileError),
92 UnsupportedValue(&'static str),
93 UnsupportedColumnType(String),
94 Bind(String),
95 Lock(String),
96}
97
98impl std::fmt::Display for MutationExecutorError {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Self::Sqlite(err) => err.fmt(f),
102 Self::SqlCompile(err) => err.fmt(f),
103 Self::UnsupportedValue(kind) => {
104 write!(
105 f,
106 "unsupported rusqlite bind value for mutation executor: {kind}"
107 )
108 }
109 Self::UnsupportedColumnType(kind) => {
110 write!(
111 f,
112 "unsupported rusqlite column type for record decoding: {kind}"
113 )
114 }
115 Self::Bind(message) => write!(f, "rusqlite bind error: {message}"),
116 Self::Lock(message) => write!(f, "rusqlite connection lock error: {message}"),
117 }
118 }
119}
120
121impl std::error::Error for MutationExecutorError {}
122
123impl From<rusqlite::Error> for MutationExecutorError {
124 fn from(value: rusqlite::Error) -> Self {
125 Self::Sqlite(value)
126 }
127}
128
129impl From<SqlCompileError> for MutationExecutorError {
130 fn from(value: SqlCompileError) -> Self {
131 Self::SqlCompile(value)
132 }
133}
134
135#[derive(Clone)]
136pub struct SqliteMutationExecutor {
137 connection: Arc<Mutex<Connection>>,
138 column_layout_cache: Arc<Mutex<HashMap<String, Arc<ColumnLayout>>>>,
139}
140
141impl SqliteMutationExecutor {
142 pub fn new(connection: Arc<Mutex<Connection>>) -> Self {
143 if let Ok(connection) = connection.lock() {
144 connection
145 .set_prepared_statement_cache_capacity(DEFAULT_PREPARED_STATEMENT_CACHE_CAPACITY);
146 }
147 Self {
148 connection,
149 column_layout_cache: Arc::new(Mutex::new(HashMap::new())),
150 }
151 }
152
153 pub fn from_connection(connection: Connection) -> Self {
154 Self::new(Arc::new(Mutex::new(connection)))
155 }
156
157 pub fn connection(&self) -> Arc<Mutex<Connection>> {
158 Arc::clone(&self.connection)
159 }
160
161 pub fn ensure_schema(
162 &self,
163 dialect: &SqliteDialect,
164 entities: &[&EntityDescriptor],
165 ) -> Result<(), MutationExecutorError> {
166 self.ensure_soundex_function()?;
167 self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
168
169 for entity in entities {
170 if !self.table_exists(&entity.table_name)? {
171 let sql = dialect.compile_create_table(entity)?;
172 self.lock()?.execute(&sql, [])?;
173 continue;
174 }
175
176 let existing_columns = self.table_columns(&entity.table_name)?;
177 for property in &entity.properties {
178 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
179 if existing_columns.contains(&bare_column) {
180 continue;
181 }
182 let sql = dialect.compile_add_column(entity, property)?;
183 self.lock()?.execute(&sql, [])?;
184 }
185
186 for sql in dialect.schema_indexes_sqls(entity)? {
187 self.lock()?.execute(&sql, [])?;
188 }
189 }
190 self.clear_query_caches();
191 Ok(())
192 }
193
194 fn ensure_soundex_function(&self) -> Result<(), MutationExecutorError> {
195 self.lock()?.create_scalar_function(
196 "soundex",
197 1,
198 FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
199 |ctx| {
200 let input = ctx.get_raw(0).as_str().ok();
201 Ok(sqlite_compatible_soundex(input))
202 },
203 )?;
204 Ok(())
205 }
206
207 fn clear_query_caches(&self) {
208 if let Ok(connection) = self.connection.lock() {
209 connection.flush_prepared_statement_cache();
210 }
211 if let Ok(mut cache) = self.column_layout_cache.lock() {
212 cache.clear();
213 }
214 }
215
216 pub fn ensure_id_space_table(&self, table_name: &str) -> Result<(), MutationExecutorError> {
217 let sql = format!(
218 "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
219 quote_ident(table_name)
220 );
221 self.lock()?.execute(&sql, [])?;
222 Ok(())
223 }
224
225 pub fn begin_transaction(&self) -> Result<(), MutationExecutorError> {
226 self.lock()?.execute("BEGIN IMMEDIATE", [])?;
227 Ok(())
228 }
229
230 pub fn commit_transaction(&self) -> Result<(), MutationExecutorError> {
231 self.lock()?.execute("COMMIT", [])?;
232 Ok(())
233 }
234
235 pub fn rollback_transaction(&self) -> Result<(), MutationExecutorError> {
236 self.lock()?.execute("ROLLBACK", [])?;
237 Ok(())
238 }
239
240 pub fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
241 let params = bind_values(&query.params)?;
242 let rows = self
243 .lock()?
244 .execute(&query.sql_with_comment(), params_from_iter(params.iter()))?;
245 Ok(rows as u64)
246 }
247
248 pub fn fetch_all_compact(
249 &self,
250 query: &CompiledQuery,
251 ) -> Result<Vec<CompactRow>, MutationExecutorError> {
252 let params = bind_values(&query.params)?;
253 let connection = self.lock()?;
254 let sql = query.sql_with_comment();
255 let mut statement = connection.prepare_cached(&sql)?;
256 let layout = cached_column_layout(&self.column_layout_cache, &query.sql, &statement);
257 let mut rows = statement.query(params_from_iter(params.iter()))?;
258 let mut result = Vec::new();
259 while let Some(row) = rows.next()? {
260 result.push(CompactRow::new(
261 layout.names.clone(),
262 decode_sqlite_values(row, &layout.columns)?,
263 ));
264 }
265 Ok(result)
266 }
267
268 pub fn fetch_stream(
271 &self,
272 query: &CompiledQuery,
273 chunk_size: usize,
274 ) -> Result<Vec<teaql_data_service::StreamChunk>, MutationExecutorError> {
275 let params = bind_values(&query.params)?;
276 let connection = self.lock()?;
277 let sql = query.sql_with_comment();
278 let mut statement = connection.prepare_cached(&sql)?;
279 let layout = cached_column_layout(&self.column_layout_cache, &query.sql, &statement);
280 let mut rows = statement.query(params_from_iter(params.iter()))?;
281
282 let mut chunks = Vec::new();
283 let mut current_chunk = Vec::new();
284 let mut chunk_index = 0;
285
286 while let Some(row) = rows.next()? {
287 current_chunk.push(CompactRow::new(
288 layout.names.clone(),
289 decode_sqlite_values(row, &layout.columns)?,
290 ));
291 if current_chunk.len() >= chunk_size {
292 chunks.push(teaql_data_service::StreamChunk {
293 rows: current_chunk,
294 chunk_index,
295 is_last: false,
296 });
297 current_chunk = Vec::new();
298 chunk_index += 1;
299 }
300 }
301
302 chunks.push(teaql_data_service::StreamChunk {
304 rows: current_chunk,
305 chunk_index,
306 is_last: true,
307 });
308
309 Ok(chunks)
310 }
311
312 pub fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
313 let exists: i64 = self.lock()?.query_row(
314 "SELECT COUNT(1) FROM sqlite_master WHERE type = 'table' AND name = ?",
315 [table_name],
316 |row| row.get(0),
317 )?;
318 Ok(exists > 0)
319 }
320
321 pub fn table_columns(
322 &self,
323 table_name: &str,
324 ) -> Result<BTreeSet<String>, MutationExecutorError> {
325 let pragma_sql = format!("PRAGMA table_info({})", quote_ident(table_name));
326 let connection = self.lock()?;
327 let mut statement = connection.prepare(&pragma_sql)?;
328 let rows = statement.query_map([], |row| row.get::<_, String>("name"))?;
329 let mut columns = BTreeSet::new();
330 for row in rows {
331 columns.insert(row?.to_lowercase());
332 }
333 Ok(columns)
334 }
335
336 fn lock(&self) -> Result<MutexGuard<'_, Connection>, MutationExecutorError> {
337 self.connection
338 .lock()
339 .map_err(|err| MutationExecutorError::Lock(err.to_string()))
340 }
341}
342
343fn sqlite_compatible_soundex(input: Option<&str>) -> String {
344 fn code(byte: u8) -> u8 {
345 match byte.to_ascii_uppercase() {
346 b'B' | b'F' | b'P' | b'V' => 1,
347 b'C' | b'G' | b'J' | b'K' | b'Q' | b'S' | b'X' | b'Z' => 2,
348 b'D' | b'T' => 3,
349 b'L' => 4,
350 b'M' | b'N' => 5,
351 b'R' => 6,
352 _ => 0,
353 }
354 }
355 let Some(input) = input else {
356 return "?000".to_owned();
357 };
358 let Some((first_index, first)) = input
359 .bytes()
360 .enumerate()
361 .find(|(_, byte)| byte.is_ascii_alphabetic())
362 else {
363 return "?000".to_owned();
364 };
365 let mut result = String::with_capacity(4);
366 result.push(char::from(first.to_ascii_uppercase()));
367 let mut previous = code(first);
368 for byte in input.bytes().skip(first_index + 1) {
369 if !byte.is_ascii_alphabetic() {
370 continue;
371 }
372 let current = code(byte);
373 if current != 0 && current != previous {
374 result.push(char::from(b'0' + current));
375 if result.len() == 4 {
376 break;
377 }
378 }
379 previous = current;
380 }
381 while result.len() < 4 {
382 result.push('0');
383 }
384 result
385}
386
387impl teaql_data_service::DataServiceExecutor for SqliteMutationExecutor {
388 type Error = MutationExecutorError;
389
390 fn capabilities(&self) -> teaql_data_service::DataServiceCapabilities {
391 teaql_data_service::DataServiceCapabilities {
392 query: true,
393 mutation: true,
394 transaction: true,
395 schema: true,
396 id_generation: true,
397 ..Default::default()
398 }
399 }
400}
401
402impl SqlTransport for SqliteMutationExecutor {
403 type Error = MutationExecutorError;
404
405 async fn fetch_all_compact_sql(
406 &self,
407 query: &CompiledQuery,
408 ) -> Result<Vec<CompactRow>, Self::Error> {
409 SqliteMutationExecutor::fetch_all_compact(self, query)
410 }
411
412 async fn fetch_repeated_compact_sql(
413 &self,
414 template: &CompiledQuery,
415 param_index: usize,
416 values: &[Value],
417 ) -> Result<Vec<CompactRow>, Self::Error> {
418 let connection = self.lock()?;
419 let sql = template.sql_with_comment();
420 let mut statement = connection.prepare_cached(&sql)?;
421 let layout = cached_column_layout(&self.column_layout_cache, &template.sql, &statement);
422 let mut result = Vec::new();
423 let mut query_params = template.params.clone();
424 for value in values {
425 query_params[param_index] = value.clone();
426 let params = bind_values(&query_params)?;
427 let mut rows = statement.query(params_from_iter(params.iter()))?;
428 while let Some(row) = rows.next()? {
429 result.push(CompactRow::new(
430 layout.names.clone(),
431 decode_sqlite_values(row, &layout.columns)?,
432 ));
433 }
434 }
435 Ok(result)
436 }
437
438 async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
439 SqliteMutationExecutor::execute(self, query)
440 }
441}
442
443impl teaql_sql::StreamingSqlTransport for SqliteMutationExecutor {
444 #[allow(clippy::await_holding_lock)]
448 fn stream_sql(
449 &self,
450 query: CompiledQuery,
451 chunk_size: usize,
452 ) -> teaql_data_service::QueryStream<'_, Self::Error> {
453 let connection = self.connection.clone();
454 let column_layout_cache = self.column_layout_cache.clone();
455 Box::pin(async_stream::try_stream! {
456 let params = bind_values(&query.params)?;
457 let guard = connection.lock().map_err(|err| MutationExecutorError::Lock(err.to_string()))?;
458 let sql = query.sql_with_comment();
459 let mut statement = guard.prepare_cached(&sql)?;
460 let layout = cached_column_layout(&column_layout_cache, &query.sql, &statement);
461 let mut rows = statement.query(params_from_iter(params.iter()))?;
462 let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
463 while let Some(row) = rows.next()? {
464 chunk.push(CompactRow::new(layout.names.clone(), decode_sqlite_values(row, &layout.columns)?));
465 if chunk.len() == chunk_size { yield teaql_data_service::StreamChunk { rows: std::mem::take(&mut chunk), chunk_index: index, is_last: false }; index += 1; }
466 }
467 if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows: chunk, chunk_index: index, is_last: true }; }
468 })
469 }
470}
471
472impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
473 fn query_stream(
474 &self,
475 request: teaql_data_service::QueryRequest,
476 chunk_size: usize,
477 ) -> teaql_data_service::QueryStream<'_, Self::Error> {
478 let dialect = SqliteDialect;
479 let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
481 match dialect.compile_select(&entity_desc, &request.query) {
482 Ok(compiled) => {
483 teaql_sql::StreamingSqlTransport::stream_sql(self, compiled, chunk_size)
484 }
485 Err(error) => Box::pin(futures_util::stream::once(async {
486 Err(MutationExecutorError::SqlCompile(error))
487 })),
488 }
489 }
490}
491
492impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
493 type Error = MutationExecutorError;
494
495 async fn commit_sql(self) -> Result<(), Self::Error> {
496 self.commit_transaction()
497 }
498
499 async fn rollback_sql(self) -> Result<(), Self::Error> {
500 self.rollback_transaction()
501 }
502}
503
504impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
505 type Tx<'a>
506 = Self
507 where
508 Self: 'a;
509
510 async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
511 self.begin_transaction()?;
512 Ok(self.clone())
513 }
514}
515
516fn initial_graph_row_sqlite(
517 executor: &SqliteMutationExecutor,
518 dialect: &SqliteDialect,
519 entity: &EntityDescriptor,
520 graph: &GraphNode,
521) -> Result<Option<teaql_core::CompactRow>, MutationExecutorError> {
522 let Some(id) = graph.values.get("id") else {
523 return Ok(None);
524 };
525 let mut select = SelectQuery::new(&graph.entity)
526 .filter(Expr::eq("id", id.clone()))
527 .limit(1);
528 for field in graph.values.keys() {
529 select = select.project(field);
530 }
531 if let Some(version) = entity
532 .version_property()
533 .filter(|version| !graph.values.contains_key(&version.name))
534 {
535 select = select.project(&version.name);
536 }
537 let query = dialect.compile_select(entity, &select)?;
538 Ok(executor.fetch_all_compact(&query)?.into_iter().next())
539}
540
541fn compile_initial_graph_insert(
542 dialect: &impl SqlDialect,
543 entity: &EntityDescriptor,
544 graph: &GraphNode,
545) -> Result<CompiledQuery, MutationExecutorError> {
546 let mut command = InsertCommand::new(&graph.entity);
547 for (field, value) in &graph.values {
548 command = command.value(field.clone(), value.clone());
549 }
550 dialect.compile_insert(entity, &command).map_err(Into::into)
551}
552
553fn compile_initial_graph_update(
554 dialect: &impl SqlDialect,
555 entity: &EntityDescriptor,
556 graph: &GraphNode,
557 current: &teaql_core::CompactRow,
558) -> Result<Option<CompiledQuery>, MutationExecutorError> {
559 let Some(id) = graph.values.get("id") else {
560 return Ok(None);
561 };
562 let mut command = UpdateCommand::new(&graph.entity, id.clone());
563 for (field, value) in &graph.values {
564 if field != "id"
565 && field != "version"
566 && !bootstrap_values_equal(current.get(field), Some(value))
567 {
568 command = command.value(field.clone(), value.clone());
569 }
570 }
571 if command.values.is_empty() {
572 return Ok(None);
573 }
574 if let Some(version) = entity
575 .version_property()
576 .and_then(|property| current.get(&property.name))
577 .and_then(Value::try_i64)
578 {
579 command = command.expected_version(version);
580 }
581 match dialect.compile_update(entity, &command) {
582 Ok(query) => Ok(Some(query)),
583 Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
584 Err(err) => Err(err.into()),
585 }
586}
587
588fn bootstrap_values_equal(left: Option<&Value>, right: Option<&Value>) -> bool {
589 let (Some(left), Some(right)) = (left, right) else {
590 return left.is_none() && right.is_none();
591 };
592 if left == right {
593 return true;
594 }
595 matches!((left.try_decimal(), right.try_decimal()), (Some(a), Some(b)) if a == b)
596}
597
598pub(crate) fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
599 let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
600 MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
601 })?;
602 let executor = context
603 .get_resource::<SqliteMutationExecutor>()
604 .ok_or_else(|| {
605 MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
606 })?;
607
608 let entities = context.all_entities();
609
610 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
612
613 for entity in &entities {
615 let field_count = entity.properties.len();
616 if !executor.table_exists(&entity.table_name)? {
617 let sql = dialect.compile_create_table(entity)?;
619 executor.lock()?.execute(&sql, [])?;
620 let _ = context.send_event(RawAuditEvent::schema_created(
621 &entity.name,
622 &entity.table_name,
623 field_count,
624 ));
625 continue;
626 }
627 let existing_columns = executor.table_columns(&entity.table_name)?;
629 let mut fields_added = 0;
630 for property in &entity.properties {
631 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
632 if existing_columns.contains(&bare_column) {
633 continue;
634 }
635 let sql = dialect.compile_add_column(entity, property)?;
636 executor.lock()?.execute(&sql, [])?;
637 let _ = context.send_event(RawAuditEvent::field_added(
638 &entity.name,
639 &entity.table_name,
640 &property.column_name,
641 ));
642 fields_added += 1;
643 }
644 let _ = context.send_event(RawAuditEvent::schema_verified(
645 &entity.name,
646 &entity.table_name,
647 field_count,
648 ));
649 let _ = fields_added; }
651
652 let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
654 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); for graph in context.initial_graphs() {
656 let entity = context.entity(&graph.entity).ok_or_else(|| {
657 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
658 })?;
659 let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
660 if let Some(current) = initial_graph_row_sqlite(executor, dialect, entity, graph)? {
661 if let Some(query) = compile_initial_graph_update(dialect, entity, graph, ¤t)? {
662 executor.execute(&query)?;
663 counts.1 += 1;
664 }
665 if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
666 id_generator.ensure_floor(&graph.entity, id)?;
667 }
668 continue;
669 }
670 let query = compile_initial_graph_insert(dialect, entity, graph)?;
671 executor.execute(&query)?;
672 counts.0 += 1; if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
674 id_generator.ensure_floor(&graph.entity, id)?;
675 }
676 }
677
678 for graph in context.root_graphs() {
680 let entity = context.entity(&graph.entity).ok_or_else(|| {
681 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
682 })?;
683 if initial_graph_row_sqlite(executor, dialect, entity, graph)?.is_some() {
684 if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
685 id_generator.ensure_floor(&graph.entity, id)?;
686 }
687 continue;
688 }
689 let query = compile_initial_graph_insert(dialect, entity, graph)?;
690 executor.execute(&query)?;
691 seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
692 if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
693 id_generator.ensure_floor(&graph.entity, id)?;
694 }
695 }
696
697 for (entity_name, (inserted, updated)) in &seed_counts {
699 let entity = context.entity(entity_name).ok_or_else(|| {
700 MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
701 })?;
702 let _ = context.send_event(RawAuditEvent::data_seeded(
703 entity_name,
704 &entity.table_name,
705 *inserted,
706 *updated,
707 ));
708 }
709
710 executor.clear_query_caches();
711 Ok(())
712}
713
714#[derive(Debug, Default, Clone, Copy)]
715pub struct SqliteSchemaProvider;
716
717impl SchemaProvider for SqliteSchemaProvider {
718 fn ensure_schema<'a>(
719 &'a self,
720 context: &'a UserContext,
721 _invocation: &'a teaql_runtime::SchemaInvocation,
722 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
723 Box::pin(async move {
724 ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
725 })
726 }
727}
728
729pub trait SqliteProviderExt {
730 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
731}
732
733impl SqliteProviderExt for UserContext {
734 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
735 self.insert_resource(SqliteDialect);
736 self.insert_resource(executor);
737 self.set_schema_provider(SqliteSchemaProvider);
738 self
739 }
740}
741
742#[derive(Clone)]
743pub struct SqliteIdSpaceGenerator {
744 executor: SqliteMutationExecutor,
745 table_name: String,
746}
747
748impl SqliteIdSpaceGenerator {
749 pub fn new(connection: Connection) -> Self {
750 Self::from_executor(SqliteMutationExecutor::from_connection(connection))
751 }
752
753 pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
754 Self {
755 executor,
756 table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
757 }
758 }
759
760 pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
761 self.table_name = table_name.into();
762 self
763 }
764
765 pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
766 self.executor.ensure_id_space_table(&self.table_name)
767 }
768
769 pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
770 let entity = canonical_id_space_entity(entity);
771 let entity = entity.as_str();
772 self.ensure_table()?;
773 let table = quote_ident(&self.table_name);
774 let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
775 let insert_sql = format!("INSERT INTO {table} (type_name, current_level) VALUES (?, 1)");
776 let update_sql = format!(
777 "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
778 );
779 for attempt in 1..=100 {
780 let connection = self.executor.lock()?;
781 let current = connection
782 .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
783 .optional()?;
784 if let Some(current) = current {
785 let next = current.checked_add(1).ok_or_else(|| {
786 MutationExecutorError::Bind(format!(
787 "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
788 ))
789 })?;
790 if connection.execute(&update_sql, params![next, entity, current])? == 1 {
791 return u64::try_from(next).map_err(|_| {
792 MutationExecutorError::Bind(format!(
793 "generated id {next} cannot be represented as u64"
794 ))
795 });
796 }
797 } else {
798 match connection.execute(&insert_sql, params![entity]) {
799 Ok(1) => return Ok(1),
800 Ok(changed) => {
801 return Err(MutationExecutorError::Bind(format!(
802 "ID space insert for {entity} changed {changed} rows"
803 )));
804 }
805 Err(error)
806 if error.sqlite_error_code()
807 == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
808 Err(error) => return Err(error.into()),
809 }
810 }
811 }
812 Err(MutationExecutorError::Bind(format!(
813 "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
814 )))
815 }
816
817 pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
818 let entity = canonical_id_space_entity(entity);
819 let entity = entity.as_str();
820 self.ensure_table()?;
821 let floor = i64::try_from(floor).map_err(|_| {
822 MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
823 })?;
824 let table = quote_ident(&self.table_name);
825 for _ in 1..=100 {
826 let connection = self.executor.lock()?;
827 let current = connection
828 .query_row(
829 &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
830 [entity],
831 |row| row.get::<_, i64>(0),
832 )
833 .optional()?;
834 match current {
835 Some(current) if current >= floor => return Ok(()),
836 Some(current) => {
837 if connection.execute(
838 &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
839 params![floor, entity, current],
840 )? == 1 { return Ok(()); }
841 }
842 None => match connection.execute(
843 &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
844 params![entity, floor],
845 ) {
846 Ok(1) => return Ok(()),
847 Ok(_) => {}
848 Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
849 Err(error) => return Err(error.into()),
850 },
851 }
852 }
853 Err(MutationExecutorError::Bind(format!(
854 "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
855 )))
856 }
857}
858
859impl InternalIdGenerator for SqliteIdSpaceGenerator {
860 fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
861 self.next_id(entity)
862 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
863 }
864}
865
866fn quote_ident(ident: &str) -> String {
867 quote_identifier_if_needed(ident, '"')
868}
869
870fn strip_identifier_quotes(ident: &str) -> &str {
878 let bytes = ident.as_bytes();
879 if bytes.len() >= 2 {
880 let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
881 if (first == b'"' && last == b'"')
882 || (first == b'`' && last == b'`')
883 || (first == b'[' && last == b']')
884 {
885 return &ident[1..ident.len() - 1];
886 }
887 }
888 ident
889}
890
891fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
892 values.iter().map(bind_sqlite_value).collect()
893}
894
895fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
896 match value {
897 Value::Null => Ok(SqliteValue::Null),
898 Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
899 Value::I64(v) => Ok(SqliteValue::Integer(*v)),
900 Value::U64(v) => i64::try_from(*v)
901 .map(SqliteValue::Integer)
902 .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
903 Value::F64(v) => Ok(SqliteValue::Real(*v)),
904 Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
908 Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
909 Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
910 Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
911 Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
912 Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
913 Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
914 Value::TypedNull(_) => Ok(SqliteValue::Null),
915 }
916}
917
918#[derive(Debug, Clone)]
919struct ColumnInfo {
920 name: String,
921 decode_kind: SqliteDecodeKind,
922}
923
924#[derive(Debug, Clone, Copy, PartialEq, Eq)]
925enum SqliteDecodeKind {
926 Infer,
927 Bool,
928 Decimal,
929 Json,
930 Date,
931 Timestamp,
932 Text,
933}
934
935#[derive(Debug)]
936struct ColumnLayout {
937 columns: Arc<[ColumnInfo]>,
938 names: Arc<[String]>,
939}
940
941fn cached_column_layout(
942 cache: &Mutex<HashMap<String, Arc<ColumnLayout>>>,
943 sql: &str,
944 statement: &rusqlite::Statement<'_>,
945) -> Arc<ColumnLayout> {
946 if let Ok(cache) = cache.lock()
947 && let Some(layout) = cache.get(sql)
948 {
949 return layout.clone();
950 }
951
952 let columns: Arc<[ColumnInfo]> = statement_columns(statement).into();
953 let names = columns
954 .iter()
955 .map(|column| column.name.clone())
956 .collect::<Vec<_>>()
957 .into();
958 let layout = Arc::new(ColumnLayout { columns, names });
959 if let Ok(mut cache) = cache.lock() {
960 if cache.len() >= DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY {
961 cache.clear();
962 }
963 cache.insert(sql.to_owned(), layout.clone());
964 }
965 layout
966}
967
968fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
969 statement
970 .columns()
971 .into_iter()
972 .map(|column| ColumnInfo {
973 name: column.name().to_owned(),
974 decode_kind: sqlite_decode_kind(column.decl_type()),
975 })
976 .collect()
977}
978
979fn sqlite_decode_kind(decl_type: Option<&str>) -> SqliteDecodeKind {
980 let Some(decl_type) = decl_type else {
981 return SqliteDecodeKind::Infer;
982 };
983 let base = decl_type.split('(').next().unwrap_or(decl_type).trim();
984 if base.eq_ignore_ascii_case("BOOLEAN") || base.eq_ignore_ascii_case("BOOL") {
985 SqliteDecodeKind::Bool
986 } else if base.eq_ignore_ascii_case("NUMERIC") || base.eq_ignore_ascii_case("DECIMAL") {
987 SqliteDecodeKind::Decimal
988 } else if base.eq_ignore_ascii_case("JSON") {
989 SqliteDecodeKind::Json
990 } else if base.eq_ignore_ascii_case("DATE") {
991 SqliteDecodeKind::Date
992 } else if base.eq_ignore_ascii_case("TIMESTAMP") || base.eq_ignore_ascii_case("DATETIME") {
993 SqliteDecodeKind::Timestamp
994 } else if ["TEXT", "VARCHAR", "CHAR", "CLOB"]
995 .iter()
996 .any(|v| base.eq_ignore_ascii_case(v))
997 {
998 SqliteDecodeKind::Text
999 } else {
1000 SqliteDecodeKind::Infer
1001 }
1002}
1003
1004fn decode_sqlite_values(
1005 row: &Row<'_>,
1006 columns: &[ColumnInfo],
1007) -> Result<Vec<Value>, MutationExecutorError> {
1008 let mut values = Vec::with_capacity(columns.len());
1009 for (index, column) in columns.iter().enumerate() {
1010 let value_ref = row.get_ref(index)?;
1011 let value = match value_ref {
1012 ValueRef::Null => Value::Null,
1013 ValueRef::Integer(value) => decode_sqlite_integer(value, column),
1014 ValueRef::Real(value) => Value::F64(value),
1015 ValueRef::Text(value) => decode_sqlite_text(value, column)?,
1016 ValueRef::Blob(_) => {
1017 return Err(MutationExecutorError::UnsupportedColumnType(
1018 "BLOB".to_owned(),
1019 ));
1020 }
1021 };
1022 values.push(value);
1023 }
1024 Ok(values)
1025}
1026
1027fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
1028 match column.decode_kind {
1029 SqliteDecodeKind::Bool => Value::Bool(value != 0),
1030 _ => Value::I64(value),
1031 }
1032}
1033
1034fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
1035 let value = std::str::from_utf8(value)
1036 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
1037 match column.decode_kind {
1038 SqliteDecodeKind::Decimal => Decimal::from_str(value)
1039 .map(Value::Decimal)
1040 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
1041 SqliteDecodeKind::Json => serde_json::from_str(value).map(Value::Json).map_err(|err| {
1042 MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
1043 }),
1044 SqliteDecodeKind::Date => NaiveDate::parse_from_str(value, "%Y-%m-%d")
1045 .map(Value::Date)
1046 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
1047 SqliteDecodeKind::Timestamp => parse_sqlite_timestamp(value),
1048 SqliteDecodeKind::Text | SqliteDecodeKind::Bool => Ok(Value::Text(value.to_owned())),
1049 SqliteDecodeKind::Infer => infer_sqlite_text(value),
1050 }
1051}
1052
1053fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
1054 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1055 return Ok(Value::Date(date));
1056 }
1057 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1058 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1059 timestamp.timestamp_millis(),
1060 )));
1061 }
1062 if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
1063 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1064 timestamp.and_utc().timestamp_millis(),
1065 )));
1066 }
1067 Ok(Value::Text(value.to_owned()))
1068}
1069
1070fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
1071 if let Some(timestamp) = parse_fixed_sqlite_timestamp(value) {
1072 return Ok(Value::Timestamp(teaql_core::time::Timestamp(timestamp)));
1073 }
1074 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1075 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1076 timestamp.timestamp_millis(),
1077 )));
1078 }
1079 if let Ok(timestamp) = DateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f%#z") {
1080 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1081 timestamp.timestamp_millis(),
1082 )));
1083 }
1084 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1085 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1086 date.and_hms_opt(0, 0, 0)
1087 .unwrap_or_default()
1088 .and_utc()
1089 .timestamp_millis(),
1090 )));
1091 }
1092 NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
1093 .map(|timestamp| {
1094 Value::Timestamp(teaql_core::time::Timestamp(
1095 timestamp.and_utc().timestamp_millis(),
1096 ))
1097 })
1098 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
1099}
1100
1101fn parse_fixed_sqlite_timestamp(value: &str) -> Option<i64> {
1102 let bytes = value.as_bytes();
1103 if bytes.len() < 19
1104 || bytes.get(4) != Some(&b'-')
1105 || bytes.get(7) != Some(&b'-')
1106 || !matches!(bytes.get(10), Some(b' ') | Some(b'T'))
1107 || bytes.get(13) != Some(&b':')
1108 || bytes.get(16) != Some(&b':')
1109 {
1110 return None;
1111 }
1112 let digits = |start: usize, len: usize| -> Option<u32> {
1113 bytes
1114 .get(start..start + len)?
1115 .iter()
1116 .try_fold(0_u32, |value, byte| {
1117 byte.is_ascii_digit()
1118 .then_some(value * 10 + u32::from(*byte - b'0'))
1119 })
1120 };
1121 let date = NaiveDate::from_ymd_opt(
1122 i32::try_from(digits(0, 4)?).ok()?,
1123 digits(5, 2)?,
1124 digits(8, 2)?,
1125 )?;
1126 let hour = digits(11, 2)?;
1127 let minute = digits(14, 2)?;
1128 let second = digits(17, 2)?;
1129 let mut cursor = 19;
1130 let mut nanos = 0_u32;
1131 if bytes.get(cursor) == Some(&b'.') {
1132 cursor += 1;
1133 let fraction_start = cursor;
1134 while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
1135 if cursor - fraction_start < 9 {
1136 nanos = nanos * 10 + u32::from(bytes[cursor] - b'0');
1137 }
1138 cursor += 1;
1139 }
1140 let kept = (cursor - fraction_start).min(9);
1141 if kept == 0 {
1142 return None;
1143 }
1144 nanos *= 10_u32.pow(u32::try_from(9 - kept).ok()?);
1145 }
1146 let datetime = date.and_hms_nano_opt(hour, minute, second, nanos)?;
1147 let offset_seconds = match bytes.get(cursor..) {
1148 Some([]) | Some([b'Z']) | Some([b'z']) => 0,
1149 Some([sign @ (b'+' | b'-'), hour_1, hour_2]) => {
1150 signed_offset(*sign, [*hour_1, *hour_2], *b"00")?
1151 }
1152 Some([sign @ (b'+' | b'-'), hour_1, hour_2, minute_1, minute_2]) => {
1153 signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?
1154 }
1155 Some(
1156 [
1157 sign @ (b'+' | b'-'),
1158 hour_1,
1159 hour_2,
1160 b':',
1161 minute_1,
1162 minute_2,
1163 ],
1164 ) => signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?,
1165 _ => return None,
1166 };
1167 FixedOffset::east_opt(offset_seconds)?
1168 .from_local_datetime(&datetime)
1169 .single()
1170 .map(|timestamp| timestamp.timestamp_millis())
1171}
1172
1173fn signed_offset(sign: u8, hours: [u8; 2], minutes: [u8; 2]) -> Option<i32> {
1174 let pair = |digits: [u8; 2]| {
1175 digits
1176 .iter()
1177 .all(u8::is_ascii_digit)
1178 .then_some(i32::from(digits[0] - b'0') * 10 + i32::from(digits[1] - b'0'))
1179 };
1180 let hours = pair(hours)?;
1181 let minutes = pair(minutes)?;
1182 if hours > 23 || minutes > 59 {
1183 return None;
1184 }
1185 let seconds = hours * 3600 + minutes * 60;
1186 Some(if sign == b'-' { -seconds } else { seconds })
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 use super::*;
1192 use futures_util::StreamExt;
1193 use teaql_core::{DeleteCommand, Record, RecoverCommand};
1194 use teaql_macros::TeaqlEntity;
1195 use teaql_runtime::InMemoryMetadataStore;
1196
1197 #[test]
1198 fn ensure_schema_registers_soundex_idempotently() {
1199 let executor =
1200 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1201 executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1202 executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1203 let connection = executor.connection();
1204 let guard = connection.lock().unwrap();
1205 let encoded: String = guard
1206 .query_row("SELECT soundex('Robert')", [], |row| row.get(0))
1207 .unwrap();
1208 let matches: i64 = guard
1209 .query_row("SELECT soundex('Robert') = soundex('Rupert')", [], |row| {
1210 row.get(0)
1211 })
1212 .unwrap();
1213 let empty: String = guard
1214 .query_row("SELECT soundex(NULL)", [], |row| row.get(0))
1215 .unwrap();
1216 assert_eq!(encoded, "R163");
1217 assert_eq!(matches, 1);
1218 assert_eq!(empty, "?000");
1219 }
1220
1221 #[test]
1222 fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
1223 let connection = Connection::open_in_memory().unwrap();
1224 connection
1225 .execute_batch(
1226 "CREATE TABLE stream_fixture(id INTEGER);\
1227 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
1228 )
1229 .unwrap();
1230 let executor = SqliteMutationExecutor::from_connection(connection);
1231 let query = CompiledQuery {
1232 sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
1233 params: vec![],
1234 comment: None,
1235 };
1236 let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
1237 let sizes = futures_executor::block_on(async {
1238 let mut result = Vec::new();
1239 while let Some(chunk) = stream.next().await {
1240 result.push(chunk.unwrap().rows.len());
1241 }
1242 result
1243 });
1244 assert_eq!(sizes, vec![2, 2, 1]);
1245
1246 let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
1247 assert_eq!(
1248 futures_executor::block_on(early.next())
1249 .unwrap()
1250 .unwrap()
1251 .rows
1252 .len(),
1253 2
1254 );
1255 drop(early);
1256 let count: i64 = executor
1257 .connection()
1258 .lock()
1259 .unwrap()
1260 .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
1261 .unwrap();
1262 assert_eq!(count, 5);
1263 }
1264
1265 #[test]
1266 fn decimal_bind_is_numeric_and_comparable() {
1267 let value =
1268 bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
1269 assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
1270 let connection = Connection::open_in_memory().unwrap();
1271 let matches: i64 = connection
1272 .query_row(
1273 "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
1274 [value],
1275 |row| row.get(0),
1276 )
1277 .unwrap();
1278 assert_eq!(matches, 1);
1279 }
1280
1281 #[test]
1282 fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
1283 let connection = Connection::open_in_memory().unwrap();
1284 connection
1285 .execute_batch(
1286 "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
1287 )
1288 .unwrap();
1289 let query = CompiledQuery {
1290 sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
1291 params: vec![
1292 Value::I64(1),
1293 Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
1294 Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
1295 ],
1296 comment: None,
1297 };
1298 let values = bind_values(&query.params).unwrap();
1299 connection
1300 .execute(&query.sql, rusqlite::params_from_iter(values))
1301 .unwrap();
1302 connection
1303 .execute(
1304 &query
1305 .debug_sql(teaql_sql::DatabaseKind::Sqlite)
1306 .replace("VALUES (1,", "VALUES (2,"),
1307 [],
1308 )
1309 .unwrap();
1310
1311 let equal_count: i64 = connection.query_row(
1312 "SELECT count(*) FROM temporal_fixture a JOIN temporal_fixture b ON a.d=b.d AND a.t=b.t WHERE a.id=1 AND b.id=2",
1313 [], |row| row.get(0),
1314 ).unwrap();
1315 let storage_type: String = connection
1316 .query_row(
1317 "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
1318 [],
1319 |row| row.get(0),
1320 )
1321 .unwrap();
1322 assert_eq!(equal_count, 1);
1323 assert_eq!(storage_type, "integer");
1324 }
1325
1326 fn entity() -> EntityDescriptor {
1327 EntityDescriptor::new("Order")
1328 .table_name("orders")
1329 .property(
1330 PropertyDescriptor::new("id", DataType::U64)
1331 .column_name("id")
1332 .id()
1333 .not_null(),
1334 )
1335 .property(
1336 PropertyDescriptor::new("version", DataType::I64)
1337 .column_name("version")
1338 .version()
1339 .not_null(),
1340 )
1341 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1342 }
1343
1344 fn order_line_entity() -> EntityDescriptor {
1345 EntityDescriptor::new("OrderLine")
1346 .table_name("order_line")
1347 .property(
1348 PropertyDescriptor::new("id", DataType::U64)
1349 .column_name("id")
1350 .id()
1351 .not_null(),
1352 )
1353 .property(
1354 PropertyDescriptor::new("order_id", DataType::U64)
1355 .column_name("order_id")
1356 .not_null(),
1357 )
1358 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1359 }
1360
1361 fn complete_query_record_entity() -> EntityDescriptor {
1362 EntityDescriptor::new("QueryRecord")
1363 .table_name("query_record_scalar")
1364 .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1365 .property(PropertyDescriptor::new("required_text", DataType::Text))
1366 .property(PropertyDescriptor::new("optional_text", DataType::Text))
1367 .property(PropertyDescriptor::new("required_integer", DataType::I64))
1368 .property(PropertyDescriptor::new("optional_long", DataType::I64))
1369 .property(PropertyDescriptor::new(
1370 "required_decimal",
1371 DataType::Decimal,
1372 ))
1373 .property(PropertyDescriptor::new("required_float", DataType::F64))
1374 .property(PropertyDescriptor::new("required_double", DataType::F64))
1375 .property(PropertyDescriptor::new("required_date", DataType::Date))
1376 .property(PropertyDescriptor::new("required_time", DataType::I64))
1377 .property(PropertyDescriptor::new(
1378 "required_timestamp",
1379 DataType::Timestamp,
1380 ))
1381 .property(PropertyDescriptor::new("active", DataType::Bool))
1382 .property(PropertyDescriptor::new("reviewed", DataType::Bool))
1383 .property(
1384 PropertyDescriptor::new("version", DataType::I64)
1385 .version()
1386 .not_null(),
1387 )
1388 }
1389
1390 #[test]
1391 fn complete_scalar_fixture_including_nullable_boolean_executes_on_sqlite() {
1392 let executor = SqliteMutationExecutor::from_connection(
1393 Connection::open_in_memory().expect("open SQLite fixture"),
1394 );
1395 executor
1396 .connection()
1397 .lock()
1398 .expect("lock SQLite fixture")
1399 .execute_batch("CREATE TABLE query_record_scalar (\
1400 id INTEGER PRIMARY KEY, required_text TEXT, optional_text TEXT,\
1401 required_integer INTEGER, optional_long INTEGER, required_decimal NUMERIC,\
1402 required_float REAL, required_double REAL, required_date DATE,\
1403 required_time INTEGER, required_timestamp TIMESTAMP,\
1404 active BOOLEAN, reviewed BOOLEAN, version INTEGER);\
1405 INSERT INTO query_record_scalar VALUES \
1406 (1,'Alpha','optional',42,42000000000,42.125,42.5,42.75,'2026-08-29',34200000,1777632600000,1,0,1),\
1407 (2,'Beta',NULL,7,NULL,7.500,7.5,7.75,'2026-08-30',36000000,1777720400000,0,NULL,1),\
1408 (3,'Gamma','tail',99,99000000000,99.875,99.5,99.75,'2026-08-31',37800000,1777808200000,1,1,1)")
1409 .expect("seed complete scalar fixture");
1410 let entity = complete_query_record_entity();
1411 let ids = |expr: Expr| {
1412 let query = SelectQuery::new("QueryRecord")
1413 .project("id")
1414 .filter(expr)
1415 .order_asc("id");
1416 executor
1417 .fetch_all_compact(&SqliteDialect.compile_select(&entity, &query).unwrap())
1418 .expect("execute scalar predicate")
1419 .into_iter()
1420 .map(|row| row.get("id").cloned().expect("projected id"))
1421 .collect::<Vec<_>>()
1422 };
1423 assert_eq!(ids(Expr::eq("required_text", "Alpha")), vec![Value::I64(1)]);
1424 assert_eq!(
1425 ids(Expr::ne("required_text", "Alpha")),
1426 vec![Value::I64(2), Value::I64(3)]
1427 );
1428 assert_eq!(
1429 ids(Expr::in_list(
1430 "required_text",
1431 [Value::from("Alpha"), Value::from("Gamma")]
1432 )),
1433 vec![Value::I64(1), Value::I64(3)]
1434 );
1435 assert_eq!(
1436 ids(Expr::contain("required_text", "et")),
1437 vec![Value::I64(2)]
1438 );
1439 assert_eq!(
1440 ids(Expr::between("required_integer", 40_i64, 100_i64)),
1441 vec![Value::I64(1), Value::I64(3)]
1442 );
1443 assert_eq!(
1444 ids(Expr::gt("required_decimal", Decimal::from(50))),
1445 vec![Value::I64(3)]
1446 );
1447 assert_eq!(
1448 ids(Expr::lte("required_float", 7.5_f64)),
1449 vec![Value::I64(2)]
1450 );
1451 assert_eq!(
1452 ids(Expr::gte("required_double", 99.75_f64)),
1453 vec![Value::I64(3)]
1454 );
1455 assert_eq!(
1456 ids(Expr::between(
1457 "required_date",
1458 NaiveDate::from_ymd_opt(2026, 8, 30).unwrap(),
1459 NaiveDate::from_ymd_opt(2026, 8, 31).unwrap(),
1460 )),
1461 vec![Value::I64(2), Value::I64(3)]
1462 );
1463 assert_eq!(
1464 ids(Expr::gt("required_time", 36_000_000_i64)),
1465 vec![Value::I64(3)]
1466 );
1467 assert_eq!(
1468 ids(Expr::lt(
1469 "required_timestamp",
1470 teaql_core::time::Timestamp(1_777_750_000_000)
1471 )),
1472 vec![Value::I64(1), Value::I64(2)]
1473 );
1474 assert_eq!(ids(Expr::is_null("optional_text")), vec![Value::I64(2)]);
1475 assert_eq!(
1476 ids(Expr::is_not_null("optional_long")),
1477 vec![Value::I64(1), Value::I64(3)]
1478 );
1479 assert_eq!(ids(Expr::eq("active", false)), vec![Value::I64(2)]);
1480 assert_eq!(ids(Expr::eq("reviewed", true)), vec![Value::I64(3)]);
1481 assert_eq!(ids(Expr::eq("reviewed", false)), vec![Value::I64(1)]);
1482 assert_eq!(ids(Expr::is_null("reviewed")), vec![Value::I64(2)]);
1483 }
1484
1485 #[test]
1486 fn relation_subqueries_execute_positive_and_negative_predicates_on_sqlite() {
1487 let executor = SqliteMutationExecutor::from_connection(
1488 Connection::open_in_memory().expect("open SQLite fixture"),
1489 );
1490 executor
1491 .connection()
1492 .lock()
1493 .expect("lock SQLite fixture")
1494 .execute_batch(
1495 "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT);\
1496 CREATE TABLE order_line (id INTEGER PRIMARY KEY, order_id INTEGER, name TEXT);\
1497 INSERT INTO orders VALUES (1, 1, 'first'), (2, 1, 'second'), (3, 1, 'third');\
1498 INSERT INTO order_line VALUES\
1499 (10, 1, 'priority'), (11, 1, 'ordinary'), (12, 2, 'ordinary'),\
1500 (13, NULL, 'orphan');",
1501 )
1502 .expect("seed relation fixture");
1503
1504 let matching_lines = SelectQuery::new("OrderLine").filter(Expr::eq("name", "priority"));
1505 let positive = SelectQuery::new("Order")
1506 .project("id")
1507 .filter(Expr::in_subquery(
1508 "id",
1509 order_line_entity(),
1510 matching_lines.clone(),
1511 "order_id",
1512 ))
1513 .order_asc("id");
1514 let negative = SelectQuery::new("Order")
1515 .project("id")
1516 .filter(Expr::not_in_subquery(
1517 "id",
1518 order_line_entity(),
1519 matching_lines,
1520 "order_id",
1521 ))
1522 .order_asc("id");
1523
1524 let ids = |rows: Vec<CompactRow>| {
1525 rows.into_iter()
1526 .map(|row| row.get("id").cloned().expect("projected id"))
1527 .collect::<Vec<_>>()
1528 };
1529 let order_ids = |query: SelectQuery| {
1530 ids(executor
1531 .fetch_all_compact(&SqliteDialect.compile_select(&entity(), &query).unwrap())
1532 .expect("execute order relation predicate"))
1533 };
1534 let line_ids = |query: SelectQuery| {
1535 ids(executor
1536 .fetch_all_compact(
1537 &SqliteDialect
1538 .compile_select(&order_line_entity(), &query)
1539 .unwrap(),
1540 )
1541 .expect("execute line relation predicate"))
1542 };
1543
1544 assert_eq!(order_ids(positive), vec![Value::I64(1)]);
1546 assert_eq!(order_ids(negative), vec![Value::I64(2), Value::I64(3)]);
1547
1548 assert_eq!(
1550 line_ids(
1551 SelectQuery::new("OrderLine")
1552 .project("id")
1553 .filter(Expr::is_not_null("order_id"))
1554 .order_asc("id")
1555 ),
1556 vec![Value::I64(10), Value::I64(11), Value::I64(12)]
1557 );
1558 assert_eq!(
1559 line_ids(
1560 SelectQuery::new("OrderLine")
1561 .project("id")
1562 .filter(Expr::is_null("order_id"))
1563 .order_asc("id")
1564 ),
1565 vec![Value::I64(13)]
1566 );
1567
1568 let first_order = SelectQuery::new("Order").filter(Expr::eq("name", "first"));
1571 assert_eq!(
1572 line_ids(
1573 SelectQuery::new("OrderLine")
1574 .project("id")
1575 .filter(Expr::in_subquery(
1576 "order_id",
1577 entity(),
1578 first_order.clone(),
1579 "id",
1580 ))
1581 .order_asc("id")
1582 ),
1583 vec![Value::I64(10), Value::I64(11)]
1584 );
1585 assert_eq!(
1586 line_ids(
1587 SelectQuery::new("OrderLine")
1588 .project("id")
1589 .filter(Expr::not_in_subquery(
1590 "order_id",
1591 entity(),
1592 first_order,
1593 "id",
1594 ))
1595 .order_asc("id")
1596 ),
1597 vec![Value::I64(12)]
1598 );
1599
1600 let all_lines = SelectQuery::new("OrderLine");
1602 assert_eq!(
1603 order_ids(
1604 SelectQuery::new("Order")
1605 .project("id")
1606 .filter(Expr::in_subquery(
1607 "id",
1608 order_line_entity(),
1609 all_lines.clone(),
1610 "order_id",
1611 ))
1612 .order_asc("id")
1613 ),
1614 vec![Value::I64(1), Value::I64(2)]
1615 );
1616 assert_eq!(
1617 order_ids(
1618 SelectQuery::new("Order")
1619 .project("id")
1620 .filter(Expr::not_in_subquery(
1621 "id",
1622 order_line_entity(),
1623 all_lines,
1624 "order_id",
1625 ))
1626 .order_asc("id")
1627 ),
1628 vec![Value::I64(3)]
1629 );
1630 }
1631
1632 #[allow(dead_code)]
1633 #[derive(Debug, PartialEq, TeaqlEntity)]
1634 #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1635 struct FeatureFlagRow {
1636 #[teaql(id)]
1637 id: u64,
1638 #[teaql(version)]
1639 version: i64,
1640 enabled: bool,
1641 optional_enabled: Option<bool>,
1642 }
1643
1644 fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1645 Record::from([
1646 ("id".to_owned(), Value::U64(1)),
1647 ("version".to_owned(), Value::I64(1)),
1648 ("enabled".to_owned(), enabled),
1649 ("optional_enabled".to_owned(), optional_enabled),
1650 ])
1651 }
1652
1653 #[test]
1654 fn sqlite_dialect_compiles_mutations_and_schema() {
1655 assert!(SqliteDialect.prefers_small_parent_relation_probes());
1656 let insert = SqliteDialect
1657 .compile_insert(
1658 &entity(),
1659 &InsertCommand::new("Order")
1660 .value("id", 1_u64)
1661 .value("name", "A"),
1662 )
1663 .unwrap();
1664 assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1665
1666 let update = SqliteDialect
1667 .compile_update(
1668 &entity(),
1669 &UpdateCommand::new("Order", 1_u64)
1670 .expected_version(3)
1671 .value("name", "B"),
1672 )
1673 .unwrap();
1674 assert_eq!(
1675 update.sql,
1676 "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1677 );
1678
1679 let delete = SqliteDialect
1680 .compile_delete(
1681 &entity(),
1682 &DeleteCommand::new("Order", 1_u64).expected_version(3),
1683 )
1684 .unwrap();
1685 let recover = SqliteDialect
1686 .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1687 .unwrap();
1688 assert_eq!(
1689 delete.sql,
1690 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1691 );
1692 assert_eq!(
1693 recover.sql,
1694 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1695 );
1696
1697 let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1698 assert_eq!(
1699 create,
1700 "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1701 );
1702 }
1703
1704 #[test]
1705 fn column_layout_cache_uses_parameterized_sql_not_comments() {
1706 let connection = Connection::open_in_memory().unwrap();
1707 connection
1708 .execute("CREATE TABLE sample (id INTEGER, enabled BOOLEAN)", [])
1709 .unwrap();
1710 connection
1711 .execute("INSERT INTO sample (id, enabled) VALUES (1, 1)", [])
1712 .unwrap();
1713 let executor = SqliteMutationExecutor::from_connection(connection);
1714 let mut first = CompiledQuery {
1715 sql: "SELECT id, enabled FROM sample WHERE id = ?".to_owned(),
1716 params: vec![Value::I64(1)],
1717 comment: Some("first purpose".to_owned()),
1718 };
1719 let rows = executor.fetch_all_compact(&first).unwrap();
1720 assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(true)));
1721
1722 first.comment = Some("different purpose".to_owned());
1723 executor.fetch_all_compact(&first).unwrap();
1724
1725 assert_eq!(executor.column_layout_cache.lock().unwrap().len(), 1);
1726 }
1727
1728 #[test]
1729 fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1730 let executor =
1731 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1732 let entity = entity();
1733 let mut context = UserContext::new()
1734 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1735
1736 context.use_sqlite_provider(executor.clone());
1737 ensure_sqlite_schema_for(&context).unwrap();
1738
1739 let insert = SqliteDialect
1740 .compile_insert(
1741 &entity,
1742 &InsertCommand::new("Order")
1743 .value("id", 1_u64)
1744 .value("version", 1_i64)
1745 .value("name", "draft"),
1746 )
1747 .unwrap();
1748 assert_eq!(executor.execute(&insert).unwrap(), 1);
1749
1750 let select = SqliteDialect
1751 .compile_select(
1752 &entity,
1753 &SelectQuery::new("Order")
1754 .filter(Expr::eq("id", 1_u64))
1755 .order_asc("id"),
1756 )
1757 .unwrap();
1758 let rows = executor.fetch_all_compact(&select).unwrap();
1759 assert_eq!(rows.len(), 1);
1760 assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1761 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1762 assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
1763 }
1764
1765 #[test]
1766 fn repeated_schema_ensure_does_not_overwrite_existing_initial_graph() {
1767 let executor =
1768 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1769 let entity = entity();
1770 let mut context = UserContext::new()
1771 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1772 context.set_root_graphs(vec![
1773 GraphNode::new("Order")
1774 .value("id", 1_u64)
1775 .value("version", 1_i64)
1776 .value("name", "module seed"),
1777 ]);
1778 context.use_sqlite_provider(executor.clone());
1779
1780 ensure_sqlite_schema_for(&context).unwrap();
1781 let customize = SqliteDialect
1782 .compile_update(
1783 &entity,
1784 &UpdateCommand::new("Order", 1_u64).value("name", "application value"),
1785 )
1786 .unwrap();
1787 assert_eq!(executor.execute(&customize).unwrap(), 1);
1788
1789 ensure_sqlite_schema_for(&context).unwrap();
1790
1791 let select = SqliteDialect
1792 .compile_select(
1793 &entity,
1794 &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1795 )
1796 .unwrap();
1797 let rows = executor.fetch_all_compact(&select).unwrap();
1798 assert_eq!(rows.len(), 1);
1799 assert_eq!(
1800 rows[0].get("name"),
1801 Some(&Value::Text("application value".to_owned()))
1802 );
1803 }
1804
1805 #[test]
1806 fn repeated_schema_ensure_reconciles_changed_constant_graph() {
1807 let executor =
1808 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1809 let entity = entity();
1810 let mut context = UserContext::new()
1811 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1812 context.set_initial_graphs(vec![
1813 GraphNode::new("Order")
1814 .value("id", 1001_u64)
1815 .value("version", 1_i64)
1816 .value("name", "red"),
1817 ]);
1818 context.use_sqlite_provider(executor.clone());
1819 ensure_sqlite_schema_for(&context).unwrap();
1820 ensure_sqlite_schema_for(&context).unwrap();
1821
1822 let unchanged = SqliteDialect
1823 .compile_select(
1824 &entity,
1825 &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1826 )
1827 .unwrap();
1828 let rows = executor.fetch_all_compact(&unchanged).unwrap();
1829 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1830
1831 context.set_initial_graphs(vec![
1832 GraphNode::new("Order")
1833 .value("id", 1001_u64)
1834 .value("version", 1_i64)
1835 .value("name", "crimson"),
1836 ]);
1837 ensure_sqlite_schema_for(&context).unwrap();
1838
1839 let select = SqliteDialect
1840 .compile_select(
1841 &entity,
1842 &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1843 )
1844 .unwrap();
1845 let rows = executor.fetch_all_compact(&select).unwrap();
1846 assert_eq!(
1847 rows[0].get("name"),
1848 Some(&Value::Text("crimson".to_owned()))
1849 );
1850 assert_eq!(rows[0].get("version"), Some(&Value::I64(2)));
1851 let generator = SqliteIdSpaceGenerator::from_executor(executor);
1852 assert_eq!(generator.next_id("Order").unwrap(), 1002);
1853 }
1854
1855 #[test]
1856 fn sqlite_executes_partitioned_relation_limit_per_parent() {
1857 let executor =
1858 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1859 let entity = order_line_entity();
1860 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1861
1862 for order_id in [11_u64, 12_u64] {
1863 for index in 1_u64..=5 {
1864 let id = order_id * 100 + index;
1865 let insert = SqliteDialect
1866 .compile_insert(
1867 &entity,
1868 &InsertCommand::new("OrderLine")
1869 .value("id", id)
1870 .value("order_id", order_id)
1871 .value("name", format!("line-{id}")),
1872 )
1873 .unwrap();
1874 executor.execute(&insert).unwrap();
1875 }
1876 }
1877
1878 let query = SelectQuery::new("OrderLine")
1879 .project("id")
1880 .project("order_id")
1881 .order_desc("id")
1882 .limit(3)
1883 .partition_by("order_id");
1884 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1885 let rows = executor.fetch_all_compact(&compiled).unwrap();
1886
1887 assert_eq!(rows.len(), 6);
1888 for order_id in [11_i64, 12_i64] {
1889 let ids = rows
1890 .iter()
1891 .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
1892 .filter_map(|row| row.get("id").cloned())
1893 .collect::<Vec<_>>();
1894 assert_eq!(
1895 ids,
1896 vec![
1897 Value::I64(order_id * 100 + 5),
1898 Value::I64(order_id * 100 + 4),
1899 Value::I64(order_id * 100 + 3),
1900 ]
1901 );
1902 }
1903 }
1904
1905 #[test]
1906 fn topn_005_007_window_and_probes_preserve_results_and_predicates() {
1907 futures_executor::block_on(async {
1908 #[derive(Clone)]
1909 struct FixedSchema(Arc<EntityDescriptor>);
1910
1911 impl teaql_data_service::SchemaProvider for FixedSchema {
1912 fn get_entity(&self, name: &str) -> Option<Arc<EntityDescriptor>> {
1913 (name == self.0.name).then(|| self.0.clone())
1914 }
1915 }
1916
1917 let transport =
1918 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1919 let entity = Arc::new(order_line_entity());
1920 transport
1921 .ensure_schema(&SqliteDialect, &[entity.as_ref()])
1922 .unwrap();
1923
1924 for order_id in [11_u64, 12_u64, 13_u64] {
1925 for index in 1_u64..=5 {
1926 let id = order_id * 100 + index;
1927 let name = if index == 4 { "excluded" } else { "visible" };
1928 let insert = SqliteDialect
1929 .compile_insert(
1930 &entity,
1931 &InsertCommand::new("OrderLine")
1932 .value("id", id)
1933 .value("order_id", order_id)
1934 .value("name", name),
1935 )
1936 .unwrap();
1937 transport.execute(&insert).unwrap();
1938 }
1939 }
1940
1941 let executor = teaql_sql::SqlDataServiceExecutor::new(
1942 SqliteDialect,
1943 transport,
1944 FixedSchema(entity),
1945 );
1946 let base = SelectQuery::new("OrderLine")
1947 .project("id")
1948 .project("order_id")
1949 .project("name")
1950 .filter(Expr::in_list("order_id", [Value::U64(11), Value::U64(12)]))
1951 .and_filter(Expr::eq("name", "visible"))
1952 .order_desc("id")
1953 .limit(3)
1954 .partition_by("order_id");
1955 let execute = |query| {
1956 teaql_data_service::QueryExecutor::query(
1957 &executor,
1958 teaql_data_service::QueryRequest {
1959 query,
1960 trace_chain: Vec::new(),
1961 comment: Some("TOPN plan equivalence".to_owned()),
1962 capture_debug_query: false,
1963 capture_execution_metadata: false,
1964 },
1965 )
1966 };
1967
1968 let probes = execute(base.clone()).await.unwrap().rows;
1969 let window = execute(base.top_n_probe_parent_threshold(0))
1970 .await
1971 .unwrap()
1972 .rows;
1973 let children_of = |rows: &[CompactRow], parent: i64| {
1974 rows.iter()
1975 .filter(|row| row.get("order_id") == Some(&Value::I64(parent)))
1976 .map(|row| (row.get("id").cloned(), row.get("name").cloned()))
1977 .collect::<Vec<_>>()
1978 };
1979
1980 for parent in [11_i64, 12_i64] {
1981 assert_eq!(children_of(&probes, parent), children_of(&window, parent));
1982 }
1983 assert_eq!(
1984 children_of(&window, 11),
1985 vec![
1986 (Some(Value::I64(1105)), Some(Value::Text("visible".into()))),
1987 (Some(Value::I64(1103)), Some(Value::Text("visible".into()))),
1988 (Some(Value::I64(1102)), Some(Value::Text("visible".into()))),
1989 ]
1990 );
1991 assert_eq!(
1992 children_of(&window, 12),
1993 vec![
1994 (Some(Value::I64(1205)), Some(Value::Text("visible".into()))),
1995 (Some(Value::I64(1203)), Some(Value::Text("visible".into()))),
1996 (Some(Value::I64(1202)), Some(Value::Text("visible".into()))),
1997 ]
1998 );
1999 assert!(children_of(&probes, 13).is_empty());
2000 assert!(children_of(&window, 13).is_empty());
2001 });
2002 }
2003
2004 #[test]
2005 fn sqlite_boolean_new_schema_roundtrips_as_bool() {
2006 let executor =
2007 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2008 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2009 let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
2010 assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
2011 assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
2012 assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
2013
2014 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
2015 for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
2016 let insert = SqliteDialect
2017 .compile_insert(
2018 &entity,
2019 &InsertCommand::new("FeatureFlag")
2020 .value("id", id)
2021 .value("version", 1_i64)
2022 .value("enabled", enabled)
2023 .value("optional_enabled", optional_enabled),
2024 )
2025 .unwrap();
2026 assert_eq!(executor.execute(&insert).unwrap(), 1);
2027 }
2028
2029 let select = SqliteDialect
2030 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2031 .unwrap();
2032 let rows = executor.fetch_all_compact(&select).unwrap();
2033 assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
2034 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
2035 assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
2036 assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
2037
2038 let first =
2039 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2040 let second =
2041 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap();
2042 assert!(!first.enabled);
2043 assert_eq!(first.optional_enabled, Some(true));
2044 assert!(second.enabled);
2045 assert_eq!(second.optional_enabled, Some(false));
2046 }
2047
2048 #[test]
2049 fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
2050 let executor =
2051 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2052 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2053 executor
2054 .execute(&CompiledQuery {
2055 sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
2056 .to_owned(),
2057 params: Vec::new(),
2058 comment: None,
2059 })
2060 .unwrap();
2061
2062 let insert = SqliteDialect
2063 .compile_insert(
2064 &entity,
2065 &InsertCommand::new("FeatureFlag")
2066 .value("id", 1_u64)
2067 .value("version", 1_i64)
2068 .value("enabled", true)
2069 .value("optional_enabled", false),
2070 )
2071 .unwrap();
2072 executor.execute(&insert).unwrap();
2073 executor
2074 .execute(&CompiledQuery {
2075 sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
2076 .to_owned(),
2077 params: vec![
2078 Value::U64(2),
2079 Value::I64(1),
2080 Value::I64(2),
2081 Value::Null,
2082 ],
2083 comment: None,
2084 })
2085 .unwrap();
2086 let select = SqliteDialect
2087 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2088 .unwrap();
2089 let rows = executor.fetch_all_compact(&select).unwrap();
2090 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
2091 assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
2092 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
2093
2094 let decoded =
2095 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2096 assert!(decoded.enabled);
2097 assert_eq!(decoded.optional_enabled, Some(false));
2098 assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
2099 let error =
2100 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap_err();
2101 assert!(error.message.contains("invalid field enabled"));
2102
2103 for (value, expected) in [
2104 (Value::I64(0), false),
2105 (Value::I64(1), true),
2106 (Value::U64(0), false),
2107 (Value::U64(1), true),
2108 ] {
2109 let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2110 teaql_core::CompactRow::from_map(feature_flag_record(value, Value::Null)),
2111 )
2112 .unwrap();
2113 assert_eq!(decoded.enabled, expected);
2114 assert_eq!(decoded.optional_enabled, None);
2115 }
2116
2117 for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
2118 let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2119 teaql_core::CompactRow::from_map(feature_flag_record(invalid, Value::Null)),
2120 )
2121 .unwrap_err();
2122 assert!(error.message.contains("invalid field enabled"));
2123 }
2124 let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2125 teaql_core::CompactRow::from_map(feature_flag_record(Value::Bool(true), Value::U64(2))),
2126 )
2127 .unwrap_err();
2128 assert!(error.message.contains("invalid field optional_enabled"));
2129 }
2130
2131 #[test]
2132 fn sqlite_executor_parses_json_only_for_json_columns() {
2133 let executor =
2134 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2135
2136 executor
2137 .execute(&CompiledQuery {
2138 sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
2139 params: Vec::new(),
2140 comment: None,
2141 })
2142 .unwrap();
2143 executor
2144 .execute(&CompiledQuery {
2145 sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
2146 params: vec![
2147 Value::Text("{\"active\":true}".to_owned()),
2148 Value::Json(serde_json::json!({"active": true})),
2149 ],
2150 comment: None,
2151 })
2152 .unwrap();
2153
2154 let rows = executor
2155 .fetch_all_compact(&CompiledQuery {
2156 sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
2157 params: Vec::new(),
2158 comment: None,
2159 })
2160 .unwrap();
2161
2162 assert_eq!(
2163 rows[0].get("text_payload"),
2164 Some(&Value::Text("{\"active\":true}".to_owned()))
2165 );
2166 assert_eq!(
2167 rows[0].get("json_payload"),
2168 Some(&Value::Json(serde_json::json!({"active": true})))
2169 );
2170 }
2171
2172 #[test]
2173 fn sqlite_id_space_generator_increments_ids() {
2174 let executor =
2175 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2176 let generator = SqliteIdSpaceGenerator::from_executor(executor);
2177 assert_eq!(generator.next_id("Order").unwrap(), 1);
2178 assert_eq!(generator.next_id("Order").unwrap(), 2);
2179 }
2180
2181 #[test]
2182 fn sqlite_id_space_generator_is_safe_across_connections() {
2183 let path = std::env::temp_dir().join(format!(
2184 "teaql-id-space-{}-{}.db",
2185 std::process::id(),
2186 std::time::SystemTime::now()
2187 .duration_since(std::time::UNIX_EPOCH)
2188 .unwrap()
2189 .as_nanos()
2190 ));
2191 let mut workers = Vec::new();
2192 for _ in 0..4 {
2193 let path = path.clone();
2194 workers.push(std::thread::spawn(move || {
2195 let connection = Connection::open(path).unwrap();
2196 connection
2197 .busy_timeout(std::time::Duration::from_secs(5))
2198 .unwrap();
2199 let generator = SqliteIdSpaceGenerator::new(connection);
2200 (0..25)
2201 .map(|_| generator.next_id("Order").unwrap())
2202 .collect::<Vec<_>>()
2203 }));
2204 }
2205 let mut ids = workers
2206 .into_iter()
2207 .flat_map(|worker| worker.join().unwrap())
2208 .collect::<Vec<_>>();
2209 ids.sort_unstable();
2210 assert_eq!(ids, (1..=100).collect::<Vec<_>>());
2211 let _ = std::fs::remove_file(path);
2212 }
2213
2214 #[test]
2215 fn sqlite_fetch_stream_returns_chunked_rows() {
2216 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2217 Connection::open_in_memory().unwrap(),
2218 )));
2219 let entity = entity();
2220
2221 executor
2223 .execute(&CompiledQuery {
2224 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2225 .to_owned(),
2226 params: Vec::new(),
2227 comment: None,
2228 })
2229 .unwrap();
2230
2231 for i in 1..=25 {
2232 let insert = SqliteDialect
2233 .compile_insert(
2234 &entity,
2235 &InsertCommand::new("Order")
2236 .value("id", i as u64)
2237 .value("version", 1_i64)
2238 .value("name", format!("order-{i}")),
2239 )
2240 .unwrap();
2241 executor.execute(&insert).unwrap();
2242 }
2243
2244 let query = SelectQuery::new("Order")
2246 .filter(Expr::gt("version", 0_i64))
2247 .order_asc("id")
2248 .stream(10);
2249
2250 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2251
2252 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2253
2254 assert_eq!(chunks.len(), 3);
2256 assert_eq!(chunks[0].rows.len(), 10);
2257 assert_eq!(chunks[0].chunk_index, 0);
2258 assert!(!chunks[0].is_last);
2259
2260 assert_eq!(chunks[1].rows.len(), 10);
2261 assert_eq!(chunks[1].chunk_index, 1);
2262 assert!(!chunks[1].is_last);
2263
2264 assert_eq!(chunks[2].rows.len(), 5);
2265 assert_eq!(chunks[2].chunk_index, 2);
2266 assert!(chunks[2].is_last);
2267
2268 assert_eq!(
2270 chunks[0].rows[0].get("name"),
2271 Some(&Value::Text("order-1".to_owned()))
2272 );
2273 assert_eq!(
2274 chunks[2].rows[4].get("name"),
2275 Some(&Value::Text("order-25".to_owned()))
2276 );
2277 }
2278
2279 #[test]
2280 fn sqlite_fetch_stream_handles_empty_result() {
2281 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2282 Connection::open_in_memory().unwrap(),
2283 )));
2284
2285 executor
2286 .execute(&CompiledQuery {
2287 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2288 .to_owned(),
2289 params: Vec::new(),
2290 comment: None,
2291 })
2292 .unwrap();
2293
2294 let entity = entity();
2295 let query = SelectQuery::new("Order")
2296 .filter(Expr::gt("version", 0_i64))
2297 .stream(10);
2298
2299 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2300
2301 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2302
2303 assert_eq!(chunks.len(), 1);
2305 assert_eq!(chunks[0].rows.len(), 0);
2306 assert!(chunks[0].is_last);
2307 }
2308
2309 #[test]
2310 fn sqlite_fetch_stream_exact_chunk_boundary() {
2311 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2312 Connection::open_in_memory().unwrap(),
2313 )));
2314 let entity = entity();
2315
2316 executor
2317 .execute(&CompiledQuery {
2318 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2319 .to_owned(),
2320 params: Vec::new(),
2321 comment: None,
2322 })
2323 .unwrap();
2324
2325 for i in 1..=20 {
2327 let insert = SqliteDialect
2328 .compile_insert(
2329 &entity,
2330 &InsertCommand::new("Order")
2331 .value("id", i as u64)
2332 .value("version", 1_i64)
2333 .value("name", format!("order-{i}")),
2334 )
2335 .unwrap();
2336 executor.execute(&insert).unwrap();
2337 }
2338
2339 let query = SelectQuery::new("Order")
2340 .filter(Expr::gt("version", 0_i64))
2341 .order_asc("id")
2342 .stream(10);
2343
2344 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2345
2346 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2347
2348 assert_eq!(chunks.len(), 3);
2350 assert_eq!(chunks[0].rows.len(), 10);
2351 assert!(!chunks[0].is_last);
2352 assert_eq!(chunks[1].rows.len(), 10);
2353 assert!(!chunks[1].is_last);
2354 assert_eq!(chunks[2].rows.len(), 0);
2355 assert!(chunks[2].is_last);
2356 }
2357
2358 #[test]
2359 fn test_parse_sqlite_timestamp() {
2360 let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
2361 assert!(matches!(ts1, Value::Timestamp(_)));
2362
2363 let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
2364 assert!(matches!(ts2, Value::Timestamp(_)));
2365
2366 let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
2367 assert!(matches!(ts3, Value::Timestamp(_)));
2368
2369 let ts4 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546+00").unwrap();
2370 assert!(matches!(ts4, Value::Timestamp(_)));
2371
2372 let ts5 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546").unwrap();
2373 assert!(matches!(ts5, Value::Timestamp(_)));
2374
2375 assert_eq!(
2376 parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+00"),
2377 Some(1_704_067_200_000)
2378 );
2379 assert_eq!(
2380 parse_fixed_sqlite_timestamp("2024-01-01T08:00:00.123+08:00"),
2381 Some(1_704_067_200_123)
2382 );
2383 assert_eq!(
2384 parse_fixed_sqlite_timestamp("2023-12-31 19:00:00-0500"),
2385 Some(1_704_067_200_000)
2386 );
2387 assert_eq!(parse_fixed_sqlite_timestamp("2024-13-01 00:00:00Z"), None);
2388 assert_eq!(parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+24"), None);
2389
2390 assert!(parse_sqlite_timestamp("invalid").is_err());
2391 }
2392
2393 #[test]
2394 fn declared_text_does_not_infer_timestamp_from_content() {
2395 for decl_type in ["TEXT", "VARCHAR(255)", "CHAR(32)", "CLOB"] {
2396 let column = ColumnInfo {
2397 name: "external_timestamp".to_owned(),
2398 decode_kind: sqlite_decode_kind(Some(decl_type)),
2399 };
2400
2401 assert_eq!(
2402 decode_sqlite_text(b"2024-01-01 00:57:55", &column).unwrap(),
2403 Value::Text("2024-01-01 00:57:55".to_owned())
2404 );
2405 }
2406 }
2407
2408 #[test]
2409 fn declared_column_types_compile_to_decode_kinds() {
2410 assert_eq!(sqlite_decode_kind(Some("BOOLEAN")), SqliteDecodeKind::Bool);
2411 assert_eq!(
2412 sqlite_decode_kind(Some("decimal(20, 4)")),
2413 SqliteDecodeKind::Decimal
2414 );
2415 assert_eq!(
2416 sqlite_decode_kind(Some(" VARCHAR(255) ")),
2417 SqliteDecodeKind::Text
2418 );
2419 assert_eq!(
2420 sqlite_decode_kind(Some("datetime")),
2421 SqliteDecodeKind::Timestamp
2422 );
2423 assert_eq!(sqlite_decode_kind(Some("custom")), SqliteDecodeKind::Infer);
2424 assert_eq!(sqlite_decode_kind(None), SqliteDecodeKind::Infer);
2425 }
2426}