1use futures::Stream;
7use futures::StreamExt;
8use rusqlite::{Connection, ToSql, params_from_iter};
9use serde_json::Value;
10use std::pin::Pin;
11
12use super::{BoundParam, SqlQueryError};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ColumnFhirType {
19 Boolean,
20 Integer,
21 Integer64,
22 Decimal,
23 Date,
24 DateTime,
25 Instant,
26 Time,
27 Base64Binary,
28 String(String),
32}
33
34impl ColumnFhirType {
35 pub fn from_code(code: &str) -> Self {
36 match code {
37 "boolean" => ColumnFhirType::Boolean,
38 "integer" | "positiveInt" | "unsignedInt" => ColumnFhirType::Integer,
39 "integer64" => ColumnFhirType::Integer64,
40 "decimal" => ColumnFhirType::Decimal,
41 "date" => ColumnFhirType::Date,
42 "dateTime" => ColumnFhirType::DateTime,
43 "instant" => ColumnFhirType::Instant,
44 "time" => ColumnFhirType::Time,
45 "base64Binary" => ColumnFhirType::Base64Binary,
46 other => ColumnFhirType::String(other.to_string()),
47 }
48 }
49
50 pub fn code(&self) -> &str {
59 match self {
60 ColumnFhirType::Boolean => "boolean",
61 ColumnFhirType::Integer => "integer",
62 ColumnFhirType::Integer64 => "integer64",
63 ColumnFhirType::Decimal => "decimal",
64 ColumnFhirType::Date => "date",
65 ColumnFhirType::DateTime => "dateTime",
66 ColumnFhirType::Instant => "instant",
67 ColumnFhirType::Time => "time",
68 ColumnFhirType::Base64Binary => "base64Binary",
69 ColumnFhirType::String(code) => code,
70 }
71 }
72
73 pub fn sqlite_affinity(&self) -> &'static str {
75 match self {
76 ColumnFhirType::Boolean | ColumnFhirType::Integer | ColumnFhirType::Integer64 => {
77 "INTEGER"
78 }
79 ColumnFhirType::Decimal => "REAL",
80 _ => "TEXT",
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct ColumnSchema {
88 pub name: String,
89 pub fhir_type: ColumnFhirType,
90}
91
92#[derive(Debug, Clone)]
94pub struct TableSchema {
95 pub columns: Vec<ColumnSchema>,
96}
97
98impl TableSchema {
99 pub fn from_view_definition(view: &Value) -> Self {
103 let mut columns = Vec::new();
104 if let Some(selects) = view.get("select").and_then(|v| v.as_array()) {
105 for s in selects {
106 collect_columns(s, &mut columns);
107 }
108 }
109 TableSchema { columns }
110 }
111}
112
113fn collect_columns(select: &Value, out: &mut Vec<ColumnSchema>) {
114 if let Some(cols) = select.get("column").and_then(|v| v.as_array()) {
115 for col in cols {
116 let Some(name) = col.get("name").and_then(|v| v.as_str()) else {
117 continue;
118 };
119 let type_code = col
120 .get("type")
121 .and_then(|v| v.as_str())
122 .unwrap_or("string")
123 .to_string();
124 out.push(ColumnSchema {
125 name: name.to_string(),
126 fhir_type: ColumnFhirType::from_code(&type_code),
127 });
128 }
129 }
130 if let Some(nested) = select.get("select").and_then(|v| v.as_array()) {
131 for s in nested {
132 collect_columns(s, out);
133 }
134 }
135 if let Some(union) = select.get("unionAll").and_then(|v| v.as_array()) {
136 for s in union {
137 collect_columns(s, out);
138 }
139 }
140}
141
142pub struct QueryResult {
144 pub columns: Vec<String>,
145 pub column_types: Vec<ColumnFhirType>,
149 pub rows: Vec<Vec<Option<Value>>>,
151}
152
153pub struct InMemorySqlEngine {
155 conn: Connection,
156}
157
158impl InMemorySqlEngine {
159 pub fn open() -> Result<Self, SqlQueryError> {
160 let conn = Connection::open_in_memory()?;
161 conn.execute_batch(
163 "PRAGMA journal_mode = MEMORY;
164 PRAGMA synchronous = OFF;
165 PRAGMA temp_store = MEMORY;
166 PRAGMA foreign_keys = OFF;",
167 )?;
168 Ok(Self { conn })
169 }
170
171 pub fn interrupt_handle(&self) -> rusqlite::InterruptHandle {
174 self.conn.get_interrupt_handle()
175 }
176
177 pub fn create_table(&self, label: &str, schema: &TableSchema) -> Result<(), SqlQueryError> {
179 validate_identifier(label)?;
180 let mut columns_ddl = Vec::with_capacity(schema.columns.len());
181 for col in &schema.columns {
182 validate_identifier(&col.name)?;
183 columns_ddl.push(format!(
184 "\"{}\" {}",
185 col.name,
186 col.fhir_type.sqlite_affinity()
187 ));
188 }
189 let sql = if columns_ddl.is_empty() {
190 format!("CREATE TABLE \"{label}\" (\"_empty\" TEXT)")
192 } else {
193 format!("CREATE TABLE \"{}\" ({})", label, columns_ddl.join(", "))
194 };
195 self.conn.execute(&sql, [])?;
196 Ok(())
197 }
198
199 pub fn create_view(&self, label: &str, source: &str) -> Result<(), SqlQueryError> {
206 validate_identifier(label)?;
207 validate_identifier(source)?;
208 self.conn.execute(
209 &format!("CREATE VIEW \"{label}\" AS SELECT * FROM \"{source}\""),
210 [],
211 )?;
212 if let Err(e) = self.conn.prepare(&format!("SELECT * FROM \"{label}\"")) {
220 let _ = self.conn.execute(&format!("DROP VIEW \"{label}\""), []);
221 return Err(e.into());
222 }
223 Ok(())
224 }
225
226 pub async fn insert_rows<S>(
248 self,
249 label: &str,
250 schema: &TableSchema,
251 rows: Pin<Box<S>>,
252 max_rows: usize,
253 ) -> Result<(Self, usize), SqlQueryError>
254 where
255 S: Stream<Item = Result<Value, String>> + Send + 'static + ?Sized,
256 {
257 let label = label.to_string();
258 let schema = schema.clone();
259 tokio::task::spawn_blocking(move || {
260 let handle = tokio::runtime::Handle::current();
261 let mut engine = self;
262 let inserted = engine.insert_rows_blocking(&label, &schema, rows, max_rows, &handle)?;
263 Ok((engine, inserted))
264 })
265 .await
266 .map_err(|e| SqlQueryError::Internal(format!("sqlquery worker panicked: {e}")))?
267 }
268
269 fn insert_rows_blocking<S>(
274 &mut self,
275 label: &str,
276 schema: &TableSchema,
277 mut rows: Pin<Box<S>>,
278 max_rows: usize,
279 handle: &tokio::runtime::Handle,
280 ) -> Result<usize, SqlQueryError>
281 where
282 S: Stream<Item = Result<Value, String>> + Send + ?Sized,
283 {
284 validate_identifier(label)?;
285 for col in &schema.columns {
286 validate_identifier(&col.name)?;
287 }
288 if schema.columns.is_empty() {
289 let mut n = 0usize;
291 while let Some(item) = handle.block_on(rows.next()) {
292 item.map_err(SqlQueryError::SourceStream)?;
293 n += 1;
294 if n > max_rows {
295 return Err(SqlQueryError::RowCapExceeded { max: max_rows });
296 }
297 }
298 return Ok(n);
299 }
300
301 let placeholders = std::iter::repeat_n("?", schema.columns.len())
302 .collect::<Vec<_>>()
303 .join(", ");
304 let cols_quoted = schema
305 .columns
306 .iter()
307 .map(|c| format!("\"{}\"", c.name))
308 .collect::<Vec<_>>()
309 .join(", ");
310 let insert_sql = format!("INSERT INTO \"{label}\" ({cols_quoted}) VALUES ({placeholders})");
311
312 self.conn.execute("BEGIN", [])?;
313 let mut inserted = 0usize;
314 let result: Result<usize, SqlQueryError> = (|| {
315 let mut stmt = self.conn.prepare(&insert_sql)?;
316 while let Some(item) = handle.block_on(rows.next()) {
317 let row = item.map_err(SqlQueryError::SourceStream)?;
318 inserted += 1;
319 if inserted > max_rows {
320 return Err(SqlQueryError::RowCapExceeded { max: max_rows });
321 }
322 let params: Vec<rusqlite::types::Value> = schema
323 .columns
324 .iter()
325 .map(|c| json_to_sqlite_value(&row, c))
326 .collect();
327 let param_refs: Vec<&dyn ToSql> = params.iter().map(|v| v as &dyn ToSql).collect();
328 stmt.execute(params_from_iter(param_refs))?;
329 }
330 Ok(inserted)
331 })();
332 match result {
333 Ok(n) => {
334 self.conn.execute("COMMIT", [])?;
335 Ok(n)
336 }
337 Err(e) => {
338 let _ = self.conn.execute("ROLLBACK", []);
339 Err(e)
340 }
341 }
342 }
343
344 pub fn execute_select(
346 &self,
347 sql: &str,
348 bindings: &[BoundParam],
349 max_rows: usize,
350 ) -> Result<QueryResult, SqlQueryError> {
351 let mut stmt = self.conn.prepare(sql)?;
352
353 for b in bindings {
357 let with_colon = format!(":{}", b.name);
358 if let Some(idx) = stmt.parameter_index(&with_colon)? {
359 stmt.raw_bind_parameter(idx, &b.value)?;
360 }
361 }
362
363 let columns: Vec<String> = stmt.column_names().into_iter().map(String::from).collect();
364 let mut column_types: Vec<ColumnFhirType> = columns
366 .iter()
367 .map(|_| ColumnFhirType::String("string".to_string()))
368 .collect();
369 let mut rows_out: Vec<Vec<Option<Value>>> = Vec::new();
370
371 let mut rows_iter = stmt.raw_query();
372 while let Some(row) = rows_iter.next()? {
373 if rows_out.len() >= max_rows {
374 break;
384 }
385 let mut row_vals: Vec<Option<Value>> = Vec::with_capacity(columns.len());
386 for (i, _) in columns.iter().enumerate() {
387 let v: rusqlite::types::Value = row.get(i)?;
388 let (json_val, inferred) = sqlite_value_to_json(v);
389 if matches!(column_types[i], ColumnFhirType::String(_)) {
390 if let Some(ft) = inferred {
391 column_types[i] = ft;
392 }
393 }
394 row_vals.push(json_val);
395 }
396 rows_out.push(row_vals);
397 }
398
399 Ok(QueryResult {
400 columns,
401 column_types,
402 rows: rows_out,
403 })
404 }
405}
406
407fn validate_identifier(name: &str) -> Result<(), SqlQueryError> {
408 if name.contains('"') || name.is_empty() {
409 return Err(SqlQueryError::InvalidIdentifier(name.to_string()));
410 }
411 Ok(())
412}
413
414fn json_to_sqlite_value(row: &Value, col: &ColumnSchema) -> rusqlite::types::Value {
415 use rusqlite::types::Value as RV;
416 let raw = row.get(&col.name).unwrap_or(&Value::Null);
417 match raw {
418 Value::Null => RV::Null,
419 Value::Bool(b) => RV::Integer(if *b { 1 } else { 0 }),
420 Value::Number(n) => {
421 if let Some(i) = n.as_i64() {
422 RV::Integer(i)
423 } else if let Some(f) = n.as_f64() {
424 RV::Real(f)
425 } else {
426 RV::Text(n.to_string())
427 }
428 }
429 Value::String(s) => match col.fhir_type {
430 ColumnFhirType::Integer | ColumnFhirType::Integer64 => s
431 .parse::<i64>()
432 .map(RV::Integer)
433 .unwrap_or(RV::Text(s.clone())),
434 ColumnFhirType::Decimal => s
435 .parse::<f64>()
436 .map(RV::Real)
437 .unwrap_or(RV::Text(s.clone())),
438 ColumnFhirType::Boolean => match s.as_str() {
439 "true" | "1" => RV::Integer(1),
440 "false" | "0" => RV::Integer(0),
441 _ => RV::Text(s.clone()),
442 },
443 _ => RV::Text(s.clone()),
444 },
445 Value::Array(_) | Value::Object(_) => RV::Text(raw.to_string()),
446 }
447}
448
449fn sqlite_value_to_json(v: rusqlite::types::Value) -> (Option<Value>, Option<ColumnFhirType>) {
452 use rusqlite::types::Value as RV;
453 match v {
454 RV::Null => (None, None),
455 RV::Integer(i) => (Some(Value::Number(i.into())), Some(ColumnFhirType::Integer)),
456 RV::Real(f) => (
457 serde_json::Number::from_f64(f).map(Value::Number),
458 Some(ColumnFhirType::Decimal),
459 ),
460 RV::Text(s) => (Some(Value::String(s)), None),
461 RV::Blob(b) => (
462 Some(Value::String(
463 base64::engine::general_purpose::STANDARD.encode(b),
464 )),
465 Some(ColumnFhirType::Base64Binary),
466 ),
467 }
468}
469
470use base64::Engine as _;
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use futures::stream;
476 use serde_json::json;
477
478 fn schema(cols: &[(&str, ColumnFhirType)]) -> TableSchema {
479 TableSchema {
480 columns: cols
481 .iter()
482 .map(|(n, t)| ColumnSchema {
483 name: (*n).to_string(),
484 fhir_type: t.clone(),
485 })
486 .collect(),
487 }
488 }
489
490 #[tokio::test]
491 async fn round_trip_basic() {
492 let engine = InMemorySqlEngine::open().unwrap();
493 let s = schema(&[
494 ("id", ColumnFhirType::String("id".into())),
495 ("n", ColumnFhirType::Integer),
496 ]);
497 engine.create_table("patients", &s).unwrap();
498 let rows = stream::iter(vec![
499 Ok(json!({"id": "a", "n": 1})),
500 Ok(json!({"id": "b", "n": 2})),
501 ]);
502 let (engine, inserted) = engine
503 .insert_rows("patients", &s, Box::pin(rows), 10)
504 .await
505 .unwrap();
506 assert_eq!(inserted, 2);
507 let result = engine
508 .execute_select("SELECT id, n FROM patients ORDER BY n", &[], 10)
509 .unwrap();
510 assert_eq!(result.columns, vec!["id", "n"]);
511 assert_eq!(result.rows.len(), 2);
512 assert_eq!(result.rows[0][0], Some(Value::String("a".into())));
513 assert_eq!(result.rows[0][1], Some(Value::Number(1.into())));
514 }
515
516 #[tokio::test]
517 async fn null_handling() {
518 let engine = InMemorySqlEngine::open().unwrap();
519 let s = schema(&[
520 ("id", ColumnFhirType::String("id".into())),
521 ("age", ColumnFhirType::Integer),
522 ]);
523 engine.create_table("t", &s).unwrap();
524 let rows = stream::iter(vec![Ok(json!({"id": "a"}))]); let (engine, _inserted) = engine
526 .insert_rows("t", &s, Box::pin(rows), 10)
527 .await
528 .unwrap();
529 let result = engine
530 .execute_select("SELECT id, age FROM t", &[], 10)
531 .unwrap();
532 assert_eq!(result.rows[0][1], None);
533 }
534
535 #[tokio::test]
536 async fn row_cap_exceeded() {
537 let engine = InMemorySqlEngine::open().unwrap();
538 let s = schema(&[("n", ColumnFhirType::Integer)]);
539 engine.create_table("t", &s).unwrap();
540 let rows = stream::iter((0..10).map(|i| Ok(json!({"n": i}))));
541 let err = match engine.insert_rows("t", &s, Box::pin(rows), 3).await {
545 Ok(_) => panic!("expected RowCapExceeded"),
546 Err(e) => e,
547 };
548 assert!(matches!(err, SqlQueryError::RowCapExceeded { max: 3 }));
549 }
550
551 #[tokio::test]
552 async fn insert_rows_reports_stream_error_as_source_stream() {
553 let engine = InMemorySqlEngine::open().unwrap();
558 let s = schema(&[("id", ColumnFhirType::String("id".into()))]);
559 engine.create_table("t", &s).unwrap();
560 let ok_rows = (0..200).map(|i| Ok(json!({"id": format!("p{i}")})));
561 let rows = stream::iter(
562 ok_rows.chain(std::iter::once(Err("connection reset by peer".to_string()))),
563 );
564 let err = match engine.insert_rows("t", &s, Box::pin(rows), 10_000).await {
565 Ok(_) => panic!("expected SourceStream"),
566 Err(e) => e,
567 };
568 let SqlQueryError::SourceStream(msg) = &err else {
569 panic!("expected SourceStream, got {err:?}");
570 };
571 assert!(
572 msg.contains("connection reset by peer"),
573 "unexpected message: {msg}"
574 );
575 assert!(
576 format!("{err}").starts_with("dependency source failed: "),
577 "unexpected display: {err}"
578 );
579 }
580
581 #[tokio::test]
582 async fn execute_select_silently_truncates_at_max_rows() {
583 let engine = InMemorySqlEngine::open().unwrap();
586 let s = schema(&[("n", ColumnFhirType::Integer)]);
587 engine.create_table("t", &s).unwrap();
588 let rows = stream::iter((1..=10).map(|i| Ok(json!({"n": i}))));
589 let (engine, _inserted) = engine
590 .insert_rows("t", &s, Box::pin(rows), 100)
591 .await
592 .unwrap();
593 let result = engine
594 .execute_select("SELECT n FROM t ORDER BY n", &[], 4)
595 .unwrap();
596 assert_eq!(result.rows.len(), 4);
597 assert_eq!(result.rows[0][0], Some(Value::Number(1.into())));
598 assert_eq!(result.rows[3][0], Some(Value::Number(4.into())));
599 }
600
601 #[test]
602 fn rejects_quote_in_identifier() {
603 let engine = InMemorySqlEngine::open().unwrap();
604 let s = schema(&[("a", ColumnFhirType::Integer)]);
605 let err = engine.create_table("bad\"name", &s).unwrap_err();
606 assert!(matches!(err, SqlQueryError::InvalidIdentifier(_)));
607 }
608
609 #[tokio::test]
610 async fn named_bindings_filter() {
611 let engine = InMemorySqlEngine::open().unwrap();
612 let s = schema(&[("n", ColumnFhirType::Integer)]);
613 engine.create_table("t", &s).unwrap();
614 let rows = stream::iter((1..=5).map(|i| Ok(json!({"n": i}))));
615 let (engine, _inserted) = engine
616 .insert_rows("t", &s, Box::pin(rows), 100)
617 .await
618 .unwrap();
619 let bindings = vec![BoundParam {
620 name: "min".to_string(),
621 value: rusqlite::types::Value::Integer(3),
622 }];
623 let result = engine
624 .execute_select("SELECT n FROM t WHERE n >= :min ORDER BY n", &bindings, 100)
625 .unwrap();
626 assert_eq!(result.rows.len(), 3);
627 }
628
629 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
645 async fn insert_rows_drains_tokio_mpsc_stream_past_coop_budget() {
646 const ROW_COUNT: i64 = 1000;
647 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Value, String>>(256);
648
649 tokio::task::spawn_blocking(move || {
650 for i in 0..ROW_COUNT {
651 let row = json!({"id": format!("p{i}"), "n": i});
652 if tx.blocking_send(Ok(row)).is_err() {
653 break;
654 }
655 }
656 });
657
658 let consumer = tokio::spawn(async move {
659 let engine = InMemorySqlEngine::open().unwrap();
660 let s = schema(&[
661 ("id", ColumnFhirType::String("id".into())),
662 ("n", ColumnFhirType::Integer),
663 ]);
664 engine.create_table("t", &s).unwrap();
665 let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
666 let (engine, inserted) = engine
667 .insert_rows("t", &s, Box::pin(stream), 10_000)
668 .await
669 .unwrap();
670 let count = engine
671 .execute_select("SELECT COUNT(*) AS c FROM t", &[], 10)
672 .unwrap();
673 (inserted, count)
674 });
675
676 let (inserted, count) = tokio::time::timeout(std::time::Duration::from_secs(10), consumer)
677 .await
678 .expect("insert_rows must not hang past the cooperative budget")
679 .expect("consumer task must not panic");
680
681 assert_eq!(inserted, ROW_COUNT as usize);
682 assert_eq!(count.rows[0][0], Some(Value::Number(ROW_COUNT.into())));
683 }
684
685 #[test]
686 fn code_returns_the_representative_fhir_type_code() {
687 assert_eq!(ColumnFhirType::Boolean.code(), "boolean");
688 assert_eq!(ColumnFhirType::Integer.code(), "integer");
689 assert_eq!(ColumnFhirType::Decimal.code(), "decimal");
690 assert_eq!(ColumnFhirType::from_code("positiveInt").code(), "integer");
693 assert_eq!(ColumnFhirType::String("id".into()).code(), "id");
696 }
697
698 #[test]
699 fn schema_from_vd_select_columns() {
700 let vd = json!({
701 "select": [{
702 "column": [
703 {"name": "id", "type": "id"},
704 {"name": "n", "type": "integer"}
705 ]
706 }]
707 });
708 let s = TableSchema::from_view_definition(&vd);
709 assert_eq!(s.columns.len(), 2);
710 assert_eq!(s.columns[0].name, "id");
711 assert!(matches!(s.columns[1].fhir_type, ColumnFhirType::Integer));
712 }
713
714 #[test]
715 fn schema_walks_nested_selects_and_union() {
716 let vd = json!({
717 "select": [{
718 "column": [{"name": "a"}],
719 "select": [{"column": [{"name": "b"}]}],
720 "unionAll": [{"column": [{"name": "c"}]}]
721 }]
722 });
723 let s = TableSchema::from_view_definition(&vd);
724 assert_eq!(
725 s.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>(),
726 vec!["a", "b", "c"]
727 );
728 }
729
730 #[tokio::test]
731 async fn create_view_exposes_source_rows_and_columns() {
732 let engine = InMemorySqlEngine::open().unwrap();
733 let s = schema(&[
734 ("id", ColumnFhirType::String("id".into())),
735 ("n", ColumnFhirType::Integer),
736 ]);
737 engine.create_table("t", &s).unwrap();
738 let rows = stream::iter(vec![
739 Ok(json!({"id": "a", "n": 1})),
740 Ok(json!({"id": "b", "n": 2})),
741 Ok(json!({"id": "c", "n": 3})),
742 ]);
743 let (engine, inserted) = engine
744 .insert_rows("t", &s, Box::pin(rows), 10)
745 .await
746 .unwrap();
747 assert_eq!(inserted, 3);
748 engine.create_view("v", "t").unwrap();
749
750 let from_view = engine
751 .execute_select("SELECT id, n FROM v ORDER BY n", &[], 100)
752 .unwrap();
753 let from_table = engine
754 .execute_select("SELECT id, n FROM t ORDER BY n", &[], 100)
755 .unwrap();
756 assert_eq!(from_view.rows.len(), 3);
757 assert_eq!(from_view.rows, from_table.rows);
758 assert_eq!(from_view.rows[0][0], Some(Value::String("a".into())));
759 assert_eq!(from_view.rows[0][1], Some(Value::Number(1.into())));
760 assert_eq!(from_view.rows[2][0], Some(Value::String("c".into())));
761 assert_eq!(from_view.rows[2][1], Some(Value::Number(3.into())));
762 }
763
764 #[tokio::test]
765 async fn create_view_reports_the_same_inferred_column_types_as_the_table() {
766 let engine = InMemorySqlEngine::open().unwrap();
767 let s = schema(&[
768 ("id", ColumnFhirType::String("id".into())),
769 ("n", ColumnFhirType::Integer),
770 ]);
771 engine.create_table("t", &s).unwrap();
772 let rows = stream::iter(vec![
773 Ok(json!({"id": "a", "n": 1})),
774 Ok(json!({"id": "b", "n": 2})),
775 Ok(json!({"id": "c", "n": 3})),
776 ]);
777 let (engine, _inserted) = engine
778 .insert_rows("t", &s, Box::pin(rows), 10)
779 .await
780 .unwrap();
781 engine.create_view("v", "t").unwrap();
782
783 let from_view = engine.execute_select("SELECT * FROM v", &[], 100).unwrap();
784 let from_table = engine.execute_select("SELECT * FROM t", &[], 100).unwrap();
785 assert_eq!(from_view.columns, from_table.columns);
786 assert_eq!(from_view.column_types.len(), from_table.column_types.len());
787 for (view_ty, table_ty) in from_view.column_types.iter().zip(&from_table.column_types) {
788 assert_eq!(view_ty.code(), table_ty.code());
789 }
790 }
791
792 #[test]
793 fn create_view_over_empty_schema_table_works() {
794 let engine = InMemorySqlEngine::open().unwrap();
795 let s = TableSchema { columns: vec![] };
796 engine.create_table("e", &s).unwrap();
797 engine.create_view("ve", "e").unwrap();
798 let result = engine
799 .execute_select("SELECT COUNT(*) AS c FROM ve", &[], 100)
800 .unwrap();
801 assert_eq!(result.rows[0][0], Some(Value::Number(0.into())));
802 }
803
804 #[test]
805 fn create_view_rejects_invalid_identifiers() {
806 let engine = InMemorySqlEngine::open().unwrap();
807 let s = schema(&[("a", ColumnFhirType::Integer)]);
808 engine.create_table("t", &s).unwrap();
809
810 let err = engine.create_view("bad\"name", "t").unwrap_err();
811 assert!(matches!(err, SqlQueryError::InvalidIdentifier(_)));
812
813 let err = engine.create_view("v", "").unwrap_err();
814 assert!(matches!(err, SqlQueryError::InvalidIdentifier(_)));
815 }
816
817 #[test]
818 fn create_view_on_missing_source_is_an_error() {
819 let engine = InMemorySqlEngine::open().unwrap();
820 let err = engine.create_view("v", "does_not_exist").unwrap_err();
821 assert!(!matches!(err, SqlQueryError::InvalidIdentifier(_)));
822 }
823}