소스 검색

parser: quote keyword identifiers in formatted expressions

FormatExpr output is persisted for generated columns, expression indexes, and
CHECK predicates, and re-parsed on every later evaluation. quoteFormatIdent
only quoted characters outside the bare-identifier set, so a column named
"order" formatted as order and parsed back as the ORDER keyword: the stored
expression could never be evaluated.

Quote an identifier that lexes as a keyword as well, and cover the formatting
and re-parse round trip with a parser test.
Danilo Fragoso 1 일 전
부모
커밋
bd4a013e5b
2개의 변경된 파일과 44개의 추가작업 그리고 1개의 파일을 삭제
  1. 4 1
      pkg/parser/format.go
  2. 40 0
      pkg/parser/format_test.go

+ 4 - 1
pkg/parser/format.go

@@ -189,7 +189,10 @@ func quoteFormatIdent(name string) string {
 			break
 		}
 	}
-	if bare {
+	// Formatted text is persisted and re-parsed, so an identifier that lexes as
+	// a SQL keyword must be quoted even when it is otherwise bare: a column
+	// named "order" has to format as "order" to stay a column reference.
+	if bare && lexer.LookupKeyword(strings.ToUpper(name)) == lexer.TokenIdent {
 		return name
 	}
 	return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`

+ 40 - 0
pkg/parser/format_test.go

@@ -0,0 +1,40 @@
+package parser
+
+import (
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/lexer"
+)
+
+// FormatExpr output is persisted in the schema and re-parsed on every later
+// evaluation, so an identifier that lexes as a keyword has to stay quoted or the
+// stored text would no longer name a column.
+func TestFormatExprQuotesKeywordIdentifiers(t *testing.T) {
+	tests := []struct {
+		name string
+		expr Expr
+		want string
+	}{
+		{"keyword column", &ColumnRef{Column: "order"}, `"order"`},
+		{"plain column", &ColumnRef{Column: "age"}, "age"},
+		{"keyword in expression", &BinaryExpr{
+			Left:  &ColumnRef{Column: "order"},
+			Op:    lexer.TokenGt,
+			Right: &LiteralExpr{Type: lexer.TokenNumber, Value: "0"},
+		}, `("order" > 0)`},
+		{"qualified keyword column", &ColumnRef{Table: "select", Column: "from"}, `"select"."from"`},
+		{"underscored column", &ColumnRef{Column: "user_id"}, "user_id"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			formatted := FormatExpr(tt.expr)
+			if formatted != tt.want {
+				t.Fatalf("FormatExpr = %q, want %q", formatted, tt.want)
+			}
+			if _, err := ParseExpr(formatted); err != nil {
+				t.Fatalf("formatted expression %q does not re-parse: %v", formatted, err)
+			}
+		})
+	}
+}