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_physical_schema_for(
599 context: &UserContext,
600) -> Result<(), MutationExecutorError> {
601 let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
602 MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
603 })?;
604 let executor = context
605 .get_resource::<SqliteMutationExecutor>()
606 .ok_or_else(|| {
607 MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
608 })?;
609
610 let entities = context.all_entities();
611
612 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
614
615 for entity in &entities {
617 let field_count = entity.properties.len();
618 if !executor.table_exists(&entity.table_name)? {
619 let sql = dialect.compile_create_table(entity)?;
621 executor.lock()?.execute(&sql, [])?;
622 let _ = context.send_event(RawAuditEvent::schema_created(
623 &entity.name,
624 &entity.table_name,
625 field_count,
626 ));
627 continue;
628 }
629 let existing_columns = executor.table_columns(&entity.table_name)?;
631 let mut fields_added = 0;
632 for property in &entity.properties {
633 let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
634 if existing_columns.contains(&bare_column) {
635 continue;
636 }
637 let sql = dialect.compile_add_column(entity, property)?;
638 executor.lock()?.execute(&sql, [])?;
639 let _ = context.send_event(RawAuditEvent::field_added(
640 &entity.name,
641 &entity.table_name,
642 &property.column_name,
643 ));
644 fields_added += 1;
645 }
646 let _ = context.send_event(RawAuditEvent::schema_verified(
647 &entity.name,
648 &entity.table_name,
649 field_count,
650 ));
651 let _ = fields_added; }
653
654 executor.clear_query_caches();
655 Ok(())
656}
657
658pub(crate) fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
659 ensure_sqlite_physical_schema_for(context)?;
660 if !context.initial_graphs().is_empty() || !context.root_graphs().is_empty() {
661 return Err(MutationExecutorError::Bind(
662 "generated root/constant bootstrap must use the typed RuntimeModule callback"
663 .to_owned(),
664 ));
665 }
666 Ok(())
667}
668
669#[allow(dead_code)]
670fn ensure_legacy_sqlite_bootstrap_for(context: &UserContext) -> Result<(), MutationExecutorError> {
671 ensure_sqlite_physical_schema_for(context)?;
672 let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
673 MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
674 })?;
675 let executor = context
676 .get_resource::<SqliteMutationExecutor>()
677 .ok_or_else(|| {
678 MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
679 })?;
680
681 let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
683 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); for graph in context.initial_graphs() {
685 let entity = context.entity(&graph.entity).ok_or_else(|| {
686 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
687 })?;
688 let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
689 if let Some(current) = initial_graph_row_sqlite(executor, dialect, entity, graph)? {
690 if let Some(query) = compile_initial_graph_update(dialect, entity, graph, ¤t)? {
691 executor.execute(&query)?;
692 counts.1 += 1;
693 }
694 if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
695 id_generator.ensure_floor(&graph.entity, id)?;
696 }
697 continue;
698 }
699 let query = compile_initial_graph_insert(dialect, entity, graph)?;
700 executor.execute(&query)?;
701 counts.0 += 1; if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
703 id_generator.ensure_floor(&graph.entity, id)?;
704 }
705 }
706
707 for graph in context.root_graphs() {
709 let entity = context.entity(&graph.entity).ok_or_else(|| {
710 MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
711 })?;
712 if initial_graph_row_sqlite(executor, dialect, entity, graph)?.is_some() {
713 if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
714 id_generator.ensure_floor(&graph.entity, id)?;
715 }
716 continue;
717 }
718 let query = compile_initial_graph_insert(dialect, entity, graph)?;
719 executor.execute(&query)?;
720 seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
721 if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
722 id_generator.ensure_floor(&graph.entity, id)?;
723 }
724 }
725
726 for (entity_name, (inserted, updated)) in &seed_counts {
728 let entity = context.entity(entity_name).ok_or_else(|| {
729 MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
730 })?;
731 let _ = context.send_event(RawAuditEvent::data_seeded(
732 entity_name,
733 &entity.table_name,
734 *inserted,
735 *updated,
736 ));
737 }
738
739 executor.clear_query_caches();
740 Ok(())
741}
742
743#[derive(Debug, Default, Clone, Copy)]
744pub struct SqliteSchemaProvider;
745
746impl SchemaProvider for SqliteSchemaProvider {
747 fn ensure_schema<'a>(
748 &'a self,
749 context: &'a UserContext,
750 _invocation: &'a teaql_runtime::SchemaInvocation,
751 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
752 Box::pin(async move {
753 ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
754 })
755 }
756}
757
758pub trait SqliteProviderExt {
759 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
760}
761
762impl SqliteProviderExt for UserContext {
763 fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
764 self.insert_resource(SqliteDialect);
765 self.insert_resource(executor);
766 self.set_schema_provider(SqliteSchemaProvider);
767 self
768 }
769}
770
771#[derive(Clone)]
772pub struct SqliteIdSpaceGenerator {
773 executor: SqliteMutationExecutor,
774 table_name: String,
775}
776
777impl SqliteIdSpaceGenerator {
778 pub fn new(connection: Connection) -> Self {
779 Self::from_executor(SqliteMutationExecutor::from_connection(connection))
780 }
781
782 pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
783 Self {
784 executor,
785 table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
786 }
787 }
788
789 pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
790 self.table_name = table_name.into();
791 self
792 }
793
794 pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
795 self.executor.ensure_id_space_table(&self.table_name)
796 }
797
798 pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
799 let entity = canonical_id_space_entity(entity);
800 let entity = entity.as_str();
801 self.ensure_table()?;
802 let table = quote_ident(&self.table_name);
803 let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
804 let insert_sql = format!("INSERT INTO {table} (type_name, current_level) VALUES (?, 1)");
805 let update_sql = format!(
806 "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
807 );
808 for attempt in 1..=100 {
809 let connection = self.executor.lock()?;
810 let current = connection
811 .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
812 .optional()?;
813 if let Some(current) = current {
814 let next = current.checked_add(1).ok_or_else(|| {
815 MutationExecutorError::Bind(format!(
816 "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
817 ))
818 })?;
819 if connection.execute(&update_sql, params![next, entity, current])? == 1 {
820 return u64::try_from(next).map_err(|_| {
821 MutationExecutorError::Bind(format!(
822 "generated id {next} cannot be represented as u64"
823 ))
824 });
825 }
826 } else {
827 match connection.execute(&insert_sql, params![entity]) {
828 Ok(1) => return Ok(1),
829 Ok(changed) => {
830 return Err(MutationExecutorError::Bind(format!(
831 "ID space insert for {entity} changed {changed} rows"
832 )));
833 }
834 Err(error)
835 if error.sqlite_error_code()
836 == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
837 Err(error) => return Err(error.into()),
838 }
839 }
840 }
841 Err(MutationExecutorError::Bind(format!(
842 "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
843 )))
844 }
845
846 pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
847 let entity = canonical_id_space_entity(entity);
848 let entity = entity.as_str();
849 self.ensure_table()?;
850 let floor = i64::try_from(floor).map_err(|_| {
851 MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
852 })?;
853 let table = quote_ident(&self.table_name);
854 for _ in 1..=100 {
855 let connection = self.executor.lock()?;
856 let current = connection
857 .query_row(
858 &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
859 [entity],
860 |row| row.get::<_, i64>(0),
861 )
862 .optional()?;
863 match current {
864 Some(current) if current >= floor => return Ok(()),
865 Some(current) => {
866 if connection.execute(
867 &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
868 params![floor, entity, current],
869 )? == 1 { return Ok(()); }
870 }
871 None => match connection.execute(
872 &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
873 params![entity, floor],
874 ) {
875 Ok(1) => return Ok(()),
876 Ok(_) => {}
877 Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
878 Err(error) => return Err(error.into()),
879 },
880 }
881 }
882 Err(MutationExecutorError::Bind(format!(
883 "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
884 )))
885 }
886}
887
888impl InternalIdGenerator for SqliteIdSpaceGenerator {
889 fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
890 self.next_id(entity)
891 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
892 }
893
894 fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), RuntimeError> {
895 SqliteIdSpaceGenerator::ensure_floor(self, entity, floor)
896 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
897 }
898}
899
900fn quote_ident(ident: &str) -> String {
901 quote_identifier_if_needed(ident, '"')
902}
903
904fn strip_identifier_quotes(ident: &str) -> &str {
912 let bytes = ident.as_bytes();
913 if bytes.len() >= 2 {
914 let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
915 if (first == b'"' && last == b'"')
916 || (first == b'`' && last == b'`')
917 || (first == b'[' && last == b']')
918 {
919 return &ident[1..ident.len() - 1];
920 }
921 }
922 ident
923}
924
925fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
926 values.iter().map(bind_sqlite_value).collect()
927}
928
929fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
930 match value {
931 Value::Null => Ok(SqliteValue::Null),
932 Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
933 Value::I64(v) => Ok(SqliteValue::Integer(*v)),
934 Value::U64(v) => i64::try_from(*v)
935 .map(SqliteValue::Integer)
936 .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
937 Value::F64(v) => Ok(SqliteValue::Real(*v)),
938 Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
942 Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
943 Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
944 Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
945 Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
946 Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
947 Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
948 Value::TypedNull(_) => Ok(SqliteValue::Null),
949 }
950}
951
952#[derive(Debug, Clone)]
953struct ColumnInfo {
954 name: String,
955 decode_kind: SqliteDecodeKind,
956}
957
958#[derive(Debug, Clone, Copy, PartialEq, Eq)]
959enum SqliteDecodeKind {
960 Infer,
961 Bool,
962 Decimal,
963 Json,
964 Date,
965 Timestamp,
966 Text,
967}
968
969#[derive(Debug)]
970struct ColumnLayout {
971 columns: Arc<[ColumnInfo]>,
972 names: Arc<[String]>,
973}
974
975fn cached_column_layout(
976 cache: &Mutex<HashMap<String, Arc<ColumnLayout>>>,
977 sql: &str,
978 statement: &rusqlite::Statement<'_>,
979) -> Arc<ColumnLayout> {
980 if let Ok(cache) = cache.lock()
981 && let Some(layout) = cache.get(sql)
982 {
983 return layout.clone();
984 }
985
986 let columns: Arc<[ColumnInfo]> = statement_columns(statement).into();
987 let names = columns
988 .iter()
989 .map(|column| column.name.clone())
990 .collect::<Vec<_>>()
991 .into();
992 let layout = Arc::new(ColumnLayout { columns, names });
993 if let Ok(mut cache) = cache.lock() {
994 if cache.len() >= DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY {
995 cache.clear();
996 }
997 cache.insert(sql.to_owned(), layout.clone());
998 }
999 layout
1000}
1001
1002fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
1003 statement
1004 .columns()
1005 .into_iter()
1006 .map(|column| ColumnInfo {
1007 name: column.name().to_owned(),
1008 decode_kind: sqlite_decode_kind(column.decl_type()),
1009 })
1010 .collect()
1011}
1012
1013fn sqlite_decode_kind(decl_type: Option<&str>) -> SqliteDecodeKind {
1014 let Some(decl_type) = decl_type else {
1015 return SqliteDecodeKind::Infer;
1016 };
1017 let base = decl_type.split('(').next().unwrap_or(decl_type).trim();
1018 if base.eq_ignore_ascii_case("BOOLEAN") || base.eq_ignore_ascii_case("BOOL") {
1019 SqliteDecodeKind::Bool
1020 } else if base.eq_ignore_ascii_case("NUMERIC") || base.eq_ignore_ascii_case("DECIMAL") {
1021 SqliteDecodeKind::Decimal
1022 } else if base.eq_ignore_ascii_case("JSON") {
1023 SqliteDecodeKind::Json
1024 } else if base.eq_ignore_ascii_case("DATE") {
1025 SqliteDecodeKind::Date
1026 } else if base.eq_ignore_ascii_case("TIMESTAMP") || base.eq_ignore_ascii_case("DATETIME") {
1027 SqliteDecodeKind::Timestamp
1028 } else if ["TEXT", "VARCHAR", "CHAR", "CLOB"]
1029 .iter()
1030 .any(|v| base.eq_ignore_ascii_case(v))
1031 {
1032 SqliteDecodeKind::Text
1033 } else {
1034 SqliteDecodeKind::Infer
1035 }
1036}
1037
1038fn decode_sqlite_values(
1039 row: &Row<'_>,
1040 columns: &[ColumnInfo],
1041) -> Result<Vec<Value>, MutationExecutorError> {
1042 let mut values = Vec::with_capacity(columns.len());
1043 for (index, column) in columns.iter().enumerate() {
1044 let value_ref = row.get_ref(index)?;
1045 let value = match value_ref {
1046 ValueRef::Null => Value::Null,
1047 ValueRef::Integer(value) => decode_sqlite_integer(value, column),
1048 ValueRef::Real(value) => Value::F64(value),
1049 ValueRef::Text(value) => decode_sqlite_text(value, column)?,
1050 ValueRef::Blob(_) => {
1051 return Err(MutationExecutorError::UnsupportedColumnType(
1052 "BLOB".to_owned(),
1053 ));
1054 }
1055 };
1056 values.push(value);
1057 }
1058 Ok(values)
1059}
1060
1061fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
1062 match column.decode_kind {
1063 SqliteDecodeKind::Bool => Value::Bool(value != 0),
1064 _ => Value::I64(value),
1065 }
1066}
1067
1068fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
1069 let value = std::str::from_utf8(value)
1070 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
1071 match column.decode_kind {
1072 SqliteDecodeKind::Decimal => Decimal::from_str(value)
1073 .map(Value::Decimal)
1074 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
1075 SqliteDecodeKind::Json => serde_json::from_str(value).map(Value::Json).map_err(|err| {
1076 MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
1077 }),
1078 SqliteDecodeKind::Date => NaiveDate::parse_from_str(value, "%Y-%m-%d")
1079 .map(Value::Date)
1080 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
1081 SqliteDecodeKind::Timestamp => parse_sqlite_timestamp(value),
1082 SqliteDecodeKind::Text | SqliteDecodeKind::Bool => Ok(Value::Text(value.to_owned())),
1083 SqliteDecodeKind::Infer => infer_sqlite_text(value),
1084 }
1085}
1086
1087fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
1088 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1089 return Ok(Value::Date(date));
1090 }
1091 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1092 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1093 timestamp.timestamp_millis(),
1094 )));
1095 }
1096 if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
1097 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1098 timestamp.and_utc().timestamp_millis(),
1099 )));
1100 }
1101 Ok(Value::Text(value.to_owned()))
1102}
1103
1104fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
1105 if let Some(timestamp) = parse_fixed_sqlite_timestamp(value) {
1106 return Ok(Value::Timestamp(teaql_core::time::Timestamp(timestamp)));
1107 }
1108 if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
1109 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1110 timestamp.timestamp_millis(),
1111 )));
1112 }
1113 if let Ok(timestamp) = DateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f%#z") {
1114 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1115 timestamp.timestamp_millis(),
1116 )));
1117 }
1118 if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1119 return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1120 date.and_hms_opt(0, 0, 0)
1121 .unwrap_or_default()
1122 .and_utc()
1123 .timestamp_millis(),
1124 )));
1125 }
1126 NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
1127 .map(|timestamp| {
1128 Value::Timestamp(teaql_core::time::Timestamp(
1129 timestamp.and_utc().timestamp_millis(),
1130 ))
1131 })
1132 .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
1133}
1134
1135fn parse_fixed_sqlite_timestamp(value: &str) -> Option<i64> {
1136 let bytes = value.as_bytes();
1137 if bytes.len() < 19
1138 || bytes.get(4) != Some(&b'-')
1139 || bytes.get(7) != Some(&b'-')
1140 || !matches!(bytes.get(10), Some(b' ') | Some(b'T'))
1141 || bytes.get(13) != Some(&b':')
1142 || bytes.get(16) != Some(&b':')
1143 {
1144 return None;
1145 }
1146 let digits = |start: usize, len: usize| -> Option<u32> {
1147 bytes
1148 .get(start..start + len)?
1149 .iter()
1150 .try_fold(0_u32, |value, byte| {
1151 byte.is_ascii_digit()
1152 .then_some(value * 10 + u32::from(*byte - b'0'))
1153 })
1154 };
1155 let date = NaiveDate::from_ymd_opt(
1156 i32::try_from(digits(0, 4)?).ok()?,
1157 digits(5, 2)?,
1158 digits(8, 2)?,
1159 )?;
1160 let hour = digits(11, 2)?;
1161 let minute = digits(14, 2)?;
1162 let second = digits(17, 2)?;
1163 let mut cursor = 19;
1164 let mut nanos = 0_u32;
1165 if bytes.get(cursor) == Some(&b'.') {
1166 cursor += 1;
1167 let fraction_start = cursor;
1168 while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
1169 if cursor - fraction_start < 9 {
1170 nanos = nanos * 10 + u32::from(bytes[cursor] - b'0');
1171 }
1172 cursor += 1;
1173 }
1174 let kept = (cursor - fraction_start).min(9);
1175 if kept == 0 {
1176 return None;
1177 }
1178 nanos *= 10_u32.pow(u32::try_from(9 - kept).ok()?);
1179 }
1180 let datetime = date.and_hms_nano_opt(hour, minute, second, nanos)?;
1181 let offset_seconds = match bytes.get(cursor..) {
1182 Some([]) | Some([b'Z']) | Some([b'z']) => 0,
1183 Some([sign @ (b'+' | b'-'), hour_1, hour_2]) => {
1184 signed_offset(*sign, [*hour_1, *hour_2], *b"00")?
1185 }
1186 Some([sign @ (b'+' | b'-'), hour_1, hour_2, minute_1, minute_2]) => {
1187 signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?
1188 }
1189 Some(
1190 [
1191 sign @ (b'+' | b'-'),
1192 hour_1,
1193 hour_2,
1194 b':',
1195 minute_1,
1196 minute_2,
1197 ],
1198 ) => signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?,
1199 _ => return None,
1200 };
1201 FixedOffset::east_opt(offset_seconds)?
1202 .from_local_datetime(&datetime)
1203 .single()
1204 .map(|timestamp| timestamp.timestamp_millis())
1205}
1206
1207fn signed_offset(sign: u8, hours: [u8; 2], minutes: [u8; 2]) -> Option<i32> {
1208 let pair = |digits: [u8; 2]| {
1209 digits
1210 .iter()
1211 .all(u8::is_ascii_digit)
1212 .then_some(i32::from(digits[0] - b'0') * 10 + i32::from(digits[1] - b'0'))
1213 };
1214 let hours = pair(hours)?;
1215 let minutes = pair(minutes)?;
1216 if hours > 23 || minutes > 59 {
1217 return None;
1218 }
1219 let seconds = hours * 3600 + minutes * 60;
1220 Some(if sign == b'-' { -seconds } else { seconds })
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::*;
1226 use futures_util::StreamExt;
1227 use teaql_core::{DeleteCommand, Entity, Record, RecoverCommand, TeaqlEntity as _};
1228 use teaql_macros::{TeaqlEntity, teaql_entity};
1229 use teaql_runtime::InMemoryMetadataStore;
1230
1231 #[teaql_entity]
1232 #[derive(Debug, TeaqlEntity)]
1233 #[teaql(entity = "TransactionSchool", table = "transaction_school")]
1234 struct TransactionSchool {
1235 #[teaql(id)]
1236 id: u64,
1237 #[teaql(version)]
1238 version: i64,
1239 name: String,
1240 }
1241
1242 impl TransactionSchool {
1243 fn new(id: u64, name: &str) -> (Self, teaql_runtime::EntityRuntimeState) {
1244 let state = teaql_runtime::EntityRuntimeState::default();
1245 let key = teaql_runtime::EntityKey::new("TransactionSchool", id);
1246 state.mark_as_new(key.clone());
1247 state.set(key.clone(), "id", id);
1248 state.set(key, "name", name);
1249 (
1250 Self {
1251 id,
1252 version: 0,
1253 name: name.to_owned(),
1254 __teaql_runtime_state: state.clone(),
1255 },
1256 state,
1257 )
1258 }
1259 }
1260
1261 #[test]
1262 fn ensure_schema_registers_soundex_idempotently() {
1263 let executor =
1264 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1265 executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1266 executor.ensure_schema(&SqliteDialect, &[]).unwrap();
1267 let connection = executor.connection();
1268 let guard = connection.lock().unwrap();
1269 let encoded: String = guard
1270 .query_row("SELECT soundex('Robert')", [], |row| row.get(0))
1271 .unwrap();
1272 let matches: i64 = guard
1273 .query_row("SELECT soundex('Robert') = soundex('Rupert')", [], |row| {
1274 row.get(0)
1275 })
1276 .unwrap();
1277 let empty: String = guard
1278 .query_row("SELECT soundex(NULL)", [], |row| row.get(0))
1279 .unwrap();
1280 assert_eq!(encoded, "R163");
1281 assert_eq!(matches, 1);
1282 assert_eq!(empty, "?000");
1283 }
1284
1285 #[test]
1286 fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
1287 let connection = Connection::open_in_memory().unwrap();
1288 connection
1289 .execute_batch(
1290 "CREATE TABLE stream_fixture(id INTEGER);\
1291 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
1292 )
1293 .unwrap();
1294 let executor = SqliteMutationExecutor::from_connection(connection);
1295 let query = CompiledQuery {
1296 sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
1297 params: vec![],
1298 comment: None,
1299 };
1300 let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
1301 let sizes = futures_executor::block_on(async {
1302 let mut result = Vec::new();
1303 while let Some(chunk) = stream.next().await {
1304 result.push(chunk.unwrap().rows.len());
1305 }
1306 result
1307 });
1308 assert_eq!(sizes, vec![2, 2, 1]);
1309
1310 let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
1311 assert_eq!(
1312 futures_executor::block_on(early.next())
1313 .unwrap()
1314 .unwrap()
1315 .rows
1316 .len(),
1317 2
1318 );
1319 drop(early);
1320 let count: i64 = executor
1321 .connection()
1322 .lock()
1323 .unwrap()
1324 .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
1325 .unwrap();
1326 assert_eq!(count, 5);
1327 }
1328
1329 #[test]
1330 fn decimal_bind_is_numeric_and_comparable() {
1331 let value =
1332 bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
1333 assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
1334 let connection = Connection::open_in_memory().unwrap();
1335 let matches: i64 = connection
1336 .query_row(
1337 "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
1338 [value],
1339 |row| row.get(0),
1340 )
1341 .unwrap();
1342 assert_eq!(matches, 1);
1343 }
1344
1345 #[test]
1346 fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
1347 let connection = Connection::open_in_memory().unwrap();
1348 connection
1349 .execute_batch(
1350 "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
1351 )
1352 .unwrap();
1353 let query = CompiledQuery {
1354 sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
1355 params: vec![
1356 Value::I64(1),
1357 Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
1358 Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
1359 ],
1360 comment: None,
1361 };
1362 let values = bind_values(&query.params).unwrap();
1363 connection
1364 .execute(&query.sql, rusqlite::params_from_iter(values))
1365 .unwrap();
1366 connection
1367 .execute(
1368 &query
1369 .debug_sql(teaql_sql::DatabaseKind::Sqlite)
1370 .replace("VALUES (1,", "VALUES (2,"),
1371 [],
1372 )
1373 .unwrap();
1374
1375 let equal_count: i64 = connection.query_row(
1376 "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",
1377 [], |row| row.get(0),
1378 ).unwrap();
1379 let storage_type: String = connection
1380 .query_row(
1381 "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
1382 [],
1383 |row| row.get(0),
1384 )
1385 .unwrap();
1386 assert_eq!(equal_count, 1);
1387 assert_eq!(storage_type, "integer");
1388 }
1389
1390 fn entity() -> EntityDescriptor {
1391 EntityDescriptor::new("Order")
1392 .table_name("orders")
1393 .property(
1394 PropertyDescriptor::new("id", DataType::U64)
1395 .column_name("id")
1396 .id()
1397 .not_null(),
1398 )
1399 .property(
1400 PropertyDescriptor::new("version", DataType::I64)
1401 .column_name("version")
1402 .version()
1403 .not_null(),
1404 )
1405 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1406 }
1407
1408 #[test]
1409 fn user_context_transaction_scope_commits_and_rolls_back_on_one_connection() {
1410 let executor = SqliteMutationExecutor::from_connection(
1411 Connection::open_in_memory().expect("open transaction fixture"),
1412 );
1413 executor
1414 .connection()
1415 .lock()
1416 .expect("lock transaction fixture")
1417 .execute_batch(
1418 "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, name TEXT)",
1419 )
1420 .expect("create transaction fixture");
1421
1422 let metadata = InMemoryMetadataStore::new().with_entity(entity());
1423 let data_service = teaql_sql::SqlDataServiceExecutor::new(
1424 SqliteDialect,
1425 executor.clone(),
1426 metadata.clone(),
1427 );
1428 let mut context = UserContext::new().with_metadata(metadata);
1429 context.register_executor(data_service);
1430
1431 type Executor = teaql_sql::SqlDataServiceExecutor<
1432 SqliteDialect,
1433 SqliteMutationExecutor,
1434 InMemoryMetadataStore,
1435 >;
1436
1437 futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1438 Box::pin(async move {
1439 scope
1440 .mutate(teaql_data_service::MutationRequest::Insert(
1441 teaql_core::InsertCommand::new("Order")
1442 .value("id", 1_u64)
1443 .value("version", 1_i64)
1444 .value("name", "first"),
1445 ))
1446 .await?;
1447 scope
1448 .mutate(teaql_data_service::MutationRequest::Insert(
1449 teaql_core::InsertCommand::new("Order")
1450 .value("id", 2_u64)
1451 .value("version", 1_i64)
1452 .value("name", "second"),
1453 ))
1454 .await?;
1455 Ok(())
1456 })
1457 }))
1458 .expect("commit transaction scope");
1459
1460 let failed =
1461 futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1462 Box::pin(async move {
1463 scope
1464 .mutate(teaql_data_service::MutationRequest::Insert(
1465 teaql_core::InsertCommand::new("Order")
1466 .value("id", 3_u64)
1467 .value("version", 1_i64)
1468 .value("name", "must roll back"),
1469 ))
1470 .await?;
1471 scope
1472 .mutate(teaql_data_service::MutationRequest::Insert(
1473 teaql_core::InsertCommand::new("Order")
1474 .value("id", 1_u64)
1475 .value("version", 1_i64)
1476 .value("name", "duplicate"),
1477 ))
1478 .await?;
1479 Ok(())
1480 })
1481 }));
1482 assert!(failed.is_err(), "duplicate key must fail the scope");
1483
1484 let connection = executor.connection();
1485 let guard = connection.lock().expect("lock committed fixture");
1486 let ids = guard
1487 .prepare("SELECT id FROM orders ORDER BY id")
1488 .expect("prepare committed ids")
1489 .query_map([], |row| row.get::<_, i64>(0))
1490 .expect("query committed ids")
1491 .collect::<Result<Vec<_>, _>>()
1492 .expect("read committed ids");
1493 assert_eq!(ids, vec![1, 2]);
1494 }
1495
1496 #[test]
1497 fn typed_audited_saves_share_commit_and_rollback_boundary() {
1498 let executor = SqliteMutationExecutor::from_connection(
1499 Connection::open_in_memory().expect("open audited transaction fixture"),
1500 );
1501 executor
1502 .connection()
1503 .lock()
1504 .expect("lock audited transaction fixture")
1505 .execute_batch(
1506 "CREATE TABLE transaction_school (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, name TEXT NOT NULL)",
1507 )
1508 .expect("create audited transaction fixture");
1509
1510 let metadata = InMemoryMetadataStore::new()
1511 .with_entity(TransactionSchool::entity_descriptor().clone());
1512 let data_service = teaql_sql::SqlDataServiceExecutor::new(
1513 SqliteDialect,
1514 executor.clone(),
1515 metadata.clone(),
1516 );
1517 let mut context = UserContext::new().with_metadata(metadata);
1518 context.register_executor(data_service);
1519
1520 type Executor = teaql_sql::SqlDataServiceExecutor<
1521 SqliteDialect,
1522 SqliteMutationExecutor,
1523 InMemoryMetadataStore,
1524 >;
1525
1526 futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1527 Box::pin(async move {
1528 let (first, _) = TransactionSchool::new(1, "first");
1529 let (second, _) = TransactionSchool::new(2, "second");
1530 let first = scope
1531 .save_audited(first.audit_as("create first school"))
1532 .await?;
1533 let second = scope
1534 .save_audited(second.audit_as("create second school"))
1535 .await?;
1536 assert_eq!(first.version, 1);
1537 assert_eq!(second.version, 1);
1538 Ok(())
1539 })
1540 }))
1541 .expect("commit audited transaction scope");
1542
1543 let (third, third_ledger) = TransactionSchool::new(3, "must roll back");
1544 let failed =
1545 futures_executor::block_on(context.execute_in_transaction::<Executor, _, _>(|scope| {
1546 Box::pin(async move {
1547 scope
1548 .save_audited(third.audit_as("create third school"))
1549 .await?;
1550 let (duplicate, _) = TransactionSchool::new(1, "duplicate");
1551 scope
1552 .save_audited(duplicate.audit_as("force duplicate failure"))
1553 .await?;
1554 Ok(())
1555 })
1556 }));
1557 assert!(
1558 failed.is_err(),
1559 "duplicate audited save must fail the scope"
1560 );
1561 assert!(
1562 !third_ledger.new_keys().is_empty(),
1563 "rolled-back ledger must retain retryable mutation intent"
1564 );
1565
1566 let connection = executor.connection();
1567 let guard = connection.lock().expect("lock audited committed fixture");
1568 let rows = guard
1569 .prepare("SELECT id, name FROM transaction_school ORDER BY id")
1570 .expect("prepare audited committed rows")
1571 .query_map([], |row| {
1572 Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
1573 })
1574 .expect("query audited committed rows")
1575 .collect::<Result<Vec<_>, _>>()
1576 .expect("read audited committed rows");
1577 assert_eq!(
1578 rows,
1579 vec![(1, "first".to_owned()), (2, "second".to_owned())]
1580 );
1581 }
1582
1583 fn order_line_entity() -> EntityDescriptor {
1584 EntityDescriptor::new("OrderLine")
1585 .table_name("order_line")
1586 .property(
1587 PropertyDescriptor::new("id", DataType::U64)
1588 .column_name("id")
1589 .id()
1590 .not_null(),
1591 )
1592 .property(
1593 PropertyDescriptor::new("order_id", DataType::U64)
1594 .column_name("order_id")
1595 .not_null(),
1596 )
1597 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1598 }
1599
1600 fn complete_query_record_entity() -> EntityDescriptor {
1601 EntityDescriptor::new("QueryRecord")
1602 .table_name("query_record_scalar")
1603 .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1604 .property(PropertyDescriptor::new("required_text", DataType::Text))
1605 .property(PropertyDescriptor::new("optional_text", DataType::Text))
1606 .property(PropertyDescriptor::new("required_integer", DataType::I64))
1607 .property(PropertyDescriptor::new("optional_long", DataType::I64))
1608 .property(PropertyDescriptor::new(
1609 "required_decimal",
1610 DataType::Decimal,
1611 ))
1612 .property(PropertyDescriptor::new("required_float", DataType::F64))
1613 .property(PropertyDescriptor::new("required_double", DataType::F64))
1614 .property(PropertyDescriptor::new("required_date", DataType::Date))
1615 .property(PropertyDescriptor::new("required_time", DataType::I64))
1616 .property(PropertyDescriptor::new(
1617 "required_timestamp",
1618 DataType::Timestamp,
1619 ))
1620 .property(PropertyDescriptor::new("active", DataType::Bool))
1621 .property(PropertyDescriptor::new("reviewed", DataType::Bool))
1622 .property(
1623 PropertyDescriptor::new("version", DataType::I64)
1624 .version()
1625 .not_null(),
1626 )
1627 }
1628
1629 #[test]
1630 fn complete_scalar_fixture_including_nullable_boolean_executes_on_sqlite() {
1631 let executor = SqliteMutationExecutor::from_connection(
1632 Connection::open_in_memory().expect("open SQLite fixture"),
1633 );
1634 executor
1635 .connection()
1636 .lock()
1637 .expect("lock SQLite fixture")
1638 .execute_batch("CREATE TABLE query_record_scalar (\
1639 id INTEGER PRIMARY KEY, required_text TEXT, optional_text TEXT,\
1640 required_integer INTEGER, optional_long INTEGER, required_decimal NUMERIC,\
1641 required_float REAL, required_double REAL, required_date DATE,\
1642 required_time INTEGER, required_timestamp TIMESTAMP,\
1643 active BOOLEAN, reviewed BOOLEAN, version INTEGER);\
1644 INSERT INTO query_record_scalar VALUES \
1645 (1,'Alpha','optional',42,42000000000,42.125,42.5,42.75,'2026-08-29',34200000,1777632600000,1,0,1),\
1646 (2,'Beta',NULL,7,NULL,7.500,7.5,7.75,'2026-08-30',36000000,1777720400000,0,NULL,1),\
1647 (3,'Gamma','tail',99,99000000000,99.875,99.5,99.75,'2026-08-31',37800000,1777808200000,1,1,1)")
1648 .expect("seed complete scalar fixture");
1649 let entity = complete_query_record_entity();
1650 let ids = |expr: Expr| {
1651 let query = SelectQuery::new("QueryRecord")
1652 .project("id")
1653 .filter(expr)
1654 .order_asc("id");
1655 executor
1656 .fetch_all_compact(&SqliteDialect.compile_select(&entity, &query).unwrap())
1657 .expect("execute scalar predicate")
1658 .into_iter()
1659 .map(|row| row.get("id").cloned().expect("projected id"))
1660 .collect::<Vec<_>>()
1661 };
1662 assert_eq!(ids(Expr::eq("required_text", "Alpha")), vec![Value::I64(1)]);
1663 assert_eq!(
1664 ids(Expr::ne("required_text", "Alpha")),
1665 vec![Value::I64(2), Value::I64(3)]
1666 );
1667 assert_eq!(
1668 ids(Expr::in_list(
1669 "required_text",
1670 [Value::from("Alpha"), Value::from("Gamma")]
1671 )),
1672 vec![Value::I64(1), Value::I64(3)]
1673 );
1674 assert_eq!(
1675 ids(Expr::contain("required_text", "et")),
1676 vec![Value::I64(2)]
1677 );
1678 assert_eq!(
1679 ids(Expr::between("required_integer", 40_i64, 100_i64)),
1680 vec![Value::I64(1), Value::I64(3)]
1681 );
1682 assert_eq!(
1683 ids(Expr::gt("required_decimal", Decimal::from(50))),
1684 vec![Value::I64(3)]
1685 );
1686 assert_eq!(
1687 ids(Expr::lte("required_float", 7.5_f64)),
1688 vec![Value::I64(2)]
1689 );
1690 assert_eq!(
1691 ids(Expr::gte("required_double", 99.75_f64)),
1692 vec![Value::I64(3)]
1693 );
1694 assert_eq!(
1695 ids(Expr::between(
1696 "required_date",
1697 NaiveDate::from_ymd_opt(2026, 8, 30).unwrap(),
1698 NaiveDate::from_ymd_opt(2026, 8, 31).unwrap(),
1699 )),
1700 vec![Value::I64(2), Value::I64(3)]
1701 );
1702 assert_eq!(
1703 ids(Expr::gt("required_time", 36_000_000_i64)),
1704 vec![Value::I64(3)]
1705 );
1706 assert_eq!(
1707 ids(Expr::lt(
1708 "required_timestamp",
1709 teaql_core::time::Timestamp(1_777_750_000_000)
1710 )),
1711 vec![Value::I64(1), Value::I64(2)]
1712 );
1713 assert_eq!(ids(Expr::is_null("optional_text")), vec![Value::I64(2)]);
1714 assert_eq!(
1715 ids(Expr::is_not_null("optional_long")),
1716 vec![Value::I64(1), Value::I64(3)]
1717 );
1718 assert_eq!(ids(Expr::eq("active", false)), vec![Value::I64(2)]);
1719 assert_eq!(ids(Expr::eq("reviewed", true)), vec![Value::I64(3)]);
1720 assert_eq!(ids(Expr::eq("reviewed", false)), vec![Value::I64(1)]);
1721 assert_eq!(ids(Expr::is_null("reviewed")), vec![Value::I64(2)]);
1722 }
1723
1724 #[test]
1725 fn relation_subqueries_execute_positive_and_negative_predicates_on_sqlite() {
1726 let executor = SqliteMutationExecutor::from_connection(
1727 Connection::open_in_memory().expect("open SQLite fixture"),
1728 );
1729 executor
1730 .connection()
1731 .lock()
1732 .expect("lock SQLite fixture")
1733 .execute_batch(
1734 "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name TEXT);\
1735 CREATE TABLE order_line (id INTEGER PRIMARY KEY, order_id INTEGER, name TEXT);\
1736 INSERT INTO orders VALUES (1, 1, 'first'), (2, 1, 'second'), (3, 1, 'third');\
1737 INSERT INTO order_line VALUES\
1738 (10, 1, 'priority'), (11, 1, 'ordinary'), (12, 2, 'ordinary'),\
1739 (13, NULL, 'orphan');",
1740 )
1741 .expect("seed relation fixture");
1742
1743 let matching_lines = SelectQuery::new("OrderLine").filter(Expr::eq("name", "priority"));
1744 let positive = SelectQuery::new("Order")
1745 .project("id")
1746 .filter(Expr::in_subquery(
1747 "id",
1748 order_line_entity(),
1749 matching_lines.clone(),
1750 "order_id",
1751 ))
1752 .order_asc("id");
1753 let negative = SelectQuery::new("Order")
1754 .project("id")
1755 .filter(Expr::not_in_subquery(
1756 "id",
1757 order_line_entity(),
1758 matching_lines,
1759 "order_id",
1760 ))
1761 .order_asc("id");
1762
1763 let ids = |rows: Vec<CompactRow>| {
1764 rows.into_iter()
1765 .map(|row| row.get("id").cloned().expect("projected id"))
1766 .collect::<Vec<_>>()
1767 };
1768 let order_ids = |query: SelectQuery| {
1769 ids(executor
1770 .fetch_all_compact(&SqliteDialect.compile_select(&entity(), &query).unwrap())
1771 .expect("execute order relation predicate"))
1772 };
1773 let line_ids = |query: SelectQuery| {
1774 ids(executor
1775 .fetch_all_compact(
1776 &SqliteDialect
1777 .compile_select(&order_line_entity(), &query)
1778 .unwrap(),
1779 )
1780 .expect("execute line relation predicate"))
1781 };
1782
1783 assert_eq!(order_ids(positive), vec![Value::I64(1)]);
1785 assert_eq!(order_ids(negative), vec![Value::I64(2), Value::I64(3)]);
1786
1787 assert_eq!(
1789 line_ids(
1790 SelectQuery::new("OrderLine")
1791 .project("id")
1792 .filter(Expr::is_not_null("order_id"))
1793 .order_asc("id")
1794 ),
1795 vec![Value::I64(10), Value::I64(11), Value::I64(12)]
1796 );
1797 assert_eq!(
1798 line_ids(
1799 SelectQuery::new("OrderLine")
1800 .project("id")
1801 .filter(Expr::is_null("order_id"))
1802 .order_asc("id")
1803 ),
1804 vec![Value::I64(13)]
1805 );
1806
1807 let first_order = SelectQuery::new("Order").filter(Expr::eq("name", "first"));
1810 assert_eq!(
1811 line_ids(
1812 SelectQuery::new("OrderLine")
1813 .project("id")
1814 .filter(Expr::in_subquery(
1815 "order_id",
1816 entity(),
1817 first_order.clone(),
1818 "id",
1819 ))
1820 .order_asc("id")
1821 ),
1822 vec![Value::I64(10), Value::I64(11)]
1823 );
1824 assert_eq!(
1825 line_ids(
1826 SelectQuery::new("OrderLine")
1827 .project("id")
1828 .filter(Expr::not_in_subquery(
1829 "order_id",
1830 entity(),
1831 first_order,
1832 "id",
1833 ))
1834 .order_asc("id")
1835 ),
1836 vec![Value::I64(12)]
1837 );
1838
1839 let all_lines = SelectQuery::new("OrderLine");
1841 assert_eq!(
1842 order_ids(
1843 SelectQuery::new("Order")
1844 .project("id")
1845 .filter(Expr::in_subquery(
1846 "id",
1847 order_line_entity(),
1848 all_lines.clone(),
1849 "order_id",
1850 ))
1851 .order_asc("id")
1852 ),
1853 vec![Value::I64(1), Value::I64(2)]
1854 );
1855 assert_eq!(
1856 order_ids(
1857 SelectQuery::new("Order")
1858 .project("id")
1859 .filter(Expr::not_in_subquery(
1860 "id",
1861 order_line_entity(),
1862 all_lines,
1863 "order_id",
1864 ))
1865 .order_asc("id")
1866 ),
1867 vec![Value::I64(3)]
1868 );
1869 }
1870
1871 #[allow(dead_code)]
1872 #[derive(Debug, PartialEq, TeaqlEntity)]
1873 #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1874 struct FeatureFlagRow {
1875 #[teaql(id)]
1876 id: u64,
1877 #[teaql(version)]
1878 version: i64,
1879 enabled: bool,
1880 optional_enabled: Option<bool>,
1881 }
1882
1883 fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1884 Record::from([
1885 ("id".to_owned(), Value::U64(1)),
1886 ("version".to_owned(), Value::I64(1)),
1887 ("enabled".to_owned(), enabled),
1888 ("optional_enabled".to_owned(), optional_enabled),
1889 ])
1890 }
1891
1892 #[test]
1893 fn sqlite_dialect_compiles_mutations_and_schema() {
1894 assert!(SqliteDialect.prefers_small_parent_relation_probes());
1895 let insert = SqliteDialect
1896 .compile_insert(
1897 &entity(),
1898 &InsertCommand::new("Order")
1899 .value("id", 1_u64)
1900 .value("name", "A"),
1901 )
1902 .unwrap();
1903 assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1904
1905 let update = SqliteDialect
1906 .compile_update(
1907 &entity(),
1908 &UpdateCommand::new("Order", 1_u64)
1909 .expected_version(3)
1910 .value("name", "B"),
1911 )
1912 .unwrap();
1913 assert_eq!(
1914 update.sql,
1915 "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1916 );
1917
1918 let delete = SqliteDialect
1919 .compile_delete(
1920 &entity(),
1921 &DeleteCommand::new("Order", 1_u64).expected_version(3),
1922 )
1923 .unwrap();
1924 let recover = SqliteDialect
1925 .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1926 .unwrap();
1927 assert_eq!(
1928 delete.sql,
1929 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1930 );
1931 assert_eq!(
1932 recover.sql,
1933 "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1934 );
1935
1936 let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1937 assert_eq!(
1938 create,
1939 "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1940 );
1941 }
1942
1943 #[test]
1944 fn column_layout_cache_uses_parameterized_sql_not_comments() {
1945 let connection = Connection::open_in_memory().unwrap();
1946 connection
1947 .execute("CREATE TABLE sample (id INTEGER, enabled BOOLEAN)", [])
1948 .unwrap();
1949 connection
1950 .execute("INSERT INTO sample (id, enabled) VALUES (1, 1)", [])
1951 .unwrap();
1952 let executor = SqliteMutationExecutor::from_connection(connection);
1953 let mut first = CompiledQuery {
1954 sql: "SELECT id, enabled FROM sample WHERE id = ?".to_owned(),
1955 params: vec![Value::I64(1)],
1956 comment: Some("first purpose".to_owned()),
1957 };
1958 let rows = executor.fetch_all_compact(&first).unwrap();
1959 assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(true)));
1960
1961 first.comment = Some("different purpose".to_owned());
1962 executor.fetch_all_compact(&first).unwrap();
1963
1964 assert_eq!(executor.column_layout_cache.lock().unwrap().len(), 1);
1965 }
1966
1967 #[test]
1968 fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1969 let executor =
1970 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1971 let entity = entity();
1972 let mut context = UserContext::new()
1973 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1974
1975 context.use_sqlite_provider(executor.clone());
1976 ensure_sqlite_schema_for(&context).unwrap();
1977
1978 let insert = SqliteDialect
1979 .compile_insert(
1980 &entity,
1981 &InsertCommand::new("Order")
1982 .value("id", 1_u64)
1983 .value("version", 1_i64)
1984 .value("name", "draft"),
1985 )
1986 .unwrap();
1987 assert_eq!(executor.execute(&insert).unwrap(), 1);
1988
1989 let select = SqliteDialect
1990 .compile_select(
1991 &entity,
1992 &SelectQuery::new("Order")
1993 .filter(Expr::eq("id", 1_u64))
1994 .order_asc("id"),
1995 )
1996 .unwrap();
1997 let rows = executor.fetch_all_compact(&select).unwrap();
1998 assert_eq!(rows.len(), 1);
1999 assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
2000 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
2001 assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
2002 }
2003
2004 #[test]
2005 fn physical_schema_never_interprets_generated_bootstrap_graphs() {
2006 let executor =
2007 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2008 let entity = entity();
2009 let mut context = UserContext::new()
2010 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
2011 context.set_root_graphs(vec![
2012 GraphNode::new("Order")
2013 .value("id", 1_u64)
2014 .value("version", 1_i64)
2015 .value("name", "module seed"),
2016 ]);
2017 context.use_sqlite_provider(executor.clone());
2018
2019 ensure_sqlite_physical_schema_for(&context).unwrap();
2020
2021 let select = SqliteDialect
2022 .compile_select(
2023 &entity,
2024 &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
2025 )
2026 .unwrap();
2027 let rows = executor.fetch_all_compact(&select).unwrap();
2028 assert!(rows.is_empty());
2029 }
2030
2031 #[test]
2032 fn public_schema_boundary_rejects_legacy_provider_owned_bootstrap_graphs() {
2033 let executor =
2034 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2035 let entity = entity();
2036 let mut context = UserContext::new()
2037 .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
2038 context.set_initial_graphs(vec![
2039 GraphNode::new("Order")
2040 .value("id", 1001_u64)
2041 .value("version", 1_i64)
2042 .value("name", "red"),
2043 ]);
2044 context.use_sqlite_provider(executor.clone());
2045 let error = ensure_sqlite_schema_for(&context).unwrap_err();
2046 assert!(error.to_string().contains(
2047 "generated root/constant bootstrap must use the typed RuntimeModule callback"
2048 ));
2049 let count: i64 = executor
2050 .lock()
2051 .unwrap()
2052 .query_row("SELECT COUNT(*) FROM orders", [], |row| row.get(0))
2053 .unwrap();
2054 assert_eq!(count, 0);
2055 }
2056
2057 #[test]
2058 fn sqlite_executes_partitioned_relation_limit_per_parent() {
2059 let executor =
2060 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2061 let entity = order_line_entity();
2062 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
2063
2064 for order_id in [11_u64, 12_u64] {
2065 for index in 1_u64..=5 {
2066 let id = order_id * 100 + index;
2067 let insert = SqliteDialect
2068 .compile_insert(
2069 &entity,
2070 &InsertCommand::new("OrderLine")
2071 .value("id", id)
2072 .value("order_id", order_id)
2073 .value("name", format!("line-{id}")),
2074 )
2075 .unwrap();
2076 executor.execute(&insert).unwrap();
2077 }
2078 }
2079
2080 let query = SelectQuery::new("OrderLine")
2081 .project("id")
2082 .project("order_id")
2083 .order_desc("id")
2084 .limit(3)
2085 .partition_by("order_id");
2086 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2087 let rows = executor.fetch_all_compact(&compiled).unwrap();
2088
2089 assert_eq!(rows.len(), 6);
2090 for order_id in [11_i64, 12_i64] {
2091 let ids = rows
2092 .iter()
2093 .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
2094 .filter_map(|row| row.get("id").cloned())
2095 .collect::<Vec<_>>();
2096 assert_eq!(
2097 ids,
2098 vec![
2099 Value::I64(order_id * 100 + 5),
2100 Value::I64(order_id * 100 + 4),
2101 Value::I64(order_id * 100 + 3),
2102 ]
2103 );
2104 }
2105 }
2106
2107 #[test]
2108 fn topn_005_007_window_and_probes_preserve_results_and_predicates() {
2109 futures_executor::block_on(async {
2110 #[derive(Clone)]
2111 struct FixedSchema(Arc<EntityDescriptor>);
2112
2113 impl teaql_data_service::SchemaProvider for FixedSchema {
2114 fn get_entity(&self, name: &str) -> Option<Arc<EntityDescriptor>> {
2115 (name == self.0.name).then(|| self.0.clone())
2116 }
2117 }
2118
2119 let transport =
2120 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2121 let entity = Arc::new(order_line_entity());
2122 transport
2123 .ensure_schema(&SqliteDialect, &[entity.as_ref()])
2124 .unwrap();
2125
2126 for order_id in [11_u64, 12_u64, 13_u64] {
2127 for index in 1_u64..=5 {
2128 let id = order_id * 100 + index;
2129 let name = if index == 4 { "excluded" } else { "visible" };
2130 let insert = SqliteDialect
2131 .compile_insert(
2132 &entity,
2133 &InsertCommand::new("OrderLine")
2134 .value("id", id)
2135 .value("order_id", order_id)
2136 .value("name", name),
2137 )
2138 .unwrap();
2139 transport.execute(&insert).unwrap();
2140 }
2141 }
2142
2143 let executor = teaql_sql::SqlDataServiceExecutor::new(
2144 SqliteDialect,
2145 transport,
2146 FixedSchema(entity),
2147 );
2148 let base = SelectQuery::new("OrderLine")
2149 .project("id")
2150 .project("order_id")
2151 .project("name")
2152 .filter(Expr::in_list("order_id", [Value::U64(11), Value::U64(12)]))
2153 .and_filter(Expr::eq("name", "visible"))
2154 .order_desc("id")
2155 .limit(3)
2156 .partition_by("order_id");
2157 let execute = |query| {
2158 teaql_data_service::QueryExecutor::query(
2159 &executor,
2160 teaql_data_service::QueryRequest {
2161 query,
2162 trace_chain: Vec::new(),
2163 comment: Some("TOPN plan equivalence".to_owned()),
2164 capture_debug_query: false,
2165 capture_execution_metadata: false,
2166 },
2167 )
2168 };
2169
2170 let probes = execute(base.clone()).await.unwrap().rows;
2171 let window = execute(base.top_n_probe_parent_threshold(0))
2172 .await
2173 .unwrap()
2174 .rows;
2175 let children_of = |rows: &[CompactRow], parent: i64| {
2176 rows.iter()
2177 .filter(|row| row.get("order_id") == Some(&Value::I64(parent)))
2178 .map(|row| (row.get("id").cloned(), row.get("name").cloned()))
2179 .collect::<Vec<_>>()
2180 };
2181
2182 for parent in [11_i64, 12_i64] {
2183 assert_eq!(children_of(&probes, parent), children_of(&window, parent));
2184 }
2185 assert_eq!(
2186 children_of(&window, 11),
2187 vec![
2188 (Some(Value::I64(1105)), Some(Value::Text("visible".into()))),
2189 (Some(Value::I64(1103)), Some(Value::Text("visible".into()))),
2190 (Some(Value::I64(1102)), Some(Value::Text("visible".into()))),
2191 ]
2192 );
2193 assert_eq!(
2194 children_of(&window, 12),
2195 vec![
2196 (Some(Value::I64(1205)), Some(Value::Text("visible".into()))),
2197 (Some(Value::I64(1203)), Some(Value::Text("visible".into()))),
2198 (Some(Value::I64(1202)), Some(Value::Text("visible".into()))),
2199 ]
2200 );
2201 assert!(children_of(&probes, 13).is_empty());
2202 assert!(children_of(&window, 13).is_empty());
2203 });
2204 }
2205
2206 #[test]
2207 fn sqlite_boolean_new_schema_roundtrips_as_bool() {
2208 let executor =
2209 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2210 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2211 let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
2212 assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
2213 assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
2214 assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
2215
2216 executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
2217 for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
2218 let insert = SqliteDialect
2219 .compile_insert(
2220 &entity,
2221 &InsertCommand::new("FeatureFlag")
2222 .value("id", id)
2223 .value("version", 1_i64)
2224 .value("enabled", enabled)
2225 .value("optional_enabled", optional_enabled),
2226 )
2227 .unwrap();
2228 assert_eq!(executor.execute(&insert).unwrap(), 1);
2229 }
2230
2231 let select = SqliteDialect
2232 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2233 .unwrap();
2234 let rows = executor.fetch_all_compact(&select).unwrap();
2235 assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
2236 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
2237 assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
2238 assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
2239
2240 let first =
2241 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2242 let second =
2243 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap();
2244 assert!(!first.enabled);
2245 assert_eq!(first.optional_enabled, Some(true));
2246 assert!(second.enabled);
2247 assert_eq!(second.optional_enabled, Some(false));
2248 }
2249
2250 #[test]
2251 fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
2252 let executor =
2253 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2254 let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
2255 executor
2256 .execute(&CompiledQuery {
2257 sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
2258 .to_owned(),
2259 params: Vec::new(),
2260 comment: None,
2261 })
2262 .unwrap();
2263
2264 let insert = SqliteDialect
2265 .compile_insert(
2266 &entity,
2267 &InsertCommand::new("FeatureFlag")
2268 .value("id", 1_u64)
2269 .value("version", 1_i64)
2270 .value("enabled", true)
2271 .value("optional_enabled", false),
2272 )
2273 .unwrap();
2274 executor.execute(&insert).unwrap();
2275 executor
2276 .execute(&CompiledQuery {
2277 sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
2278 .to_owned(),
2279 params: vec![
2280 Value::U64(2),
2281 Value::I64(1),
2282 Value::I64(2),
2283 Value::Null,
2284 ],
2285 comment: None,
2286 })
2287 .unwrap();
2288 let select = SqliteDialect
2289 .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
2290 .unwrap();
2291 let rows = executor.fetch_all_compact(&select).unwrap();
2292 assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
2293 assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
2294 assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
2295
2296 let decoded =
2297 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
2298 assert!(decoded.enabled);
2299 assert_eq!(decoded.optional_enabled, Some(false));
2300 assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
2301 let error =
2302 <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap_err();
2303 assert!(error.message.contains("invalid field enabled"));
2304
2305 for (value, expected) in [
2306 (Value::I64(0), false),
2307 (Value::I64(1), true),
2308 (Value::U64(0), false),
2309 (Value::U64(1), true),
2310 ] {
2311 let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2312 teaql_core::CompactRow::from_map(feature_flag_record(value, Value::Null)),
2313 )
2314 .unwrap();
2315 assert_eq!(decoded.enabled, expected);
2316 assert_eq!(decoded.optional_enabled, None);
2317 }
2318
2319 for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
2320 let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2321 teaql_core::CompactRow::from_map(feature_flag_record(invalid, Value::Null)),
2322 )
2323 .unwrap_err();
2324 assert!(error.message.contains("invalid field enabled"));
2325 }
2326 let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
2327 teaql_core::CompactRow::from_map(feature_flag_record(Value::Bool(true), Value::U64(2))),
2328 )
2329 .unwrap_err();
2330 assert!(error.message.contains("invalid field optional_enabled"));
2331 }
2332
2333 #[test]
2334 fn sqlite_executor_parses_json_only_for_json_columns() {
2335 let executor =
2336 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2337
2338 executor
2339 .execute(&CompiledQuery {
2340 sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
2341 params: Vec::new(),
2342 comment: None,
2343 })
2344 .unwrap();
2345 executor
2346 .execute(&CompiledQuery {
2347 sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
2348 params: vec![
2349 Value::Text("{\"active\":true}".to_owned()),
2350 Value::Json(serde_json::json!({"active": true})),
2351 ],
2352 comment: None,
2353 })
2354 .unwrap();
2355
2356 let rows = executor
2357 .fetch_all_compact(&CompiledQuery {
2358 sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
2359 params: Vec::new(),
2360 comment: None,
2361 })
2362 .unwrap();
2363
2364 assert_eq!(
2365 rows[0].get("text_payload"),
2366 Some(&Value::Text("{\"active\":true}".to_owned()))
2367 );
2368 assert_eq!(
2369 rows[0].get("json_payload"),
2370 Some(&Value::Json(serde_json::json!({"active": true})))
2371 );
2372 }
2373
2374 #[test]
2375 fn sqlite_id_space_generator_increments_ids() {
2376 let executor =
2377 SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
2378 let generator = SqliteIdSpaceGenerator::from_executor(executor);
2379 assert_eq!(generator.next_id("Order").unwrap(), 1);
2380 assert_eq!(generator.next_id("Order").unwrap(), 2);
2381 }
2382
2383 #[test]
2384 fn sqlite_id_space_generator_is_safe_across_connections() {
2385 let path = std::env::temp_dir().join(format!(
2386 "teaql-id-space-{}-{}.db",
2387 std::process::id(),
2388 std::time::SystemTime::now()
2389 .duration_since(std::time::UNIX_EPOCH)
2390 .unwrap()
2391 .as_nanos()
2392 ));
2393 let mut workers = Vec::new();
2394 for _ in 0..4 {
2395 let path = path.clone();
2396 workers.push(std::thread::spawn(move || {
2397 let connection = Connection::open(path).unwrap();
2398 connection
2399 .busy_timeout(std::time::Duration::from_secs(5))
2400 .unwrap();
2401 let generator = SqliteIdSpaceGenerator::new(connection);
2402 (0..25)
2403 .map(|_| generator.next_id("Order").unwrap())
2404 .collect::<Vec<_>>()
2405 }));
2406 }
2407 let mut ids = workers
2408 .into_iter()
2409 .flat_map(|worker| worker.join().unwrap())
2410 .collect::<Vec<_>>();
2411 ids.sort_unstable();
2412 assert_eq!(ids, (1..=100).collect::<Vec<_>>());
2413 let _ = std::fs::remove_file(path);
2414 }
2415
2416 #[test]
2417 fn sqlite_fetch_stream_returns_chunked_rows() {
2418 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2419 Connection::open_in_memory().unwrap(),
2420 )));
2421 let entity = entity();
2422
2423 executor
2425 .execute(&CompiledQuery {
2426 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2427 .to_owned(),
2428 params: Vec::new(),
2429 comment: None,
2430 })
2431 .unwrap();
2432
2433 for i in 1..=25 {
2434 let insert = SqliteDialect
2435 .compile_insert(
2436 &entity,
2437 &InsertCommand::new("Order")
2438 .value("id", i as u64)
2439 .value("version", 1_i64)
2440 .value("name", format!("order-{i}")),
2441 )
2442 .unwrap();
2443 executor.execute(&insert).unwrap();
2444 }
2445
2446 let query = SelectQuery::new("Order")
2448 .filter(Expr::gt("version", 0_i64))
2449 .order_asc("id")
2450 .stream(10);
2451
2452 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2453
2454 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2455
2456 assert_eq!(chunks.len(), 3);
2458 assert_eq!(chunks[0].rows.len(), 10);
2459 assert_eq!(chunks[0].chunk_index, 0);
2460 assert!(!chunks[0].is_last);
2461
2462 assert_eq!(chunks[1].rows.len(), 10);
2463 assert_eq!(chunks[1].chunk_index, 1);
2464 assert!(!chunks[1].is_last);
2465
2466 assert_eq!(chunks[2].rows.len(), 5);
2467 assert_eq!(chunks[2].chunk_index, 2);
2468 assert!(chunks[2].is_last);
2469
2470 assert_eq!(
2472 chunks[0].rows[0].get("name"),
2473 Some(&Value::Text("order-1".to_owned()))
2474 );
2475 assert_eq!(
2476 chunks[2].rows[4].get("name"),
2477 Some(&Value::Text("order-25".to_owned()))
2478 );
2479 }
2480
2481 #[test]
2482 fn sqlite_fetch_stream_handles_empty_result() {
2483 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2484 Connection::open_in_memory().unwrap(),
2485 )));
2486
2487 executor
2488 .execute(&CompiledQuery {
2489 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2490 .to_owned(),
2491 params: Vec::new(),
2492 comment: None,
2493 })
2494 .unwrap();
2495
2496 let entity = entity();
2497 let query = SelectQuery::new("Order")
2498 .filter(Expr::gt("version", 0_i64))
2499 .stream(10);
2500
2501 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2502
2503 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2504
2505 assert_eq!(chunks.len(), 1);
2507 assert_eq!(chunks[0].rows.len(), 0);
2508 assert!(chunks[0].is_last);
2509 }
2510
2511 #[test]
2512 fn sqlite_fetch_stream_exact_chunk_boundary() {
2513 let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
2514 Connection::open_in_memory().unwrap(),
2515 )));
2516 let entity = entity();
2517
2518 executor
2519 .execute(&CompiledQuery {
2520 sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
2521 .to_owned(),
2522 params: Vec::new(),
2523 comment: None,
2524 })
2525 .unwrap();
2526
2527 for i in 1..=20 {
2529 let insert = SqliteDialect
2530 .compile_insert(
2531 &entity,
2532 &InsertCommand::new("Order")
2533 .value("id", i as u64)
2534 .value("version", 1_i64)
2535 .value("name", format!("order-{i}")),
2536 )
2537 .unwrap();
2538 executor.execute(&insert).unwrap();
2539 }
2540
2541 let query = SelectQuery::new("Order")
2542 .filter(Expr::gt("version", 0_i64))
2543 .order_asc("id")
2544 .stream(10);
2545
2546 let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
2547
2548 let chunks = executor.fetch_stream(&compiled, 10).unwrap();
2549
2550 assert_eq!(chunks.len(), 3);
2552 assert_eq!(chunks[0].rows.len(), 10);
2553 assert!(!chunks[0].is_last);
2554 assert_eq!(chunks[1].rows.len(), 10);
2555 assert!(!chunks[1].is_last);
2556 assert_eq!(chunks[2].rows.len(), 0);
2557 assert!(chunks[2].is_last);
2558 }
2559
2560 #[test]
2561 fn test_parse_sqlite_timestamp() {
2562 let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
2563 assert!(matches!(ts1, Value::Timestamp(_)));
2564
2565 let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
2566 assert!(matches!(ts2, Value::Timestamp(_)));
2567
2568 let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
2569 assert!(matches!(ts3, Value::Timestamp(_)));
2570
2571 let ts4 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546+00").unwrap();
2572 assert!(matches!(ts4, Value::Timestamp(_)));
2573
2574 let ts5 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546").unwrap();
2575 assert!(matches!(ts5, Value::Timestamp(_)));
2576
2577 assert_eq!(
2578 parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+00"),
2579 Some(1_704_067_200_000)
2580 );
2581 assert_eq!(
2582 parse_fixed_sqlite_timestamp("2024-01-01T08:00:00.123+08:00"),
2583 Some(1_704_067_200_123)
2584 );
2585 assert_eq!(
2586 parse_fixed_sqlite_timestamp("2023-12-31 19:00:00-0500"),
2587 Some(1_704_067_200_000)
2588 );
2589 assert_eq!(parse_fixed_sqlite_timestamp("2024-13-01 00:00:00Z"), None);
2590 assert_eq!(parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+24"), None);
2591
2592 assert!(parse_sqlite_timestamp("invalid").is_err());
2593 }
2594
2595 #[test]
2596 fn declared_text_does_not_infer_timestamp_from_content() {
2597 for decl_type in ["TEXT", "VARCHAR(255)", "CHAR(32)", "CLOB"] {
2598 let column = ColumnInfo {
2599 name: "external_timestamp".to_owned(),
2600 decode_kind: sqlite_decode_kind(Some(decl_type)),
2601 };
2602
2603 assert_eq!(
2604 decode_sqlite_text(b"2024-01-01 00:57:55", &column).unwrap(),
2605 Value::Text("2024-01-01 00:57:55".to_owned())
2606 );
2607 }
2608 }
2609
2610 #[test]
2611 fn declared_column_types_compile_to_decode_kinds() {
2612 assert_eq!(sqlite_decode_kind(Some("BOOLEAN")), SqliteDecodeKind::Bool);
2613 assert_eq!(
2614 sqlite_decode_kind(Some("decimal(20, 4)")),
2615 SqliteDecodeKind::Decimal
2616 );
2617 assert_eq!(
2618 sqlite_decode_kind(Some(" VARCHAR(255) ")),
2619 SqliteDecodeKind::Text
2620 );
2621 assert_eq!(
2622 sqlite_decode_kind(Some("datetime")),
2623 SqliteDecodeKind::Timestamp
2624 );
2625 assert_eq!(sqlite_decode_kind(Some("custom")), SqliteDecodeKind::Infer);
2626 assert_eq!(sqlite_decode_kind(None), SqliteDecodeKind::Infer);
2627 }
2628}