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 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 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 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 #[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 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 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
461
462 for entity in &entities {
464 let field_count = entity.properties.len();
465 if !executor.table_exists(&entity.table_name)? {
466 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 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; }
500
501 let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
503 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); 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; 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 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 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
721fn 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 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 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 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 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 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 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 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 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}