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