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
23 changes: 15 additions & 8 deletions pkg/sql/parser/expressions_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,21 @@ func (p *Parser) parseComparisonExpression() (ast.Expression, error) {
// Check for LIKE/ILIKE operator
if p.isType(models.TokenTypeLike) || strings.EqualFold(p.currentToken.Token.Value, "ILIKE") {
operator := p.currentToken.Token.Value
// Reject ILIKE in non-PostgreSQL dialects - it is a PostgreSQL extension.
if strings.EqualFold(operator, "ILIKE") &&
p.dialect != "" &&
p.dialect != string(keywords.DialectPostgreSQL) {
return nil, fmt.Errorf(
"ILIKE is a PostgreSQL-specific operator and is not supported in %s; "+
"use LIKE or LOWER() for case-insensitive matching", p.dialect,
)
// ILIKE is supported by PostgreSQL, Snowflake, and ClickHouse natively.
// Reject in other dialects (e.g. MySQL, SQL Server, SQLite, Oracle) where
// it is not a recognized operator.
if strings.EqualFold(operator, "ILIKE") && p.dialect != "" {
switch p.dialect {
case string(keywords.DialectPostgreSQL),
string(keywords.DialectSnowflake),
string(keywords.DialectClickHouse):
// supported
default:
return nil, fmt.Errorf(
"ILIKE is not supported in %s; "+
"use LIKE or LOWER() for case-insensitive matching", p.dialect,
)
}
}
p.advance() // Consume LIKE/ILIKE

Expand Down
8 changes: 5 additions & 3 deletions pkg/sql/parser/pivot.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ func (p *Parser) parsePivotAlias(ref *ast.TableReference) {
}

// pivotDialectAllowed reports whether PIVOT/UNPIVOT is a recognized clause
// for the parser's current dialect. PIVOT/UNPIVOT are SQL Server / Oracle
// extensions; in other dialects the words must remain valid identifiers.
// for the parser's current dialect. PIVOT/UNPIVOT are SQL Server, Oracle,
// and Snowflake extensions; in other dialects the words must remain valid
// identifiers.
func (p *Parser) pivotDialectAllowed() bool {
return p.dialect == string(keywords.DialectSQLServer) ||
p.dialect == string(keywords.DialectOracle)
p.dialect == string(keywords.DialectOracle) ||
p.dialect == string(keywords.DialectSnowflake)
}

// isPivotKeyword returns true if the current token is the contextual PIVOT
Expand Down
74 changes: 74 additions & 0 deletions pkg/sql/parser/snowflake_ilike_pivot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2026 GoSQLX Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");

package parser_test

import (
"strings"
"testing"

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

// TestSnowflakeILIKE verifies that ILIKE is accepted in the Snowflake dialect.
// Snowflake natively supports ILIKE; the parser previously rejected it with a
// "PostgreSQL-specific" error. Regression for #483.
func TestSnowflakeILIKE(t *testing.T) {
queries := []string{
`SELECT * FROM users WHERE name ILIKE 'alice%'`,
`SELECT * FROM users WHERE name NOT ILIKE 'alice%'`,
}
for _, q := range queries {
t.Run(q, func(t *testing.T) {
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectSnowflake); err != nil {
t.Fatalf("ParseWithDialect(Snowflake) failed: %v", err)
}
})
}
}

// TestClickHouseILIKE verifies ILIKE is accepted in the ClickHouse dialect.
func TestClickHouseILIKE(t *testing.T) {
q := `SELECT * FROM events WHERE message ILIKE '%error%'`
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectClickHouse); err != nil {
t.Fatalf("ParseWithDialect(ClickHouse) failed: %v", err)
}
}

// TestMySQLILIKERejected verifies ILIKE is still rejected in dialects that
// do not natively support it.
func TestMySQLILIKERejected(t *testing.T) {
q := `SELECT * FROM users WHERE name ILIKE 'alice%'`
_, err := gosqlx.ParseWithDialect(q, keywords.DialectMySQL)
if err == nil {
t.Fatal("expected MySQL ILIKE to be rejected")
}
if !strings.Contains(err.Error(), "ILIKE is not supported") {
t.Fatalf("unexpected error message: %v", err)
}
}

// TestSnowflakePivot verifies PIVOT is parsed in the Snowflake dialect, where
// it was previously gated to SQL Server / Oracle only. Regression for #483.
func TestSnowflakePivot(t *testing.T) {
q := `SELECT *
FROM monthly_sales
PIVOT (SUM(amount) FOR month IN ('JAN', 'FEB', 'MAR'))
AS p`
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectSnowflake); err != nil {
t.Fatalf("Snowflake PIVOT parse failed: %v", err)
}
}

// TestSnowflakeUnpivot verifies UNPIVOT is parsed in the Snowflake dialect.
func TestSnowflakeUnpivot(t *testing.T) {
q := `SELECT *
FROM monthly_sales
UNPIVOT (amount FOR month IN (jan, feb, mar))
AS u`
if _, err := gosqlx.ParseWithDialect(q, keywords.DialectSnowflake); err != nil {
t.Fatalf("Snowflake UNPIVOT parse failed: %v", err)
}
}
Loading