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