diff --git a/pkg/writers/pgsql/writer.go b/pkg/writers/pgsql/writer.go index 50ad275..51f1a93 100644 --- a/pkg/writers/pgsql/writer.go +++ b/pkg/writers/pgsql/writer.go @@ -1593,7 +1593,12 @@ func (w *Writer) executeDatabaseSQL(db *models.Database, connString string) erro } stmtType := detectStatementType(stmtTrimmed) - fmt.Fprintf(os.Stderr, "Executing statement %d/%d [%s]...\n", i+1, len(statements), stmtType) + stmtCtx := extractStatementContext(stmtTrimmed) + if stmtCtx != "" { + fmt.Fprintf(os.Stderr, "Executing statement %d/%d [%s] %s...\n", i+1, len(statements), stmtType, stmtCtx) + } else { + fmt.Fprintf(os.Stderr, "Executing statement %d/%d [%s]...\n", i+1, len(statements), stmtType) + } _, execErr := conn.Exec(ctx, stmt) if execErr != nil { @@ -1728,6 +1733,170 @@ func getCurrentTimestamp() string { return time.Now().Format("2006-01-02 15:04:05") } +// extractStatementContext returns a human-readable schema/table/column context string for a SQL statement. +func extractStatementContext(stmt string) string { + upper := strings.ToUpper(stmt) + + // DO $$ blocks: extract identifiers from information_schema WHERE clauses + if strings.HasPrefix(upper, "DO $$") || strings.HasPrefix(upper, "DO $") { + schema := extractSQLStringValue(stmt, "table_schema") + table := extractSQLStringValue(stmt, "table_name") + column := extractSQLStringValue(stmt, "column_name") + constraint := extractSQLStringValue(stmt, "constraint_name") + return buildStmtContext(schema, table, column, constraint) + } + + // ALTER TABLE [schema.]table ... + if strings.HasPrefix(upper, "ALTER TABLE") { + schema, table := parseQualifiedIdent(strings.TrimSpace(stmt[11:])) + if strings.Contains(upper, "ADD COLUMN") { + col := firstIdentAfterKeyword(stmt, upper, "ADD COLUMN") + return buildStmtContext(schema, table, col, "") + } + if strings.Contains(upper, "ALTER COLUMN") { + col := firstIdentAfterKeyword(stmt, upper, "ALTER COLUMN") + return buildStmtContext(schema, table, col, "") + } + if strings.Contains(upper, "ADD CONSTRAINT") { + con := firstIdentAfterKeyword(stmt, upper, "ADD CONSTRAINT") + return buildStmtContext(schema, table, "", con) + } + if strings.Contains(upper, "DROP CONSTRAINT") { + con := firstIdentAfterKeyword(stmt, upper, "DROP CONSTRAINT") + return buildStmtContext(schema, table, "", con) + } + return buildStmtContext(schema, table, "", "") + } + + // CREATE TABLE [IF NOT EXISTS] [schema.]table + if strings.HasPrefix(upper, "CREATE TABLE") { + rest := strings.TrimSpace(stmt[12:]) + if strings.HasPrefix(strings.ToUpper(rest), "IF NOT EXISTS") { + rest = strings.TrimSpace(rest[13:]) + } + schema, table := parseQualifiedIdent(rest) + return buildStmtContext(schema, table, "", "") + } + + // CREATE SCHEMA name + if strings.HasPrefix(upper, "CREATE SCHEMA") { + name := firstBareIdent(strings.TrimSpace(stmt[13:])) + return name + } + + // CREATE [UNIQUE] INDEX ... ON [schema.]table + if strings.HasPrefix(upper, "CREATE INDEX") || strings.HasPrefix(upper, "CREATE UNIQUE INDEX") { + onIdx := strings.Index(upper, " ON ") + if onIdx != -1 { + schema, table := parseQualifiedIdent(strings.TrimSpace(stmt[onIdx+4:])) + return buildStmtContext(schema, table, "", "") + } + } + + // COMMENT ON TABLE [schema.]table + if strings.HasPrefix(upper, "COMMENT ON TABLE") { + schema, table := parseQualifiedIdent(strings.TrimSpace(stmt[16:])) + return buildStmtContext(schema, table, "", "") + } + + // COMMENT ON COLUMN [schema.]table.col + if strings.HasPrefix(upper, "COMMENT ON COLUMN") { + return firstBareIdent(strings.TrimSpace(stmt[17:])) + } + + return "" +} + +// extractSQLStringValue extracts 'value' from patterns like: key = 'value' (case-insensitive key match). +func extractSQLStringValue(stmt, key string) string { + lower := strings.ToLower(stmt) + idx := strings.Index(lower, strings.ToLower(key)) + if idx == -1 { + return "" + } + rest := strings.TrimSpace(stmt[idx+len(key):]) + eqIdx := strings.IndexByte(rest, '=') + if eqIdx == -1 || eqIdx > 5 { + return "" + } + rest = strings.TrimSpace(rest[eqIdx+1:]) + if len(rest) == 0 || rest[0] != '\'' { + return "" + } + rest = rest[1:] + end := strings.IndexByte(rest, '\'') + if end == -1 { + return "" + } + return rest[:end] +} + +// parseQualifiedIdent extracts (schema, name) from the start of s, handling optional "schema"."table" quoting. +func parseQualifiedIdent(s string) (schema, name string) { + token := firstBareIdent(s) + parts := strings.SplitN(token, ".", 2) + if len(parts) == 2 { + return stripQuotes(parts[0]), stripQuotes(parts[1]) + } + return "", stripQuotes(token) +} + +// firstBareIdent returns the first whitespace/delimiter-terminated token, stripping outer quotes. +func firstBareIdent(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + end := strings.IndexAny(s, " \t\n(,;") + if end == -1 { + return s + } + return s[:end] +} + +// firstIdentAfterKeyword returns the first identifier token after keyword (matched case-insensitively via upperStmt). +func firstIdentAfterKeyword(stmt, upperStmt, keyword string) string { + idx := strings.Index(upperStmt, keyword) + if idx == -1 { + return "" + } + return stripQuotes(firstBareIdent(strings.TrimSpace(stmt[idx+len(keyword):]))) +} + +// stripQuotes removes surrounding double-quotes from an identifier. +func stripQuotes(s string) string { + return strings.Trim(s, "\"") +} + +// buildStmtContext assembles a display string from available identifiers. +func buildStmtContext(schema, table, column, constraint string) string { + var b strings.Builder + if schema != "" && table != "" { + b.WriteString(schema) + b.WriteByte('.') + b.WriteString(table) + } else if table != "" { + b.WriteString(table) + } + if column != "" { + if b.Len() > 0 { + b.WriteByte(' ') + } + b.WriteByte('(') + b.WriteString(column) + b.WriteByte(')') + } + if constraint != "" { + if b.Len() > 0 { + b.WriteByte(' ') + } + b.WriteByte('[') + b.WriteString(constraint) + b.WriteByte(']') + } + return b.String() +} + // detectStatementType detects the type of SQL statement for logging func detectStatementType(stmt string) string { upperStmt := strings.ToUpper(stmt)