Browse Source

99.9999% sqllogictest

Danilo Fragoso 4 months ago
parent
commit
fa08280858
5 changed files with 209 additions and 3 deletions
  1. BIN
      bin/pizzasql
  2. BIN
      bin/sqllogictest
  3. 81 0
      pkg/executor/collect_refs_test.go
  4. 28 3
      pkg/executor/executor.go
  5. 100 0
      pkg/executor/executor_test.go

BIN
bin/pizzasql


BIN
bin/sqllogictest


+ 81 - 0
pkg/executor/collect_refs_test.go

@@ -0,0 +1,81 @@
+package executor
+
+import (
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/lexer"
+	"github.com/danfragoso/pizzasql-next/pkg/parser"
+)
+
+func TestCollectColumnRefsWithExists(t *testing.T) {
+	sql := "SELECT * FROM t1 WHERE EXISTS(SELECT 1 FROM t1 AS x WHERE x.b<t1.b)"
+	l := lexer.New(sql)
+	p := parser.New(l)
+	stmt, err := p.Parse()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	sel := stmt.(*parser.SelectStmt)
+	refs := collectColumnRefs(sel.Where)
+
+	t.Logf("WHERE type: %T", sel.Where)
+	t.Logf("Column refs: %v", refs)
+	t.Logf("Ref count: %d", len(refs))
+
+	if len(refs) == 0 {
+		t.Error("Expected non-empty refs for EXISTS clause, got 0")
+	}
+
+	hasSubquery := false
+	for _, ref := range refs {
+		if ref == "__subquery__" {
+			hasSubquery = true
+			break
+		}
+	}
+
+	if !hasSubquery {
+		t.Error("Expected __subquery__ sentinel in refs, but didn't find it")
+	}
+}
+
+func TestCollectColumnRefsWithSubquery(t *testing.T) {
+	sql := "SELECT c FROM t1 WHERE c>(SELECT avg(c) FROM t1)"
+	l := lexer.New(sql)
+	p := parser.New(l)
+	stmt, err := p.Parse()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	sel := stmt.(*parser.SelectStmt)
+	refs := collectColumnRefs(sel.Where)
+
+	t.Logf("WHERE type: %T", sel.Where)
+	t.Logf("Column refs: %v", refs)
+	t.Logf("Ref count: %d", len(refs))
+
+	// Should have "c" and "__subquery__"
+	if len(refs) < 2 {
+		t.Errorf("Expected at least 2 refs (column and subquery sentinel), got %d", len(refs))
+	}
+
+	hasSubquery := false
+	hasColumn := false
+	for _, ref := range refs {
+		if ref == "__subquery__" {
+			hasSubquery = true
+		}
+		if ref == "c" {
+			hasColumn = true
+		}
+	}
+
+	if !hasSubquery {
+		t.Error("Expected __subquery__ sentinel in refs")
+	}
+	if !hasColumn {
+		t.Error("Expected column 'c' in refs")
+	}
+}

+ 28 - 3
pkg/executor/executor.go

@@ -229,10 +229,16 @@ func (e *Executor) executeSelect(stmt *parser.SelectStmt) (*Result, error) {
 
 	// If WHERE is constant false, check if we have aggregates first
 	if constantWhereResult != nil && !*constantWhereResult {
-		// If query has aggregates, we still need to evaluate them on empty row set
+		// If query has GROUP BY, return empty result (no groups match)
+		// If query has aggregates but no GROUP BY, evaluate them on empty row set
 		if e.hasAggregates(stmt.Columns) {
-			// Pass empty row set to aggregate evaluation
-			return e.executeAggregateSelect(stmt, []storage.Row{}, schema)
+			if len(stmt.GroupBy) > 0 {
+				// GROUP BY with no matching rows: return empty result (no groups)
+				// Fall through to the non-aggregate case below
+			} else {
+				// Aggregate without GROUP BY: return single row with aggregate results on empty set
+				return e.executeAggregateSelect(stmt, []storage.Row{}, schema)
+			}
 		}
 		// Non-aggregate query with WHERE false: return empty result
 		result := NewResult("SELECT")
@@ -4464,6 +4470,7 @@ func splitANDClauses(expr parser.Expr) []parser.Expr {
 // collectColumnRefs returns all unqualified column names referenced in an expression.
 func collectColumnRefs(expr parser.Expr) []string {
 	var refs []string
+	var hasSubquery bool
 	var walk func(parser.Expr)
 	walk = func(e parser.Expr) {
 		if e == nil {
@@ -4479,6 +4486,10 @@ func collectColumnRefs(expr parser.Expr) []string {
 			walk(n.Operand)
 		case *parser.InExpr:
 			walk(n.Left)
+			// Check for subquery
+			if n.Subquery != nil {
+				hasSubquery = true
+			}
 			for _, v := range n.Values {
 				walk(v)
 			}
@@ -4504,9 +4515,23 @@ func collectColumnRefs(expr parser.Expr) []string {
 			}
 		case *parser.ParenExpr:
 			walk(n.Expr)
+		case *parser.CastExpr:
+			walk(n.Expr)
+		case *parser.SubqueryExpr:
+			// Subqueries may reference outer columns
+			hasSubquery = true
+		case *parser.ExistsExpr:
+			// EXISTS subqueries may reference outer columns
+			hasSubquery = true
+		case *parser.LiteralExpr:
+			// Literals have no column refs
 		}
 	}
 	walk(expr)
+	// If we have subqueries, add a sentinel value to indicate non-constant
+	if hasSubquery {
+		refs = append(refs, "__subquery__")
+	}
 	return refs
 }
 

+ 100 - 0
pkg/executor/executor_test.go

@@ -1491,3 +1491,103 @@ func TestDistinct(t *testing.T) {
 		}
 	})
 }
+
+// TestGroupByWithConstantFalseWhere tests the distinction between:
+// 1. Aggregate without GROUP BY + WHERE false -> returns [NULL] (one row with aggregate result on empty set)
+// 2. Aggregate with GROUP BY + WHERE false -> returns [] (no groups match, so no rows)
+func TestGroupByWithConstantFalseWhere(t *testing.T) {
+	pool, err := storage.NewKVPool("localhost:8085", 5, 5*time.Second)
+	if err != nil {
+		t.Skip("PizzaKV not available, skipping test")
+	}
+	defer pool.Close()
+
+	schema := storage.NewSchemaManager(pool, "test_groupby_db")
+	table := storage.NewTableManager(pool, schema, "test_groupby_db")
+	exec := New(schema, table)
+
+	// Setup test tables
+	execSQL(exec, "DROP TABLE IF EXISTS tab0")
+	execSQL(exec, "DROP TABLE IF EXISTS tab1")
+
+	_, err = execSQL(exec, "CREATE TABLE tab0 (col0 INTEGER, col1 INTEGER, col2 INTEGER)")
+	if err != nil {
+		t.Fatalf("failed to create tab0: %v", err)
+	}
+
+	_, err = execSQL(exec, "CREATE TABLE tab1 (col0 INTEGER, col1 INTEGER, col2 INTEGER)")
+	if err != nil {
+		t.Fatalf("failed to create tab1: %v", err)
+	}
+
+	// Insert some test data
+	execSQL(exec, "INSERT INTO tab0 VALUES (1, 10, 100)")
+	execSQL(exec, "INSERT INTO tab0 VALUES (2, 20, 200)")
+	execSQL(exec, "INSERT INTO tab0 VALUES (3, 30, 300)")
+
+	execSQL(exec, "INSERT INTO tab1 VALUES (1, 10, 100)")
+	execSQL(exec, "INSERT INTO tab1 VALUES (2, 20, 200)")
+	execSQL(exec, "INSERT INTO tab1 VALUES (3, 30, 300)")
+
+	// Test 1: Aggregate with GROUP BY and constant FALSE WHERE -> should return empty result []
+	t.Run("aggregate_with_groupby_where_false", func(t *testing.T) {
+		result, err := execSQL(exec, "SELECT AVG(col1) FROM tab1 WHERE NULL IS NOT NULL GROUP BY col1")
+		if err != nil {
+			t.Fatalf("query failed: %v", err)
+		}
+		if result.RowCount != 0 {
+			t.Errorf("expected 0 rows (no groups), got %d rows with values: %v", result.RowCount, result.Rows)
+		}
+	})
+
+	// Test 2: Aggregate without GROUP BY and constant FALSE WHERE -> should return [NULL]
+	t.Run("aggregate_without_groupby_where_false", func(t *testing.T) {
+		result, err := execSQL(exec, "SELECT AVG(col1) FROM tab1 WHERE NULL IS NOT NULL")
+		if err != nil {
+			t.Fatalf("query failed: %v", err)
+		}
+		if result.RowCount != 1 {
+			t.Errorf("expected 1 row, got %d", result.RowCount)
+		}
+		if result.RowCount == 1 && result.Rows[0][0] != nil {
+			t.Errorf("expected NULL for aggregate on empty set, got %v", result.Rows[0][0])
+		}
+	})
+
+	// Test 3: More complex case from test failures
+	t.Run("complex_groupby_where_false", func(t *testing.T) {
+		result, err := execSQL(exec, "SELECT ALL AVG(+ col1) FROM tab1 WHERE NULL IS NULL AND NOT NULL IS NULL GROUP BY col1")
+		if err != nil {
+			t.Fatalf("query failed: %v", err)
+		}
+		if result.RowCount != 0 {
+			t.Errorf("expected 0 rows (no groups), got %d rows with values: %v", result.RowCount, result.Rows)
+		}
+	})
+
+	// Test 4: DISTINCT aggregate with GROUP BY and constant FALSE WHERE
+	t.Run("distinct_aggregate_with_groupby_where_false", func(t *testing.T) {
+		result, err := execSQL(exec, "SELECT DISTINCT AVG(DISTINCT - col2) FROM tab0 WHERE NOT NULL IS NULL GROUP BY col2")
+		if err != nil {
+			t.Fatalf("query failed: %v", err)
+		}
+		if result.RowCount != 0 {
+			t.Errorf("expected 0 rows (no groups), got %d rows with values: %v", result.RowCount, result.Rows)
+		}
+	})
+
+	// Test 5: Verify normal GROUP BY still works (WHERE true)
+	t.Run("normal_groupby_sanity_check", func(t *testing.T) {
+		result, err := execSQL(exec, "SELECT AVG(col1) FROM tab1 WHERE NULL IS NULL GROUP BY col1")
+		if err != nil {
+			t.Fatalf("query failed: %v", err)
+		}
+		if result.RowCount != 3 {
+			t.Errorf("expected 3 groups, got %d", result.RowCount)
+		}
+	})
+
+	// Cleanup
+	execSQL(exec, "DROP TABLE IF EXISTS tab0")
+	execSQL(exec, "DROP TABLE IF EXISTS tab1")
+}