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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1,738 changes: 912 additions & 826 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ rust-version = "1.86.0"
[workspace.dependencies]
# supporting crates unrelated to postgres
anyhow = "1.0.92"
biome_console = "=0.5.7"
biome_deserialize = "0.6.0"
biome_deserialize_macros = "0.6.0"
biome_diagnostics = "=0.5.7"
biome_js_factory = "0.5.7"
biome_js_formatter = "0.5.7"
biome_js_syntax = "0.5.7"
biome_rowan = "0.5.7"
biome_string_case = "0.5.8"
biome_json_parser = "=0.5.7"
biome_parser = "=0.5.7"
biome_rowan = "=0.5.7"
biome_string_case = "=0.5.8"
biome_text_edit = "=0.5.7"
biome_text_size = "=0.5.7"
biome_unicode_table = "=0.5.7"
bpaf = { version = "0.9.15", features = ["derive"] }
criterion = "0.5"
crossbeam = "0.8.4"
Expand All @@ -44,7 +51,9 @@ slotmap = "1.0.7"
smallvec = { version = "1.13.2", features = ["union", "const_new", "serde"] }
strum = { version = "0.27.1", features = ["derive"] }
# this will use tokio if available, otherwise async-std
camino = "1.1.9"
convert_case = "0.6.0"
dir-test = "0.4.1"
prost = "0.13.5"
prost-reflect = "0.15.3"
protox = "0.8.0"
Expand Down Expand Up @@ -79,6 +88,8 @@ pgls_lsp = { path = "./crates/pgls_lsp", version = "0.0.0" }
pgls_markup = { path = "./crates/pgls_markup", version = "0.0.0" }
pgls_matcher = { path = "./crates/pgls_matcher", version = "0.0.0" }
pgls_plpgsql_check = { path = "./crates/pgls_plpgsql_check", version = "0.0.0" }
pgls_pretty_print = { path = "./crates/pgls_pretty_print", version = "0.0.0" }
pgls_pretty_print_codegen = { path = "./crates/pgls_pretty_print_codegen", version = "0.0.0" }
pgls_query = { path = "./crates/pgls_query", version = "0.0.0" }
pgls_query_ext = { path = "./crates/pgls_query_ext", version = "0.0.0" }
pgls_query_macros = { path = "./crates/pgls_query_macros", version = "0.0.0" }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The following features are implemented:
- Syntax Error Highlighting
- Type-checking (via `EXPLAIN` error insights)
- Linter, inspired by [Squawk](https://squawkhq.com)
- Formatting

Our current focus is on refining and enhancing these core features while building a robust and easily accessible infrastructure. For future plans and opportunities to contribute, please check out the issues and discussions. Any contributions are welcome!

Expand Down
54 changes: 24 additions & 30 deletions crates/pgls_analyser/src/lint/safety/add_serial_column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ impl LinterRule for AddSerialColumn {

if let pgls_query::NodeEnum::AlterTableStmt(stmt) = &ctx.stmt() {
for cmd in &stmt.cmds {
if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node {
if cmd.subtype() == pgls_query::protobuf::AlterTableType::AtAddColumn {
if let Some(pgls_query::NodeEnum::ColumnDef(col_def)) =
&cmd.def.as_ref().and_then(|d| d.node.as_ref())
{
// Check for SERIAL types
if let Some(type_name) = &col_def.type_name {
let type_str = get_type_name(type_name);
if is_serial_type(&type_str) {
diagnostics.push(
if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node
&& cmd.subtype() == pgls_query::protobuf::AlterTableType::AtAddColumn
&& let Some(pgls_query::NodeEnum::ColumnDef(col_def)) =
&cmd.def.as_ref().and_then(|d| d.node.as_ref())
{
// Check for SERIAL types
if let Some(type_name) = &col_def.type_name {
let type_str = get_type_name(type_name);
if is_serial_type(&type_str) {
diagnostics.push(
LinterDiagnostic::new(
rule_category!(),
None,
Expand All @@ -66,26 +66,22 @@ impl LinterRule for AddSerialColumn {
.detail(None, "SERIAL types require rewriting the entire table with an ACCESS EXCLUSIVE lock, blocking all reads and writes.")
.note("SERIAL types cannot be added to existing tables without a full table rewrite. Consider using a non-serial type with a sequence instead."),
);
continue;
}
}
continue;
}
}

// Check for GENERATED ALWAYS AS ... STORED
let has_stored_generated =
col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) =
&constraint.node
{
c.contype()
== pgls_query::protobuf::ConstrType::ConstrGenerated
&& c.generated_when == "a" // 'a' = ALWAYS
} else {
false
}
});
// Check for GENERATED ALWAYS AS ... STORED
let has_stored_generated = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node {
c.contype() == pgls_query::protobuf::ConstrType::ConstrGenerated
&& c.generated_when == "a" // 'a' = ALWAYS
} else {
false
}
});

if has_stored_generated {
diagnostics.push(
if has_stored_generated {
diagnostics.push(
LinterDiagnostic::new(
rule_category!(),
None,
Expand All @@ -96,8 +92,6 @@ impl LinterRule for AddSerialColumn {
.detail(None, "GENERATED ... STORED columns require rewriting the entire table with an ACCESS EXCLUSIVE lock, blocking all reads and writes.")
.note("GENERATED ... STORED columns cannot be added to existing tables without a full table rewrite."),
);
}
}
}
}
}
Expand Down
89 changes: 44 additions & 45 deletions crates/pgls_analyser/src/lint/safety/adding_field_with_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,31 +50,29 @@ impl LinterRule for AddingFieldWithDefault {

if let pgls_query::NodeEnum::AlterTableStmt(stmt) = &ctx.stmt() {
for cmd in &stmt.cmds {
if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node {
if cmd.subtype() == pgls_query::protobuf::AlterTableType::AtAddColumn {
if let Some(pgls_query::NodeEnum::ColumnDef(col_def)) =
&cmd.def.as_ref().and_then(|d| d.node.as_ref())
{
let has_default = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node
{
c.contype() == pgls_query::protobuf::ConstrType::ConstrDefault
} else {
false
}
});
if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node
&& cmd.subtype() == pgls_query::protobuf::AlterTableType::AtAddColumn
&& let Some(pgls_query::NodeEnum::ColumnDef(col_def)) =
&cmd.def.as_ref().and_then(|d| d.node.as_ref())
{
let has_default = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node {
c.contype() == pgls_query::protobuf::ConstrType::ConstrDefault
} else {
false
}
});

let has_generated = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node
{
c.contype() == pgls_query::protobuf::ConstrType::ConstrGenerated
} else {
false
}
});
let has_generated = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node {
c.contype() == pgls_query::protobuf::ConstrType::ConstrGenerated
} else {
false
}
});

if has_generated {
diagnostics.push(
if has_generated {
diagnostics.push(
LinterDiagnostic::new(
rule_category!(),
None,
Expand All @@ -85,23 +83,26 @@ impl LinterRule for AddingFieldWithDefault {
.detail(None, "This operation requires an ACCESS EXCLUSIVE lock and rewrites the entire table.")
.note("Add the column as nullable, backfill existing rows, and add a trigger to update the column on write instead."),
);
} else if has_default {
// For PG 11+, check if the default is volatile
if pg_version.is_some_and(|v| v >= 11) {
// Check if default is non-volatile
let is_safe_default = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node {
if c.contype() == pgls_query::protobuf::ConstrType::ConstrDefault {
if let Some(raw_expr) = &c.raw_expr {
return is_safe_default_expr(&raw_expr.node.as_ref().map(|n| Box::new(n.clone())), ctx.schema_cache());
}
}
}
false
});
} else if has_default {
// For PG 11+, check if the default is volatile
if pg_version.is_some_and(|v| v >= 11) {
// Check if default is non-volatile
let is_safe_default = col_def.constraints.iter().any(|constraint| {
if let Some(pgls_query::NodeEnum::Constraint(c)) = &constraint.node
&& c.contype()
== pgls_query::protobuf::ConstrType::ConstrDefault
&& let Some(raw_expr) = &c.raw_expr
{
return is_safe_default_expr(
&raw_expr.node.as_ref().map(|n| Box::new(n.clone())),
ctx.schema_cache(),
);
}
false
});

if !is_safe_default {
diagnostics.push(
if !is_safe_default {
diagnostics.push(
LinterDiagnostic::new(
rule_category!(),
None,
Expand All @@ -112,10 +113,10 @@ impl LinterRule for AddingFieldWithDefault {
.detail(None, "Even in PostgreSQL 11+, volatile default values require a full table rewrite.")
.note("Add the column without a default, then set the default in a separate statement."),
);
}
} else {
// Pre PG 11, all defaults cause rewrites
diagnostics.push(
}
} else {
// Pre PG 11, all defaults cause rewrites
diagnostics.push(
LinterDiagnostic::new(
rule_category!(),
None,
Expand All @@ -126,8 +127,6 @@ impl LinterRule for AddingFieldWithDefault {
.detail(None, "This operation requires an ACCESS EXCLUSIVE lock and rewrites the entire table.")
.note("Add the column without a default, then set the default in a separate statement."),
);
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ impl LinterRule for AddingForeignKeyConstraint {
pgls_query::protobuf::AlterTableType::AtAddConstraint => {
if let Some(pgls_query::NodeEnum::Constraint(constraint)) =
cmd.def.as_ref().and_then(|d| d.node.as_ref())
{
if let Some(diagnostic) =
&& let Some(diagnostic) =
check_foreign_key_constraint(constraint, false)
{
diagnostics.push(diagnostic);
}
{
diagnostics.push(diagnostic);
}
}
pgls_query::protobuf::AlterTableType::AtAddColumn => {
Expand All @@ -72,12 +70,10 @@ impl LinterRule for AddingForeignKeyConstraint {
for constraint in &col_def.constraints {
if let Some(pgls_query::NodeEnum::Constraint(constr)) =
&constraint.node
{
if let Some(diagnostic) =
&& let Some(diagnostic) =
check_foreign_key_constraint(constr, true)
{
diagnostics.push(diagnostic);
}
{
diagnostics.push(diagnostic);
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions crates/pgls_analyser/src/lint/safety/adding_not_null_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,17 @@ impl LinterRule for AddingNotNullField {

if let pgls_query::NodeEnum::AlterTableStmt(stmt) = &ctx.stmt() {
for cmd in &stmt.cmds {
if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node {
if cmd.subtype() == pgls_query::protobuf::AlterTableType::AtSetNotNull {
diagnostics.push(LinterDiagnostic::new(
if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) = &cmd.node
&& cmd.subtype() == pgls_query::protobuf::AlterTableType::AtSetNotNull
{
diagnostics.push(LinterDiagnostic::new(
rule_category!(),
None,
markup! {
"Setting a column NOT NULL blocks reads while the table is scanned."
},
).detail(None, "This operation requires an ACCESS EXCLUSIVE lock and a full table scan to verify all rows.")
.note("Use a CHECK constraint with NOT VALID instead, then validate it in a separate transaction."));
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ impl LinterRule for AddingPrimaryKeyConstraint {
pgls_query::protobuf::AlterTableType::AtAddConstraint => {
if let Some(pgls_query::NodeEnum::Constraint(constraint)) =
cmd.def.as_ref().and_then(|d| d.node.as_ref())
{
if let Some(diagnostic) =
&& let Some(diagnostic) =
check_for_primary_key_constraint(constraint)
{
diagnostics.push(diagnostic);
}
{
diagnostics.push(diagnostic);
}
}
// Check for ADD COLUMN with PRIMARY KEY
Expand All @@ -70,12 +68,10 @@ impl LinterRule for AddingPrimaryKeyConstraint {
for constraint in &col_def.constraints {
if let Some(pgls_query::NodeEnum::Constraint(constr)) =
&constraint.node
{
if let Some(diagnostic) =
&& let Some(diagnostic) =
check_for_primary_key_constraint(constr)
{
diagnostics.push(diagnostic);
}
{
diagnostics.push(diagnostic);
}
}
}
Expand Down
Loading
Loading