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_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    // Ensure id space table exists
557    executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
558
559    // Process each entity table individually with granular events
560    for entity in &entities {
561        let field_count = entity.properties.len();
562        if !executor.table_exists(&entity.table_name)? {
563            // New table: create it
564            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        // Existing table: check for missing columns
574        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; // used above for FieldAdded events
596    }
597
598    // Constant graphs are reconciled so model changes are propagated.
599    let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
600    let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); // (inserted, updated)
601    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, &current)? {
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; // inserted
619        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    // Roots are create-if-absent. Once present, application-owned values win.
625    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    // Fire DataSeeded events per entity type
644    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
823/// Strip wrapping identifier quotes from a SQL identifier.
824///
825/// SQLite `PRAGMA table_info` returns bare column names (e.g. `description`),
826/// but generated `PropertyDescriptor::column_name` may carry quotes
827/// (e.g. `"description"`) when the name is a reserved keyword.  This helper
828/// normalises the column name so the two can be compared correctly during
829/// schema migration.
830fn 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        // Bind the canonical numeric spelling. SQLite NUMERIC affinity keeps
858        // predicates and aggregates numeric; an application-only text prefix
859        // makes range comparisons silently return the wrong result.
860        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        // Create table and insert 25 rows
1781        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        // Stream with chunk_size = 10
1804        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        // 25 rows / 10 per chunk = 3 chunks
1814        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        // Verify first and last row
1828        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        // Empty result = 1 chunk with 0 rows, marked as last
1863        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        // Insert exactly 20 rows
1885        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        // 20 rows / 10 per chunk = 2 full chunks + 1 empty final chunk
1908        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}