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