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
116 changes: 116 additions & 0 deletions pkg/sql/parser/clickhouse_table_identifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2026 GoSQLX Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0

package parser_test

import (
"testing"

"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
)

// TestClickHouseTableAsIdentifier verifies that the ClickHouse dialect accepts
// `table` as a column identifier in SELECT lists, function arguments, and
// GROUP BY clauses. ClickHouse system tables (system.replicas, system.tables,
// system.parts) all expose a `table` column, so this is a common real-world
// pattern. Regression test for issue #480.
func TestClickHouseTableAsIdentifier(t *testing.T) {
queries := map[string]string{
"replicas_with_table_column": `SELECT
database,
table,
is_leader,
is_readonly,
is_session_expired,
parts_to_check,
queue_size,
inserts_in_queue,
merges_in_queue,
absolute_delay,
last_queue_update,
zookeeper_path
FROM system.replicas
ORDER BY absolute_delay DESC`,

"tables_with_bytes_on_disk": `SELECT
database,
table,
engine,
formatReadableSize(bytes_on_disk) AS size,
parts,
active_parts
FROM system.tables
WHERE engine LIKE '%MergeTree%'
AND is_temporary = 0
ORDER BY bytes_on_disk DESC
LIMIT 10`,

"tables_with_total_bytes": `SELECT
database,
table,
engine,
formatReadableSize(total_bytes) AS size,
parts,
active_parts
FROM system.tables
WHERE engine LIKE '%MergeTree%'
AND is_temporary = 0
ORDER BY total_bytes DESC
LIMIT 10`,

"parts_with_concat_table": `SELECT
concat(database, '.' ,table) AS table_name,
count() AS part_count,
max(partition) AS latest_partition,
formatReadableSize(sum(bytes_on_disk)) AS total_size
FROM system.parts
WHERE active = 1
AND database NOT IN ('system')
GROUP BY database, table
ORDER BY part_count DESC
LIMIT 10`,

"parts_having_count": `SELECT
database,
table,
count() AS parts,
formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.parts
WHERE active = 1
AND database NOT IN ('system')
GROUP BY database, table
HAVING parts > 300
ORDER BY parts DESC`,

// system.tables exposes a `rows` column. ROWS is a reserved keyword
// (used in window frames: ROWS BETWEEN ...), but in ClickHouse it is
// a valid column name in system tables.
"tables_with_rows_column": `SELECT
database,
table,
rows,
total_bytes
FROM system.tables
WHERE database = 'default'
ORDER BY rows DESC`,
}

for name, query := range queries {
query := query
t.Run(name, func(t *testing.T) {
parsed, err := gosqlx.ParseWithDialect(query, keywords.DialectClickHouse)
if err != nil {
t.Fatalf("ParseWithDialect failed: %v", err)
}
if parsed == nil {
t.Fatal("expected non-nil AST")
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/sql/parser/expressions_literal.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (p *Parser) parsePrimaryExpression() (ast.Expression, error) {
return funcCall, nil
}

if p.isType(models.TokenTypeIdentifier) || p.isType(models.TokenTypeDoubleQuotedString) || (p.dialect == string(keywords.DialectSQLServer) && p.isNonReservedKeyword()) {
if p.isType(models.TokenTypeIdentifier) || p.isType(models.TokenTypeDoubleQuotedString) || ((p.dialect == string(keywords.DialectSQLServer) || p.dialect == string(keywords.DialectClickHouse)) && p.isNonReservedKeyword()) {
// Handle identifiers and function calls
// Double-quoted strings are treated as identifiers in SQL (e.g., "column_name")
// Non-reserved keywords (TARGET, SOURCE, etc.) can also be used as identifiers
Expand Down
10 changes: 7 additions & 3 deletions pkg/sql/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -958,14 +958,18 @@ func (p *Parser) isNonReservedKeyword() bool {
case models.TokenTypeTarget, models.TokenTypeSource, models.TokenTypeMatched:
return true
case models.TokenTypeTable, models.TokenTypeIndex, models.TokenTypeView,
models.TokenTypeKey, models.TokenTypeColumn, models.TokenTypeDatabase:
models.TokenTypeKey, models.TokenTypeColumn, models.TokenTypeDatabase,
models.TokenTypePartition, models.TokenTypeRows:
// DDL keywords that are commonly used as quoted identifiers in MySQL (backtick)
// and SQL Server (bracket) dialects.
// and SQL Server (bracket) dialects, and as plain column names in ClickHouse
// system tables (system.parts.partition, system.replicas.table,
// system.tables.rows, etc).
return true
case models.TokenTypeKeyword:
// Token may have generic Type; check value for specific keywords
switch strings.ToUpper(p.currentToken.Token.Value) {
case "TARGET", "SOURCE", "MATCHED", "VALUE", "NAME", "TYPE", "STATUS":
case "TARGET", "SOURCE", "MATCHED", "VALUE", "NAME", "TYPE", "STATUS",
"TABLES":
return true
}
}
Expand Down
Loading