Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 107 additions & 10 deletions src/impls/expr_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ pub(crate) fn try_map_expr_children<E>(
InTarget::Subquery(Box::new(f_query(subquery)?))
}
InTarget::Unnest(array_expr) => InTarget::Unnest(Box::new(f(array_expr)?)),
InTarget::Table { name, args } => {
InTarget::Table {
name: name.clone(),
args: args
.as_ref()
.map(|args| {
args.iter()
.map(|arg| try_map_function_arg(arg, f))
.collect::<Result<_, _>>()
})
.transpose()?,
}
}
};
Ok(Expr::In { expr: inner, target, negated: *negated, global: *global })
})?
Expand Down Expand Up @@ -786,7 +799,7 @@ pub(crate) fn for_each_child_expr(expr: &Expr, f: &mut impl FnMut(&Expr)) {
}
}

// list and unnest children, subquery targets stay untouched
// Subquery targets stay untouched.
Expr::In { expr: inner, target, .. } => {
f(inner);
match target {
Expand All @@ -797,6 +810,13 @@ pub(crate) fn for_each_child_expr(expr: &Expr, f: &mut impl FnMut(&Expr)) {
}
InTarget::Subquery(_) => {}
InTarget::Unnest(array_expr) => f(array_expr),
InTarget::Table { name: _, args } => {
if let Some(args) = args {
for arg in args {
for_each_function_arg_expr(arg, f);
}
}
}
}
}
Expr::Tuple(items) => {
Expand Down Expand Up @@ -945,15 +965,7 @@ fn for_each_function_child_expr(function: &sqlparser::ast::Function, f: &mut imp
for arguments in [parameters, args] {
if let sqlparser::ast::FunctionArguments::List(list) = arguments {
for arg in &list.args {
if let sqlparser::ast::FunctionArg::ExprNamed { name, .. } = arg {
f(name);
}
let (sqlparser::ast::FunctionArg::Named { arg, .. }
| sqlparser::ast::FunctionArg::ExprNamed { arg, .. }
| sqlparser::ast::FunctionArg::Unnamed(arg)) = arg;
if let sqlparser::ast::FunctionArgExpr::Expr(expr) = arg {
f(expr);
}
for_each_function_arg_expr(arg, f);
}
for clause in &list.clauses {
for_each_function_argument_clause_expr(clause, f);
Expand Down Expand Up @@ -987,6 +999,19 @@ fn for_each_function_child_expr(function: &sqlparser::ast::Function, f: &mut imp
}
}

/// Visits argument values and expression-valued argument names.
fn for_each_function_arg_expr(arg: &sqlparser::ast::FunctionArg, f: &mut impl FnMut(&Expr)) {
if let sqlparser::ast::FunctionArg::ExprNamed { name, .. } = arg {
f(name);
}
let (sqlparser::ast::FunctionArg::Named { arg, .. }
| sqlparser::ast::FunctionArg::ExprNamed { arg, .. }
| sqlparser::ast::FunctionArg::Unnamed(arg)) = arg;
if let sqlparser::ast::FunctionArgExpr::Expr(expr) = arg {
f(expr);
}
}

fn for_each_function_argument_clause_expr(
clause: &sqlparser::ast::FunctionArgumentClause,
f: &mut impl FnMut(&Expr),
Expand Down Expand Up @@ -1141,6 +1166,60 @@ mod tests {
assert_eq!(expr.to_string(), "(A, B)");
}

#[test]
fn table_membership_walkers_transform_every_argument_expression() {
use sqlparser::ast::{FunctionArg, FunctionArgExpr, FunctionArgOperator, ObjectName};

let expr = Expr::In {
expr: Box::new(ident_expr("lhs")),
target: InTarget::Table {
name: ObjectName::from(Ident::new("lookup")),
args: Some(vec![
FunctionArg::Unnamed(FunctionArgExpr::Expr(ident_expr("positional"))),
FunctionArg::Named {
name: Ident::new("label"),
arg: FunctionArgExpr::Expr(ident_expr("named")),
operator: FunctionArgOperator::RightArrow,
},
FunctionArg::ExprNamed {
name: ident_expr("key"),
arg: FunctionArgExpr::Expr(ident_expr("value")),
operator: FunctionArgOperator::RightArrow,
},
FunctionArg::Unnamed(FunctionArgExpr::Wildcard),
]),
},
negated: true,
global: false,
};
let mut children = Vec::new();
for_each_child_expr(&expr, &mut |child| children.push(child.to_string()));
assert_eq!(children, ["lhs", "positional", "named", "key", "value"]);

let rewritten = try_map_expr_children::<()>(
&expr,
&mut |child| Ok(Expr::Nested(Box::new(child.clone()))),
&mut |_| panic!("table membership has no query child"),
)
.unwrap();
assert_eq!(
rewritten.to_string(),
"(lhs) NOT IN lookup((positional), label => (named), (key) => (value), *)"
);
let failure = try_map_expr_children(
&expr,
&mut |child| {
if *child == ident_expr("value") {
Err("argument refused")
} else {
Ok(child.clone())
}
},
&mut |query| Ok(query.clone()),
);
assert_eq!(failure, Err("argument refused"));
}

/// Parse a single SQL expression using the PostgreSQL dialect.
fn parse_expr(sql: &str) -> Expr {
let full = format!("SELECT {sql} FROM dummy");
Expand Down Expand Up @@ -1530,6 +1609,24 @@ mod tests {
corpus.push(("map_literal".to_string(), map_literal_expr()));
corpus.push(("lambda".to_string(), lambda_expr()));
corpus.push(("member_of".to_string(), member_of_expr()));
for sql in ["SELECT a IN t", "SELECT a NOT IN f(b, c)"] {
let sqlparser::ast::Statement::Query(query) =
sqlparser::parser::Parser::parse_sql(&sqlparser::dialect::SQLiteDialect {}, sql)
.unwrap()
.remove(0)
else {
panic!("expected query");
};
let sqlparser::ast::SetExpr::Select(select) = *query.body else {
panic!("expected select");
};
let sqlparser::ast::SelectItem::UnnamedExpr(expr) =
select.projection.into_iter().next().unwrap()
else {
panic!("expected expression");
};
corpus.push((sql.to_string(), expr));
}

for (label, expr) in corpus {
let mut read_only = Vec::new();
Expand Down
5 changes: 5 additions & 0 deletions src/impls/reverse_translator_impls/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,11 @@ impl ReverseTranslator for Expr {
Expr::In { expr: operand, target: InTarget::List(list), negated, global } => {
reverse_in_list(operand, list, *negated, *global, schema, options)
}
Expr::In { target: InTarget::Table { .. }, .. } => {
Err(Error::reverse_refusal(
"IN table and IN table_function(...) have no PostgreSQL spelling. Use IN (SELECT ...) with explicit output columns.",
))
}

Expr::Between { expr: operand, negated, low, high } => {
reverse_between(operand, *negated, low, high, schema, options)
Expand Down
5 changes: 5 additions & 0 deletions src/impls/translator_impls/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3937,6 +3937,11 @@ impl crate::traits::translator::TranslatorWithContext for Expr {
.to_string(),
));
}
Expr::In { target: InTarget::Table { .. }, .. } => {
return Err(crate::errors::Error::forward_refusal(
"IN table and IN table_function(...) are SQLite-only syntax. Use IN (SELECT ...) with explicit output columns.",
));
}
Expr::In { target: InTarget::Unnest(_), .. } => {
return Err(crate::errors::Error::forward_refusal(
"IN UNNEST (array unnesting) is not supported in SQLite. \
Expand Down
3 changes: 1 addition & 2 deletions src/impls/translator_impls/plpgsql/translator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,7 @@ impl PlPgSqlTranslator {
expr,
Expr::Subquery(_)
| Expr::Exists { .. }
| Expr::In { target: InTarget::Subquery(_), .. }
| Expr::In { target: InTarget::Unnest(_), .. }
| Expr::In { target: InTarget::Subquery(_) | InTarget::Unnest(_), .. }
) {
ControlFlow::Break(())
} else {
Expand Down
15 changes: 15 additions & 0 deletions src/impls/translator_impls/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1971,6 +1971,9 @@ impl crate::traits::translator::TranslatorWithContext for Statement {
drop_with_warning("ALTER FUNCTION", REASON_ACCESS_CONTROL, emit)
}
Self::CreateServer(_) => drop_with_warning("CREATE SERVER", REASON_FOREIGN_DATA, emit),
Self::CreateForeignDataWrapper(_) => {
drop_with_warning("CREATE FOREIGN DATA WRAPPER", REASON_FOREIGN_DATA, emit)
}
Self::CreateForeignTable(_) => {
drop_with_warning("CREATE FOREIGN TABLE", REASON_FOREIGN_DATA, emit)
}
Expand Down Expand Up @@ -2130,6 +2133,18 @@ impl crate::traits::translator::TranslatorWithContext for Statement {
| Self::CreateSchema { .. }
| Self::AlterSchema(_) => Vec::new(),
// Outcome 2, hard error, grouped by reason.
Self::CreateRule(_) => {
return Err(reject_unsupported_statement(
self,
"SQLite has no query rewrite rules. Rewrite the rule as explicit statements or triggers.",
));
}
Self::AlterTrigger(_) => {
return Err(reject_unsupported_statement(
self,
"SQLite cannot alter a trigger. Drop and recreate the trigger with its intended definition.",
));
}
data_movement_patterns!() => {
return Err(reject_unsupported_statement(
self,
Expand Down
1 change: 1 addition & 0 deletions src/initialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ impl<S> InitializationPlan<S> {
}

/// Complete initialization in source order.
#[must_use = "initialization statements are only visited when the iterator is consumed"]
pub fn iter(&self) -> impl ExactSizeIterator<Item = &S> + FusedIterator {
self.selected(FreshFiles::Both)
}
Expand Down
1 change: 1 addition & 0 deletions src/pg2sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,7 @@ impl Pg2Sqlite {
/// [`crate::errors::Error::UnsupportedReverseStatement`] for non-DML
/// statements, or [`crate::errors::Error::RlsTableDetected`] for RLS
/// backing table references.
/// Refuses SQLite's `IN table` and `IN table_function(...)` shorthand.
///
/// # Example
///
Expand Down
2 changes: 1 addition & 1 deletion tests/gauntlet/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fn corpus_verdict_classification() {
(false, true) => translator_refuses += 1,
(true, false) => {
let pg_err = pg_result.unwrap_err();
forbidden.push((group.to_string(), (*row).to_string(), pg_err));
forbidden.push(((*group).to_string(), (*row).to_string(), pg_err));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/test_crash_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fn test_all_crash_cases() {

if let Err(panic) = result {
let panic_msg = if let Some(s) = panic.downcast_ref::<&str>() {
s.to_string()
(*s).to_string()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
Expand Down
93 changes: 93 additions & 0 deletions tests/test_sqlparser_ast_compatibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Translation policies for table membership and PostgreSQL-only statements.

use pg2sqlite::{
errors::{Error, RefusalCategory, TranslationDirection},
prelude::{Pg2Sqlite, Pg2SqliteOptions, Translator},
warnings::TranslationWarning,
};
use sqlparser::{
ast::{Expr, InTarget, SelectItem, SetExpr, Statement},
dialect::SQLiteDialect,
parser::Parser,
};

const SCHEMA: &str = "CREATE TABLE t (id INTEGER PRIMARY KEY);";

fn assert_refusal(error: Error, direction: TranslationDirection, construct: &str) {
let Error::TranslationRefusal(refusal) = error else {
panic!("expected translation refusal, got {error}");
};
assert_eq!(refusal.direction(), direction);
assert_eq!(refusal.category(), RefusalCategory::UnrepresentableSemantics);
assert!(refusal.detail().contains(construct), "{refusal}");
}

#[test]
fn foreign_data_wrapper_is_reported_as_a_lossy_drop() {
let report = Pg2Sqlite::default()
.sql("CREATE FOREIGN DATA WRAPPER remote NO HANDLER NO VALIDATOR;")
.unwrap()
.translate_with_report(&Pg2SqliteOptions::default())
.unwrap();
assert_eq!(report.script.into_vec(), Vec::<Statement>::new());
assert!(matches!(
report.warnings.as_slice(),
[TranslationWarning::LossyDrop { construct, .. }] if construct == "CREATE FOREIGN DATA WRAPPER"
));
}

#[test]
fn rules_and_trigger_alterations_are_refused() {
for (sql, construct) in [
("CREATE RULE r AS ON DELETE TO t DO INSTEAD NOTHING;", "CREATE RULE"),
("ALTER TRIGGER tr ON t RENAME TO renamed;", "ALTER TRIGGER"),
("ALTER TRIGGER tr ON t DEPENDS ON EXTENSION ext;", "ALTER TRIGGER"),
] {
let error = Pg2Sqlite::default()
.sql(SCHEMA)
.unwrap()
.sql(sql)
.unwrap()
.translate(&Pg2SqliteOptions::default())
.unwrap_err();
assert_refusal(error, TranslationDirection::PostgreSqlToSqlite, construct);
}
}

#[test]
fn reverse_table_membership_is_refused() {
let translator = Pg2Sqlite::default().sql(SCHEMA).unwrap();
let schema = translator.build_schema().unwrap();
for sql in [
"SELECT 1 IN t",
"SELECT 1 NOT IN main.t",
"SELECT 1 IN json_each('[1,2]')",
"UPDATE t SET id = 2 WHERE id IN t",
"DELETE FROM t WHERE id NOT IN t",
"SELECT 1 IN (SELECT id FROM t WHERE id IN t)",
] {
let error = translator.reverse_sql(sql, &schema, &Pg2SqliteOptions::default()).unwrap_err();
assert_refusal(error, TranslationDirection::SqliteToPostgreSql, "IN (SELECT");
}
}

#[test]
fn forward_table_membership_ast_is_refused() {
let translator = Pg2Sqlite::default().sql(SCHEMA).unwrap();
let schema = translator.build_schema().unwrap();
for sql in ["SELECT 1 IN t", "SELECT 1 NOT IN json_each('[1,2]')"] {
let Statement::Query(query) = Parser::parse_sql(&SQLiteDialect {}, sql).unwrap().remove(0)
else {
panic!("expected query");
};
let SetExpr::Select(select) = *query.body else {
panic!("expected select");
};
let SelectItem::UnnamedExpr(expr) = &select.projection[0] else {
panic!("expected expression");
};
assert!(matches!(expr, Expr::In { target: InTarget::Table { .. }, .. }));
let error = expr.translate(&schema, &Pg2SqliteOptions::default()).unwrap_err();
assert_refusal(error, TranslationDirection::PostgreSqlToSqlite, "IN (SELECT");
}
}