2
0
Эх сурвалжийг харах

fix(parser): reject array parameters and ANY/ALL with 0A000

PizzaSQL has no array type. After a pg client binds a text[] parameter, the
proxy inlines it as a quoted string and "id = ANY(...)" was parsed as an
unknown scalar function, silently evaluating to NULL instead of returning rows.
Reject ANY(...) and ALL(...) predicates with a feature-not-supported error and
a migration hint, and map it to SQLSTATE 0A000 on the simple and extended
protocols. Ordinary columns named any keep parsing.

Task: #65
PizzaSQL Bot 1 өдөр өмнө
parent
commit
2884cb2c5f

+ 33 - 0
pkg/parser/arrays_test.go

@@ -0,0 +1,33 @@
+package parser
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/lexer"
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
+)
+
+func TestArrayParametersAndAnyPredicatesRejected(t *testing.T) {
+	for _, sql := range []string{
+		"SELECT * FROM t WHERE id = ANY('{1,2,3}')",
+		"SELECT * FROM t WHERE id <> ANY('{1,2,3}')",
+		"SELECT * FROM t WHERE id = ALL('{1,2,3}')",
+		"SELECT * FROM t WHERE id = ANY(ARRAY[1,2])",
+	} {
+		t.Run(sql, func(t *testing.T) {
+			_, err := New(lexer.New(sql)).Parse()
+			if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
+				t.Fatalf("error = %v, want ErrFeatureNotSupported", err)
+			}
+		})
+	}
+}
+
+func TestAnyAsOrdinaryColumnStillParses(t *testing.T) {
+	// ANY is only a predicate keyword when followed by "(". A column literally
+	// named any must keep working.
+	if _, err := New(lexer.New("SELECT any FROM t")).Parse(); err != nil {
+		t.Fatalf("parse: %v", err)
+	}
+}

+ 10 - 0
pkg/parser/parser.go

@@ -4,6 +4,7 @@ import (
 	"strings"
 
 	"github.com/danfragoso/pizzasql-next/pkg/lexer"
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
 )
 
 // Parser parses SQL statements into an AST.
@@ -2487,6 +2488,12 @@ func (p *Parser) parseUnaryExpr() (Expr, error) {
 
 func (p *Parser) parsePrimaryExpr() (Expr, error) {
 	switch p.curToken.Type {
+	case lexer.TokenALL:
+		if p.peekTokenIs(lexer.TokenLParen) {
+			return nil, pgerror.Unsupported("array parameters and ANY/ALL predicates are not supported (PizzaSQL has no array type); expand the list into IN (...) with individual parameters")
+		}
+		return nil, p.curError("unexpected token in expression: " + p.curToken.Type.String())
+
 	case lexer.TokenNumber:
 		expr := &LiteralExpr{Type: lexer.TokenNumber, Value: p.curToken.Literal}
 		p.nextToken()
@@ -2590,6 +2597,9 @@ func (p *Parser) parseIdentOrFunction() (Expr, error) {
 
 	// Check for function call
 	if p.curTokenIs(lexer.TokenLParen) {
+		if strings.EqualFold(name, "ANY") {
+			return nil, pgerror.Unsupported("array parameters and ANY/ALL predicates are not supported (PizzaSQL has no array type); expand the list into IN (...) with individual parameters")
+		}
 		return p.parseFunctionCall(name)
 	}
 

+ 28 - 0
pkg/pgerror/pgerror.go

@@ -0,0 +1,28 @@
+// Package pgerror holds SQLSTATE metadata and sentinel errors shared by the
+// parser, executor, and protocol servers.
+package pgerror
+
+import (
+	"errors"
+	"fmt"
+)
+
+// SQLSTATE codes used by PizzaSQL's protocol and HTTP surfaces.
+const (
+	CodeFeatureNotSupported  = "0A000"
+	CodeUniqueViolation      = "23505"
+	CodeSerializationFailure = "40001"
+	CodeSyntaxError          = "42601"
+	CodeInternalError        = "XX000"
+)
+
+// ErrFeatureNotSupported marks SQL that PizzaSQL intentionally rejects instead
+// of accepting with different semantics. Protocol layers map it to SQLSTATE
+// 0A000 (feature_not_supported) rather than a generic syntax or internal error.
+var ErrFeatureNotSupported = errors.New("feature not supported")
+
+// Unsupported returns an error that wraps ErrFeatureNotSupported with a
+// client-facing explanation and migration hint.
+func Unsupported(format string, args ...interface{}) error {
+	return fmt.Errorf("%w: %s", ErrFeatureNotSupported, fmt.Sprintf(format, args...))
+}

+ 57 - 0
pkg/pgserver/array_param_test.go

@@ -0,0 +1,57 @@
+package pgserver
+
+import (
+	"bufio"
+	"testing"
+)
+
+// A pg client that binds a text[] parameter for "id = ANY($1)" must receive a
+// clear feature-not-supported error instead of a silently empty result.
+func TestArrayParameterRejectedOverExtendedQuery(t *testing.T) {
+	c, client := newDataConnection(t)
+	reader := bufio.NewReader(client)
+
+	parse := NewMessageBuilder()
+	parse.WriteString("s1")
+	parse.WriteString("SELECT * FROM t WHERE id = ANY($1)")
+	parse.WriteInt16(1)
+	parse.WriteInt32(1009) // text[]
+	if err := c.handleParse(&Message{Type: MsgParse, Data: parse.Bytes()}); err != nil {
+		t.Fatalf("handleParse: %v", err)
+	}
+
+	bind := NewMessageBuilder()
+	bind.WriteString("p1")
+	bind.WriteString("s1")
+	bind.WriteInt16(0) // all-text parameter formats
+	bind.WriteInt16(1) // one parameter
+	bind.WriteInt32(5)
+	bind.WriteBytes([]byte("{1,2}"))
+	bind.WriteInt16(0) // all-text result formats
+	if err := c.handleBind(&Message{Type: MsgBind, Data: bind.Bytes()}); err != nil {
+		t.Fatalf("handleBind: %v", err)
+	}
+	execute := NewMessageBuilder()
+	execute.WriteString("p1")
+	execute.WriteInt32(0)
+	done := make(chan error, 1)
+	// handleBind buffered BindComplete without flushing; handleExecute's error
+	// path flushes both messages, so read them in order.
+	go func() { done <- c.handleExecute(&Message{Type: MsgExecute, Data: execute.Bytes()}) }()
+	var errMsg *Message
+	for errMsg == nil {
+		msg, err := ReadMessage(reader)
+		if err != nil {
+			t.Fatalf("read response: %v", err)
+		}
+		if msg.Type == MsgErrorResponse {
+			errMsg = msg
+		}
+	}
+	if err := <-done; err != nil {
+		t.Fatalf("handleExecute: %v", err)
+	}
+	if code := errorCode(errMsg); code != ErrCodeFeatureNotSupported {
+		t.Fatalf("error code = %q, want %q", code, ErrCodeFeatureNotSupported)
+	}
+}

+ 17 - 2
pkg/pgserver/connection.go

@@ -18,6 +18,7 @@ import (
 	"github.com/danfragoso/pizzasql-next/pkg/executor"
 	"github.com/danfragoso/pizzasql-next/pkg/lexer"
 	"github.com/danfragoso/pizzasql-next/pkg/parser"
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
 	"github.com/danfragoso/pizzasql-next/pkg/storage"
 )
 
@@ -306,7 +307,14 @@ func (c *Connection) handleQuery(msg *Message) error {
 	p := parser.New(l)
 	stmts, err := p.ParseMultiple()
 	if err != nil {
-		c.sendError("ERROR", ErrCodeSyntaxError, fmt.Sprintf("Syntax error: %v", err))
+		code := ErrCodeSyntaxError
+		if errors.Is(err, pgerror.ErrFeatureNotSupported) {
+			code = ErrCodeFeatureNotSupported
+			if c.txStatus == TxStatusInBlock {
+				c.txStatus = TxStatusFailed
+			}
+		}
+		c.sendError("ERROR", code, fmt.Sprintf("Syntax error: %v", err))
 		return c.sendReadyForQuery()
 	}
 
@@ -484,7 +492,14 @@ func (c *Connection) handleExecute(msg *Message) error {
 	l := lexer.New(portal.query)
 	stmt, err := parser.New(l).Parse()
 	if err != nil {
-		return c.failExtended(ErrCodeSyntaxError, fmt.Errorf("syntax error: %w", err))
+		code := ErrCodeSyntaxError
+		if errors.Is(err, pgerror.ErrFeatureNotSupported) {
+			code = ErrCodeFeatureNotSupported
+			if c.txStatus == TxStatusInBlock {
+				c.txStatus = TxStatusFailed
+			}
+		}
+		return c.failExtended(code, fmt.Errorf("syntax error: %w", err))
 	}
 	result, err := c.executor.Execute(stmt)
 	if err != nil {