From 396eaf006b23584bedfb0327df3ad35c5b304475 Mon Sep 17 00:00:00 2001 From: jay23606 <4602056+jay23606@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:44:15 -0400 Subject: [PATCH] fix(cli): format NOT EXISTS without a nil dereference The parser represents NOT EXISTS (subquery) as a NOT with the EXISTS on the left and no right operand. The formatter passed that nil right operand on, and the fallback called TokenLiteral on it, so gosqlx format panicked on any query containing NOT EXISTS. Format the NOT followed by its left operand in that case, and add a test. Fixes #587 --- cmd/gosqlx/cmd/sql_formatter.go | 6 ++++++ cmd/gosqlx/cmd/sql_formatter_test.go | 30 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/cmd/gosqlx/cmd/sql_formatter.go b/cmd/gosqlx/cmd/sql_formatter.go index 0d6e9517..b31ce9b1 100644 --- a/cmd/gosqlx/cmd/sql_formatter.go +++ b/cmd/gosqlx/cmd/sql_formatter.go @@ -663,6 +663,12 @@ func (f *SQLFormatter) formatExpression(expr ast.Expression) error { } } case *ast.BinaryExpression: + // NOT EXISTS (subquery) is parsed as a NOT with the EXISTS on the left and no right operand. + if e.Operator == "NOT" && e.Right == nil { + f.writeKeyword("NOT") + f.builder.WriteString(" ") + return f.formatExpression(e.Left) + } // Handle IS NULL / IS NOT NULL specially if e.Operator == "IS NULL" { if err := f.formatExpression(e.Left); err != nil { diff --git a/cmd/gosqlx/cmd/sql_formatter_test.go b/cmd/gosqlx/cmd/sql_formatter_test.go index ba59478a..aebac65c 100644 --- a/cmd/gosqlx/cmd/sql_formatter_test.go +++ b/cmd/gosqlx/cmd/sql_formatter_test.go @@ -600,3 +600,33 @@ func TestSQLFormatter_ParenthesizedExpression(t *testing.T) { } } } + +// TestSQLFormatter_NotExistsDoesNotPanic covers NOT EXISTS (subquery), which the parser +// represents as a NOT with the EXISTS on the left and no right operand. +func TestSQLFormatter_NotExistsDoesNotPanic(t *testing.T) { + sql := "SELECT a FROM t WHERE NOT EXISTS (SELECT 1 FROM u WHERE u.id = t.id)" + tkz := tokenizer.GetTokenizer() + defer tokenizer.PutTokenizer(tkz) + tokens, err := tkz.Tokenize([]byte(sql)) + if err != nil { + t.Fatalf("Tokenization failed: %v", err) + } + + p := parser.NewParser() + astObj := ast.NewAST() + defer ast.ReleaseAST(astObj) + result, err := p.ParseFromModelTokens(tokens) + if err != nil { + t.Fatalf("Parsing failed: %v", err) + } + astObj.Statements = result.Statements + + formatter := NewSQLFormatter(FormatterOptions{Indent: " ", UppercaseKw: true}) + output, err := formatter.Format(astObj) + if err != nil { + t.Fatalf("Formatting failed: %v", err) + } + if !strings.Contains(output, "NOT EXISTS (") { + t.Errorf("expected NOT EXISTS in output, got:\n%s", output) + } +}