Skip to content

Commit 93bb29c

Browse files
CodingAnarchyclaude
andcommitted
fix: Fix PostgreSQL migration 014 trigger function syntax error
Fixed critical bug where sqlparser was dropping empty parentheses from EXECUTE FUNCTION in CREATE TRIGGER statements. PostgreSQL requires () after function names in trigger definitions even when there are no parameters. Migration 014 was failing with "syntax error at or near ';'" due to missing parentheses in trigger creation. Added automatic detection and restoration of missing parentheses for EXECUTE FUNCTION statements. Changes: - Enhanced migration parsing to handle both CREATE FUNCTION and CREATE TRIGGER sqlparser bugs - Improved debug logging for migration statement parsing - Updated migration 014 to use correct PostgreSQL function syntax - This ensures migration 014 (queue pause functionality) executes correctly in production 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2f88587 commit 93bb29c

File tree

4 files changed

+89
-10
lines changed

4 files changed

+89
-10
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.15.4] - 2025-08-26
11+
12+
### Fixed
13+
- **🔧 PostgreSQL Migration 014 Trigger Function Syntax Fix**
14+
- Fixed critical bug where sqlparser was dropping empty parentheses from `EXECUTE FUNCTION` in CREATE TRIGGER statements
15+
- PostgreSQL requires `()` after function names in trigger definitions even when there are no parameters
16+
- Migration 014 was failing with "syntax error at or near ';'" due to missing parentheses in trigger creation
17+
- Added automatic detection and restoration of missing parentheses for `EXECUTE FUNCTION` statements
18+
- Enhanced migration parsing to handle both CREATE FUNCTION and CREATE TRIGGER sqlparser bugs
19+
- This fix ensures migration 014 (queue pause functionality) executes correctly in production environments
20+
- Improved debug logging for migration statement parsing to aid in future troubleshooting
21+
1022
## [1.15.3] - 2025-01-25
1123

1224
### Fixed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ members = [
99
resolver = "2"
1010

1111
[workspace.package]
12-
version = "1.15.3"
12+
version = "1.15.4"
1313
edition = "2024"
1414
license = "MIT"
1515
repository = "https://github.com/CodingAnarchy/hammerwork"

src/migrations/014_add_queue_pause.postgres.sql

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ CREATE INDEX IF NOT EXISTS idx_hammerwork_queue_pause_paused_at ON hammerwork_qu
1616

1717
-- Add function to automatically update the updated_at timestamp
1818
CREATE OR REPLACE FUNCTION update_hammerwork_queue_pause_updated_at()
19-
RETURNS TRIGGER AS $$
19+
RETURNS TRIGGER
20+
LANGUAGE plpgsql AS $BODY$
2021
BEGIN
2122
NEW.updated_at = NOW();
2223
RETURN NEW;
23-
END;
24-
$$ LANGUAGE plpgsql;
24+
END
25+
$BODY$;
2526

2627
-- Create trigger to automatically update updated_at
2728
DROP TRIGGER IF EXISTS trigger_update_hammerwork_queue_pause_updated_at ON hammerwork_queue_pause;

src/migrations/postgres.rs

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,26 @@ use crate::Result;
55
use chrono::Utc;
66
use sqlparser::{dialect::PostgreSqlDialect, parser::Parser};
77
use sqlx::{PgPool, Row};
8-
use tracing::{debug, info, warn};
8+
use tracing::{debug, error, info, warn};
99

1010
/// Parse SQL text into individual statements using sqlparser-rs
1111
/// This properly handles quoted strings, dollar-quoted strings, and comments
1212
fn parse_sql_statements(
1313
sql: &str,
1414
) -> std::result::Result<Vec<String>, sqlparser::parser::ParserError> {
15+
debug!(
16+
"parse_sql_statements called with {} characters of SQL",
17+
sql.len()
18+
);
1519
let dialect = PostgreSqlDialect {};
1620

1721
// First, try to parse the entire SQL as a series of statements
1822
match Parser::parse_sql(&dialect, sql) {
1923
Ok(statements) => {
24+
debug!(
25+
"Successfully parsed {} statements with sqlparser",
26+
statements.len()
27+
);
2028
// Convert parsed statements back to SQL strings
2129
Ok(statements
2230
.iter()
@@ -41,13 +49,44 @@ fn parse_sql_statements(
4149
}
4250
}
4351

52+
// Fix sqlparser bug: it drops empty parentheses from EXECUTE FUNCTION in triggers
53+
// PostgreSQL requires () even when there are no parameters
54+
if sql_string.contains("EXECUTE FUNCTION") {
55+
debug!("Before EXECUTE FUNCTION fix: {}", sql_string);
56+
// Pattern: "EXECUTE FUNCTION function_name" should be "EXECUTE FUNCTION function_name()"
57+
// Note: semicolon might not be present yet since it's added later
58+
if let Some(execute_pos) = sql_string.find("EXECUTE FUNCTION ") {
59+
let after_execute = &sql_string[execute_pos + 17..]; // Skip "EXECUTE FUNCTION "
60+
61+
// Find the end of the function name (could be end of line, semicolon, or whitespace)
62+
let end_pos = after_execute
63+
.find(';')
64+
.or_else(|| after_execute.find('\n'))
65+
.unwrap_or(after_execute.len());
66+
67+
let function_part = &after_execute[..end_pos];
68+
debug!("Function part: '{}'", function_part.trim());
69+
70+
// If function name doesn't end with ), add ()
71+
if !function_part.trim().ends_with(')') {
72+
let insert_pos = execute_pos + 17 + function_part.trim().len();
73+
debug!("Adding () at position {}", insert_pos);
74+
sql_string.insert_str(insert_pos, "()");
75+
debug!("After EXECUTE FUNCTION fix: {}", sql_string);
76+
}
77+
}
78+
}
79+
4480
format!("{};", sql_string)
4581
})
4682
.collect())
4783
}
48-
Err(_) => {
84+
Err(e) => {
85+
error!("sqlparser failed to parse SQL: {}", e);
86+
debug!("Failed SQL content: {}", sql);
4987
// If parsing fails, try to split manually while respecting SQL syntax
5088
// This is a more conservative approach for complex migrations
89+
debug!("Falling back to manual SQL splitting");
5190
Ok(split_sql_respecting_quotes(sql))
5291
}
5392
}
@@ -195,7 +234,16 @@ impl MigrationRunner<sqlx::Postgres> for PostgresMigrationRunner {
195234

196235
// Parse SQL using sqlparser-rs for proper statement splitting
197236
let statements = match parse_sql_statements(sql) {
198-
Ok(stmts) => stmts,
237+
Ok(stmts) => {
238+
debug!(
239+
"sqlparser-rs successfully parsed {} statements",
240+
stmts.len()
241+
);
242+
for (i, stmt) in stmts.iter().enumerate() {
243+
debug!("Parsed statement {}: '{}'", i + 1, stmt.trim());
244+
}
245+
stmts
246+
}
199247
Err(e) => {
200248
warn!(
201249
"Failed to parse SQL with sqlparser, falling back to naive splitting: {}",
@@ -209,6 +257,15 @@ impl MigrationRunner<sqlx::Postgres> for PostgresMigrationRunner {
209257
}
210258
};
211259

260+
debug!(
261+
"Parsed {} statements for migration {}",
262+
statements.len(),
263+
migration.id
264+
);
265+
for (i, statement) in statements.iter().enumerate() {
266+
debug!("Statement {}: '{}'", i + 1, statement.trim());
267+
}
268+
212269
for (i, statement) in statements.iter().enumerate() {
213270
// Add semicolon back if it was removed by split
214271
let full_statement = if statement.ends_with(';') {
@@ -218,13 +275,22 @@ impl MigrationRunner<sqlx::Postgres> for PostgresMigrationRunner {
218275
};
219276

220277
debug!(
221-
"Executing statement {} of {} for migration {}",
278+
"Executing statement {} of {} for migration {}: {}",
222279
i + 1,
223280
statements.len(),
224-
migration.id
281+
migration.id,
282+
full_statement
225283
);
226284

227-
sqlx::query(&full_statement).execute(&mut *tx).await?;
285+
if let Err(e) = sqlx::query(&full_statement).execute(&mut *tx).await {
286+
error!(
287+
"Failed to execute statement {}: {} - Error: {}",
288+
i + 1,
289+
full_statement,
290+
e
291+
);
292+
return Err(e.into());
293+
}
228294
}
229295

230296
tx.commit().await?;

0 commit comments

Comments
 (0)