From adb38aa0ada36ea3b5c3645e2a70466e42cac09f Mon Sep 17 00:00:00 2001 From: wsl-11-2023 Date: Sat, 26 Sep 2026 11:07:52 +0100 Subject: [PATCH] Fix ParamRef.Number always 0 for $N bind parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parserLexer.Lex() copies lexer token data into the yacc semantic value via a switch on token type, but had no case for PARAM ($1, $2, ...). The lexer (Lexer.lexParam) correctly scans the parameter's number into Token.Ival, but without a matching case it fell through to default, which only ever sets lval.str — so lval.ival stayed at its zero value and every ParamRef parsed with Number 0 regardless of its actual position in the query. Adds a case copying tok.Ival into lval.ival, plus a regression test covering repeated/multiple parameters in both the target list and a WHERE clause. --- parser/parse.go | 6 +++ parser/parsertest/paramref_test.go | 63 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 parser/parsertest/paramref_test.go diff --git a/parser/parse.go b/parser/parse.go index 89c2fcb..1bb45d9 100644 --- a/parser/parse.go +++ b/parser/parse.go @@ -74,6 +74,12 @@ func (l *parserLexer) Lex(lval *pgSymType) int { lval.str = tok.Str case ICONST: lval.ival = tok.Ival + case PARAM: + // The PARAM token ($1, $2, ...) carries its number in Token.Ival + // (see Lexer.lexParam), but this switch never copied it into lval, + // so every ParamRef parsed with Number 0 regardless of its actual + // position. + lval.ival = tok.Ival case FCONST, SCONST, BCONST, XCONST: lval.str = tok.Str case Op: diff --git a/parser/parsertest/paramref_test.go b/parser/parsertest/paramref_test.go new file mode 100644 index 0000000..5900603 --- /dev/null +++ b/parser/parsertest/paramref_test.go @@ -0,0 +1,63 @@ +package parsertest + +import ( + "testing" + + "github.com/pgplex/pgparser/nodes" + "github.com/pgplex/pgparser/parser" +) + +// Regression test: parserLexer.Lex() had no case for the PARAM token in its +// switch that copies lexer token data into the yacc semantic value, so +// lval.ival was never set for a bind parameter even though the lexer itself +// (Lexer.lexParam) scans its number correctly. Every $N parsed with +// ParamRef.Number == 0 regardless of its actual position. +func TestParseParamRefNumber(t *testing.T) { + input := "SELECT $1, $2 FROM t WHERE id = $1" + + result, err := parser.Parse(input) + if err != nil { + t.Fatalf("Parse error: %v", err) + } + if result == nil || len(result.Items) != 1 { + t.Fatal("expected 1 statement") + } + + stmt, ok := result.Items[0].(*nodes.SelectStmt) + if !ok { + t.Fatalf("expected *nodes.SelectStmt, got %T", result.Items[0]) + } + if stmt.TargetList == nil || len(stmt.TargetList.Items) != 2 { + t.Fatal("expected 2 target list entries") + } + + wantNumber := func(t *testing.T, n nodes.Node, want int) { + t.Helper() + rt, ok := n.(*nodes.ResTarget) + if !ok { + t.Fatalf("expected *nodes.ResTarget, got %T", n) + } + pr, ok := rt.Val.(*nodes.ParamRef) + if !ok { + t.Fatalf("expected *nodes.ParamRef, got %T", rt.Val) + } + if pr.Number != want { + t.Errorf("ParamRef.Number = %d, want %d", pr.Number, want) + } + } + + wantNumber(t, stmt.TargetList.Items[0], 1) + wantNumber(t, stmt.TargetList.Items[1], 2) + + where, ok := stmt.WhereClause.(*nodes.A_Expr) + if !ok { + t.Fatalf("expected *nodes.A_Expr, got %T", stmt.WhereClause) + } + pr, ok := where.Rexpr.(*nodes.ParamRef) + if !ok { + t.Fatalf("expected *nodes.ParamRef, got %T", where.Rexpr) + } + if pr.Number != 1 { + t.Errorf("WHERE clause ParamRef.Number = %d, want 1", pr.Number) + } +}