Skip to content

Commit c815337

Browse files
h3n4lclaude
andauthored
feat(postgresql): add bare_label_keyword support for column aliases without AS (#45)
Fix parsing of statements like `SELECT 1 name;` where `name` is used as a column alias without the AS keyword. PostgreSQL's keyword system has 5 categories, but our grammar only implemented 4: - reserved_keyword - unreserved_keyword - col_name_keyword - type_func_name_keyword - bare_label_keyword (NEW - orthogonal to the above) The bare_label_keyword category (455 keywords) determines which keywords can be used as column labels without the AS keyword. Keywords marked as AS_LABEL (39) require the AS keyword when used as aliases. Changes: - Updated keyword-generator to extract and generate bare_label_keyword rule - Added bare_col_label rule to PostgreSQLParser.g4 - Updated target_alias to use bare_col_label instead of identifier - Added regression test for bare column labels 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 17b1619 commit c815337

File tree

10 files changed

+17180
-14037
lines changed

10 files changed

+17180
-14037
lines changed

postgresql/PostgreSQLKeywords.g4

Lines changed: 470 additions & 1 deletion
Large diffs are not rendered by default.

postgresql/PostgreSQLLexer.g4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ options {
5252
//
5353
// Source: PostgreSQL REL_18_STABLE kwlist.h
5454
// URL: https://raw.githubusercontent.com/postgres/postgres/REL_18_STABLE/src/include/parser/kwlist.h
55-
// Generated: 2025-11-13T17:32:00+08:00
55+
// Generated: 2025-12-22T17:51:41+08:00
5656
// Total Keywords: 494
5757
//
5858
// NOTE: These keyword rules must appear BEFORE the Identifier rule

postgresql/PostgreSQLParser.g4

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4043,7 +4043,7 @@ target_el
40434043

40444044
target_alias
40454045
: AS collabel
4046-
| identifier
4046+
| bare_col_label
40474047
;
40484048

40494049
qualified_name_list
@@ -4182,6 +4182,14 @@ collabel
41824182
| reserved_keyword
41834183
;
41844184

4185+
// Bare column label - names that can be column labels without writing "AS".
4186+
// This classification is orthogonal to the other keyword categories.
4187+
// See PostgreSQL's gram.y BareColLabel rule.
4188+
bare_col_label
4189+
: identifier
4190+
| bare_label_keyword
4191+
;
4192+
41854193
identifier
41864194
: Identifier opt_uescape?
41874195
| QuotedIdentifier
@@ -4197,9 +4205,9 @@ plsqlidentifier
41974205

41984206
// ============================================================================
41994207
// NOTE: The keyword rules (reserved_keyword, unreserved_keyword,
4200-
// col_name_keyword, type_func_name_keyword) are now imported from
4201-
// PostgreSQLKeywords.g4, which is auto-generated from PostgreSQL's official
4202-
// kwlist.h file.
4208+
// col_name_keyword, type_func_name_keyword, bare_label_keyword) are now
4209+
// imported from PostgreSQLKeywords.g4, which is auto-generated from
4210+
// PostgreSQL's official kwlist.h file.
42034211
//
42044212
// To regenerate: make generate-keywords
42054213
// ============================================================================
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
-- Test bare column labels (aliases without AS keyword)
2+
-- These should parse successfully because 'name', 'value', 'action' etc are bare_label_keywords
3+
4+
-- Basic bare labels with unreserved keywords
5+
SELECT 1 name;
6+
SELECT 1 value;
7+
SELECT 1 action;
8+
SELECT 1 data;
9+
10+
-- Bare labels with reserved keywords that are also bare_label_keywords
11+
SELECT 1 all;
12+
SELECT 1 table;
13+
SELECT 1 select;
14+
15+
-- Bare labels with col_name_keywords
16+
SELECT 1 int;
17+
SELECT 1 timestamp;
18+
SELECT 1 json;
19+
20+
-- Bare labels with type_func_name_keywords
21+
SELECT 1 left;
22+
SELECT 1 right;
23+
SELECT 1 join;
24+
25+
-- Multiple columns with bare labels
26+
SELECT 1 name, 2 value, 3 action;
27+
28+
-- Mixed with and without AS
29+
SELECT 1 AS alias1, 2 alias2, 3 AS alias3;
30+
31+
-- Expression with bare label
32+
SELECT 1 + 2 result;
33+
SELECT a.id name FROM t a;
34+
35+
-- Subquery with bare label
36+
SELECT * FROM (SELECT 1 name) sub;
37+
38+
-- NOTE: The following require AS keyword (AS_LABEL keywords):
39+
-- SELECT 1 AS year; -- 'year' is AS_LABEL, needs AS
40+
-- SELECT 1 AS month; -- 'month' is AS_LABEL, needs AS
41+
-- SELECT 1 AS day; -- 'day' is AS_LABEL, needs AS
42+
-- SELECT 1 AS hour; -- 'hour' is AS_LABEL, needs AS
43+
-- SELECT 1 AS char; -- 'char' is AS_LABEL, needs AS

postgresql/keyword-generator/main.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const (
2727
CategoryColName = "COL_NAME_KEYWORD"
2828
CategoryTypeFuncName = "TYPE_FUNC_NAME_KEYWORD"
2929

30+
// Label types for bare column labels (orthogonal to keyword categories)
31+
LabelBare = "BARE_LABEL"
32+
LabelAS = "AS_LABEL"
33+
3034
// PostgreSQL version/branch to fetch keywords from
3135
PostgreSQLVersion = "REL_18_STABLE"
3236
PostgreSQLKwlistURL = "https://raw.githubusercontent.com/postgres/postgres/" + PostgreSQLVersion + "/src/include/parser/kwlist.h"
@@ -49,9 +53,12 @@ func main() {
4953

5054
fmt.Printf("✓ Parsed %d keywords\n\n", len(keywords))
5155

52-
// Categorize keywords
56+
// Categorize keywords by category
5357
categorized := categorizeKeywords(keywords)
5458

59+
// Categorize keywords by label type
60+
labelCategorized := categorizeByLabel(keywords)
61+
5562
fmt.Printf("Keyword Statistics:\n")
5663
fmt.Printf(" Reserved keywords: %3d\n", len(categorized[CategoryReserved]))
5764
fmt.Printf(" Unreserved keywords: %3d\n", len(categorized[CategoryUnreserved]))
@@ -60,9 +67,13 @@ func main() {
6067
fmt.Printf(" ───────────────────────────────\n")
6168
fmt.Printf(" Total: %3d\n\n", len(keywords))
6269

70+
fmt.Printf("Label Statistics (orthogonal to categories):\n")
71+
fmt.Printf(" Bare label keywords: %3d\n", len(labelCategorized[LabelBare]))
72+
fmt.Printf(" AS-only label keywords: %3d\n\n", len(labelCategorized[LabelAS]))
73+
6374
// Generate ANTLR parser grammar fragment
6475
parserOutputPath := path.Join(*outputDir, "PostgreSQLKeywords.g4")
65-
err = generateANTLRGrammar(categorized, parserOutputPath)
76+
err = generateANTLRGrammar(categorized, labelCategorized, parserOutputPath)
6677
if err != nil {
6778
fmt.Fprintf(os.Stderr, "Error generating parser grammar: %v\n", err)
6879
os.Exit(1)
@@ -146,6 +157,17 @@ func categorizeKeywords(keywords []Keyword) map[string][]Keyword {
146157
return categorized
147158
}
148159

160+
// categorizeByLabel groups keywords by their label type (BARE_LABEL vs AS_LABEL)
161+
func categorizeByLabel(keywords []Keyword) map[string][]Keyword {
162+
categorized := make(map[string][]Keyword)
163+
164+
for _, kw := range keywords {
165+
categorized[kw.Label] = append(categorized[kw.Label], kw)
166+
}
167+
168+
return categorized
169+
}
170+
149171
// applyTokenRename applies ANTLR reserved name renaming to token names
150172
func applyTokenRename(token string) string {
151173
antlrReservedNames := map[string]bool{
@@ -160,7 +182,7 @@ func applyTokenRename(token string) string {
160182
}
161183

162184
// generateANTLRGrammar generates ANTLR grammar file with keyword rules
163-
func generateANTLRGrammar(categorized map[string][]Keyword, outputPath string) error {
185+
func generateANTLRGrammar(categorized map[string][]Keyword, labelCategorized map[string][]Keyword, outputPath string) error {
164186
f, err := os.Create(outputPath)
165187
if err != nil {
166188
return fmt.Errorf("failed to create output file: %w", err)
@@ -261,6 +283,26 @@ func generateANTLRGrammar(categorized map[string][]Keyword, outputPath string) e
261283
fmt.Fprintf(w, " ;\n\n")
262284
}
263285

286+
// Generate bare_label_keyword rule (orthogonal to the other categories)
287+
if keywords := labelCategorized[LabelBare]; len(keywords) > 0 {
288+
fmt.Fprintf(w, "// ============================================================================\n")
289+
fmt.Fprintf(w, "// Bare Label Keywords (%d total)\n", len(keywords))
290+
fmt.Fprintf(w, "// ============================================================================\n")
291+
fmt.Fprintf(w, "// These keywords can be used as column labels WITHOUT the AS keyword.\n")
292+
fmt.Fprintf(w, "// This classification is orthogonal to the other keyword categories.\n")
293+
fmt.Fprintf(w, "// Keywords not in this list require the AS keyword when used as labels.\n")
294+
fmt.Fprintf(w, "//\n")
295+
fmt.Fprintf(w, "// Usage: SELECT 1 name; -- 'name' is a bare_label_keyword, no AS needed\n")
296+
fmt.Fprintf(w, "// SELECT 1 AS year; -- 'year' requires AS (not a bare_label_keyword)\n")
297+
fmt.Fprintf(w, "// ============================================================================\n\n")
298+
fmt.Fprintf(w, "bare_label_keyword\n")
299+
fmt.Fprintf(w, " : %s\n", applyTokenRename(keywords[0].Token))
300+
for i := 1; i < len(keywords); i++ {
301+
fmt.Fprintf(w, " | %s\n", applyTokenRename(keywords[i].Token))
302+
}
303+
fmt.Fprintf(w, " ;\n\n")
304+
}
305+
264306
return nil
265307
}
266308

0 commit comments

Comments
 (0)