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
4 changes: 2 additions & 2 deletions qusql-sqlx-type-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "qusql-sqlx-type-macro"
version = "0.4.3"
version = "0.4.4"
authors = ["Jakob Truelsen <antialize@gmail.com>"]
edition = "2021"
license = "Apache-2.0"
Expand All @@ -27,7 +27,7 @@ proc-macro = true
quote = "1"
syn = { version = "3", features = ["full", "parsing"] }
proc-macro2 = "1"
qusql-type = { path = "../qusql-type", version="0.8.0"}
qusql-type = { path = "../qusql-type", version="0.8.1"}
ariadne = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
7 changes: 7 additions & 0 deletions qusql-sqlx-type-test/sqlx-type-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ CREATE TABLE IF NOT EXISTS partial_files (
last_modified timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS bytea_array_items (
a bigint NOT NULL,
b bigint NOT NULL,
data bytea NOT NULL,
PRIMARY KEY (a, b)
);

25 changes: 25 additions & 0 deletions qusql-sqlx-type-test/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,28 @@ async fn test_int8multirange_update_returning_unnest(pool: PgPool) {
let range: qusql_sqlx_type::PgRange<i64> = row.range.expect("unnest() row must be present");
assert_eq!(range, qusql_sqlx_type::PgRange::from(0..4_294_967_296i64));
}

/// Reported real-world usage: `INSERT ... SELECT * FROM UNNEST($1::bigint[], $2::bigint[],
/// $3::bytea[]) ON CONFLICT DO NOTHING RETURNING ...` bound with a `Vec<&[u8]>` (borrowed
/// byte slices) for the `bytea[]` argument, rather than an owned `Vec<Vec<u8>>`.
#[sqlx::test]
async fn test_insert_unnest_bytea_array_borrowed_slices(pool: PgPool) {
setup(&pool).await;
let ins_a: Vec<i64> = vec![1, 1];
let ins_b: Vec<i64> = vec![10, 20];
let ins_data: Vec<&[u8]> = vec![b"one", b"two"];

let rows = query!(
"INSERT INTO bytea_array_items (a, b, data)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bytea[])
ON CONFLICT DO NOTHING
RETURNING a, b",
&ins_a,
&ins_b,
&ins_data
)
.fetch_all(&pool)
.await
.unwrap();
assert_eq!(rows.len(), 2);
}
4 changes: 2 additions & 2 deletions qusql-sqlx-type/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "qusql-sqlx-type"
version = "0.4.3"
version = "0.4.4"
authors = ["Jakob Truelsen <antialize@gmail.com>"]
edition = "2021"
license = "Apache-2.0"
Expand All @@ -25,7 +25,7 @@ missing_docs_in_private_items = "deny"
sqlx = { version = "0.9", default-features = false, features = ["chrono", "runtime-tokio", "mysql", "postgres"] }

[dependencies]
qusql-sqlx-type-macro = { version = "0.4.3", path = "../qusql-sqlx-type-macro"}
qusql-sqlx-type-macro = { version = "0.4.4", path = "../qusql-sqlx-type-macro"}
chrono = "0.4"
uuid = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
Expand Down
1 change: 1 addition & 0 deletions qusql-sqlx-type/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ arg_io!(Any, Vec<i32>);
arg_io!(Any, Vec<i64>);
arg_io!(Any, Vec<String>);
arg_io!(Any, Vec<Vec<u8>>);
arg_io!(Any, Vec<&[u8]>);
arg_io!(Any, Vec<bool>);
arg_io!(Any, Vec<f64>);

Expand Down
2 changes: 1 addition & 1 deletion qusql-type/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "qusql-type"
version = "0.8.0"
version = "0.8.1"
edition = "2024"
authors = ["Jakob Truelsen <antialize@gmail.com>"]
keywords = [ "mysql", "postgesql", "sql", "typer" ]
Expand Down
164 changes: 164 additions & 0 deletions qusql-type/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2171,6 +2171,170 @@ mod tests {
}
}

/// A row-constructor `IN` a subquery that yields matching columns via
/// `UNNEST($1::bigint[], $2::bigint[])`. Reported as a real-world usage that
/// should type-check.
#[test]
fn postgresql_row_in_unnest_subquery() {
let schema_src = "
CREATE TABLE t1 (
id bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
a bigint NOT NULL,
b bigint NOT NULL
);
";

let opts = TypeOptions::new()
.dialect(SQLDialect::PostgreSQL)
.arguments(SQLArguments::Dollar);
let mut issues = Issues::new(schema_src);
let schema = parse_schemas(schema_src, &mut issues, &opts);
let mut errors = 0;
check_no_errors("schema", schema_src, issues.get(), &mut errors);

let src = "SELECT id, a, b FROM t1
WHERE (a, b) IN (
SELECT * FROM UNNEST($1::bigint[], $2::bigint[])
)";
let mut issues = Issues::new(src);
let q = type_statement(&schema, src, &mut issues, &opts);
check_no_errors("query", src, issues.get(), &mut errors);
if let StatementType::Select { columns, arguments } = q {
if columns.len() != 3 {
println!("query: expected 3 columns, got {}", columns.len());
errors += 1;
}
if arguments.len() != 2 {
println!("query: expected 2 arguments, got {}", arguments.len());
errors += 1;
}
} else {
println!("query: expected Select");
errors += 1;
}

if errors != 0 {
panic!("{errors} errors in postgresql_row_in_unnest_subquery test");
}
}

/// `col = ANY($1)` with a plain, uncast argument (no `$1::bigint[]`) should infer the
/// argument as an array matching the column type. Reported as a real-world usage that
/// should type-check with exactly one (array-typed) argument.
#[test]
fn postgresql_eq_any_uncast_arg() {
let schema_src = "
CREATE TABLE t1 (
a_id bigint NOT NULL,
b_id bigint NOT NULL
);
CREATE TABLE t2 (
id bigint NOT NULL PRIMARY KEY,
x bigint NOT NULL
);
";

let opts = TypeOptions::new()
.dialect(SQLDialect::PostgreSQL)
.arguments(SQLArguments::Dollar);
let mut issues = Issues::new(schema_src);
let schema = parse_schemas(schema_src, &mut issues, &opts);
let mut errors = 0;
check_no_errors("schema", schema_src, issues.get(), &mut errors);

let src = "SELECT a_id, b_id, x FROM t1, t2 WHERE a_id = ANY($1) AND b_id = t2.id";
let mut issues = Issues::new(src);
let q = type_statement(&schema, src, &mut issues, &opts);
check_no_errors("query", src, issues.get(), &mut errors);
if let StatementType::Select { columns, arguments } = q {
if columns.len() != 3 {
println!("query: expected 3 columns, got {}", columns.len());
errors += 1;
}
match arguments.first() {
Some((_, t)) if t.t == Type::Array(Box::new(BaseType::Integer.into())) => {}
Some((_, t)) => {
println!("query: expected argument 0 of type integer[] got {}", t.t);
errors += 1;
}
None => {
println!("query: expected an argument, got none");
errors += 1;
}
}
if arguments.len() != 1 {
println!("query: expected 1 argument, got {}", arguments.len());
errors += 1;
}
} else {
println!("query: expected Select");
errors += 1;
}

if errors != 0 {
panic!("{errors} errors in postgresql_eq_any_uncast_arg test");
}
}

/// `INSERT ... SELECT * FROM UNNEST(...)` where one of the unnested arrays is `bytea[]`.
/// Reported as a real-world usage that should type-check.
#[test]
fn postgresql_insert_unnest_bytea_array() {
let schema_src = "
CREATE TABLE t1 (
a bigint NOT NULL,
b bigint NOT NULL,
payload bytea NOT NULL,
PRIMARY KEY (a, b)
);
";

let opts = TypeOptions::new()
.dialect(SQLDialect::PostgreSQL)
.arguments(SQLArguments::Dollar);
let mut issues = Issues::new(schema_src);
let schema = parse_schemas(schema_src, &mut issues, &opts);
let mut errors = 0;
check_no_errors("schema", schema_src, issues.get(), &mut errors);

let src = "INSERT INTO t1 (a, b, payload)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::bytea[])
ON CONFLICT DO NOTHING
RETURNING a, b";
let mut issues = Issues::new(src);
let q = type_statement(&schema, src, &mut issues, &opts);
check_no_errors("query", src, issues.get(), &mut errors);
if let StatementType::Insert {
arguments,
returning,
..
} = q
{
match returning {
Some(columns) if columns.len() == 2 => {}
Some(columns) => {
println!("query: expected 2 returning columns, got {}", columns.len());
errors += 1;
}
None => {
println!("query: expected returning columns, got none");
errors += 1;
}
}
if arguments.len() != 3 {
println!("query: expected 3 arguments, got {}", arguments.len());
errors += 1;
}
} else {
println!("query: expected Insert");
errors += 1;
}

if errors != 0 {
panic!("{errors} errors in postgresql_insert_unnest_bytea_array test");
}
}

/// A writable CTE that unions a range into an `int8multirange` column and then
/// `unnest()`s the result. Reported as a real-world usage that should type-check.
#[test]
Expand Down
17 changes: 16 additions & 1 deletion qusql-type/src/type_binary_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,22 @@ pub(crate) fn type_binary_expression<'a>(
};

let lhs_type = type_expression(typer, lhs, flags, context);
let rhs_type = type_expression(typer, rhs, flags, context);
// For comparisons, thread the lhs's resolved type down as context for the rhs so a
// bare, uncast `ANY($1)` can infer $1 is an array of the lhs's element type.
let rhs_context = match op {
BinaryOperator::Eq(_)
| BinaryOperator::Neq(_)
| BinaryOperator::GtEq(_)
| BinaryOperator::Gt(_)
| BinaryOperator::LtEq(_)
| BinaryOperator::Lt(_)
if lhs_type.base() != BaseType::Any =>
{
lhs_type.base()
}
_ => context,
};
let rhs_type = type_expression(typer, rhs, flags, rhs_context);
match op {
BinaryOperator::Or(_) => {
typer.ensure_base(lhs, &lhs_type, BaseType::Bool);
Expand Down
Loading
Loading