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