From cf0a3f6fd8749666dd437cd0dcebd06a81f61274 Mon Sep 17 00:00:00 2001 From: Sebastian Jeltsch Date: Mon, 21 Jul 2025 17:56:38 +0200 Subject: [PATCH] Support trivial non-filtering sub-selects for views and add more tests. #99 --- trailbase-schema/src/metadata.rs | 178 +++++++++++++++++++++++-------- trailbase-schema/src/sqlite.rs | 22 +++- 2 files changed, 150 insertions(+), 50 deletions(-) diff --git a/trailbase-schema/src/metadata.rs b/trailbase-schema/src/metadata.rs index fa557aab..cf0aa9dd 100644 --- a/trailbase-schema/src/metadata.rs +++ b/trailbase-schema/src/metadata.rs @@ -518,50 +518,36 @@ mod tests { #[test] fn test_parse_create_view() { - let table_name = QualifiedName { - name: "table_name".to_string(), - database_schema: Some("main".to_string()), - }; - let table_sql = format!( - r#" - CREATE TABLE {table_name} ( + let table: Table = { + let table_sql = r#" + CREATE TABLE table0 ( id BLOB PRIMARY KEY NOT NULL CHECK(is_uuid_v7(id)) DEFAULT (uuid_v7()), col0 TEXT NOT NULL DEFAULT '', col1 BLOB NOT NULL, hidden INTEGER DEFAULT 42 - ) STRICT;"#, - table_name = table_name.escaped_string(), - ); + ) STRICT; + "#; - let create_table_statement = sqlite3_parse_into_statement(&table_sql).unwrap().unwrap(); - - let table: Table = create_table_statement.try_into().unwrap(); - - { - let metadata = TableMetadata::new(table.clone(), &[table.clone()], "_user"); - - assert_eq!(table_name, *metadata.name()); - assert_eq!("col1", metadata.columns().unwrap()[2].name); - assert_eq!(1, *metadata.name_to_index.get("col0").unwrap()); - } - - let view_name = QualifiedName { - name: "view_name".to_string(), - database_schema: Some("main".to_string()), + let create_table_statement = sqlite3_parse_into_statement(table_sql).unwrap().unwrap(); + create_table_statement.try_into().unwrap() }; + let tables = [table.clone()]; + let metadata = TableMetadata::new(table, &tables, "_user"); + + assert_eq!("table0", metadata.name().name); + assert_eq!("col1", metadata.columns().unwrap()[2].name); + assert_eq!(1, *metadata.name_to_index.get("col0").unwrap()); + { - let query = format!("SELECT col0, col1 FROM {}", table_name.escaped_string()); - let view_sql = format!( - "CREATE VIEW {view_name} AS {query}", - view_name = view_name.escaped_string() - ); - let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap(); + let table_view: View = { + let view_sql = "CREATE VIEW view0 AS SELECT col0, col1 FROM table0"; + let create_view_statement = sqlite3_parse_into_statement(view_sql).unwrap().unwrap(); - let table_view = View::from(create_view_statement, &[table.clone()]).unwrap(); - - assert_eq!(table_view.name, view_name); - assert_eq!(table_view.query, query); + View::from(create_view_statement, &tables).unwrap() + }; + assert_eq!(table_view.name.name, "view0"); + assert_eq!(table_view.query, "SELECT col0, col1 FROM table0"); assert_eq!(table_view.temporary, false); let view_columns = table_view.columns.as_ref().unwrap(); @@ -573,27 +559,26 @@ mod tests { assert_eq!(view_columns[1].name, "col1"); assert_eq!(view_columns[1].data_type, ColumnDataType::Blob); - let view_metadata = ViewMetadata::new(table_view, &[table.clone()]); + let view_metadata = ViewMetadata::new(table_view, &tables); assert!(view_metadata.record_pk_column().is_none()); assert_eq!(view_metadata.columns().as_ref().unwrap().len(), 2); } { - let query = format!("SELECT id, col0, col1 FROM {}", table_name.escaped_string()); - let view_sql = format!( - "CREATE VIEW {view_name} AS {query}", - view_name = view_name.escaped_string() - ); - let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap(); + let query = "SELECT id, col0, col1 FROM table0"; + let table_view: View = { + let view_sql = format!("CREATE VIEW view0 AS {query}"); + let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap(); - let table_view = View::from(create_view_statement, &[table.clone()]).unwrap(); + View::from(create_view_statement, &tables).unwrap() + }; - assert_eq!(table_view.name, view_name); + assert_eq!(table_view.name.name, "view0"); assert_eq!(table_view.query, query); assert_eq!(table_view.temporary, false); - let view_metadata = ViewMetadata::new(table_view, &[table.clone()]); + let view_metadata = ViewMetadata::new(table_view, &tables); let uuidv7_col = view_metadata.record_pk_column().unwrap(); let columns = view_metadata.columns().unwrap(); @@ -602,6 +587,109 @@ mod tests { } } + #[test] + fn test_parse_create_view_with_subquery() { + let table_a: Table = { + let table_sql = + "CREATE TABLE a (id INTEGER PRIMARY KEY, data TEXT NOT NULL DEFAULT '') STRICT"; + let stmt = sqlite3_parse_into_statement(table_sql).unwrap().unwrap(); + stmt.try_into().unwrap() + }; + + let tables = [table_a]; + + { + let view: View = { + let view_sql = "CREATE VIEW view0 AS SELECT * FROM (SELECT * FROM a);"; + let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap(); + View::from(create_view_statement, &tables).unwrap() + }; + let view_columns = view.columns.as_ref().unwrap(); + + assert_eq!(view_columns.len(), 2); + assert_eq!(view_columns[0].name, "id"); + assert_eq!(view_columns[0].data_type, ColumnDataType::Integer); + + assert_eq!(view_columns[1].name, "data"); + assert_eq!(view_columns[1].data_type, ColumnDataType::Text); + + let metadata = ViewMetadata::new(view, &tables); + let (pk_index, pk_col) = metadata.record_pk_column().unwrap(); + assert_eq!(pk_index, 0); + assert_eq!(pk_col.name, "id"); + } + + { + let _view_result: Result = { + let view_sql = "CREATE VIEW view0 AS SELECT id FROM (SELECT * FROM a);"; + let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap(); + + View::from(create_view_statement, &tables) + }; + // TODO: Support column filter on sub-queries. + // let view = _view_result.unwrap(); + + // let view_columns = view.columns.as_ref().unwrap(); + // + // assert_eq!(view_columns.len(), 1); + // assert_eq!(view_columns[0].name, "id"); + // assert_eq!(view_columns[0].data_type, ColumnDataType::Integer); + // + // let metadata = ViewMetadata::new(view, &tables); + // let (pk_index, pk_col) = metadata.record_pk_column().unwrap(); + // assert_eq!(pk_index, 0); + // assert_eq!(pk_col.name, "id"); + } + } + + #[test] + fn test_parse_create_view_with_joins() { + let table_a: Table = { + let table_sql = + "CREATE TABLE a (id INTEGER PRIMARY KEY, data TEXT NOT NULL DEFAULT '') STRICT"; + let stmt = sqlite3_parse_into_statement(table_sql).unwrap().unwrap(); + stmt.try_into().unwrap() + }; + let table_b: Table = { + let table_sql = r#" + CREATE TABLE b ( + id INTEGER PRIMARY KEY, + fk INTEGER NOT NULL REFERENCES a(id) + ) STRICT"#; + let stmt = sqlite3_parse_into_statement(table_sql).unwrap().unwrap(); + stmt.try_into().unwrap() + }; + + let tables = [table_a, table_b]; + + { + // LEFT JOIN + let view: View = { + let view_sql = r#" + CREATE VIEW view0 AS SELECT a.data, b.fk, a.id FROM a AS a LEFT JOIN b AS b ON a.id = b.fk; + "#; + let create_view_statement = sqlite3_parse_into_statement(&view_sql).unwrap().unwrap(); + View::from(create_view_statement, &tables).unwrap() + }; + let view_columns = view.columns.as_ref().unwrap(); + + assert_eq!(view_columns.len(), 3); + assert_eq!(view_columns[2].name, "id"); + assert_eq!(view_columns[2].data_type, ColumnDataType::Integer); + + assert_eq!(view_columns[0].name, "data"); + assert_eq!(view_columns[0].data_type, ColumnDataType::Text); + + assert_eq!(view_columns[1].name, "fk"); + assert_eq!(view_columns[1].data_type, ColumnDataType::Integer); + + let metadata = ViewMetadata::new(view, &tables); + let (pk_index, pk_col) = metadata.record_pk_column().unwrap(); + assert_eq!(pk_index, 2); + assert_eq!(pk_col.name, "id"); + } + } + #[test] fn test_metadata_hash_set_by_name() { let table_name = QualifiedName { diff --git a/trailbase-schema/src/sqlite.rs b/trailbase-schema/src/sqlite.rs index 3ecadc1f..78e71eda 100644 --- a/trailbase-schema/src/sqlite.rs +++ b/trailbase-schema/src/sqlite.rs @@ -4,8 +4,8 @@ use log::*; use serde::{Deserialize, Serialize}; use sqlite3_parser::ast::{ ColumnDefinition, CreateTableBody, DeferSubclause, Expr, ForeignKeyClause, FromClause, - IndexedColumn, Literal, Name, QualifiedName as AstQualifiedName, SelectTable, Stmt, TabFlags, - TableConstraint, fmt::ToTokens, + IndexedColumn, Literal, Name, QualifiedName as AstQualifiedName, ResultColumn, SelectTable, Stmt, + TabFlags, TableConstraint, fmt::ToTokens, }; use std::collections::HashMap; use std::hash::{Hash, Hasher}; @@ -1034,8 +1034,21 @@ fn try_extract_column_mapping( let Some(select) = select else { return Ok(None); }; - let SelectTable::Table(fqn, alias, _indexed) = *select else { - return Ok(None); + let (fqn, alias) = match *select { + SelectTable::Table(fqn, alias, _indexed) => (fqn, alias), + SelectTable::Select(select, _as) => { + if Some(&ResultColumn::Star) == columns.get(0) { + // Recurse + return try_extract_column_mapping(*select, tables); + } + // Support more complex + debug!("The following sub-query is not (yet) supported: {select:?}"); + return Ok(None); + } + _ => { + debug!("The following select is not (yet) supported: {select:?}"); + return Ok(None); + } }; // Use IndexMap to preserve insertion order. @@ -1081,7 +1094,6 @@ fn try_extract_column_mapping( let mut mapping: Vec = vec![]; for col in columns { use sqlite3_parser::ast::Expr; - use sqlite3_parser::ast::ResultColumn; match col { ResultColumn::Star => {