Skip to main content

teaql_provider_sqlite/
lib.rs

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        // SQLite does not support adding NOT NULL columns without a DEFAULT.
73        // Since TeaQL enforces nullability at the application layer, we can safely
74        // strip the NOT NULL constraint when adding columns to existing tables.
75        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    /// Fetch rows in streaming mode (chunked).
253    /// Returns a Vec of StreamChunk, each containing up to `chunk_size` rows.
254    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        // Push the final chunk (may be empty if exactly aligned)
287        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    // A rusqlite statement and its rows borrow the guarded connection for the
385    // lifetime of the stream. This stream is intentionally local/non-Send, so
386    // retaining the synchronous guard across yields is required and safe.
387    #[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        // Use a dummy entity descriptor for compilation
420        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_exists_sqlite(
457    executor: &SqliteMutationExecutor,
458    dialect: &SqliteDialect,
459    entity: &EntityDescriptor,
460    graph: &GraphNode,
461) -> Result<bool, MutationExecutorError> {
462    let Some(id) = graph.values.get("id") else {
463        return Ok(false);
464    };
465    let query = dialect.compile_select(
466        entity,
467        &SelectQuery::new(&graph.entity)
468            .project("id")
469            .filter(Expr::eq("id", id.clone()))
470            .limit(1),
471    )?;
472    Ok(!executor.fetch_all_compact(&query)?.is_empty())
473}
474
475fn compile_initial_graph_insert(
476    dialect: &impl SqlDialect,
477    entity: &EntityDescriptor,
478    graph: &GraphNode,
479) -> Result<CompiledQuery, MutationExecutorError> {
480    let mut command = InsertCommand::new(&graph.entity);
481    for (field, value) in &graph.values {
482        command = command.value(field.clone(), value.clone());
483    }
484    dialect.compile_insert(entity, &command).map_err(Into::into)
485}
486
487fn compile_initial_graph_update(
488    dialect: &impl SqlDialect,
489    entity: &EntityDescriptor,
490    graph: &GraphNode,
491) -> Result<Option<CompiledQuery>, MutationExecutorError> {
492    let Some(id) = graph.values.get("id") else {
493        return Ok(None);
494    };
495    let mut command = UpdateCommand::new(&graph.entity, id.clone());
496    for (field, value) in &graph.values {
497        if field != "id" {
498            command = command.value(field.clone(), value.clone());
499        }
500    }
501    match dialect.compile_update(entity, &command) {
502        Ok(query) => Ok(Some(query)),
503        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
504        Err(err) => Err(err.into()),
505    }
506}
507
508pub trait SqliteSchemaExt {
509    fn ensure_sqlite_schema(
510        &self,
511    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>>;
512}
513
514pub fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
515    let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
516        MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
517    })?;
518    let executor = context
519        .get_resource::<SqliteMutationExecutor>()
520        .ok_or_else(|| {
521            MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
522        })?;
523
524    let entities = context.all_entities();
525
526    // Ensure id space table exists
527    executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
528
529    // Process each entity table individually with granular events
530    for entity in &entities {
531        let field_count = entity.properties.len();
532        if !executor.table_exists(&entity.table_name)? {
533            // New table: create it
534            let sql = dialect.compile_create_table(entity)?;
535            executor.lock()?.execute(&sql, [])?;
536            let _ = context.send_event(RawAuditEvent::schema_created(
537                &entity.name,
538                &entity.table_name,
539                field_count,
540            ));
541            continue;
542        }
543        // Existing table: check for missing columns
544        let existing_columns = executor.table_columns(&entity.table_name)?;
545        let mut fields_added = 0;
546        for property in &entity.properties {
547            let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
548            if existing_columns.contains(&bare_column) {
549                continue;
550            }
551            let sql = dialect.compile_add_column(entity, property)?;
552            executor.lock()?.execute(&sql, [])?;
553            let _ = context.send_event(RawAuditEvent::field_added(
554                &entity.name,
555                &entity.table_name,
556                &property.column_name,
557            ));
558            fields_added += 1;
559        }
560        let _ = context.send_event(RawAuditEvent::schema_verified(
561            &entity.name,
562            &entity.table_name,
563            field_count,
564        ));
565        let _ = fields_added; // used above for FieldAdded events
566    }
567
568    // Constant graphs are reconciled so model changes are propagated.
569    let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
570    let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); // (inserted, updated)
571    for graph in context.initial_graphs() {
572        let entity = context.entity(&graph.entity).ok_or_else(|| {
573            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
574        })?;
575        let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
576        if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
577            if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
578                executor.execute(&query)?;
579            }
580            counts.1 += 1;
581            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
582                id_generator.ensure_floor(&graph.entity, id)?;
583            }
584            continue;
585        }
586        let query = compile_initial_graph_insert(dialect, entity, graph)?;
587        executor.execute(&query)?;
588        counts.0 += 1; // inserted
589        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
590            id_generator.ensure_floor(&graph.entity, id)?;
591        }
592    }
593
594    // Roots are create-if-absent. Once present, application-owned values win.
595    for graph in context.root_graphs() {
596        let entity = context.entity(&graph.entity).ok_or_else(|| {
597            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
598        })?;
599        if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
600            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
601                id_generator.ensure_floor(&graph.entity, id)?;
602            }
603            continue;
604        }
605        let query = compile_initial_graph_insert(dialect, entity, graph)?;
606        executor.execute(&query)?;
607        seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
608        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
609            id_generator.ensure_floor(&graph.entity, id)?;
610        }
611    }
612
613    // Fire DataSeeded events per entity type
614    for (entity_name, (inserted, updated)) in &seed_counts {
615        let entity = context.entity(entity_name).ok_or_else(|| {
616            MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
617        })?;
618        let _ = context.send_event(RawAuditEvent::data_seeded(
619            entity_name,
620            &entity.table_name,
621            *inserted,
622            *updated,
623        ));
624    }
625
626    executor.clear_query_caches();
627    Ok(())
628}
629
630impl SqliteSchemaExt for UserContext {
631    fn ensure_sqlite_schema(
632        &self,
633    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>> {
634        Box::pin(async move { ensure_sqlite_schema_for(self) })
635    }
636}
637
638#[derive(Debug, Default, Clone, Copy)]
639pub struct SqliteSchemaProvider;
640
641impl SchemaProvider for SqliteSchemaProvider {
642    fn ensure_schema<'a>(
643        &'a self,
644        context: &'a UserContext,
645    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
646        Box::pin(async move {
647            ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
648        })
649    }
650}
651
652pub trait SqliteProviderExt {
653    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
654}
655
656impl SqliteProviderExt for UserContext {
657    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
658        self.insert_resource(SqliteDialect);
659        self.insert_resource(executor);
660        self.set_schema_provider(SqliteSchemaProvider);
661        self
662    }
663}
664
665#[derive(Clone)]
666pub struct SqliteIdSpaceGenerator {
667    executor: SqliteMutationExecutor,
668    table_name: String,
669}
670
671impl SqliteIdSpaceGenerator {
672    pub fn new(connection: Connection) -> Self {
673        Self::from_executor(SqliteMutationExecutor::from_connection(connection))
674    }
675
676    pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
677        Self {
678            executor,
679            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
680        }
681    }
682
683    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
684        self.table_name = table_name.into();
685        self
686    }
687
688    pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
689        self.executor.ensure_id_space_table(&self.table_name)
690    }
691
692    pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
693        let entity = canonical_id_space_entity(entity);
694        let entity = entity.as_str();
695        self.ensure_table()?;
696        let table = quote_ident(&self.table_name);
697        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
698        let insert_sql = format!("INSERT INTO {table} (type_name, current_level) VALUES (?, 1)");
699        let update_sql = format!(
700            "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
701        );
702        for attempt in 1..=100 {
703            let connection = self.executor.lock()?;
704            let current = connection
705                .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
706                .optional()?;
707            if let Some(current) = current {
708                let next = current.checked_add(1).ok_or_else(|| {
709                    MutationExecutorError::Bind(format!(
710                        "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
711                    ))
712                })?;
713                if connection.execute(&update_sql, params![next, entity, current])? == 1 {
714                    return u64::try_from(next).map_err(|_| {
715                        MutationExecutorError::Bind(format!(
716                            "generated id {next} cannot be represented as u64"
717                        ))
718                    });
719                }
720            } else {
721                match connection.execute(&insert_sql, params![entity]) {
722                    Ok(1) => return Ok(1),
723                    Ok(changed) => {
724                        return Err(MutationExecutorError::Bind(format!(
725                            "ID space insert for {entity} changed {changed} rows"
726                        )));
727                    }
728                    Err(error)
729                        if error.sqlite_error_code()
730                            == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
731                    Err(error) => return Err(error.into()),
732                }
733            }
734        }
735        Err(MutationExecutorError::Bind(format!(
736            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
737        )))
738    }
739
740    pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
741        let entity = canonical_id_space_entity(entity);
742        let entity = entity.as_str();
743        self.ensure_table()?;
744        let floor = i64::try_from(floor).map_err(|_| {
745            MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
746        })?;
747        let table = quote_ident(&self.table_name);
748        for _ in 1..=100 {
749            let connection = self.executor.lock()?;
750            let current = connection
751                .query_row(
752                    &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
753                    [entity],
754                    |row| row.get::<_, i64>(0),
755                )
756                .optional()?;
757            match current {
758                Some(current) if current >= floor => return Ok(()),
759                Some(current) => {
760                    if connection.execute(
761                        &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
762                        params![floor, entity, current],
763                    )? == 1 { return Ok(()); }
764                }
765                None => match connection.execute(
766                    &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
767                    params![entity, floor],
768                ) {
769                    Ok(1) => return Ok(()),
770                    Ok(_) => {}
771                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
772                    Err(error) => return Err(error.into()),
773                },
774            }
775        }
776        Err(MutationExecutorError::Bind(format!(
777            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
778        )))
779    }
780}
781
782impl InternalIdGenerator for SqliteIdSpaceGenerator {
783    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
784        self.next_id(entity)
785            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
786    }
787}
788
789fn quote_ident(ident: &str) -> String {
790    quote_identifier_if_needed(ident, '"')
791}
792
793/// Strip wrapping identifier quotes from a SQL identifier.
794///
795/// SQLite `PRAGMA table_info` returns bare column names (e.g. `description`),
796/// but generated `PropertyDescriptor::column_name` may carry quotes
797/// (e.g. `"description"`) when the name is a reserved keyword.  This helper
798/// normalises the column name so the two can be compared correctly during
799/// schema migration.
800fn strip_identifier_quotes(ident: &str) -> &str {
801    let bytes = ident.as_bytes();
802    if bytes.len() >= 2 {
803        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
804        if (first == b'"' && last == b'"')
805            || (first == b'`' && last == b'`')
806            || (first == b'[' && last == b']')
807        {
808            return &ident[1..ident.len() - 1];
809        }
810    }
811    ident
812}
813
814fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
815    values.iter().map(bind_sqlite_value).collect()
816}
817
818fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
819    match value {
820        Value::Null => Ok(SqliteValue::Null),
821        Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
822        Value::I64(v) => Ok(SqliteValue::Integer(*v)),
823        Value::U64(v) => i64::try_from(*v)
824            .map(SqliteValue::Integer)
825            .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
826        Value::F64(v) => Ok(SqliteValue::Real(*v)),
827        // Bind the canonical numeric spelling. SQLite NUMERIC affinity keeps
828        // predicates and aggregates numeric; an application-only text prefix
829        // makes range comparisons silently return the wrong result.
830        Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
831        Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
832        Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
833        Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
834        Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
835        Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
836        Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
837        Value::TypedNull(_) => Ok(SqliteValue::Null),
838    }
839}
840
841#[derive(Debug, Clone)]
842struct ColumnInfo {
843    name: String,
844    decode_kind: SqliteDecodeKind,
845}
846
847#[derive(Debug, Clone, Copy, PartialEq, Eq)]
848enum SqliteDecodeKind {
849    Infer,
850    Bool,
851    Decimal,
852    Json,
853    Date,
854    Timestamp,
855    Text,
856}
857
858#[derive(Debug)]
859struct ColumnLayout {
860    columns: Arc<[ColumnInfo]>,
861    names: Arc<[String]>,
862}
863
864fn cached_column_layout(
865    cache: &Mutex<HashMap<String, Arc<ColumnLayout>>>,
866    sql: &str,
867    statement: &rusqlite::Statement<'_>,
868) -> Arc<ColumnLayout> {
869    if let Ok(cache) = cache.lock() {
870        if let Some(layout) = cache.get(sql) {
871            return layout.clone();
872        }
873    }
874
875    let columns: Arc<[ColumnInfo]> = statement_columns(statement).into();
876    let names = columns
877        .iter()
878        .map(|column| column.name.clone())
879        .collect::<Vec<_>>()
880        .into();
881    let layout = Arc::new(ColumnLayout { columns, names });
882    if let Ok(mut cache) = cache.lock() {
883        if cache.len() >= DEFAULT_COLUMN_LAYOUT_CACHE_CAPACITY {
884            cache.clear();
885        }
886        cache.insert(sql.to_owned(), layout.clone());
887    }
888    layout
889}
890
891fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
892    statement
893        .columns()
894        .into_iter()
895        .map(|column| ColumnInfo {
896            name: column.name().to_owned(),
897            decode_kind: sqlite_decode_kind(column.decl_type()),
898        })
899        .collect()
900}
901
902fn sqlite_decode_kind(decl_type: Option<&str>) -> SqliteDecodeKind {
903    let Some(decl_type) = decl_type else {
904        return SqliteDecodeKind::Infer;
905    };
906    let base = decl_type.split('(').next().unwrap_or(decl_type).trim();
907    if base.eq_ignore_ascii_case("BOOLEAN") || base.eq_ignore_ascii_case("BOOL") {
908        SqliteDecodeKind::Bool
909    } else if base.eq_ignore_ascii_case("NUMERIC") || base.eq_ignore_ascii_case("DECIMAL") {
910        SqliteDecodeKind::Decimal
911    } else if base.eq_ignore_ascii_case("JSON") {
912        SqliteDecodeKind::Json
913    } else if base.eq_ignore_ascii_case("DATE") {
914        SqliteDecodeKind::Date
915    } else if base.eq_ignore_ascii_case("TIMESTAMP") || base.eq_ignore_ascii_case("DATETIME") {
916        SqliteDecodeKind::Timestamp
917    } else if ["TEXT", "VARCHAR", "CHAR", "CLOB"]
918        .iter()
919        .any(|v| base.eq_ignore_ascii_case(v))
920    {
921        SqliteDecodeKind::Text
922    } else {
923        SqliteDecodeKind::Infer
924    }
925}
926
927fn decode_sqlite_values(
928    row: &Row<'_>,
929    columns: &[ColumnInfo],
930) -> Result<Vec<Value>, MutationExecutorError> {
931    let mut values = Vec::with_capacity(columns.len());
932    for (index, column) in columns.iter().enumerate() {
933        let value_ref = row.get_ref(index)?;
934        let value = match value_ref {
935            ValueRef::Null => Value::Null,
936            ValueRef::Integer(value) => decode_sqlite_integer(value, column),
937            ValueRef::Real(value) => Value::F64(value),
938            ValueRef::Text(value) => decode_sqlite_text(value, column)?,
939            ValueRef::Blob(_) => {
940                return Err(MutationExecutorError::UnsupportedColumnType(
941                    "BLOB".to_owned(),
942                ));
943            }
944        };
945        values.push(value);
946    }
947    Ok(values)
948}
949
950fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
951    match column.decode_kind {
952        SqliteDecodeKind::Bool => Value::Bool(value != 0),
953        _ => Value::I64(value),
954    }
955}
956
957fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
958    let value = std::str::from_utf8(value)
959        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
960    match column.decode_kind {
961        SqliteDecodeKind::Decimal => Decimal::from_str(value)
962            .map(Value::Decimal)
963            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
964        SqliteDecodeKind::Json => serde_json::from_str(value).map(Value::Json).map_err(|err| {
965            MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
966        }),
967        SqliteDecodeKind::Date => NaiveDate::parse_from_str(value, "%Y-%m-%d")
968            .map(Value::Date)
969            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
970        SqliteDecodeKind::Timestamp => parse_sqlite_timestamp(value),
971        SqliteDecodeKind::Text | SqliteDecodeKind::Bool => Ok(Value::Text(value.to_owned())),
972        SqliteDecodeKind::Infer => infer_sqlite_text(value),
973    }
974}
975
976fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
977    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
978        return Ok(Value::Date(date));
979    }
980    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
981        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
982            timestamp.timestamp_millis(),
983        )));
984    }
985    if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
986        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
987            timestamp.and_utc().timestamp_millis(),
988        )));
989    }
990    Ok(Value::Text(value.to_owned()))
991}
992
993fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
994    if let Some(timestamp) = parse_fixed_sqlite_timestamp(value) {
995        return Ok(Value::Timestamp(teaql_core::time::Timestamp(timestamp)));
996    }
997    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
998        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
999            timestamp.timestamp_millis(),
1000        )));
1001    }
1002    if let Ok(timestamp) = DateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f%#z") {
1003        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1004            timestamp.timestamp_millis(),
1005        )));
1006    }
1007    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
1008        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
1009            date.and_hms_opt(0, 0, 0)
1010                .unwrap_or_default()
1011                .and_utc()
1012                .timestamp_millis(),
1013        )));
1014    }
1015    NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S%.f")
1016        .map(|timestamp| {
1017            Value::Timestamp(teaql_core::time::Timestamp(
1018                timestamp.and_utc().timestamp_millis(),
1019            ))
1020        })
1021        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
1022}
1023
1024fn parse_fixed_sqlite_timestamp(value: &str) -> Option<i64> {
1025    let bytes = value.as_bytes();
1026    if bytes.len() < 19
1027        || bytes.get(4) != Some(&b'-')
1028        || bytes.get(7) != Some(&b'-')
1029        || !matches!(bytes.get(10), Some(b' ') | Some(b'T'))
1030        || bytes.get(13) != Some(&b':')
1031        || bytes.get(16) != Some(&b':')
1032    {
1033        return None;
1034    }
1035    let digits = |start: usize, len: usize| -> Option<u32> {
1036        bytes
1037            .get(start..start + len)?
1038            .iter()
1039            .try_fold(0_u32, |value, byte| {
1040                byte.is_ascii_digit()
1041                    .then_some(value * 10 + u32::from(*byte - b'0'))
1042            })
1043    };
1044    let date = NaiveDate::from_ymd_opt(
1045        i32::try_from(digits(0, 4)?).ok()?,
1046        digits(5, 2)?,
1047        digits(8, 2)?,
1048    )?;
1049    let hour = digits(11, 2)?;
1050    let minute = digits(14, 2)?;
1051    let second = digits(17, 2)?;
1052    let mut cursor = 19;
1053    let mut nanos = 0_u32;
1054    if bytes.get(cursor) == Some(&b'.') {
1055        cursor += 1;
1056        let fraction_start = cursor;
1057        while bytes.get(cursor).is_some_and(u8::is_ascii_digit) {
1058            if cursor - fraction_start < 9 {
1059                nanos = nanos * 10 + u32::from(bytes[cursor] - b'0');
1060            }
1061            cursor += 1;
1062        }
1063        let kept = (cursor - fraction_start).min(9);
1064        if kept == 0 {
1065            return None;
1066        }
1067        nanos *= 10_u32.pow(u32::try_from(9 - kept).ok()?);
1068    }
1069    let datetime = date.and_hms_nano_opt(hour, minute, second, nanos)?;
1070    let offset_seconds = match bytes.get(cursor..) {
1071        Some([]) | Some([b'Z']) | Some([b'z']) => 0,
1072        Some([sign @ (b'+' | b'-'), hour_1, hour_2]) => {
1073            signed_offset(*sign, [*hour_1, *hour_2], [b'0', b'0'])?
1074        }
1075        Some([sign @ (b'+' | b'-'), hour_1, hour_2, minute_1, minute_2]) => {
1076            signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?
1077        }
1078        Some(
1079            [
1080                sign @ (b'+' | b'-'),
1081                hour_1,
1082                hour_2,
1083                b':',
1084                minute_1,
1085                minute_2,
1086            ],
1087        ) => signed_offset(*sign, [*hour_1, *hour_2], [*minute_1, *minute_2])?,
1088        _ => return None,
1089    };
1090    FixedOffset::east_opt(offset_seconds)?
1091        .from_local_datetime(&datetime)
1092        .single()
1093        .map(|timestamp| timestamp.timestamp_millis())
1094}
1095
1096fn signed_offset(sign: u8, hours: [u8; 2], minutes: [u8; 2]) -> Option<i32> {
1097    let pair = |digits: [u8; 2]| {
1098        digits
1099            .iter()
1100            .all(u8::is_ascii_digit)
1101            .then_some(i32::from(digits[0] - b'0') * 10 + i32::from(digits[1] - b'0'))
1102    };
1103    let hours = pair(hours)?;
1104    let minutes = pair(minutes)?;
1105    if hours > 23 || minutes > 59 {
1106        return None;
1107    }
1108    let seconds = hours * 3600 + minutes * 60;
1109    Some(if sign == b'-' { -seconds } else { seconds })
1110}
1111
1112#[cfg(test)]
1113mod tests {
1114    use super::*;
1115    use futures_util::StreamExt;
1116    use teaql_core::{DeleteCommand, Record, RecoverCommand};
1117    use teaql_macros::TeaqlEntity;
1118    use teaql_runtime::InMemoryMetadataStore;
1119
1120    #[test]
1121    fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
1122        let connection = Connection::open_in_memory().unwrap();
1123        connection
1124            .execute_batch(
1125                "CREATE TABLE stream_fixture(id INTEGER);\
1126                 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
1127            )
1128            .unwrap();
1129        let executor = SqliteMutationExecutor::from_connection(connection);
1130        let query = CompiledQuery {
1131            sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
1132            params: vec![],
1133            comment: None,
1134        };
1135        let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
1136        let sizes = futures_executor::block_on(async {
1137            let mut result = Vec::new();
1138            while let Some(chunk) = stream.next().await {
1139                result.push(chunk.unwrap().rows.len());
1140            }
1141            result
1142        });
1143        assert_eq!(sizes, vec![2, 2, 1]);
1144
1145        let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
1146        assert_eq!(
1147            futures_executor::block_on(early.next())
1148                .unwrap()
1149                .unwrap()
1150                .rows
1151                .len(),
1152            2
1153        );
1154        drop(early);
1155        let count: i64 = executor
1156            .connection()
1157            .lock()
1158            .unwrap()
1159            .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
1160            .unwrap();
1161        assert_eq!(count, 5);
1162    }
1163
1164    #[test]
1165    fn decimal_bind_is_numeric_and_comparable() {
1166        let value =
1167            bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
1168        assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
1169        let connection = Connection::open_in_memory().unwrap();
1170        let matches: i64 = connection
1171            .query_row(
1172                "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
1173                [value],
1174                |row| row.get(0),
1175            )
1176            .unwrap();
1177        assert_eq!(matches, 1);
1178    }
1179
1180    #[test]
1181    fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
1182        let connection = Connection::open_in_memory().unwrap();
1183        connection
1184            .execute_batch(
1185                "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
1186            )
1187            .unwrap();
1188        let query = CompiledQuery {
1189            sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
1190            params: vec![
1191                Value::I64(1),
1192                Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
1193                Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
1194            ],
1195            comment: None,
1196        };
1197        let values = bind_values(&query.params).unwrap();
1198        connection
1199            .execute(&query.sql, rusqlite::params_from_iter(values))
1200            .unwrap();
1201        connection
1202            .execute(
1203                &query
1204                    .debug_sql(teaql_sql::DatabaseKind::Sqlite)
1205                    .replace("VALUES (1,", "VALUES (2,"),
1206                [],
1207            )
1208            .unwrap();
1209
1210        let equal_count: i64 = connection.query_row(
1211            "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",
1212            [], |row| row.get(0),
1213        ).unwrap();
1214        let storage_type: String = connection
1215            .query_row(
1216                "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
1217                [],
1218                |row| row.get(0),
1219            )
1220            .unwrap();
1221        assert_eq!(equal_count, 1);
1222        assert_eq!(storage_type, "integer");
1223    }
1224
1225    fn entity() -> EntityDescriptor {
1226        EntityDescriptor::new("Order")
1227            .table_name("orders")
1228            .property(
1229                PropertyDescriptor::new("id", DataType::U64)
1230                    .column_name("id")
1231                    .id()
1232                    .not_null(),
1233            )
1234            .property(
1235                PropertyDescriptor::new("version", DataType::I64)
1236                    .column_name("version")
1237                    .version()
1238                    .not_null(),
1239            )
1240            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1241    }
1242
1243    fn order_line_entity() -> EntityDescriptor {
1244        EntityDescriptor::new("OrderLine")
1245            .table_name("order_line")
1246            .property(
1247                PropertyDescriptor::new("id", DataType::U64)
1248                    .column_name("id")
1249                    .id()
1250                    .not_null(),
1251            )
1252            .property(
1253                PropertyDescriptor::new("order_id", DataType::U64)
1254                    .column_name("order_id")
1255                    .not_null(),
1256            )
1257            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1258    }
1259
1260    #[allow(dead_code)]
1261    #[derive(Debug, PartialEq, TeaqlEntity)]
1262    #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1263    struct FeatureFlagRow {
1264        #[teaql(id)]
1265        id: u64,
1266        #[teaql(version)]
1267        version: i64,
1268        enabled: bool,
1269        optional_enabled: Option<bool>,
1270    }
1271
1272    fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1273        Record::from([
1274            ("id".to_owned(), Value::U64(1)),
1275            ("version".to_owned(), Value::I64(1)),
1276            ("enabled".to_owned(), enabled),
1277            ("optional_enabled".to_owned(), optional_enabled),
1278        ])
1279    }
1280
1281    #[test]
1282    fn sqlite_dialect_compiles_mutations_and_schema() {
1283        assert!(SqliteDialect.prefers_small_parent_relation_probes());
1284        let insert = SqliteDialect
1285            .compile_insert(
1286                &entity(),
1287                &InsertCommand::new("Order")
1288                    .value("id", 1_u64)
1289                    .value("name", "A"),
1290            )
1291            .unwrap();
1292        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1293
1294        let update = SqliteDialect
1295            .compile_update(
1296                &entity(),
1297                &UpdateCommand::new("Order", 1_u64)
1298                    .expected_version(3)
1299                    .value("name", "B"),
1300            )
1301            .unwrap();
1302        assert_eq!(
1303            update.sql,
1304            "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1305        );
1306
1307        let delete = SqliteDialect
1308            .compile_delete(
1309                &entity(),
1310                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1311            )
1312            .unwrap();
1313        let recover = SqliteDialect
1314            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1315            .unwrap();
1316        assert_eq!(
1317            delete.sql,
1318            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1319        );
1320        assert_eq!(
1321            recover.sql,
1322            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1323        );
1324
1325        let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1326        assert_eq!(
1327            create,
1328            "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1329        );
1330    }
1331
1332    #[test]
1333    fn column_layout_cache_uses_parameterized_sql_not_comments() {
1334        let connection = Connection::open_in_memory().unwrap();
1335        connection
1336            .execute("CREATE TABLE sample (id INTEGER, enabled BOOLEAN)", [])
1337            .unwrap();
1338        connection
1339            .execute("INSERT INTO sample (id, enabled) VALUES (1, 1)", [])
1340            .unwrap();
1341        let executor = SqliteMutationExecutor::from_connection(connection);
1342        let mut first = CompiledQuery {
1343            sql: "SELECT id, enabled FROM sample WHERE id = ?".to_owned(),
1344            params: vec![Value::I64(1)],
1345            comment: Some("first purpose".to_owned()),
1346        };
1347        let rows = executor.fetch_all_compact(&first).unwrap();
1348        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(true)));
1349
1350        first.comment = Some("different purpose".to_owned());
1351        executor.fetch_all_compact(&first).unwrap();
1352
1353        assert_eq!(executor.column_layout_cache.lock().unwrap().len(), 1);
1354    }
1355
1356    #[test]
1357    fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1358        let executor =
1359            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1360        let entity = entity();
1361        let mut context = UserContext::new()
1362            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1363
1364        context.use_sqlite_provider(executor.clone());
1365        ensure_sqlite_schema_for(&context).unwrap();
1366
1367        let insert = SqliteDialect
1368            .compile_insert(
1369                &entity,
1370                &InsertCommand::new("Order")
1371                    .value("id", 1_u64)
1372                    .value("version", 1_i64)
1373                    .value("name", "draft"),
1374            )
1375            .unwrap();
1376        assert_eq!(executor.execute(&insert).unwrap(), 1);
1377
1378        let select = SqliteDialect
1379            .compile_select(
1380                &entity,
1381                &SelectQuery::new("Order")
1382                    .filter(Expr::eq("id", 1_u64))
1383                    .order_asc("id"),
1384            )
1385            .unwrap();
1386        let rows = executor.fetch_all_compact(&select).unwrap();
1387        assert_eq!(rows.len(), 1);
1388        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1389        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1390        assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
1391    }
1392
1393    #[test]
1394    fn repeated_schema_ensure_does_not_overwrite_existing_initial_graph() {
1395        let executor =
1396            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1397        let entity = entity();
1398        let mut context = UserContext::new()
1399            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1400        context.set_root_graphs(vec![
1401            GraphNode::new("Order")
1402                .value("id", 1_u64)
1403                .value("version", 1_i64)
1404                .value("name", "module seed"),
1405        ]);
1406        context.use_sqlite_provider(executor.clone());
1407
1408        ensure_sqlite_schema_for(&context).unwrap();
1409        let customize = SqliteDialect
1410            .compile_update(
1411                &entity,
1412                &UpdateCommand::new("Order", 1_u64).value("name", "application value"),
1413            )
1414            .unwrap();
1415        assert_eq!(executor.execute(&customize).unwrap(), 1);
1416
1417        ensure_sqlite_schema_for(&context).unwrap();
1418
1419        let select = SqliteDialect
1420            .compile_select(
1421                &entity,
1422                &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1423            )
1424            .unwrap();
1425        let rows = executor.fetch_all_compact(&select).unwrap();
1426        assert_eq!(rows.len(), 1);
1427        assert_eq!(
1428            rows[0].get("name"),
1429            Some(&Value::Text("application value".to_owned()))
1430        );
1431    }
1432
1433    #[test]
1434    fn repeated_schema_ensure_reconciles_changed_constant_graph() {
1435        let executor =
1436            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1437        let entity = entity();
1438        let mut context = UserContext::new()
1439            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1440        context.set_initial_graphs(vec![
1441            GraphNode::new("Order")
1442                .value("id", 1001_u64)
1443                .value("version", 1_i64)
1444                .value("name", "red"),
1445        ]);
1446        context.use_sqlite_provider(executor.clone());
1447        ensure_sqlite_schema_for(&context).unwrap();
1448
1449        context.set_initial_graphs(vec![
1450            GraphNode::new("Order")
1451                .value("id", 1001_u64)
1452                .value("version", 1_i64)
1453                .value("name", "crimson"),
1454        ]);
1455        ensure_sqlite_schema_for(&context).unwrap();
1456
1457        let select = SqliteDialect
1458            .compile_select(
1459                &entity,
1460                &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1461            )
1462            .unwrap();
1463        let rows = executor.fetch_all_compact(&select).unwrap();
1464        assert_eq!(
1465            rows[0].get("name"),
1466            Some(&Value::Text("crimson".to_owned()))
1467        );
1468        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1469        assert_eq!(generator.next_id("Order").unwrap(), 1002);
1470    }
1471
1472    #[test]
1473    fn sqlite_executes_partitioned_relation_limit_per_parent() {
1474        let executor =
1475            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1476        let entity = order_line_entity();
1477        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1478
1479        for order_id in [11_u64, 12_u64] {
1480            for index in 1_u64..=5 {
1481                let id = order_id * 100 + index;
1482                let insert = SqliteDialect
1483                    .compile_insert(
1484                        &entity,
1485                        &InsertCommand::new("OrderLine")
1486                            .value("id", id)
1487                            .value("order_id", order_id)
1488                            .value("name", format!("line-{id}")),
1489                    )
1490                    .unwrap();
1491                executor.execute(&insert).unwrap();
1492            }
1493        }
1494
1495        let query = SelectQuery::new("OrderLine")
1496            .project("id")
1497            .project("order_id")
1498            .order_desc("id")
1499            .limit(3)
1500            .partition_by("order_id");
1501        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1502        let rows = executor.fetch_all_compact(&compiled).unwrap();
1503
1504        assert_eq!(rows.len(), 6);
1505        for order_id in [11_i64, 12_i64] {
1506            let ids = rows
1507                .iter()
1508                .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
1509                .filter_map(|row| row.get("id").cloned())
1510                .collect::<Vec<_>>();
1511            assert_eq!(
1512                ids,
1513                vec![
1514                    Value::I64(order_id * 100 + 5),
1515                    Value::I64(order_id * 100 + 4),
1516                    Value::I64(order_id * 100 + 3),
1517                ]
1518            );
1519        }
1520    }
1521
1522    #[test]
1523    fn sqlite_boolean_new_schema_roundtrips_as_bool() {
1524        let executor =
1525            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1526        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1527        let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
1528        assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
1529        assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
1530        assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
1531
1532        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1533        for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
1534            let insert = SqliteDialect
1535                .compile_insert(
1536                    &entity,
1537                    &InsertCommand::new("FeatureFlag")
1538                        .value("id", id)
1539                        .value("version", 1_i64)
1540                        .value("enabled", enabled)
1541                        .value("optional_enabled", optional_enabled),
1542                )
1543                .unwrap();
1544            assert_eq!(executor.execute(&insert).unwrap(), 1);
1545        }
1546
1547        let select = SqliteDialect
1548            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1549            .unwrap();
1550        let rows = executor.fetch_all_compact(&select).unwrap();
1551        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
1552        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
1553        assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
1554        assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
1555
1556        let first =
1557            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
1558        let second =
1559            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap();
1560        assert!(!first.enabled);
1561        assert_eq!(first.optional_enabled, Some(true));
1562        assert!(second.enabled);
1563        assert_eq!(second.optional_enabled, Some(false));
1564    }
1565
1566    #[test]
1567    fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
1568        let executor =
1569            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1570        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1571        executor
1572            .execute(&CompiledQuery {
1573                sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
1574                    .to_owned(),
1575                params: Vec::new(),
1576                comment: None,
1577            })
1578            .unwrap();
1579
1580        let insert = SqliteDialect
1581            .compile_insert(
1582                &entity,
1583                &InsertCommand::new("FeatureFlag")
1584                    .value("id", 1_u64)
1585                    .value("version", 1_i64)
1586                    .value("enabled", true)
1587                    .value("optional_enabled", false),
1588            )
1589            .unwrap();
1590        executor.execute(&insert).unwrap();
1591        executor
1592            .execute(&CompiledQuery {
1593                sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
1594                    .to_owned(),
1595                params: vec![
1596                    Value::U64(2),
1597                    Value::I64(1),
1598                    Value::I64(2),
1599                    Value::Null,
1600                ],
1601                comment: None,
1602            })
1603            .unwrap();
1604        let select = SqliteDialect
1605            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1606            .unwrap();
1607        let rows = executor.fetch_all_compact(&select).unwrap();
1608        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1609        assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
1610        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
1611
1612        let decoded =
1613            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[0].clone()).unwrap();
1614        assert!(decoded.enabled);
1615        assert_eq!(decoded.optional_enabled, Some(false));
1616        assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
1617        let error =
1618            <FeatureFlagRow as teaql_core::Entity>::from_compact_row(rows[1].clone()).unwrap_err();
1619        assert!(error.message.contains("invalid field enabled"));
1620
1621        for (value, expected) in [
1622            (Value::I64(0), false),
1623            (Value::I64(1), true),
1624            (Value::U64(0), false),
1625            (Value::U64(1), true),
1626        ] {
1627            let decoded = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
1628                teaql_core::CompactRow::from_map(feature_flag_record(value, Value::Null)),
1629            )
1630            .unwrap();
1631            assert_eq!(decoded.enabled, expected);
1632            assert_eq!(decoded.optional_enabled, None);
1633        }
1634
1635        for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
1636            let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
1637                teaql_core::CompactRow::from_map(feature_flag_record(invalid, Value::Null)),
1638            )
1639            .unwrap_err();
1640            assert!(error.message.contains("invalid field enabled"));
1641        }
1642        let error = <FeatureFlagRow as teaql_core::Entity>::from_compact_row(
1643            teaql_core::CompactRow::from_map(feature_flag_record(Value::Bool(true), Value::U64(2))),
1644        )
1645        .unwrap_err();
1646        assert!(error.message.contains("invalid field optional_enabled"));
1647    }
1648
1649    #[test]
1650    fn sqlite_executor_parses_json_only_for_json_columns() {
1651        let executor =
1652            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1653
1654        executor
1655            .execute(&CompiledQuery {
1656                sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
1657                params: Vec::new(),
1658                comment: None,
1659            })
1660            .unwrap();
1661        executor
1662            .execute(&CompiledQuery {
1663                sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
1664                params: vec![
1665                    Value::Text("{\"active\":true}".to_owned()),
1666                    Value::Json(serde_json::json!({"active": true})),
1667                ],
1668                comment: None,
1669            })
1670            .unwrap();
1671
1672        let rows = executor
1673            .fetch_all_compact(&CompiledQuery {
1674                sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
1675                params: Vec::new(),
1676                comment: None,
1677            })
1678            .unwrap();
1679
1680        assert_eq!(
1681            rows[0].get("text_payload"),
1682            Some(&Value::Text("{\"active\":true}".to_owned()))
1683        );
1684        assert_eq!(
1685            rows[0].get("json_payload"),
1686            Some(&Value::Json(serde_json::json!({"active": true})))
1687        );
1688    }
1689
1690    #[test]
1691    fn sqlite_id_space_generator_increments_ids() {
1692        let executor =
1693            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1694        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1695        assert_eq!(generator.next_id("Order").unwrap(), 1);
1696        assert_eq!(generator.next_id("Order").unwrap(), 2);
1697    }
1698
1699    #[test]
1700    fn sqlite_id_space_generator_is_safe_across_connections() {
1701        let path = std::env::temp_dir().join(format!(
1702            "teaql-id-space-{}-{}.db",
1703            std::process::id(),
1704            std::time::SystemTime::now()
1705                .duration_since(std::time::UNIX_EPOCH)
1706                .unwrap()
1707                .as_nanos()
1708        ));
1709        let mut workers = Vec::new();
1710        for _ in 0..4 {
1711            let path = path.clone();
1712            workers.push(std::thread::spawn(move || {
1713                let connection = Connection::open(path).unwrap();
1714                connection
1715                    .busy_timeout(std::time::Duration::from_secs(5))
1716                    .unwrap();
1717                let generator = SqliteIdSpaceGenerator::new(connection);
1718                (0..25)
1719                    .map(|_| generator.next_id("Order").unwrap())
1720                    .collect::<Vec<_>>()
1721            }));
1722        }
1723        let mut ids = workers
1724            .into_iter()
1725            .flat_map(|worker| worker.join().unwrap())
1726            .collect::<Vec<_>>();
1727        ids.sort_unstable();
1728        assert_eq!(ids, (1..=100).collect::<Vec<_>>());
1729        let _ = std::fs::remove_file(path);
1730    }
1731
1732    #[test]
1733    fn sqlite_fetch_stream_returns_chunked_rows() {
1734        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1735            Connection::open_in_memory().unwrap(),
1736        )));
1737        let entity = entity();
1738
1739        // Create table and insert 25 rows
1740        executor
1741            .execute(&CompiledQuery {
1742                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1743                    .to_owned(),
1744                params: Vec::new(),
1745                comment: None,
1746            })
1747            .unwrap();
1748
1749        for i in 1..=25 {
1750            let insert = SqliteDialect
1751                .compile_insert(
1752                    &entity,
1753                    &InsertCommand::new("Order")
1754                        .value("id", i as u64)
1755                        .value("version", 1_i64)
1756                        .value("name", format!("order-{i}")),
1757                )
1758                .unwrap();
1759            executor.execute(&insert).unwrap();
1760        }
1761
1762        // Stream with chunk_size = 10
1763        let query = SelectQuery::new("Order")
1764            .filter(Expr::gt("version", 0_i64))
1765            .order_asc("id")
1766            .stream(10);
1767
1768        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1769
1770        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1771
1772        // 25 rows / 10 per chunk = 3 chunks
1773        assert_eq!(chunks.len(), 3);
1774        assert_eq!(chunks[0].rows.len(), 10);
1775        assert_eq!(chunks[0].chunk_index, 0);
1776        assert!(!chunks[0].is_last);
1777
1778        assert_eq!(chunks[1].rows.len(), 10);
1779        assert_eq!(chunks[1].chunk_index, 1);
1780        assert!(!chunks[1].is_last);
1781
1782        assert_eq!(chunks[2].rows.len(), 5);
1783        assert_eq!(chunks[2].chunk_index, 2);
1784        assert!(chunks[2].is_last);
1785
1786        // Verify first and last row
1787        assert_eq!(
1788            chunks[0].rows[0].get("name"),
1789            Some(&Value::Text("order-1".to_owned()))
1790        );
1791        assert_eq!(
1792            chunks[2].rows[4].get("name"),
1793            Some(&Value::Text("order-25".to_owned()))
1794        );
1795    }
1796
1797    #[test]
1798    fn sqlite_fetch_stream_handles_empty_result() {
1799        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1800            Connection::open_in_memory().unwrap(),
1801        )));
1802
1803        executor
1804            .execute(&CompiledQuery {
1805                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1806                    .to_owned(),
1807                params: Vec::new(),
1808                comment: None,
1809            })
1810            .unwrap();
1811
1812        let entity = entity();
1813        let query = SelectQuery::new("Order")
1814            .filter(Expr::gt("version", 0_i64))
1815            .stream(10);
1816
1817        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1818
1819        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1820
1821        // Empty result = 1 chunk with 0 rows, marked as last
1822        assert_eq!(chunks.len(), 1);
1823        assert_eq!(chunks[0].rows.len(), 0);
1824        assert!(chunks[0].is_last);
1825    }
1826
1827    #[test]
1828    fn sqlite_fetch_stream_exact_chunk_boundary() {
1829        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1830            Connection::open_in_memory().unwrap(),
1831        )));
1832        let entity = entity();
1833
1834        executor
1835            .execute(&CompiledQuery {
1836                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1837                    .to_owned(),
1838                params: Vec::new(),
1839                comment: None,
1840            })
1841            .unwrap();
1842
1843        // Insert exactly 20 rows
1844        for i in 1..=20 {
1845            let insert = SqliteDialect
1846                .compile_insert(
1847                    &entity,
1848                    &InsertCommand::new("Order")
1849                        .value("id", i as u64)
1850                        .value("version", 1_i64)
1851                        .value("name", format!("order-{i}")),
1852                )
1853                .unwrap();
1854            executor.execute(&insert).unwrap();
1855        }
1856
1857        let query = SelectQuery::new("Order")
1858            .filter(Expr::gt("version", 0_i64))
1859            .order_asc("id")
1860            .stream(10);
1861
1862        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1863
1864        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1865
1866        // 20 rows / 10 per chunk = 2 full chunks + 1 empty final chunk
1867        assert_eq!(chunks.len(), 3);
1868        assert_eq!(chunks[0].rows.len(), 10);
1869        assert!(!chunks[0].is_last);
1870        assert_eq!(chunks[1].rows.len(), 10);
1871        assert!(!chunks[1].is_last);
1872        assert_eq!(chunks[2].rows.len(), 0);
1873        assert!(chunks[2].is_last);
1874    }
1875
1876    #[test]
1877    fn test_parse_sqlite_timestamp() {
1878        let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
1879        assert!(matches!(ts1, Value::Timestamp(_)));
1880
1881        let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
1882        assert!(matches!(ts2, Value::Timestamp(_)));
1883
1884        let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
1885        assert!(matches!(ts3, Value::Timestamp(_)));
1886
1887        let ts4 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546+00").unwrap();
1888        assert!(matches!(ts4, Value::Timestamp(_)));
1889
1890        let ts5 = parse_sqlite_timestamp("2026-08-23 10:43:16.152546").unwrap();
1891        assert!(matches!(ts5, Value::Timestamp(_)));
1892
1893        assert_eq!(
1894            parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+00"),
1895            Some(1_704_067_200_000)
1896        );
1897        assert_eq!(
1898            parse_fixed_sqlite_timestamp("2024-01-01T08:00:00.123+08:00"),
1899            Some(1_704_067_200_123)
1900        );
1901        assert_eq!(
1902            parse_fixed_sqlite_timestamp("2023-12-31 19:00:00-0500"),
1903            Some(1_704_067_200_000)
1904        );
1905        assert_eq!(parse_fixed_sqlite_timestamp("2024-13-01 00:00:00Z"), None);
1906        assert_eq!(parse_fixed_sqlite_timestamp("2024-01-01 00:00:00+24"), None);
1907
1908        assert!(parse_sqlite_timestamp("invalid").is_err());
1909    }
1910
1911    #[test]
1912    fn declared_text_does_not_infer_timestamp_from_content() {
1913        for decl_type in ["TEXT", "VARCHAR(255)", "CHAR(32)", "CLOB"] {
1914            let column = ColumnInfo {
1915                name: "external_timestamp".to_owned(),
1916                decode_kind: sqlite_decode_kind(Some(decl_type)),
1917            };
1918
1919            assert_eq!(
1920                decode_sqlite_text(b"2024-01-01 00:57:55", &column).unwrap(),
1921                Value::Text("2024-01-01 00:57:55".to_owned())
1922            );
1923        }
1924    }
1925
1926    #[test]
1927    fn declared_column_types_compile_to_decode_kinds() {
1928        assert_eq!(sqlite_decode_kind(Some("BOOLEAN")), SqliteDecodeKind::Bool);
1929        assert_eq!(
1930            sqlite_decode_kind(Some("decimal(20, 4)")),
1931            SqliteDecodeKind::Decimal
1932        );
1933        assert_eq!(
1934            sqlite_decode_kind(Some(" VARCHAR(255) ")),
1935            SqliteDecodeKind::Text
1936        );
1937        assert_eq!(
1938            sqlite_decode_kind(Some("datetime")),
1939            SqliteDecodeKind::Timestamp
1940        );
1941        assert_eq!(sqlite_decode_kind(Some("custom")), SqliteDecodeKind::Infer);
1942        assert_eq!(sqlite_decode_kind(None), SqliteDecodeKind::Infer);
1943    }
1944}