use super::{ERROR, packages}; fn checked( sqlite: &str, postgres: &str, entry: &str, ) -> Result { let model = format!( "type Account {{ name: Text }}\nsql lookup(name: Text) (row: app.Summary?) {{ sqlite = {sqlite:?}\npostgres = {postgres:?} }}" ); packages(&[ ("dever.database", ERROR), ("demo.account.model", &model), ( "demo.account.app", "type Summary { name: Text }\nread(name: Text) (value: Text) { row = model.lookup(name)\nvalue = text(row) }\ntext(row: Summary?) (value: Text) { value = \"present\" }", ), ("demo.account.api", entry), ]) } #[test] fn native_sql_proves_only_owned_static_projections_readonly() { let sqlite = "SELECT name FROM demo_account_model WHERE name = ?1 ORDER BY id DESC"; let postgres = sqlite.replace('?', "$"); let program = checked(sqlite, &postgres, "public get read = app.read").unwrap(); assert!(program.models[0].sql[0].readonly); let complex = "SELECT max(name) AS name FROM demo_account_model WHERE name = ?1"; assert!(checked(complex, &complex.replace('?', "$"), "cmd read = app.read").is_ok()); assert!( checked( complex, &complex.replace('?', "$"), "public get read = app.read" ) .is_err() ); assert!( checked( sqlite, "SELECT name FROM other_table WHERE name = $1", "public get read = app.read" ) .is_err() ); } #[test] fn native_sql_accepts_readonly_alias_count_and_parameterized_paging() { for sql in [ "SELECT name AS label FROM demo_account_model WHERE name = ?1", "SELECT COUNT(*) AS total FROM demo_account_model WHERE name = ?1", "SELECT name AS label FROM demo_account_model ORDER BY id DESC LIMIT ?1 OFFSET ?1", ] { assert!( checked(sql, &sql.replace('?', "$"), "public get read = app.read") .unwrap() .models[0] .sql[0] .readonly ); } for sql in [ "SELECT name AS label, unknown() FROM demo_account_model WHERE name = ?1", "SELECT count(unknown()) FROM demo_account_model WHERE name = ?1", "SELECT name FROM demo_account_model LIMIT ?1 OFFSET unknown()", ] { assert!( checked(sql, &sql.replace('?', "$"), "public get read = app.read").is_err(), "{sql}" ); } } #[test] fn native_sql_rejects_binding_and_statement_escape() { for sql in [ "SELECT name FROM demo_account_model WHERE name = ?2", "SELECT name FROM demo_account_model WHERE name = :name", "SELECT name FROM demo_account_model WHERE name = ?1; DELETE FROM demo_account_model", "SELECT name FROM _dever_role WHERE name = ?1", "SELECT name FROM \"_dever_role\" WHERE name = ?1", "SELECT name FROM demo_account_model WHERE name = '?1'", "SELECT name FROM demo_account_model WHERE name = ?1 /*", "SELECT name FROM demo_account_model, other_table WHERE name = ?1", "SELECT name FROM demo_account_model.other_table WHERE name = ?1", "SELECT name FROM demo_account_model JOIN other_table ON name = ?1", ] { assert!( checked(sql, &sql.replace('?', "$"), "cmd read = app.read").is_err(), "{sql}" ); } } #[test] fn native_sql_accepts_same_domain_app_view_and_rejects_constrained_results() { let model = "type Account { name: Text }\nsql lookup(name: Text) (row: app.Summary?) { sqlite = \"SELECT name FROM demo_account_model WHERE name = ?1\"\npostgres = \"SELECT name FROM demo_account_model WHERE name = $1\" }"; let app = "type Summary { name: Text }\nread(name: Text) (row: Summary?) { row = model.lookup(name) }"; assert!( packages(&[ ("dever.database", ERROR), ("demo.account.model", model), ("demo.account.app", app), ("demo.account.api", "public get read = app.read") ]) .is_ok() ); for record in [ "type Extra { value: Int }", "type Extra { value: Secret }", "type Extra { value: List }", ] { let model = format!("type Account {{ name: Text }}\n{record}"); assert!(packages(&[("demo.account.model", &model)]).is_err()); } } #[test] fn native_sql_records_stay_private_and_all_joined_models_are_dependencies() { let model = "type Account { name: Text }\ntype Summary { name: Text }\nsql joined() (row: Summary) { sqlite = \"SELECT demo_account_model.name FROM demo_account_model, demo_profile_model\"\npostgres = \"SELECT demo_account_model.name FROM demo_account_model JOIN demo_profile_model ON true\" }"; let app = "read() (name: Text) { row = model.joined()\nname = row.name }"; let mut program = packages(&[ ("dever.database", ERROR), ("demo.account.model", model), ("demo.profile.model", "type Profile { name: Text }"), ("demo.account.app", app), ("demo.account.api", "cmd read = app.read"), ]) .unwrap(); assert_eq!(program.models[0].sql[0].dependencies, [0, 1]); program.commands.clear(); assert!( program .functions .iter() .all(|function| !function.signature.transaction) ); let generated = dever::compiler::emit::generate(&program, &[], &Default::default()); assert!(generated.contains("registry.check_transactions(&[&[0,1]])")); let leaked = "read() (row: model.Summary) { row = model.joined() }"; assert!( packages(&[ ("dever.database", ERROR), ("demo.account.model", model), ("demo.profile.model", "type Profile { name: Text }"), ("demo.account.app", leaked), ("demo.account.api", "cmd read = app.read") ]) .unwrap_err() .message .contains("private type") ); } #[test] fn native_sql_reuses_model_columns_but_rejects_unproved_projection_bounds() { let model = "type Account { name: Text(8) }\nsql snapshot() (row: Account) { sqlite = \"SELECT id, created_at, name FROM demo_account_model\"\npostgres = \"SELECT id, created_at, name FROM demo_account_model\" }"; assert!( packages(&[ ("dever.database", ERROR), ("demo.account.model", model), ( "demo.account.app", "read() (name: Text) { row = model.snapshot()\nname = row.name }" ), ("demo.account.api", "cmd read = app.read") ]) .is_ok() ); for field in ["name: Text(8)", "name: Text unique", "name: Text index"] { let model = format!( "type Account {{ name: Text }}\ntype Summary {{ {field} }}\nsql summary() (row: Summary) {{ sqlite = \"SELECT name FROM demo_account_model\"\npostgres = \"SELECT name FROM demo_account_model\" }}" ); assert!( packages(&[("demo.account.model", &model)]).is_err(), "{field}" ); } } #[test] fn quoted_sql_fields_are_identifiers_and_do_not_create_fake_table_dependencies() { let sql = "SELECT \"from\" FROM \"demo_account_model\" WHERE \"from\" = ?1"; let model = format!( "type Account {{ from: Text }}\nsql lookup(value: Text) (row: app.View) {{ sqlite = {sql:?}\npostgres = {:?} }}", sql.replace('?', "$") ); let program = packages(&[ ("dever.database", ERROR), ("demo.account.model", &model), ( "demo.account.app", "type View { from: Text }\nread(value: Text) (row: View) { row = model.lookup(value) }", ), ("demo.account.api", "public get read = app.read"), ]) .unwrap(); assert!(program.models[0].sql[0].readonly); }