Skip to main content

teaql_provider_sqlite/
lib.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::future::Future;
3use std::pin::Pin;
4use std::str::FromStr;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use chrono::{DateTime, NaiveDate, NaiveDateTime};
8use rusqlite::types::{Value as SqliteValue, ValueRef};
9use rusqlite::{Connection, OptionalExtension, Row, params, params_from_iter};
10use rust_decimal::Decimal;
11use teaql_core::{
12    DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, Record, SelectQuery,
13    UpdateCommand, Value,
14};
15use teaql_runtime::{
16    GraphNode, InternalIdGenerator, RawAuditEvent, RuntimeError, SchemaProvider, UserContext,
17};
18use teaql_sql::{
19    CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
20    quote_identifier_if_needed,
21};
22
23pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
24
25#[derive(Debug, Default, Clone, Copy)]
26pub struct SqliteDialect;
27
28impl SqlDialect for SqliteDialect {
29    fn kind(&self) -> DatabaseKind {
30        DatabaseKind::Sqlite
31    }
32
33    fn quote_ident(&self, ident: &str) -> String {
34        quote_ident(ident)
35    }
36
37    fn placeholder(&self, _index: usize) -> String {
38        "?".to_owned()
39    }
40
41    fn schema_type_sql(
42        &self,
43        data_type: DataType,
44        property: &PropertyDescriptor,
45    ) -> Result<&'static str, SqlCompileError> {
46        match data_type {
47            DataType::Bool => Ok("BOOLEAN"),
48            DataType::I64 | DataType::U64 if property.is_id => Ok("INTEGER"),
49            DataType::I64 | DataType::U64 => Ok("INTEGER"),
50            DataType::F64 => Ok("REAL"),
51            DataType::Decimal => Ok("NUMERIC"),
52            DataType::Text => Ok("VARCHAR(255)"),
53            DataType::LargeText => Ok("TEXT"),
54            DataType::Json => Ok("JSON"),
55            DataType::Date => Ok("DATE"),
56            DataType::Timestamp => Ok("TIMESTAMP"),
57        }
58    }
59
60    fn compile_add_column(
61        &self,
62        entity: &EntityDescriptor,
63        property: &PropertyDescriptor,
64    ) -> Result<String, SqlCompileError> {
65        // SQLite does not support adding NOT NULL columns without a DEFAULT.
66        // Since TeaQL enforces nullability at the application layer, we can safely
67        // strip the NOT NULL constraint when adding columns to existing tables.
68        let def = self.column_definition_sql(property)?;
69        let def_without_not_null = def.replace(" NOT NULL", "");
70
71        Ok(format!(
72            "ALTER TABLE {} ADD COLUMN {}",
73            self.quote_ident(&entity.table_name),
74            def_without_not_null
75        ))
76    }
77}
78
79#[derive(Debug)]
80pub enum MutationExecutorError {
81    Sqlite(rusqlite::Error),
82    SqlCompile(SqlCompileError),
83    UnsupportedValue(&'static str),
84    UnsupportedColumnType(String),
85    Bind(String),
86    Lock(String),
87}
88
89impl std::fmt::Display for MutationExecutorError {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            Self::Sqlite(err) => err.fmt(f),
93            Self::SqlCompile(err) => err.fmt(f),
94            Self::UnsupportedValue(kind) => {
95                write!(
96                    f,
97                    "unsupported rusqlite bind value for mutation executor: {kind}"
98                )
99            }
100            Self::UnsupportedColumnType(kind) => {
101                write!(
102                    f,
103                    "unsupported rusqlite column type for record decoding: {kind}"
104                )
105            }
106            Self::Bind(message) => write!(f, "rusqlite bind error: {message}"),
107            Self::Lock(message) => write!(f, "rusqlite connection lock error: {message}"),
108        }
109    }
110}
111
112impl std::error::Error for MutationExecutorError {}
113
114impl From<rusqlite::Error> for MutationExecutorError {
115    fn from(value: rusqlite::Error) -> Self {
116        Self::Sqlite(value)
117    }
118}
119
120impl From<SqlCompileError> for MutationExecutorError {
121    fn from(value: SqlCompileError) -> Self {
122        Self::SqlCompile(value)
123    }
124}
125
126#[derive(Clone)]
127pub struct SqliteMutationExecutor {
128    connection: Arc<Mutex<Connection>>,
129}
130
131impl SqliteMutationExecutor {
132    pub fn new(connection: Arc<Mutex<Connection>>) -> Self {
133        Self { connection }
134    }
135
136    pub fn from_connection(connection: Connection) -> Self {
137        Self::new(Arc::new(Mutex::new(connection)))
138    }
139
140    pub fn connection(&self) -> Arc<Mutex<Connection>> {
141        Arc::clone(&self.connection)
142    }
143
144    pub fn ensure_schema(
145        &self,
146        dialect: &SqliteDialect,
147        entities: &[&EntityDescriptor],
148    ) -> Result<(), MutationExecutorError> {
149        self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
150
151        for entity in entities {
152            if !self.table_exists(&entity.table_name)? {
153                let sql = dialect.compile_create_table(entity)?;
154                self.lock()?.execute(&sql, [])?;
155                continue;
156            }
157
158            let existing_columns = self.table_columns(&entity.table_name)?;
159            for property in &entity.properties {
160                let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
161                if existing_columns.contains(&bare_column) {
162                    continue;
163                }
164                let sql = dialect.compile_add_column(entity, property)?;
165                self.lock()?.execute(&sql, [])?;
166            }
167
168            for sql in dialect.schema_indexes_sqls(entity)? {
169                self.lock()?.execute(&sql, [])?;
170            }
171        }
172        Ok(())
173    }
174
175    pub fn ensure_id_space_table(&self, table_name: &str) -> Result<(), MutationExecutorError> {
176        let sql = format!(
177            "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
178            quote_ident(table_name)
179        );
180        self.lock()?.execute(&sql, [])?;
181        Ok(())
182    }
183
184    pub fn begin_transaction(&self) -> Result<(), MutationExecutorError> {
185        self.lock()?.execute("BEGIN IMMEDIATE", [])?;
186        Ok(())
187    }
188
189    pub fn commit_transaction(&self) -> Result<(), MutationExecutorError> {
190        self.lock()?.execute("COMMIT", [])?;
191        Ok(())
192    }
193
194    pub fn rollback_transaction(&self) -> Result<(), MutationExecutorError> {
195        self.lock()?.execute("ROLLBACK", [])?;
196        Ok(())
197    }
198
199    pub fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
200        let params = bind_values(&query.params)?;
201        let rows = self
202            .lock()?
203            .execute(&query.sql_with_comment(), params_from_iter(params.iter()))?;
204        Ok(rows as u64)
205    }
206
207    pub fn fetch_all(&self, query: &CompiledQuery) -> Result<Vec<Record>, MutationExecutorError> {
208        let params = bind_values(&query.params)?;
209        let connection = self.lock()?;
210        let mut statement = connection.prepare(&query.sql_with_comment())?;
211        let columns = statement_columns(&statement);
212        let mut rows = statement.query(params_from_iter(params.iter()))?;
213        let mut records = Vec::new();
214        while let Some(row) = rows.next()? {
215            records.push(decode_sqlite_row(row, &columns)?);
216        }
217        Ok(records)
218    }
219
220    /// Fetch rows in streaming mode (chunked).
221    /// Returns a Vec of StreamChunk, each containing up to `chunk_size` rows.
222    pub fn fetch_stream(
223        &self,
224        query: &CompiledQuery,
225        chunk_size: usize,
226    ) -> Result<Vec<teaql_data_service::StreamChunk>, MutationExecutorError> {
227        let params = bind_values(&query.params)?;
228        let connection = self.lock()?;
229        let mut statement = connection.prepare(&query.sql_with_comment())?;
230        let columns = statement_columns(&statement);
231        let mut rows = statement.query(params_from_iter(params.iter()))?;
232
233        let mut chunks = Vec::new();
234        let mut current_chunk = Vec::new();
235        let mut chunk_index = 0;
236
237        while let Some(row) = rows.next()? {
238            current_chunk.push(decode_sqlite_row(row, &columns)?);
239            if current_chunk.len() >= chunk_size {
240                chunks.push(teaql_data_service::StreamChunk {
241                    rows: current_chunk,
242                    chunk_index,
243                    is_last: false,
244                });
245                current_chunk = Vec::new();
246                chunk_index += 1;
247            }
248        }
249
250        // Push the final chunk (may be empty if exactly aligned)
251        chunks.push(teaql_data_service::StreamChunk {
252            rows: current_chunk,
253            chunk_index,
254            is_last: true,
255        });
256
257        Ok(chunks)
258    }
259
260    pub fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
261        let exists: i64 = self.lock()?.query_row(
262            "SELECT COUNT(1) FROM sqlite_master WHERE type = 'table' AND name = ?",
263            [table_name],
264            |row| row.get(0),
265        )?;
266        Ok(exists > 0)
267    }
268
269    pub fn table_columns(
270        &self,
271        table_name: &str,
272    ) -> Result<BTreeSet<String>, MutationExecutorError> {
273        let pragma_sql = format!("PRAGMA table_info({})", quote_ident(table_name));
274        let connection = self.lock()?;
275        let mut statement = connection.prepare(&pragma_sql)?;
276        let rows = statement.query_map([], |row| row.get::<_, String>("name"))?;
277        let mut columns = BTreeSet::new();
278        for row in rows {
279            columns.insert(row?.to_lowercase());
280        }
281        Ok(columns)
282    }
283
284    fn lock(&self) -> Result<MutexGuard<'_, Connection>, MutationExecutorError> {
285        self.connection
286            .lock()
287            .map_err(|err| MutationExecutorError::Lock(err.to_string()))
288    }
289}
290
291impl teaql_data_service::DataServiceExecutor for SqliteMutationExecutor {
292    type Error = MutationExecutorError;
293
294    fn capabilities(&self) -> teaql_data_service::DataServiceCapabilities {
295        teaql_data_service::DataServiceCapabilities {
296            query: true,
297            mutation: true,
298            transaction: true,
299            schema: true,
300            id_generation: true,
301            ..Default::default()
302        }
303    }
304}
305
306impl SqlTransport for SqliteMutationExecutor {
307    type Error = MutationExecutorError;
308
309    async fn fetch_all_sql(&self, query: &CompiledQuery) -> Result<Vec<Record>, Self::Error> {
310        SqliteMutationExecutor::fetch_all(self, query)
311    }
312
313    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
314        SqliteMutationExecutor::execute(self, query)
315    }
316}
317
318impl teaql_sql::StreamingSqlTransport for SqliteMutationExecutor {
319    // A rusqlite statement and its rows borrow the guarded connection for the
320    // lifetime of the stream. This stream is intentionally local/non-Send, so
321    // retaining the synchronous guard across yields is required and safe.
322    #[allow(clippy::await_holding_lock)]
323    fn stream_sql(
324        &self,
325        query: CompiledQuery,
326        chunk_size: usize,
327    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
328        let connection = self.connection.clone();
329        Box::pin(async_stream::try_stream! {
330            let params = bind_values(&query.params)?;
331            let guard = connection.lock().map_err(|err| MutationExecutorError::Lock(err.to_string()))?;
332            let mut statement = guard.prepare(&query.sql_with_comment())?;
333            let columns = statement_columns(&statement);
334            let mut rows = statement.query(params_from_iter(params.iter()))?;
335            let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
336            while let Some(row) = rows.next()? {
337                chunk.push(decode_sqlite_row(row, &columns)?);
338                if chunk.len() == chunk_size { yield teaql_data_service::StreamChunk { rows: std::mem::take(&mut chunk), chunk_index: index, is_last: false }; index += 1; }
339            }
340            if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows: chunk, chunk_index: index, is_last: true }; }
341        })
342    }
343}
344
345impl teaql_data_service::StreamQueryExecutor for SqliteMutationExecutor {
346    fn query_stream(
347        &self,
348        request: teaql_data_service::QueryRequest,
349        chunk_size: usize,
350    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
351        let dialect = SqliteDialect;
352        // Use a dummy entity descriptor for compilation
353        let entity_desc = teaql_core::EntityDescriptor::new(&request.query.entity);
354        match dialect.compile_select(&entity_desc, &request.query) {
355            Ok(compiled) => {
356                teaql_sql::StreamingSqlTransport::stream_sql(self, compiled, chunk_size)
357            }
358            Err(error) => Box::pin(futures_util::stream::once(async {
359                Err(MutationExecutorError::SqlCompile(error))
360            })),
361        }
362    }
363}
364
365impl teaql_sql::SqlTransaction for SqliteMutationExecutor {
366    type Error = MutationExecutorError;
367
368    async fn commit_sql(self) -> Result<(), Self::Error> {
369        self.commit_transaction()
370    }
371
372    async fn rollback_sql(self) -> Result<(), Self::Error> {
373        self.rollback_transaction()
374    }
375}
376
377impl teaql_sql::SqlTransactionTransport for SqliteMutationExecutor {
378    type Tx<'a>
379        = Self
380    where
381        Self: 'a;
382
383    async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
384        self.begin_transaction()?;
385        Ok(self.clone())
386    }
387}
388
389fn initial_graph_exists_sqlite(
390    executor: &SqliteMutationExecutor,
391    dialect: &SqliteDialect,
392    entity: &EntityDescriptor,
393    graph: &GraphNode,
394) -> Result<bool, MutationExecutorError> {
395    let Some(id) = graph.values.get("id") else {
396        return Ok(false);
397    };
398    let query = dialect.compile_select(
399        entity,
400        &SelectQuery::new(&graph.entity)
401            .project("id")
402            .filter(Expr::eq("id", id.clone()))
403            .limit(1),
404    )?;
405    Ok(!executor.fetch_all(&query)?.is_empty())
406}
407
408fn compile_initial_graph_insert(
409    dialect: &impl SqlDialect,
410    entity: &EntityDescriptor,
411    graph: &GraphNode,
412) -> Result<CompiledQuery, MutationExecutorError> {
413    let mut command = InsertCommand::new(&graph.entity);
414    for (field, value) in &graph.values {
415        command = command.value(field.clone(), value.clone());
416    }
417    dialect.compile_insert(entity, &command).map_err(Into::into)
418}
419
420fn compile_initial_graph_update(
421    dialect: &impl SqlDialect,
422    entity: &EntityDescriptor,
423    graph: &GraphNode,
424) -> Result<Option<CompiledQuery>, MutationExecutorError> {
425    let Some(id) = graph.values.get("id") else {
426        return Ok(None);
427    };
428    let mut command = UpdateCommand::new(&graph.entity, id.clone());
429    for (field, value) in &graph.values {
430        if field != "id" {
431            command = command.value(field.clone(), value.clone());
432        }
433    }
434    match dialect.compile_update(entity, &command) {
435        Ok(query) => Ok(Some(query)),
436        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
437        Err(err) => Err(err.into()),
438    }
439}
440
441pub trait SqliteSchemaExt {
442    fn ensure_sqlite_schema(
443        &self,
444    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>>;
445}
446
447pub fn ensure_sqlite_schema_for(context: &UserContext) -> Result<(), MutationExecutorError> {
448    let dialect = context.get_resource::<SqliteDialect>().ok_or_else(|| {
449        MutationExecutorError::Bind("missing typed resource: SqliteDialect".to_owned())
450    })?;
451    let executor = context
452        .get_resource::<SqliteMutationExecutor>()
453        .ok_or_else(|| {
454            MutationExecutorError::Bind("missing typed resource: SqliteMutationExecutor".to_owned())
455        })?;
456
457    let entities = context.all_entities();
458
459    // Ensure id space table exists
460    executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
461
462    // Process each entity table individually with granular events
463    for entity in &entities {
464        let field_count = entity.properties.len();
465        if !executor.table_exists(&entity.table_name)? {
466            // New table: create it
467            let sql = dialect.compile_create_table(entity)?;
468            executor.lock()?.execute(&sql, [])?;
469            let _ = context.send_event(RawAuditEvent::schema_created(
470                &entity.name,
471                &entity.table_name,
472                field_count,
473            ));
474            continue;
475        }
476        // Existing table: check for missing columns
477        let existing_columns = executor.table_columns(&entity.table_name)?;
478        let mut fields_added = 0;
479        for property in &entity.properties {
480            let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
481            if existing_columns.contains(&bare_column) {
482                continue;
483            }
484            let sql = dialect.compile_add_column(entity, property)?;
485            executor.lock()?.execute(&sql, [])?;
486            let _ = context.send_event(RawAuditEvent::field_added(
487                &entity.name,
488                &entity.table_name,
489                &property.column_name,
490            ));
491            fields_added += 1;
492        }
493        let _ = context.send_event(RawAuditEvent::schema_verified(
494            &entity.name,
495            &entity.table_name,
496            field_count,
497        ));
498        let _ = fields_added; // used above for FieldAdded events
499    }
500
501    // Constant graphs are reconciled so model changes are propagated.
502    let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
503    let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); // (inserted, updated)
504    for graph in context.initial_graphs() {
505        let entity = context.entity(&graph.entity).ok_or_else(|| {
506            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
507        })?;
508        let counts = seed_counts.entry(graph.entity.clone()).or_insert((0, 0));
509        if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
510            if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
511                executor.execute(&query)?;
512            }
513            counts.1 += 1;
514            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
515                id_generator.ensure_floor(&graph.entity, id)?;
516            }
517            continue;
518        }
519        let query = compile_initial_graph_insert(dialect, entity, graph)?;
520        executor.execute(&query)?;
521        counts.0 += 1; // inserted
522        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
523            id_generator.ensure_floor(&graph.entity, id)?;
524        }
525    }
526
527    // Roots are create-if-absent. Once present, application-owned values win.
528    for graph in context.root_graphs() {
529        let entity = context.entity(&graph.entity).ok_or_else(|| {
530            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
531        })?;
532        if initial_graph_exists_sqlite(executor, dialect, entity, graph)? {
533            if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
534                id_generator.ensure_floor(&graph.entity, id)?;
535            }
536            continue;
537        }
538        let query = compile_initial_graph_insert(dialect, entity, graph)?;
539        executor.execute(&query)?;
540        seed_counts.entry(graph.entity.clone()).or_insert((0, 0)).0 += 1;
541        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
542            id_generator.ensure_floor(&graph.entity, id)?;
543        }
544    }
545
546    // Fire DataSeeded events per entity type
547    for (entity_name, (inserted, updated)) in &seed_counts {
548        let entity = context.entity(entity_name).ok_or_else(|| {
549            MutationExecutorError::Bind(format!("missing entity: {}", entity_name))
550        })?;
551        let _ = context.send_event(RawAuditEvent::data_seeded(
552            entity_name,
553            &entity.table_name,
554            *inserted,
555            *updated,
556        ));
557    }
558
559    Ok(())
560}
561
562impl SqliteSchemaExt for UserContext {
563    fn ensure_sqlite_schema(
564        &self,
565    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + Send + '_>> {
566        Box::pin(async move { ensure_sqlite_schema_for(self) })
567    }
568}
569
570#[derive(Debug, Default, Clone, Copy)]
571pub struct SqliteSchemaProvider;
572
573impl SchemaProvider for SqliteSchemaProvider {
574    fn ensure_schema<'a>(
575        &'a self,
576        context: &'a UserContext,
577    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
578        Box::pin(async move {
579            ensure_sqlite_schema_for(context).map_err(|err| RuntimeError::Schema(err.to_string()))
580        })
581    }
582}
583
584pub trait SqliteProviderExt {
585    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self;
586}
587
588impl SqliteProviderExt for UserContext {
589    fn use_sqlite_provider(&mut self, executor: SqliteMutationExecutor) -> &mut Self {
590        self.insert_resource(SqliteDialect);
591        self.insert_resource(executor);
592        self.set_schema_provider(SqliteSchemaProvider);
593        self
594    }
595}
596
597#[derive(Clone)]
598pub struct SqliteIdSpaceGenerator {
599    executor: SqliteMutationExecutor,
600    table_name: String,
601}
602
603impl SqliteIdSpaceGenerator {
604    pub fn new(connection: Connection) -> Self {
605        Self::from_executor(SqliteMutationExecutor::from_connection(connection))
606    }
607
608    pub fn from_executor(executor: SqliteMutationExecutor) -> Self {
609        Self {
610            executor,
611            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
612        }
613    }
614
615    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
616        self.table_name = table_name.into();
617        self
618    }
619
620    pub fn ensure_table(&self) -> Result<(), MutationExecutorError> {
621        self.executor.ensure_id_space_table(&self.table_name)
622    }
623
624    pub fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
625        self.ensure_table()?;
626        let table = quote_ident(&self.table_name);
627        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = ?");
628        let insert_sql = format!(
629            "INSERT INTO {table} (type_name, current_level) VALUES (?, 1)"
630        );
631        let update_sql = format!(
632            "UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"
633        );
634        for attempt in 1..=100 {
635            let connection = self.executor.lock()?;
636            let current = connection
637                .query_row(&select_sql, [entity], |row| row.get::<_, i64>(0))
638                .optional()?;
639            if let Some(current) = current {
640                let next = current.checked_add(1).ok_or_else(|| {
641                    MutationExecutorError::Bind(format!(
642                        "ID space overflow for {entity} on optimistic-lock attempt {attempt}"
643                    ))
644                })?;
645                if connection.execute(&update_sql, params![next, entity, current])? == 1 {
646                    return u64::try_from(next).map_err(|_| {
647                        MutationExecutorError::Bind(format!(
648                            "generated id {next} cannot be represented as u64"
649                        ))
650                    });
651                }
652            } else {
653                match connection.execute(&insert_sql, params![entity]) {
654                    Ok(1) => return Ok(1),
655                    Ok(changed) => {
656                        return Err(MutationExecutorError::Bind(format!(
657                            "ID space insert for {entity} changed {changed} rows"
658                        )))
659                    }
660                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
661                    Err(error) => return Err(error.into()),
662                }
663            }
664        }
665        Err(MutationExecutorError::Bind(format!(
666            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
667        )))
668    }
669
670    pub fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), MutationExecutorError> {
671        self.ensure_table()?;
672        let floor = i64::try_from(floor).map_err(|_| {
673            MutationExecutorError::Bind(format!("ID space floor {floor} for {entity} exceeds i64"))
674        })?;
675        let table = quote_ident(&self.table_name);
676        for _ in 1..=100 {
677            let connection = self.executor.lock()?;
678            let current = connection
679                .query_row(
680                    &format!("SELECT current_level FROM {table} WHERE type_name = ?"),
681                    [entity],
682                    |row| row.get::<_, i64>(0),
683                )
684                .optional()?;
685            match current {
686                Some(current) if current >= floor => return Ok(()),
687                Some(current) => {
688                    if connection.execute(
689                        &format!("UPDATE {table} SET current_level = ? WHERE type_name = ? AND current_level = ?"),
690                        params![floor, entity, current],
691                    )? == 1 { return Ok(()); }
692                }
693                None => match connection.execute(
694                    &format!("INSERT INTO {table}(type_name, current_level) VALUES (?, ?)"),
695                    params![entity, floor],
696                ) {
697                    Ok(1) => return Ok(()),
698                    Ok(_) => {}
699                    Err(error) if error.sqlite_error_code() == Some(rusqlite::ErrorCode::ConstraintViolation) => {}
700                    Err(error) => return Err(error.into()),
701                },
702            }
703        }
704        Err(MutationExecutorError::Bind(format!(
705            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
706        )))
707    }
708}
709
710impl InternalIdGenerator for SqliteIdSpaceGenerator {
711    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
712        self.next_id(entity)
713            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))
714    }
715}
716
717fn quote_ident(ident: &str) -> String {
718    quote_identifier_if_needed(ident, '"')
719}
720
721/// Strip wrapping identifier quotes from a SQL identifier.
722///
723/// SQLite `PRAGMA table_info` returns bare column names (e.g. `description`),
724/// but generated `PropertyDescriptor::column_name` may carry quotes
725/// (e.g. `"description"`) when the name is a reserved keyword.  This helper
726/// normalises the column name so the two can be compared correctly during
727/// schema migration.
728fn strip_identifier_quotes(ident: &str) -> &str {
729    let bytes = ident.as_bytes();
730    if bytes.len() >= 2 {
731        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
732        if (first == b'"' && last == b'"')
733            || (first == b'`' && last == b'`')
734            || (first == b'[' && last == b']')
735        {
736            return &ident[1..ident.len() - 1];
737        }
738    }
739    ident
740}
741
742fn bind_values(values: &[Value]) -> Result<Vec<SqliteValue>, MutationExecutorError> {
743    values.iter().map(bind_sqlite_value).collect()
744}
745
746fn bind_sqlite_value(value: &Value) -> Result<SqliteValue, MutationExecutorError> {
747    match value {
748        Value::Null => Ok(SqliteValue::Null),
749        Value::Bool(v) => Ok(SqliteValue::Integer(i64::from(*v))),
750        Value::I64(v) => Ok(SqliteValue::Integer(*v)),
751        Value::U64(v) => i64::try_from(*v)
752            .map(SqliteValue::Integer)
753            .map_err(|_| MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))),
754        Value::F64(v) => Ok(SqliteValue::Real(*v)),
755        // Bind the canonical numeric spelling. SQLite NUMERIC affinity keeps
756        // predicates and aggregates numeric; an application-only text prefix
757        // makes range comparisons silently return the wrong result.
758        Value::Decimal(v) => Ok(SqliteValue::Text(v.to_string())),
759        Value::Text(v) => Ok(SqliteValue::Text(v.clone())),
760        Value::Json(v) => Ok(SqliteValue::Text(v.to_string())),
761        Value::Date(v) => Ok(SqliteValue::Text(v.format("%Y-%m-%d").to_string())),
762        Value::Timestamp(v) => Ok(SqliteValue::Integer(v.0)),
763        Value::Object(_) => Err(MutationExecutorError::UnsupportedValue("object")),
764        Value::List(_) => Err(MutationExecutorError::UnsupportedValue("list")),
765        Value::TypedNull(_) => Ok(SqliteValue::Null),
766    }
767}
768
769#[derive(Debug, Clone)]
770struct ColumnInfo {
771    name: String,
772    decl_type: Option<String>,
773}
774
775fn statement_columns(statement: &rusqlite::Statement<'_>) -> Vec<ColumnInfo> {
776    statement
777        .columns()
778        .into_iter()
779        .map(|column| ColumnInfo {
780            name: column.name().to_owned(),
781            decl_type: column.decl_type().map(|value| value.to_ascii_uppercase()),
782        })
783        .collect()
784}
785
786fn decode_sqlite_row(
787    row: &Row<'_>,
788    columns: &[ColumnInfo],
789) -> Result<Record, MutationExecutorError> {
790    let mut record = BTreeMap::new();
791    for (index, column) in columns.iter().enumerate() {
792        let value_ref = row.get_ref(index)?;
793        let value = match value_ref {
794            ValueRef::Null => Value::Null,
795            ValueRef::Integer(value) => decode_sqlite_integer(value, column),
796            ValueRef::Real(value) => Value::F64(value),
797            ValueRef::Text(value) => decode_sqlite_text(value, column)?,
798            ValueRef::Blob(_) => {
799                return Err(MutationExecutorError::UnsupportedColumnType(
800                    "BLOB".to_owned(),
801                ));
802            }
803        };
804        record.insert(column.name.clone(), value);
805    }
806    Ok(record)
807}
808
809fn decode_sqlite_integer(value: i64, column: &ColumnInfo) -> Value {
810    match column_decl_type(column).as_deref() {
811        Some("BOOLEAN") | Some("BOOL") => Value::Bool(value != 0),
812        _ => Value::I64(value),
813    }
814}
815
816fn decode_sqlite_text(value: &[u8], column: &ColumnInfo) -> Result<Value, MutationExecutorError> {
817    let value = std::str::from_utf8(value)
818        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite text: {err}")))?;
819    match column_decl_type(column).as_deref() {
820        Some("NUMERIC") | Some("DECIMAL") => Decimal::from_str(value)
821            .map(Value::Decimal)
822            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite decimal: {err}"))),
823        Some("JSON") => serde_json::from_str(value).map(Value::Json).map_err(|err| {
824            MutationExecutorError::Bind(format!("invalid sqlite json value: {err}"))
825        }),
826        Some("DATE") => NaiveDate::parse_from_str(value, "%Y-%m-%d")
827            .map(Value::Date)
828            .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite date: {err}"))),
829        Some("TIMESTAMP") | Some("DATETIME") => parse_sqlite_timestamp(value),
830        _ => infer_sqlite_text(value),
831    }
832}
833
834fn infer_sqlite_text(value: &str) -> Result<Value, MutationExecutorError> {
835    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
836        return Ok(Value::Date(date));
837    }
838    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
839        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
840            timestamp.timestamp_millis(),
841        )));
842    }
843    if let Ok(timestamp) = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S") {
844        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
845            timestamp.and_utc().timestamp_millis(),
846        )));
847    }
848    Ok(Value::Text(value.to_owned()))
849}
850
851fn parse_sqlite_timestamp(value: &str) -> Result<Value, MutationExecutorError> {
852    if let Ok(timestamp) = DateTime::parse_from_rfc3339(value) {
853        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
854            timestamp.timestamp_millis(),
855        )));
856    }
857    if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
858        return Ok(Value::Timestamp(teaql_core::time::Timestamp(
859            date.and_hms_opt(0, 0, 0)
860                .unwrap_or_default()
861                .and_utc()
862                .timestamp_millis(),
863        )));
864    }
865    NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
866        .map(|timestamp| {
867            Value::Timestamp(teaql_core::time::Timestamp(
868                timestamp.and_utc().timestamp_millis(),
869            ))
870        })
871        .map_err(|err| MutationExecutorError::Bind(format!("invalid sqlite timestamp: {err}")))
872}
873
874fn column_decl_type(column: &ColumnInfo) -> Option<String> {
875    column
876        .decl_type
877        .as_ref()
878        .map(|value| value.split('(').next().unwrap_or(value).trim().to_owned())
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884    use futures_util::StreamExt;
885    use teaql_core::{DeleteCommand, RecoverCommand};
886    use teaql_macros::TeaqlEntity;
887    use teaql_runtime::InMemoryMetadataStore;
888
889    #[test]
890    fn streaming_sql_yields_bounded_chunks_and_releases_cursor_on_drop() {
891        let connection = Connection::open_in_memory().unwrap();
892        connection
893            .execute_batch(
894                "CREATE TABLE stream_fixture(id INTEGER);\
895                 INSERT INTO stream_fixture VALUES (1), (2), (3), (4), (5);",
896            )
897            .unwrap();
898        let executor = SqliteMutationExecutor::from_connection(connection);
899        let query = CompiledQuery {
900            sql: "SELECT id FROM stream_fixture ORDER BY id".to_owned(),
901            params: vec![],
902            comment: None,
903        };
904        let mut stream = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query.clone(), 2);
905        let sizes = futures_executor::block_on(async {
906            let mut result = Vec::new();
907            while let Some(chunk) = stream.next().await {
908                result.push(chunk.unwrap().rows.len());
909            }
910            result
911        });
912        assert_eq!(sizes, vec![2, 2, 1]);
913
914        let mut early = teaql_sql::StreamingSqlTransport::stream_sql(&executor, query, 2);
915        assert_eq!(
916            futures_executor::block_on(early.next())
917                .unwrap()
918                .unwrap()
919                .rows
920                .len(),
921            2
922        );
923        drop(early);
924        let count: i64 = executor
925            .connection()
926            .lock()
927            .unwrap()
928            .query_row("SELECT count(*) FROM stream_fixture", [], |row| row.get(0))
929            .unwrap();
930        assert_eq!(count, 5);
931    }
932
933    #[test]
934    fn decimal_bind_is_numeric_and_comparable() {
935        let value =
936            bind_sqlite_value(&Value::Decimal(Decimal::from_str("123.450").unwrap())).unwrap();
937        assert_eq!(value, SqliteValue::Text("123.450".to_owned()));
938        let connection = Connection::open_in_memory().unwrap();
939        let matches: i64 = connection
940            .query_row(
941                "SELECT 1 WHERE CAST(? AS NUMERIC) BETWEEN 120 AND 130",
942                [value],
943                |row| row.get(0),
944            )
945            .unwrap();
946        assert_eq!(matches, 1);
947    }
948
949    #[test]
950    fn temporal_debug_sql_is_executable_and_matches_prepared_storage() {
951        let connection = Connection::open_in_memory().unwrap();
952        connection
953            .execute_batch(
954                "CREATE TABLE temporal_fixture (id INTEGER PRIMARY KEY, d DATE, t TIMESTAMP)",
955            )
956            .unwrap();
957        let query = CompiledQuery {
958            sql: "INSERT INTO temporal_fixture VALUES (?, ?, ?)".to_owned(),
959            params: vec![
960                Value::I64(1),
961                Value::Date(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
962                Value::Timestamp(teaql_core::time::Timestamp(1_787_110_200_123)),
963            ],
964            comment: None,
965        };
966        let values = bind_values(&query.params).unwrap();
967        connection
968            .execute(&query.sql, rusqlite::params_from_iter(values))
969            .unwrap();
970        connection
971            .execute(
972                &query
973                    .debug_sql(teaql_sql::DatabaseKind::Sqlite)
974                    .replace("VALUES (1,", "VALUES (2,"),
975                [],
976            )
977            .unwrap();
978
979        let equal_count: i64 = connection.query_row(
980            "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",
981            [], |row| row.get(0),
982        ).unwrap();
983        let storage_type: String = connection
984            .query_row(
985                "SELECT typeof(t) FROM temporal_fixture WHERE id=1",
986                [],
987                |row| row.get(0),
988            )
989            .unwrap();
990        assert_eq!(equal_count, 1);
991        assert_eq!(storage_type, "integer");
992    }
993
994    fn entity() -> EntityDescriptor {
995        EntityDescriptor::new("Order")
996            .table_name("orders")
997            .property(
998                PropertyDescriptor::new("id", DataType::U64)
999                    .column_name("id")
1000                    .id()
1001                    .not_null(),
1002            )
1003            .property(
1004                PropertyDescriptor::new("version", DataType::I64)
1005                    .column_name("version")
1006                    .version()
1007                    .not_null(),
1008            )
1009            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1010    }
1011
1012    fn order_line_entity() -> EntityDescriptor {
1013        EntityDescriptor::new("OrderLine")
1014            .table_name("order_line")
1015            .property(
1016                PropertyDescriptor::new("id", DataType::U64)
1017                    .column_name("id")
1018                    .id()
1019                    .not_null(),
1020            )
1021            .property(
1022                PropertyDescriptor::new("order_id", DataType::U64)
1023                    .column_name("order_id")
1024                    .not_null(),
1025            )
1026            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1027    }
1028
1029    #[allow(dead_code)]
1030    #[derive(Debug, PartialEq, TeaqlEntity)]
1031    #[teaql(entity = "FeatureFlag", table = "feature_flags")]
1032    struct FeatureFlagRow {
1033        #[teaql(id)]
1034        id: u64,
1035        #[teaql(version)]
1036        version: i64,
1037        enabled: bool,
1038        optional_enabled: Option<bool>,
1039    }
1040
1041    fn feature_flag_record(enabled: Value, optional_enabled: Value) -> Record {
1042        Record::from([
1043            ("id".to_owned(), Value::U64(1)),
1044            ("version".to_owned(), Value::I64(1)),
1045            ("enabled".to_owned(), enabled),
1046            ("optional_enabled".to_owned(), optional_enabled),
1047        ])
1048    }
1049
1050    #[test]
1051    fn sqlite_dialect_compiles_mutations_and_schema() {
1052        let insert = SqliteDialect
1053            .compile_insert(
1054                &entity(),
1055                &InsertCommand::new("Order")
1056                    .value("id", 1_u64)
1057                    .value("name", "A"),
1058            )
1059            .unwrap();
1060        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES (?, ?)");
1061
1062        let update = SqliteDialect
1063            .compile_update(
1064                &entity(),
1065                &UpdateCommand::new("Order", 1_u64)
1066                    .expected_version(3)
1067                    .value("name", "B"),
1068            )
1069            .unwrap();
1070        assert_eq!(
1071            update.sql,
1072            "UPDATE orders SET name = ?, version = ? WHERE id = ? AND version = ?"
1073        );
1074
1075        let delete = SqliteDialect
1076            .compile_delete(
1077                &entity(),
1078                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1079            )
1080            .unwrap();
1081        let recover = SqliteDialect
1082            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1083            .unwrap();
1084        assert_eq!(
1085            delete.sql,
1086            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1087        );
1088        assert_eq!(
1089            recover.sql,
1090            "UPDATE orders SET version = ? WHERE id = ? AND version = ?"
1091        );
1092
1093        let create = SqliteDialect.compile_create_table(&entity()).unwrap();
1094        assert_eq!(
1095            create,
1096            "CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY NOT NULL, version INTEGER NOT NULL, name VARCHAR(255))"
1097        );
1098    }
1099
1100    #[test]
1101    fn sqlite_executor_ensures_schema_and_roundtrips_rows() {
1102        let executor =
1103            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1104        let entity = entity();
1105        let mut context = UserContext::new()
1106            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1107
1108        context.use_sqlite_provider(executor.clone());
1109        ensure_sqlite_schema_for(&context).unwrap();
1110
1111        let insert = SqliteDialect
1112            .compile_insert(
1113                &entity,
1114                &InsertCommand::new("Order")
1115                    .value("id", 1_u64)
1116                    .value("version", 1_i64)
1117                    .value("name", "draft"),
1118            )
1119            .unwrap();
1120        assert_eq!(executor.execute(&insert).unwrap(), 1);
1121
1122        let select = SqliteDialect
1123            .compile_select(
1124                &entity,
1125                &SelectQuery::new("Order")
1126                    .filter(Expr::eq("id", 1_u64))
1127                    .order_asc("id"),
1128            )
1129            .unwrap();
1130        let rows = executor.fetch_all(&select).unwrap();
1131        assert_eq!(rows.len(), 1);
1132        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1133        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1134        assert_eq!(rows[0].get("name"), Some(&Value::Text("draft".to_owned())));
1135    }
1136
1137    #[test]
1138    fn repeated_schema_ensure_does_not_overwrite_existing_initial_graph() {
1139        let executor =
1140            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1141        let entity = entity();
1142        let mut context = UserContext::new()
1143            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1144        context.set_root_graphs(vec![
1145            GraphNode::new("Order")
1146                .value("id", 1_u64)
1147                .value("version", 1_i64)
1148                .value("name", "module seed"),
1149        ]);
1150        context.use_sqlite_provider(executor.clone());
1151
1152        ensure_sqlite_schema_for(&context).unwrap();
1153        let customize = SqliteDialect
1154            .compile_update(
1155                &entity,
1156                &UpdateCommand::new("Order", 1_u64).value("name", "application value"),
1157            )
1158            .unwrap();
1159        assert_eq!(executor.execute(&customize).unwrap(), 1);
1160
1161        ensure_sqlite_schema_for(&context).unwrap();
1162
1163        let select = SqliteDialect
1164            .compile_select(
1165                &entity,
1166                &SelectQuery::new("Order").filter(Expr::eq("id", 1_u64)),
1167            )
1168            .unwrap();
1169        let rows = executor.fetch_all(&select).unwrap();
1170        assert_eq!(rows.len(), 1);
1171        assert_eq!(
1172            rows[0].get("name"),
1173            Some(&Value::Text("application value".to_owned()))
1174        );
1175    }
1176
1177    #[test]
1178    fn repeated_schema_ensure_reconciles_changed_constant_graph() {
1179        let executor =
1180            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1181        let entity = entity();
1182        let mut context = UserContext::new()
1183            .with_metadata(InMemoryMetadataStore::new().with_entity(entity.clone()));
1184        context.set_initial_graphs(vec![
1185            GraphNode::new("Order")
1186                .value("id", 1001_u64)
1187                .value("version", 1_i64)
1188                .value("name", "red"),
1189        ]);
1190        context.use_sqlite_provider(executor.clone());
1191        ensure_sqlite_schema_for(&context).unwrap();
1192
1193        context.set_initial_graphs(vec![
1194            GraphNode::new("Order")
1195                .value("id", 1001_u64)
1196                .value("version", 1_i64)
1197                .value("name", "crimson"),
1198        ]);
1199        ensure_sqlite_schema_for(&context).unwrap();
1200
1201        let select = SqliteDialect
1202            .compile_select(
1203                &entity,
1204                &SelectQuery::new("Order").filter(Expr::eq("id", 1001_u64)),
1205            )
1206            .unwrap();
1207        let rows = executor.fetch_all(&select).unwrap();
1208        assert_eq!(
1209            rows[0].get("name"),
1210            Some(&Value::Text("crimson".to_owned()))
1211        );
1212        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1213        assert_eq!(generator.next_id("Order").unwrap(), 1002);
1214    }
1215
1216    #[test]
1217    fn sqlite_executes_partitioned_relation_limit_per_parent() {
1218        let executor =
1219            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1220        let entity = order_line_entity();
1221        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1222
1223        for order_id in [11_u64, 12_u64] {
1224            for index in 1_u64..=5 {
1225                let id = order_id * 100 + index;
1226                let insert = SqliteDialect
1227                    .compile_insert(
1228                        &entity,
1229                        &InsertCommand::new("OrderLine")
1230                            .value("id", id)
1231                            .value("order_id", order_id)
1232                            .value("name", format!("line-{id}")),
1233                    )
1234                    .unwrap();
1235                executor.execute(&insert).unwrap();
1236            }
1237        }
1238
1239        let query = SelectQuery::new("OrderLine")
1240            .project("id")
1241            .project("order_id")
1242            .order_desc("id")
1243            .limit(3)
1244            .partition_by("order_id");
1245        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1246        let rows = executor.fetch_all(&compiled).unwrap();
1247
1248        assert_eq!(rows.len(), 6);
1249        for order_id in [11_i64, 12_i64] {
1250            let ids = rows
1251                .iter()
1252                .filter(|row| row.get("order_id") == Some(&Value::I64(order_id)))
1253                .filter_map(|row| row.get("id").cloned())
1254                .collect::<Vec<_>>();
1255            assert_eq!(
1256                ids,
1257                vec![
1258                    Value::I64(order_id * 100 + 5),
1259                    Value::I64(order_id * 100 + 4),
1260                    Value::I64(order_id * 100 + 3),
1261                ]
1262            );
1263        }
1264    }
1265
1266    #[test]
1267    fn sqlite_boolean_new_schema_roundtrips_as_bool() {
1268        let executor =
1269            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1270        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1271        let ddl = SqliteDialect.compile_create_table(&entity).unwrap();
1272        assert!(ddl.contains("enabled BOOLEAN NOT NULL"), "{ddl}");
1273        assert!(ddl.contains("optional_enabled BOOLEAN"), "{ddl}");
1274        assert!(!ddl.contains("enabled INTEGER"), "{ddl}");
1275
1276        executor.ensure_schema(&SqliteDialect, &[&entity]).unwrap();
1277        for (id, enabled, optional_enabled) in [(1_u64, false, true), (2_u64, true, false)] {
1278            let insert = SqliteDialect
1279                .compile_insert(
1280                    &entity,
1281                    &InsertCommand::new("FeatureFlag")
1282                        .value("id", id)
1283                        .value("version", 1_i64)
1284                        .value("enabled", enabled)
1285                        .value("optional_enabled", optional_enabled),
1286                )
1287                .unwrap();
1288            assert_eq!(executor.execute(&insert).unwrap(), 1);
1289        }
1290
1291        let select = SqliteDialect
1292            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1293            .unwrap();
1294        let rows = executor.fetch_all(&select).unwrap();
1295        assert_eq!(rows[0].get("enabled"), Some(&Value::Bool(false)));
1296        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::Bool(true)));
1297        assert_eq!(rows[1].get("enabled"), Some(&Value::Bool(true)));
1298        assert_eq!(rows[1].get("optional_enabled"), Some(&Value::Bool(false)));
1299
1300        let first = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[0].clone()).unwrap();
1301        let second = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[1].clone()).unwrap();
1302        assert!(!first.enabled);
1303        assert_eq!(first.optional_enabled, Some(true));
1304        assert!(second.enabled);
1305        assert_eq!(second.optional_enabled, Some(false));
1306    }
1307
1308    #[test]
1309    fn sqlite_boolean_legacy_integer_schema_maps_only_binary_values() {
1310        let executor =
1311            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1312        let entity = <FeatureFlagRow as teaql_core::TeaqlEntity>::entity_descriptor();
1313        executor
1314            .execute(&CompiledQuery {
1315                sql: "CREATE TABLE feature_flags (id INTEGER PRIMARY KEY, version INTEGER NOT NULL, enabled INTEGER NOT NULL, optional_enabled INTEGER)"
1316                    .to_owned(),
1317                params: Vec::new(),
1318                comment: None,
1319            })
1320            .unwrap();
1321
1322        let insert = SqliteDialect
1323            .compile_insert(
1324                &entity,
1325                &InsertCommand::new("FeatureFlag")
1326                    .value("id", 1_u64)
1327                    .value("version", 1_i64)
1328                    .value("enabled", true)
1329                    .value("optional_enabled", false),
1330            )
1331            .unwrap();
1332        executor.execute(&insert).unwrap();
1333        executor
1334            .execute(&CompiledQuery {
1335                sql: "INSERT INTO feature_flags (id, version, enabled, optional_enabled) VALUES (?, ?, ?, ?)"
1336                    .to_owned(),
1337                params: vec![
1338                    Value::U64(2),
1339                    Value::I64(1),
1340                    Value::I64(2),
1341                    Value::Null,
1342                ],
1343                comment: None,
1344            })
1345            .unwrap();
1346        let select = SqliteDialect
1347            .compile_select(&entity, &SelectQuery::new("FeatureFlag").order_asc("id"))
1348            .unwrap();
1349        let rows = executor.fetch_all(&select).unwrap();
1350        assert_eq!(rows[0].get("version"), Some(&Value::I64(1)));
1351        assert_eq!(rows[0].get("enabled"), Some(&Value::I64(1)));
1352        assert_eq!(rows[0].get("optional_enabled"), Some(&Value::I64(0)));
1353
1354        let decoded = <FeatureFlagRow as teaql_core::Entity>::from_record(rows[0].clone()).unwrap();
1355        assert!(decoded.enabled);
1356        assert_eq!(decoded.optional_enabled, Some(false));
1357        assert_eq!(rows[1].get("enabled"), Some(&Value::I64(2)));
1358        let error =
1359            <FeatureFlagRow as teaql_core::Entity>::from_record(rows[1].clone()).unwrap_err();
1360        assert!(error.message.contains("invalid field enabled"));
1361
1362        for (value, expected) in [
1363            (Value::I64(0), false),
1364            (Value::I64(1), true),
1365            (Value::U64(0), false),
1366            (Value::U64(1), true),
1367        ] {
1368            let decoded = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1369                value,
1370                Value::Null,
1371            ))
1372            .unwrap();
1373            assert_eq!(decoded.enabled, expected);
1374            assert_eq!(decoded.optional_enabled, None);
1375        }
1376
1377        for invalid in [Value::I64(-1), Value::I64(2), Value::U64(2)] {
1378            let error = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1379                invalid,
1380                Value::Null,
1381            ))
1382            .unwrap_err();
1383            assert!(error.message.contains("invalid field enabled"));
1384        }
1385        let error = <FeatureFlagRow as teaql_core::Entity>::from_record(feature_flag_record(
1386            Value::Bool(true),
1387            Value::U64(2),
1388        ))
1389        .unwrap_err();
1390        assert!(error.message.contains("invalid field optional_enabled"));
1391    }
1392
1393    #[test]
1394    fn sqlite_executor_parses_json_only_for_json_columns() {
1395        let executor =
1396            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1397
1398        executor
1399            .execute(&CompiledQuery {
1400                sql: "CREATE TABLE payloads (text_payload TEXT, json_payload JSON)".to_owned(),
1401                params: Vec::new(),
1402                comment: None,
1403            })
1404            .unwrap();
1405        executor
1406            .execute(&CompiledQuery {
1407                sql: "INSERT INTO payloads (text_payload, json_payload) VALUES (?, ?)".to_owned(),
1408                params: vec![
1409                    Value::Text("{\"active\":true}".to_owned()),
1410                    Value::Json(serde_json::json!({"active": true})),
1411                ],
1412                comment: None,
1413            })
1414            .unwrap();
1415
1416        let rows = executor
1417            .fetch_all(&CompiledQuery {
1418                sql: "SELECT text_payload, json_payload FROM payloads".to_owned(),
1419                params: Vec::new(),
1420                comment: None,
1421            })
1422            .unwrap();
1423
1424        assert_eq!(
1425            rows[0].get("text_payload"),
1426            Some(&Value::Text("{\"active\":true}".to_owned()))
1427        );
1428        assert_eq!(
1429            rows[0].get("json_payload"),
1430            Some(&Value::Json(serde_json::json!({"active": true})))
1431        );
1432    }
1433
1434    #[test]
1435    fn sqlite_id_space_generator_increments_ids() {
1436        let executor =
1437            SqliteMutationExecutor::from_connection(Connection::open_in_memory().unwrap());
1438        let generator = SqliteIdSpaceGenerator::from_executor(executor);
1439        assert_eq!(generator.next_id("Order").unwrap(), 1);
1440        assert_eq!(generator.next_id("Order").unwrap(), 2);
1441    }
1442
1443    #[test]
1444    fn sqlite_id_space_generator_is_safe_across_connections() {
1445        let path = std::env::temp_dir().join(format!(
1446            "teaql-id-space-{}-{}.db",
1447            std::process::id(),
1448            std::time::SystemTime::now()
1449                .duration_since(std::time::UNIX_EPOCH)
1450                .unwrap()
1451                .as_nanos()
1452        ));
1453        let mut workers = Vec::new();
1454        for _ in 0..4 {
1455            let path = path.clone();
1456            workers.push(std::thread::spawn(move || {
1457                let connection = Connection::open(path).unwrap();
1458                connection
1459                    .busy_timeout(std::time::Duration::from_secs(5))
1460                    .unwrap();
1461                let generator = SqliteIdSpaceGenerator::new(connection);
1462                (0..25)
1463                    .map(|_| generator.next_id("Order").unwrap())
1464                    .collect::<Vec<_>>()
1465            }));
1466        }
1467        let mut ids = workers
1468            .into_iter()
1469            .flat_map(|worker| worker.join().unwrap())
1470            .collect::<Vec<_>>();
1471        ids.sort_unstable();
1472        assert_eq!(ids, (1..=100).collect::<Vec<_>>());
1473        let _ = std::fs::remove_file(path);
1474    }
1475
1476    #[test]
1477    fn sqlite_fetch_stream_returns_chunked_rows() {
1478        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1479            Connection::open_in_memory().unwrap(),
1480        )));
1481        let entity = entity();
1482
1483        // Create table and insert 25 rows
1484        executor
1485            .execute(&CompiledQuery {
1486                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1487                    .to_owned(),
1488                params: Vec::new(),
1489                comment: None,
1490            })
1491            .unwrap();
1492
1493        for i in 1..=25 {
1494            let insert = SqliteDialect
1495                .compile_insert(
1496                    &entity,
1497                    &InsertCommand::new("Order")
1498                        .value("id", i as u64)
1499                        .value("version", 1_i64)
1500                        .value("name", format!("order-{i}")),
1501                )
1502                .unwrap();
1503            executor.execute(&insert).unwrap();
1504        }
1505
1506        // Stream with chunk_size = 10
1507        let query = SelectQuery::new("Order")
1508            .filter(Expr::gt("version", 0_i64))
1509            .order_asc("id")
1510            .stream(10);
1511
1512        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1513
1514        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1515
1516        // 25 rows / 10 per chunk = 3 chunks
1517        assert_eq!(chunks.len(), 3);
1518        assert_eq!(chunks[0].rows.len(), 10);
1519        assert_eq!(chunks[0].chunk_index, 0);
1520        assert!(!chunks[0].is_last);
1521
1522        assert_eq!(chunks[1].rows.len(), 10);
1523        assert_eq!(chunks[1].chunk_index, 1);
1524        assert!(!chunks[1].is_last);
1525
1526        assert_eq!(chunks[2].rows.len(), 5);
1527        assert_eq!(chunks[2].chunk_index, 2);
1528        assert!(chunks[2].is_last);
1529
1530        // Verify first and last row
1531        assert_eq!(
1532            chunks[0].rows[0].get("name"),
1533            Some(&Value::Text("order-1".to_owned()))
1534        );
1535        assert_eq!(
1536            chunks[2].rows[4].get("name"),
1537            Some(&Value::Text("order-25".to_owned()))
1538        );
1539    }
1540
1541    #[test]
1542    fn sqlite_fetch_stream_handles_empty_result() {
1543        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1544            Connection::open_in_memory().unwrap(),
1545        )));
1546
1547        executor
1548            .execute(&CompiledQuery {
1549                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1550                    .to_owned(),
1551                params: Vec::new(),
1552                comment: None,
1553            })
1554            .unwrap();
1555
1556        let entity = entity();
1557        let query = SelectQuery::new("Order")
1558            .filter(Expr::gt("version", 0_i64))
1559            .stream(10);
1560
1561        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1562
1563        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1564
1565        // Empty result = 1 chunk with 0 rows, marked as last
1566        assert_eq!(chunks.len(), 1);
1567        assert_eq!(chunks[0].rows.len(), 0);
1568        assert!(chunks[0].is_last);
1569    }
1570
1571    #[test]
1572    fn sqlite_fetch_stream_exact_chunk_boundary() {
1573        let executor = SqliteMutationExecutor::new(Arc::new(Mutex::new(
1574            Connection::open_in_memory().unwrap(),
1575        )));
1576        let entity = entity();
1577
1578        executor
1579            .execute(&CompiledQuery {
1580                sql: "CREATE TABLE orders (id INTEGER PRIMARY KEY, version INTEGER, name VARCHAR(255))"
1581                    .to_owned(),
1582                params: Vec::new(),
1583                comment: None,
1584            })
1585            .unwrap();
1586
1587        // Insert exactly 20 rows
1588        for i in 1..=20 {
1589            let insert = SqliteDialect
1590                .compile_insert(
1591                    &entity,
1592                    &InsertCommand::new("Order")
1593                        .value("id", i as u64)
1594                        .value("version", 1_i64)
1595                        .value("name", format!("order-{i}")),
1596                )
1597                .unwrap();
1598            executor.execute(&insert).unwrap();
1599        }
1600
1601        let query = SelectQuery::new("Order")
1602            .filter(Expr::gt("version", 0_i64))
1603            .order_asc("id")
1604            .stream(10);
1605
1606        let compiled = SqliteDialect.compile_select(&entity, &query).unwrap();
1607
1608        let chunks = executor.fetch_stream(&compiled, 10).unwrap();
1609
1610        // 20 rows / 10 per chunk = 2 full chunks + 1 empty final chunk
1611        assert_eq!(chunks.len(), 3);
1612        assert_eq!(chunks[0].rows.len(), 10);
1613        assert!(!chunks[0].is_last);
1614        assert_eq!(chunks[1].rows.len(), 10);
1615        assert!(!chunks[1].is_last);
1616        assert_eq!(chunks[2].rows.len(), 0);
1617        assert!(chunks[2].is_last);
1618    }
1619
1620    #[test]
1621    fn test_parse_sqlite_timestamp() {
1622        let ts1 = parse_sqlite_timestamp("2023-01-01 12:30:45").unwrap();
1623        assert!(matches!(ts1, Value::Timestamp(_)));
1624
1625        let ts2 = parse_sqlite_timestamp("2023-01-01").unwrap();
1626        assert!(matches!(ts2, Value::Timestamp(_)));
1627
1628        let ts3 = parse_sqlite_timestamp("2023-01-01T12:30:45Z").unwrap();
1629        assert!(matches!(ts3, Value::Timestamp(_)));
1630
1631        assert!(parse_sqlite_timestamp("invalid").is_err());
1632    }
1633}