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 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 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 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 #[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 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 executor.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE)?;
484
485 for entity in &entities {
487 let field_count = entity.properties.len();
488 if !executor.table_exists(&entity.table_name)? {
489 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 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; }
523
524 let id_generator = SqliteIdSpaceGenerator::from_executor(executor.clone());
526 let mut seed_counts: BTreeMap<String, (usize, usize)> = BTreeMap::new(); 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; 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 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 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
744fn 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 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 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 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 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 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 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 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 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}