From 40cb5ed91a2fe1492ffabd769dd669490a250149 Mon Sep 17 00:00:00 2001 From: apstndb <803393+apstndb@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:46:16 +0900 Subject: [PATCH] feat: support IS UNKNOWN expressions --- ast/ast.go | 14 ++++++++ ast/expr_impls_test.go | 1 + ast/pos.go | 8 +++++ ast/sql.go | 7 +++- ast/walk_internal.go | 3 ++ parser.go | 9 +++++- testdata/input/query/select_is_unknown.sql | 1 + .../result/query/select_is_unknown.sql.txt | 32 +++++++++++++++++++ .../statement/select_is_unknown.sql.txt | 32 +++++++++++++++++++ 9 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 testdata/input/query/select_is_unknown.sql create mode 100644 testdata/result/query/select_is_unknown.sql.txt create mode 100644 testdata/result/statement/select_is_unknown.sql.txt diff --git a/ast/ast.go b/ast/ast.go index 51e4b471..cea9c9a1 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -212,6 +212,7 @@ func (UnaryExpr) isExpr() {} func (InExpr) isExpr() {} func (IsNullExpr) isExpr() {} func (IsBoolExpr) isExpr() {} +func (IsUnknownExpr) isExpr() {} func (IsSourceExpr) isExpr() {} func (IsDestinationExpr) isExpr() {} func (IsLabeledExpr) isExpr() {} @@ -1438,6 +1439,19 @@ type IsBoolExpr struct { Right bool } +// IsUnknownExpr is IS UNKNOWN expression node. +// +// {{.Left | sql}} IS {{if .Not}}NOT{{end}} UNKNOWN +type IsUnknownExpr struct { + // pos = Left.pos + // end = Unknown + 7 + + Unknown token.Pos // position of "UNKNOWN" + + Not bool + Left Expr +} + // IsSourceExpr is IS SOURCE [OF] expression node. // // {{.Left | sql}} IS {{if .Not}}NOT {{end}}SOURCE{{if .Of.Invalid | not}} OF{{end}} {{.Right | sql}} diff --git a/ast/expr_impls_test.go b/ast/expr_impls_test.go index 70606da0..980db054 100644 --- a/ast/expr_impls_test.go +++ b/ast/expr_impls_test.go @@ -14,6 +14,7 @@ var exprPrecTestCases = []exprPrecTestCase{ {name: "InExpr", expr: &InExpr{}}, {name: "IsNullExpr", expr: &IsNullExpr{}}, {name: "IsBoolExpr", expr: &IsBoolExpr{}}, + {name: "IsUnknownExpr", expr: &IsUnknownExpr{}}, {name: "IsSourceExpr", expr: &IsSourceExpr{}}, {name: "IsDestinationExpr", expr: &IsDestinationExpr{}}, {name: "IsLabeledExpr", expr: &IsLabeledExpr{}}, diff --git a/ast/pos.go b/ast/pos.go index 31ea4460..fea4bc6d 100644 --- a/ast/pos.go +++ b/ast/pos.go @@ -478,6 +478,14 @@ func (i *IsBoolExpr) End() token.Pos { return posAdd(i.RightPos, ifThenElse(i.Right, 4, 5)) } +func (i *IsUnknownExpr) Pos() token.Pos { + return nodePos(wrapNode(i.Left)) +} + +func (i *IsUnknownExpr) End() token.Pos { + return posAdd(i.Unknown, 7) +} + func (i *IsSourceExpr) Pos() token.Pos { return nodePos(wrapNode(i.Left)) } diff --git a/ast/sql.go b/ast/sql.go index 8a4f7ed9..4661bb1e 100644 --- a/ast/sql.go +++ b/ast/sql.go @@ -113,7 +113,7 @@ func exprPrec(e Expr) prec { return precLit case *IndexExpr, *SelectorExpr: return precSelector - case *InExpr, *IsNullExpr, *IsBoolExpr, *IsSourceExpr, *IsDestinationExpr, *IsLabeledExpr, *BetweenExpr: + case *InExpr, *IsNullExpr, *IsBoolExpr, *IsUnknownExpr, *IsSourceExpr, *IsDestinationExpr, *IsLabeledExpr, *BetweenExpr: return precComparison case *BinaryExpr: switch e.Op { @@ -449,6 +449,11 @@ func (i *IsBoolExpr) SQL() string { return paren(p, i.Left) + " IS " + strOpt(i.Not, "NOT ") + formatBoolUpper(i.Right) } +func (i *IsUnknownExpr) SQL() string { + p := exprPrec(i) + return paren(p, i.Left) + " IS " + strOpt(i.Not, "NOT ") + "UNKNOWN" +} + func (i *IsSourceExpr) SQL() string { p := exprPrec(i) return paren(p, i.Left) + " IS " + strOpt(i.Not, "NOT ") + "SOURCE" + strOpt(!i.Of.Invalid(), " OF") + " " + paren(p, i.Right) diff --git a/ast/walk_internal.go b/ast/walk_internal.go index 8a1daf6e..690880cf 100644 --- a/ast/walk_internal.go +++ b/ast/walk_internal.go @@ -225,6 +225,9 @@ func walkInternal(node Node, v Visitor, stack []*stackItem) []*stackItem { case *IsBoolExpr: stack = append(stack, &stackItem{node: wrapNode(n.Left), visitor: v.Field("Left")}) + case *IsUnknownExpr: + stack = append(stack, &stackItem{node: wrapNode(n.Left), visitor: v.Field("Left")}) + case *IsSourceExpr: stack = append(stack, &stackItem{node: wrapNode(n.Right), visitor: v.Field("Right")}) stack = append(stack, &stackItem{node: wrapNode(n.Left), visitor: v.Field("Left")}) diff --git a/parser.go b/parser.go index 59284a71..51c90bf6 100644 --- a/parser.go +++ b/parser.go @@ -1679,6 +1679,13 @@ func (p *Parser) parseComparison() ast.Expr { } case token.TokenIdent: switch { + case p.Token.IsKeywordLike("UNKNOWN"): + p.nextToken() + return &ast.IsUnknownExpr{ + Unknown: pos, + Left: expr, + Not: not, + } case p.Token.IsKeywordLike("SOURCE") || p.Token.IsKeywordLike("DESTINATION"): isSource := p.Token.IsKeywordLike("SOURCE") p.nextToken() @@ -1711,7 +1718,7 @@ func (p *Parser) parseComparison() ast.Expr { } } } - p.panicfAtToken(&p.Token, "expected token: NULL, TRUE, FALSE, SOURCE, DESTINATION, LABELED, but: %s", p.Token.Kind) + p.panicfAtToken(&p.Token, "expected token: NULL, TRUE, FALSE, UNKNOWN, SOURCE, DESTINATION, LABELED, but: %s", p.Token.Kind) default: return expr } diff --git a/testdata/input/query/select_is_unknown.sql b/testdata/input/query/select_is_unknown.sql new file mode 100644 index 00000000..e26969db --- /dev/null +++ b/testdata/input/query/select_is_unknown.sql @@ -0,0 +1 @@ +SELECT true IS UNKNOWN, true IS NOT UNKNOWN diff --git a/testdata/result/query/select_is_unknown.sql.txt b/testdata/result/query/select_is_unknown.sql.txt new file mode 100644 index 00000000..6c940909 --- /dev/null +++ b/testdata/result/query/select_is_unknown.sql.txt @@ -0,0 +1,32 @@ +--- select_is_unknown.sql +SELECT true IS UNKNOWN, true IS NOT UNKNOWN + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Results: []ast.SelectItem{ + &ast.ExprSelectItem{ + Expr: &ast.IsUnknownExpr{ + Unknown: 15, + Left: &ast.BoolLiteral{ + ValuePos: 7, + Value: true, + }, + }, + }, + &ast.ExprSelectItem{ + Expr: &ast.IsUnknownExpr{ + Unknown: 36, + Not: true, + Left: &ast.BoolLiteral{ + ValuePos: 24, + Value: true, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT TRUE IS UNKNOWN, TRUE IS NOT UNKNOWN diff --git a/testdata/result/statement/select_is_unknown.sql.txt b/testdata/result/statement/select_is_unknown.sql.txt new file mode 100644 index 00000000..6c940909 --- /dev/null +++ b/testdata/result/statement/select_is_unknown.sql.txt @@ -0,0 +1,32 @@ +--- select_is_unknown.sql +SELECT true IS UNKNOWN, true IS NOT UNKNOWN + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Results: []ast.SelectItem{ + &ast.ExprSelectItem{ + Expr: &ast.IsUnknownExpr{ + Unknown: 15, + Left: &ast.BoolLiteral{ + ValuePos: 7, + Value: true, + }, + }, + }, + &ast.ExprSelectItem{ + Expr: &ast.IsUnknownExpr{ + Unknown: 36, + Not: true, + Left: &ast.BoolLiteral{ + ValuePos: 24, + Value: true, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT TRUE IS UNKNOWN, TRUE IS NOT UNKNOWN