Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
471 changes: 470 additions & 1 deletion postgresql/PostgreSQLKeywords.g4

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion postgresql/PostgreSQLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ options {
//
// Source: PostgreSQL REL_18_STABLE kwlist.h
// URL: https://raw.githubusercontent.com/postgres/postgres/REL_18_STABLE/src/include/parser/kwlist.h
// Generated: 2025-11-13T17:32:00+08:00
// Generated: 2025-12-22T17:51:41+08:00
// Total Keywords: 494
//
// NOTE: These keyword rules must appear BEFORE the Identifier rule
Expand Down
16 changes: 12 additions & 4 deletions postgresql/PostgreSQLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -4043,7 +4043,7 @@ target_el

target_alias
: AS collabel
| identifier
| bare_col_label
;

qualified_name_list
Expand Down Expand Up @@ -4182,6 +4182,14 @@ collabel
| reserved_keyword
;

// Bare column label - names that can be column labels without writing "AS".
// This classification is orthogonal to the other keyword categories.
// See PostgreSQL's gram.y BareColLabel rule.
bare_col_label
: identifier
| bare_label_keyword
;

identifier
: Identifier opt_uescape?
| QuotedIdentifier
Expand All @@ -4197,9 +4205,9 @@ plsqlidentifier

// ============================================================================
// NOTE: The keyword rules (reserved_keyword, unreserved_keyword,
// col_name_keyword, type_func_name_keyword) are now imported from
// PostgreSQLKeywords.g4, which is auto-generated from PostgreSQL's official
// kwlist.h file.
// col_name_keyword, type_func_name_keyword, bare_label_keyword) are now
// imported from PostgreSQLKeywords.g4, which is auto-generated from
// PostgreSQL's official kwlist.h file.
//
// To regenerate: make generate-keywords
// ============================================================================
Expand Down
43 changes: 43 additions & 0 deletions postgresql/examples/regression/bare_col_label.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-- Test bare column labels (aliases without AS keyword)
-- These should parse successfully because 'name', 'value', 'action' etc are bare_label_keywords

-- Basic bare labels with unreserved keywords
SELECT 1 name;
SELECT 1 value;
SELECT 1 action;
SELECT 1 data;

-- Bare labels with reserved keywords that are also bare_label_keywords
SELECT 1 all;
SELECT 1 table;
SELECT 1 select;

-- Bare labels with col_name_keywords
SELECT 1 int;
SELECT 1 timestamp;
SELECT 1 json;

-- Bare labels with type_func_name_keywords
SELECT 1 left;
SELECT 1 right;
SELECT 1 join;

-- Multiple columns with bare labels
SELECT 1 name, 2 value, 3 action;

-- Mixed with and without AS
SELECT 1 AS alias1, 2 alias2, 3 AS alias3;

-- Expression with bare label
SELECT 1 + 2 result;
SELECT a.id name FROM t a;

-- Subquery with bare label
SELECT * FROM (SELECT 1 name) sub;

-- NOTE: The following require AS keyword (AS_LABEL keywords):
-- SELECT 1 AS year; -- 'year' is AS_LABEL, needs AS
-- SELECT 1 AS month; -- 'month' is AS_LABEL, needs AS
-- SELECT 1 AS day; -- 'day' is AS_LABEL, needs AS
-- SELECT 1 AS hour; -- 'hour' is AS_LABEL, needs AS
-- SELECT 1 AS char; -- 'char' is AS_LABEL, needs AS
48 changes: 45 additions & 3 deletions postgresql/keyword-generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const (
CategoryColName = "COL_NAME_KEYWORD"
CategoryTypeFuncName = "TYPE_FUNC_NAME_KEYWORD"

// Label types for bare column labels (orthogonal to keyword categories)
LabelBare = "BARE_LABEL"
LabelAS = "AS_LABEL"

// PostgreSQL version/branch to fetch keywords from
PostgreSQLVersion = "REL_18_STABLE"
PostgreSQLKwlistURL = "https://raw.githubusercontent.com/postgres/postgres/" + PostgreSQLVersion + "/src/include/parser/kwlist.h"
Expand All @@ -49,9 +53,12 @@ func main() {

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

// Categorize keywords
// Categorize keywords by category
categorized := categorizeKeywords(keywords)

// Categorize keywords by label type
labelCategorized := categorizeByLabel(keywords)

fmt.Printf("Keyword Statistics:\n")
fmt.Printf(" Reserved keywords: %3d\n", len(categorized[CategoryReserved]))
fmt.Printf(" Unreserved keywords: %3d\n", len(categorized[CategoryUnreserved]))
Expand All @@ -60,9 +67,13 @@ func main() {
fmt.Printf(" ───────────────────────────────\n")
fmt.Printf(" Total: %3d\n\n", len(keywords))

fmt.Printf("Label Statistics (orthogonal to categories):\n")
fmt.Printf(" Bare label keywords: %3d\n", len(labelCategorized[LabelBare]))
fmt.Printf(" AS-only label keywords: %3d\n\n", len(labelCategorized[LabelAS]))

// Generate ANTLR parser grammar fragment
parserOutputPath := path.Join(*outputDir, "PostgreSQLKeywords.g4")
err = generateANTLRGrammar(categorized, parserOutputPath)
err = generateANTLRGrammar(categorized, labelCategorized, parserOutputPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating parser grammar: %v\n", err)
os.Exit(1)
Expand Down Expand Up @@ -146,6 +157,17 @@ func categorizeKeywords(keywords []Keyword) map[string][]Keyword {
return categorized
}

// categorizeByLabel groups keywords by their label type (BARE_LABEL vs AS_LABEL)
func categorizeByLabel(keywords []Keyword) map[string][]Keyword {
categorized := make(map[string][]Keyword)

for _, kw := range keywords {
categorized[kw.Label] = append(categorized[kw.Label], kw)
}

return categorized
}

// applyTokenRename applies ANTLR reserved name renaming to token names
func applyTokenRename(token string) string {
antlrReservedNames := map[string]bool{
Expand All @@ -160,7 +182,7 @@ func applyTokenRename(token string) string {
}

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

// Generate bare_label_keyword rule (orthogonal to the other categories)
if keywords := labelCategorized[LabelBare]; len(keywords) > 0 {
fmt.Fprintf(w, "// ============================================================================\n")
fmt.Fprintf(w, "// Bare Label Keywords (%d total)\n", len(keywords))
fmt.Fprintf(w, "// ============================================================================\n")
fmt.Fprintf(w, "// These keywords can be used as column labels WITHOUT the AS keyword.\n")
fmt.Fprintf(w, "// This classification is orthogonal to the other keyword categories.\n")
fmt.Fprintf(w, "// Keywords not in this list require the AS keyword when used as labels.\n")
fmt.Fprintf(w, "//\n")
fmt.Fprintf(w, "// Usage: SELECT 1 name; -- 'name' is a bare_label_keyword, no AS needed\n")
fmt.Fprintf(w, "// SELECT 1 AS year; -- 'year' requires AS (not a bare_label_keyword)\n")
fmt.Fprintf(w, "// ============================================================================\n\n")
fmt.Fprintf(w, "bare_label_keyword\n")
fmt.Fprintf(w, " : %s\n", applyTokenRename(keywords[0].Token))
for i := 1; i < len(keywords); i++ {
fmt.Fprintf(w, " | %s\n", applyTokenRename(keywords[i].Token))
}
fmt.Fprintf(w, " ;\n\n")
}

return nil
}

Expand Down
Loading