use crate::ast::*;
use crate::migrate::policy::RlsPolicy;
use crate::parser::parse;
use crate::transpiler::{Dialect, ToSql};
#[test]
fn test_index_sql_basic() {
let cmd = parse("index idx_email on users email").unwrap();
let sql = cmd.to_sql();
assert!(sql.contains("CREATE INDEX idx_email ON users"));
assert!(sql.contains("email"));
}
#[test]
fn test_index_sql_unique() {
let cmd = parse("index idx_unique_email on users email unique").unwrap();
let sql = cmd.to_sql();
assert!(sql.contains("CREATE UNIQUE INDEX"));
}
#[test]
fn test_composite_pk_sql() {
let cmd = parse("make order_items order_id:uuid, item_id:uuid primary key(order_id, item_id)")
.unwrap();
let sql = cmd.to_sql();
assert!(sql.contains("PRIMARY KEY (order_id, item_id)"));
}
#[test]
fn test_drop_column() {
let mut cmd = Qail::get("users");
cmd.action = Action::DropCol;
cmd.columns.push(Expr::Named("password".to_string()));
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("ALTER TABLE users DROP COLUMN password"));
}
#[test]
fn test_rename_column() {
let mut cmd = Qail::get("users");
cmd.action = Action::RenameCol;
cmd.columns.push(Expr::Named("old_name".to_string()));
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("to".to_string()),
op: Operator::Eq,
value: Value::String("new_name".to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("ALTER TABLE users RENAME COLUMN old_name TO new_name"));
}
#[test]
fn test_grant_sql() {
let cmd = Qail {
action: Action::Grant,
table: "users".to_string(),
columns: vec![
Expr::Named("SELECT".to_string()),
Expr::Named("INSERT".to_string()),
],
payload: Some("app_role".to_string()),
..Default::default()
};
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(sql, "GRANT SELECT, INSERT ON users TO app_role");
}
#[test]
fn test_create_database_quotes_hyphenated_name() {
let cmd = Qail::create_database("qail-engine-db_shadow");
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(sql, "CREATE DATABASE \"qail-engine-db_shadow\"");
}
#[test]
fn test_drop_database_quotes_hyphenated_name() {
let cmd = Qail::drop_database("qail-engine-db_shadow");
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(sql, "DROP DATABASE IF EXISTS \"qail-engine-db_shadow\"");
}
#[test]
fn test_revoke_sql() {
let cmd = Qail {
action: Action::Revoke,
table: "users".to_string(),
columns: vec![Expr::Named("UPDATE".to_string())],
payload: Some("app_role".to_string()),
..Default::default()
};
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(sql, "REVOKE UPDATE ON users FROM app_role");
}
#[test]
fn test_create_function_with_args_sql() {
let cmd = Qail {
action: Action::CreateFunction,
function_def: Some(FunctionDef {
name: "sum_one".to_string(),
args: vec!["v int".to_string()],
returns: "int".to_string(),
body: "BEGIN RETURN v + 1; END;".to_string(),
language: Some("plpgsql".to_string()),
volatility: None,
}),
..Default::default()
};
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(
sql,
"CREATE OR REPLACE FUNCTION sum_one(v int) RETURNS int LANGUAGE plpgsql AS $$ BEGIN RETURN v + 1; END; $$"
);
}
#[test]
fn test_create_policy_sql() {
let policy = RlsPolicy::create("users_isolation", "users")
.for_all()
.restrictive()
.to_role("app_role")
.using(Expr::Named(
"tenant_id = current_setting('app.current_tenant_id')::uuid".to_string(),
))
.with_check(Expr::Named(
"tenant_id = current_setting('app.current_tenant_id')::uuid".to_string(),
));
let cmd = Qail {
action: Action::CreatePolicy,
policy_def: Some(policy),
..Default::default()
};
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("CREATE POLICY users_isolation ON users"));
assert!(sql.contains("AS RESTRICTIVE"));
assert!(sql.contains("FOR ALL"));
assert!(sql.contains("TO app_role"));
assert!(sql.contains("USING (tenant_id = current_setting('app.current_tenant_id')::uuid)"));
assert!(
sql.contains("WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid)")
);
}
#[test]
fn test_drop_policy_sql() {
let cmd = Qail {
action: Action::DropPolicy,
table: "users".to_string(),
payload: Some("users_isolation".to_string()),
..Default::default()
};
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(sql, "DROP POLICY IF EXISTS users_isolation ON users");
}
#[test]
fn test_drop_index_sql_uses_if_exists() {
let cmd = Qail {
action: Action::DropIndex,
table: "idx_users_email".to_string(),
..Default::default()
};
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert_eq!(sql, "DROP INDEX IF EXISTS idx_users_email");
}
#[test]
fn test_upsert_postgres() {
let mut cmd = Qail::put("users");
cmd.columns.push(Expr::Named("id".to_string())); cmd.cages.push(Cage {
kind: CageKind::Payload,
conditions: vec![
Condition {
left: Expr::Named("id".to_string()),
op: Operator::Eq,
value: Value::Int(1),
is_array_unnest: false,
},
Condition {
left: Expr::Named("name".to_string()),
op: Operator::Eq,
value: Value::String("John".to_string()),
is_array_unnest: false,
},
Condition {
left: Expr::Named("role".to_string()),
op: Operator::Eq,
value: Value::String("admin".to_string()),
is_array_unnest: false,
},
],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("INSERT INTO users"));
assert!(sql.contains("ON CONFLICT (id) DO UPDATE SET"));
assert!(sql.contains("name = EXCLUDED.name"));
assert!(sql.contains("RETURNING *"));
}
#[test]
fn test_json_access() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("meta.theme".to_string()),
op: Operator::Eq,
value: Value::String("dark".to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains(r#"meta->>'theme' = 'dark'"#));
}
#[test]
fn test_json_contains() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("metadata".to_string()),
op: Operator::Contains,
value: Value::String(r#"{"theme": "dark"}"#.to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains(r#"@> '{"theme": "dark"}'"#));
}
#[test]
fn test_json_key_exists() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("metadata".to_string()),
op: Operator::KeyExists,
value: Value::String("theme".to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("metadata ? 'theme'"));
}
#[test]
fn test_json_table() {
let mut cmd = Qail::get("orders.items");
cmd.action = Action::JsonTable;
cmd.columns = vec![
Expr::Named("name=$.product".to_string()),
Expr::Named("qty=$.quantity".to_string()),
];
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("JSON_TABLE("));
assert!(sql.contains("COLUMNS"));
}
#[test]
fn test_tablesample() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Sample(10),
conditions: vec![],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("TABLESAMPLE BERNOULLI(10)"));
}
#[test]
fn test_qualify() {
let mut cmd = Qail::get("users");
cmd.columns.push(Expr::Named("id".to_string()));
cmd.cages.push(Cage {
kind: CageKind::Qualify,
conditions: vec![Condition {
left: Expr::Named("rn".to_string()),
op: Operator::Eq,
value: Value::Int(1),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
}
#[test]
fn test_lateral_join() {
let mut cmd = Qail::get("users");
cmd.columns.push(Expr::Named("*".to_string()));
cmd.joins.push(Join {
table: "orders".to_string(),
kind: JoinKind::Lateral,
on: None,
on_true: false,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
assert!(sql.contains("LATERAL JOIN"));
}
#[test]
fn test_json_exists() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("metadata".to_string()),
op: Operator::JsonExists,
value: Value::String("$.theme".to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
println!("JSON_EXISTS: {}", sql);
assert!(sql.contains("JSON_EXISTS("));
assert!(sql.contains("$.theme"));
}
#[test]
fn test_json_query() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("settings".to_string()),
op: Operator::JsonQuery,
value: Value::String("$.notifications".to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
println!("JSON_QUERY: {}", sql);
assert!(sql.contains("JSON_QUERY("));
}
#[test]
fn test_json_value() {
let mut cmd = Qail::get("users");
cmd.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("profile".to_string()),
op: Operator::JsonValue,
value: Value::String("$.name".to_string()),
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
println!("JSON_VALUE: {}", sql);
assert!(sql.contains("JSON_VALUE("));
}
#[test]
fn test_union() {
let mut users_cmd = Qail::get("users");
users_cmd.columns.push(Expr::Named("name".to_string()));
let mut admins_cmd = Qail::get("admins");
admins_cmd.columns.push(Expr::Named("name".to_string()));
users_cmd.set_ops.push((SetOp::Union, Box::new(admins_cmd)));
let sql = users_cmd.to_sql_with_dialect(Dialect::Postgres);
println!("UNION: {}", sql);
assert!(sql.contains("UNION"));
assert!(sql.contains("users"));
assert!(sql.contains("admins"));
}
#[test]
fn test_union_all() {
let mut q1 = Qail::get("active_users");
let q2 = Qail::get("inactive_users");
q1.set_ops.push((SetOp::UnionAll, Box::new(q2)));
let sql = q1.to_sql();
println!("UNION ALL: {}", sql);
assert!(sql.contains("UNION ALL"));
}
#[test]
fn test_intersect() {
let mut q1 = Qail::get("premium_users");
q1.columns.push(Expr::Named("id".to_string()));
let mut q2 = Qail::get("verified_users");
q2.columns.push(Expr::Named("id".to_string()));
q1.set_ops.push((SetOp::Intersect, Box::new(q2)));
let sql = q1.to_sql();
println!("INTERSECT: {}", sql);
assert!(sql.contains("INTERSECT"));
}
#[test]
fn test_case_expression() {
let mut cmd = Qail::get("users");
cmd.columns.push(Expr::Named("name".to_string()));
cmd.columns.push(Expr::Case {
when_clauses: vec![
(
Condition {
left: Expr::Named("status".to_string()),
op: Operator::Eq,
value: Value::String("active".to_string()),
is_array_unnest: false,
},
Box::new(Expr::Named("1".to_string())),
),
(
Condition {
left: Expr::Named("status".to_string()),
op: Operator::Eq,
value: Value::String("pending".to_string()),
is_array_unnest: false,
},
Box::new(Expr::Named("2".to_string())),
),
],
else_value: Some(Box::new(Expr::Named("0".to_string()))),
alias: Some("priority".to_string()),
});
let sql = cmd.to_sql_with_dialect(Dialect::Postgres);
println!("CASE: {}", sql);
assert!(sql.contains("CASE"));
assert!(sql.contains("WHEN"));
assert!(sql.contains("THEN"));
assert!(sql.contains("ELSE"));
assert!(sql.contains("END"));
assert!(sql.contains("AS"));
}
#[test]
fn test_having_clause() {
let mut cmd = Qail::get("orders");
cmd.columns.push(Expr::Named("customer_id".to_string()));
cmd.columns.push(Expr::Aggregate {
col: "total".to_string(),
func: AggregateFunc::Sum,
distinct: false,
filter: None,
alias: None,
});
cmd.having.push(Condition {
left: Expr::Named("SUM(total)".to_string()),
op: Operator::Gt,
value: Value::Int(100),
is_array_unnest: false,
});
let sql = cmd.to_sql();
println!("HAVING: {}", sql);
assert!(sql.contains("HAVING"));
assert!(sql.contains("SUM(total)"));
}
#[test]
fn test_group_by_rollup() {
let mut cmd = Qail::get("sales");
cmd.columns.push(Expr::Named("region".to_string()));
cmd.columns.push(Expr::Named("year".to_string()));
cmd.columns.push(Expr::Aggregate {
col: "amount".to_string(),
func: AggregateFunc::Sum,
distinct: false,
filter: None,
alias: None,
});
cmd.group_by_mode = GroupByMode::Rollup;
let sql = cmd.to_sql();
println!("ROLLUP: {}", sql);
assert!(sql.contains("GROUP BY ROLLUP("));
}
#[test]
fn test_group_by_cube() {
let mut cmd = Qail::get("sales");
cmd.columns.push(Expr::Named("region".to_string()));
cmd.columns.push(Expr::Named("product".to_string()));
cmd.columns.push(Expr::Aggregate {
col: "amount".to_string(),
func: AggregateFunc::Sum,
distinct: false,
filter: None,
alias: None,
});
cmd.group_by_mode = GroupByMode::Cube;
let sql = cmd.to_sql();
println!("CUBE: {}", sql);
assert!(sql.contains("GROUP BY CUBE("));
}
#[test]
fn test_aggregate_filter() {
let mut cmd = Qail::get("messages");
cmd.columns.push(Expr::Aggregate {
col: "*".to_string(),
func: AggregateFunc::Count,
distinct: false,
filter: Some(vec![Condition {
left: Expr::Named("direction".to_string()),
op: Operator::Eq,
value: Value::String("outbound".to_string()),
is_array_unnest: false,
}]),
alias: Some("sent_count".to_string()),
});
let sql = cmd.to_sql();
println!("FILTER clause: {}", sql);
assert!(sql.contains("FILTER"));
assert!(sql.contains("WHERE"));
assert!(sql.contains("direction"));
}
#[test]
fn test_recursive_cte() {
let mut base = Qail::get("employees");
base.columns.push(Expr::Named("id".to_string()));
base.columns.push(Expr::Named("name".to_string()));
base.columns.push(Expr::Named("manager_id".to_string()));
base.cages.push(Cage {
kind: CageKind::Filter,
conditions: vec![Condition {
left: Expr::Named("manager_id".to_string()),
op: Operator::IsNull,
value: Value::Null,
is_array_unnest: false,
}],
logical_op: LogicalOp::And,
});
let mut recursive = Qail::get("employees");
recursive.columns.push(Expr::Named("id".to_string()));
recursive.columns.push(Expr::Named("name".to_string()));
recursive
.columns
.push(Expr::Named("manager_id".to_string()));
let mut cmd = Qail::get("emp_tree");
cmd.ctes = vec![CTEDef {
name: "emp_tree".to_string(),
recursive: true,
columns: vec![
"id".to_string(),
"name".to_string(),
"manager_id".to_string(),
],
base_query: Box::new(base),
recursive_query: Some(Box::new(recursive)),
source_table: Some("employees".to_string()),
}];
cmd.action = Action::With;
use crate::transpiler::dml::cte::build_cte;
let sql = build_cte(&cmd, Dialect::Postgres);
println!("RECURSIVE CTE: {}", sql);
assert!(sql.contains("WITH RECURSIVE"));
assert!(sql.contains("emp_tree"));
assert!(sql.contains("UNION ALL"));
}
#[test]
fn test_custom_join_on() {
let mut cmd = Qail::get("users");
cmd.joins.push(Join {
table: "orders".to_string(),
kind: JoinKind::Inner,
on: Some(vec![Condition {
left: Expr::Named("users.id".to_string()),
op: Operator::Eq,
value: Value::Column("orders.user_id".to_string()),
is_array_unnest: false,
}]),
on_true: false,
});
let sql = cmd.to_sql();
assert!(
sql.contains("INNER JOIN orders ON users.id = orders.user_id"),
"SQL was: {}",
sql
);
}
#[test]
fn test_custom_join_multiple_conditions() {
let mut cmd = Qail::get("A");
cmd.joins.push(Join {
table: "B".to_string(),
kind: JoinKind::Inner,
on: Some(vec![
Condition {
left: Expr::Named("A.x".to_string()),
op: Operator::Eq,
value: Value::Column("B.x".to_string()),
is_array_unnest: false,
},
Condition {
left: Expr::Named("A.y".to_string()),
op: Operator::Eq,
value: Value::Column("B.y".to_string()),
is_array_unnest: false,
},
]),
on_true: false,
});
let sql = cmd.to_sql();
assert!(
sql.contains("INNER JOIN B ON A.x = B.x AND A.y = B.y"),
"SQL was: {}",
sql
);
assert!(cmd.joins[0].on.is_some());
assert_eq!(cmd.joins[0].on.as_ref().unwrap().len(), 2);
}
#[test]
fn test_distinct_on() {
let mut cmd = Qail::get("employees");
cmd.distinct_on = vec![
Expr::Named("department".to_string()),
Expr::Named("role".to_string()),
];
let sql = cmd.to_sql();
assert!(
sql.starts_with("SELECT DISTINCT ON (department, role)"),
"SQL was: {}",
sql
);
}