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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions crates/pgls_schema_cache/src/queries/types.sql
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ from
pg_class c
join pg_attribute a on a.attrelid = c.oid
where
c.relkind = 'c'
c.relkind in ('c', 'r', 'p', 'v', 'm', 'f')
and a.attnum > 0
and not a.attisdropped
group by
c.oid
Expand All @@ -44,7 +45,7 @@ where
t.typrelid = 0
or (
select
c.relkind = 'c'
c.relkind in ('c', 'r', 'p', 'v', 'm', 'f')
from
pg_class c
where
Expand Down
107 changes: 107 additions & 0 deletions crates/pgls_typecheck/src/typed_identifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,113 @@ mod tests {
);
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn test_apply_identifiers_table_row_type(test_db: PgPool) {
// A SQL function argument may be a table's row (composite) type, e.g.
// `CREATE FUNCTION f(row_arg public.tbl) ... SELECT row_arg.name`.
// Field access on such a parameter must resolve to the column's type
// and be replaced with that type's default literal.
let input = "select row_arg.id + row_arg.name";

let identifiers = vec![
super::TypedIdentifier {
path: "get_tbl".to_string(),
name: Some("row_arg".to_string()),
type_: super::IdentifierType {
schema: Some("public".to_string()),
name: "tbl".to_string(),
is_array: false,
},
},
super::TypedIdentifier {
path: "get_tbl".to_string(),
name: Some("row_arg".to_string()),
type_: super::IdentifierType {
schema: Some("public".to_string()),
name: "tbl".to_string(),
is_array: false,
},
},
];

let setup = r#"
CREATE TABLE "public"."tbl" (
id integer,
name text
);
"#;

test_db
.execute(setup)
.await
.expect("Failed to setup test database");

let mut parser = tree_sitter::Parser::new();
parser
.set_language(&pgls_treesitter_grammar::LANGUAGE.into())
.expect("Error loading sql language");

let schema_cache = pgls_schema_cache::SchemaCache::load(&test_db)
.await
.expect("Failed to load Schema Cache");

let tree = parser.parse(input, None).unwrap();

let replacement = super::apply_identifiers(identifiers, &schema_cache, &tree, input);

assert_eq!(
replacement.text_replacement.text(),
// `id` (integer) -> 0, `name` (text) -> ''
"select 0 + ''"
);
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn test_row_type_excludes_system_columns(test_db: PgPool) {
// System columns (e.g. `ctid`) are not real fields of a table's row
// type, so they must not be loaded as attributes: a reference to one is
// left unreplaced so the downstream typecheck still flags it.
let input = "select row_arg.ctid";

let identifiers = vec![super::TypedIdentifier {
path: "get_tbl".to_string(),
name: Some("row_arg".to_string()),
type_: super::IdentifierType {
schema: Some("public".to_string()),
name: "tbl".to_string(),
is_array: false,
},
}];

let setup = r#"
CREATE TABLE "public"."tbl" (
id integer,
name text
);
"#;

test_db
.execute(setup)
.await
.expect("Failed to setup test database");

let mut parser = tree_sitter::Parser::new();
parser
.set_language(&pgls_treesitter_grammar::LANGUAGE.into())
.expect("Error loading sql language");

let schema_cache = pgls_schema_cache::SchemaCache::load(&test_db)
.await
.expect("Failed to load Schema Cache");

let tree = parser.parse(input, None).unwrap();

let replacement = super::apply_identifiers(identifiers, &schema_cache, &tree, input);

// `ctid` is a system column, so it is not resolved and stays as-is.
assert_eq!(replacement.text_replacement.text(), "select row_arg.ctid");
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn test_longer_identifiers(pool: PgPool) {
// create or replace function retrieve(uid uuid, mail text)
Expand Down
65 changes: 65 additions & 0 deletions crates/pgls_typecheck/tests/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,71 @@ impl TestSetup<'_> {
}
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn function_arg_row_type(test_db: PgPool) {
// A SQL function whose argument is a table's row type, e.g.
// create function get_tbl_name(row_arg public.tbl) returns text
// language sql as $$ select row_arg.name $$;
// Accessing a field of the row argument must not raise a false-positive
// "missing FROM-clause entry" diagnostic.
let setup = r#"
create table public.tbl (
id serial primary key,
name text not null
);
"#;

TestSetup {
name: "function_arg_row_type",
setup: Some(setup),
query: r#"select row_arg.name"#,
test_db: &test_db,
typed_identifiers: vec![TypedIdentifier {
path: "get_tbl_name".to_string(),
name: Some("row_arg".to_string()),
type_: IdentifierType {
schema: Some("public".to_string()),
name: "tbl".to_string(),
is_array: false,
},
}],
}
.test()
.await;
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn function_arg_view_row_type(test_db: PgPool) {
// A view (like a table) exposes a composite row type, so a function
// argument typed as the view must also resolve field access without a
// false-positive diagnostic.
let setup = r#"
create table public.tbl (
id serial primary key,
name text not null
);
create view public.tbl_view as select id, name from public.tbl;
"#;

TestSetup {
name: "function_arg_view_row_type",
setup: Some(setup),
query: r#"select row_arg.name"#,
test_db: &test_db,
typed_identifiers: vec![TypedIdentifier {
path: "get_view_name".to_string(),
name: Some("row_arg".to_string()),
type_: IdentifierType {
schema: Some("public".to_string()),
name: "tbl_view".to_string(),
is_array: false,
},
}],
}
.test()
.await;
}

#[sqlx::test(migrator = "pgls_test_utils::MIGRATIONS")]
async fn invalid_column(test_db: PgPool) {
TestSetup {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: crates/pgls_typecheck/tests/diagnostics.rs
expression: content
---
No Diagnostic
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: crates/pgls_typecheck/tests/diagnostics.rs
expression: content
---
No Diagnostic
Loading