| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- package executor
- import (
- "fmt"
- "testing"
- "time"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- // newOptExec creates an executor backed by PizzaKV, skipping the test when it
- // is unavailable (mirroring the convention in executor_test.go).
- func newOptExec(t *testing.T, db string) *Executor {
- t.Helper()
- pool, err := storage.NewKVPool("localhost:8085", 5, 5*time.Second)
- if err != nil {
- t.Skipf("PizzaKV not available: %v", err)
- }
- t.Cleanup(func() { pool.Close() })
- schema := storage.NewSchemaManager(pool, db)
- table := storage.NewTableManager(pool, schema, db)
- return New(schema, table)
- }
- // intColumn extracts a column of int64 values from a result.
- func intColumn(t *testing.T, r *Result, col int) []int64 {
- t.Helper()
- out := make([]int64, 0, len(r.Rows))
- for _, row := range r.Rows {
- if col >= len(row) {
- t.Fatalf("row too short: %v", row)
- }
- out = append(out, row[col].(int64))
- }
- return out
- }
- // TestTopNOrderByLimitMatchesFullSort verifies bounded top-N execution returns
- // exactly the same rows (order included) as a full sort followed by LIMIT/OFFSET.
- func TestTopNOrderByLimitMatchesFullSort(t *testing.T) {
- exec := newOptExec(t, "test_topn_db")
- execSQL(exec, "DROP TABLE IF EXISTS nums")
- if _, err := execSQL(exec, "CREATE TABLE nums (id INTEGER PRIMARY KEY, v INTEGER)"); err != nil {
- t.Fatalf("create: %v", err)
- }
- defer execSQL(exec, "DROP TABLE IF EXISTS nums")
- const n = 500
- for i := 0; i < n; i++ {
- // Deterministic permutation of 0..n-1.
- v := (i*137 + 41) % n
- if _, err := execSQL(exec, fmt.Sprintf("INSERT INTO nums VALUES (%d, %d)", i+1, v)); err != nil {
- t.Fatalf("insert: %v", err)
- }
- }
- for _, desc := range []bool{false, true} {
- for _, offset := range []int{0, 3, 47, n - 1} {
- for _, limit := range []int{1, 2, 17, 100, n + 5} {
- dir := "ASC"
- if desc {
- dir = "DESC"
- }
- fullRes, err := execSQL(exec, fmt.Sprintf("SELECT v FROM nums ORDER BY v %s", dir))
- if err != nil {
- t.Fatalf("full: %v", err)
- }
- full := intColumn(t, fullRes, 0)
- q := fmt.Sprintf("SELECT v FROM nums ORDER BY v %s LIMIT %d OFFSET %d", dir, limit, offset)
- limRes, err := execSQL(exec, q)
- if err != nil {
- t.Fatalf("%s: %v", q, err)
- }
- got := intColumn(t, limRes, 0)
- want := sliceRange(full, offset, limit)
- if !equalInt64s(got, want) {
- t.Fatalf("%s: got %v want %v", q, got, want)
- }
- }
- }
- }
- }
- // TestTopNOrderByLimitTies verifies LIMIT/OFFSET with tied ORDER BY keys returns
- // the correct multiset of values even though tie ordering is unspecified.
- func TestTopNOrderByLimitTies(t *testing.T) {
- exec := newOptExec(t, "test_topn_ties_db")
- execSQL(exec, "DROP TABLE IF EXISTS ties")
- if _, err := execSQL(exec, "CREATE TABLE ties (id INTEGER PRIMARY KEY, v INTEGER)"); err != nil {
- t.Fatalf("create: %v", err)
- }
- defer execSQL(exec, "DROP TABLE IF EXISTS ties")
- // 4 rows with v=1, 2 rows with v=2, 1 row with v=3.
- vals := []int{1, 1, 1, 1, 2, 2, 3}
- for i, v := range vals {
- if _, err := execSQL(exec, fmt.Sprintf("INSERT INTO ties VALUES (%d, %d)", i+1, v)); err != nil {
- t.Fatalf("insert: %v", err)
- }
- }
- // LIMIT 3: the three smallest, all v=1.
- res, err := execSQL(exec, "SELECT v FROM ties ORDER BY v LIMIT 3")
- if err != nil {
- t.Fatalf("limit 3: %v", err)
- }
- got := intColumn(t, res, 0)
- if len(got) != 3 || got[0] != 1 || got[1] != 1 || got[2] != 1 {
- t.Fatalf("LIMIT 3 got %v, want [1 1 1]", got)
- }
- // OFFSET 3 LIMIT 3: skip three v=1 rows, then one v=1 + two v=2.
- res, err = execSQL(exec, "SELECT v FROM ties ORDER BY v LIMIT 3 OFFSET 3")
- if err != nil {
- t.Fatalf("offset 3 limit 3: %v", err)
- }
- got = intColumn(t, res, 0)
- if len(got) != 3 || got[0] != 1 || got[1] != 2 || got[2] != 2 {
- t.Fatalf("OFFSET 3 LIMIT 3 got %v, want [1 2 2]", got)
- }
- // OFFSET beyond the ties boundary.
- res, err = execSQL(exec, "SELECT v FROM ties ORDER BY v LIMIT 2 OFFSET 5")
- if err != nil {
- t.Fatalf("offset 5 limit 2: %v", err)
- }
- got = intColumn(t, res, 0)
- if len(got) != 2 || got[0] != 2 || got[1] != 3 {
- t.Fatalf("OFFSET 5 LIMIT 2 got %v, want [2 3]", got)
- }
- }
- // TestTopNGroupByOrderLimit verifies the grouped/result-row top-N path.
- func TestTopNGroupByOrderLimit(t *testing.T) {
- exec := newOptExec(t, "test_topn_group_db")
- execSQL(exec, "DROP TABLE IF EXISTS sales")
- if _, err := execSQL(exec, "CREATE TABLE sales (id INTEGER PRIMARY KEY, region TEXT, amount INTEGER)"); err != nil {
- t.Fatalf("create: %v", err)
- }
- defer execSQL(exec, "DROP TABLE IF EXISTS sales")
- for _, r := range []struct {
- id int
- region string
- amount int
- }{
- {1, "east", 10}, {2, "west", 20}, {3, "north", 30},
- {4, "south", 40}, {5, "east", 50},
- } {
- if _, err := execSQL(exec, fmt.Sprintf("INSERT INTO sales VALUES (%d, '%s', %d)", r.id, r.region, r.amount)); err != nil {
- t.Fatalf("insert: %v", err)
- }
- }
- res, err := execSQL(exec, "SELECT region, COUNT(*) AS c FROM sales GROUP BY region ORDER BY region LIMIT 2")
- if err != nil {
- t.Fatalf("group topn: %v", err)
- }
- if len(res.Rows) != 2 {
- t.Fatalf("expected 2 rows, got %d: %v", len(res.Rows), res.Rows)
- }
- if res.Rows[0][0] != "east" || res.Rows[1][0] != "north" {
- t.Fatalf("unexpected top-2 regions: %v", res.Rows)
- }
- if res.Rows[0][1] != int64(2) {
- t.Fatalf("unexpected east count: %v", res.Rows[0][1])
- }
- }
- // TestCountFastPathLifecycleAndRollback verifies the exact COUNT(*) fast path
- // across writes and transaction rollback.
- func TestCountFastPathLifecycleAndRollback(t *testing.T) {
- exec := newOptExec(t, "test_count_db")
- execSQL(exec, "DROP TABLE IF EXISTS items")
- if _, err := execSQL(exec, "CREATE TABLE items (id INTEGER PRIMARY KEY, v TEXT)"); err != nil {
- t.Fatalf("create: %v", err)
- }
- defer execSQL(exec, "DROP TABLE IF EXISTS items")
- countStar := func() int64 {
- t.Helper()
- res, err := execSQL(exec, "SELECT COUNT(*) FROM items")
- if err != nil {
- t.Fatalf("count: %v", err)
- }
- return res.Rows[0][0].(int64)
- }
- if got := countStar(); got != 0 {
- t.Fatalf("initial count = %d, want 0", got)
- }
- for i := 1; i <= 5; i++ {
- if _, err := execSQL(exec, fmt.Sprintf("INSERT INTO items VALUES (%d, 'x%d')", i, i)); err != nil {
- t.Fatalf("insert: %v", err)
- }
- }
- if got := countStar(); got != 5 {
- t.Fatalf("after inserts = %d, want 5", got)
- }
- // UPDATE keeps the count exact.
- if _, err := execSQL(exec, "UPDATE items SET v = 'y' WHERE id = 1"); err != nil {
- t.Fatalf("update: %v", err)
- }
- if got := countStar(); got != 5 {
- t.Fatalf("after update = %d, want 5", got)
- }
- // Bulk insert via INSERT ... SELECT.
- if _, err := execSQL(exec, "INSERT INTO items (id, v) SELECT id + 100, v FROM items"); err != nil {
- t.Fatalf("insert-select: %v", err)
- }
- if got := countStar(); got != 10 {
- t.Fatalf("after insert-select = %d, want 10", got)
- }
- // DELETE decrements.
- if _, err := execSQL(exec, "DELETE FROM items WHERE id > 100"); err != nil {
- t.Fatalf("delete: %v", err)
- }
- if got := countStar(); got != 5 {
- t.Fatalf("after delete = %d, want 5", got)
- }
- // Transaction rollback restores the exact count.
- execSQL(exec, "BEGIN")
- if _, err := execSQL(exec, "INSERT INTO items VALUES (999, 'tmp')"); err != nil {
- t.Fatalf("tx insert: %v", err)
- }
- if got := countStar(); got != 6 {
- t.Fatalf("inside tx = %d, want 6", got)
- }
- if _, err := execSQL(exec, "ROLLBACK"); err != nil {
- t.Fatalf("rollback: %v", err)
- }
- if got := countStar(); got != 5 {
- t.Fatalf("after rollback = %d, want 5", got)
- }
- }
- // TestCountFastPathRestart verifies a second executor (fresh process state)
- // derives the same exact count from durable rows.
- func TestCountFastPathRestart(t *testing.T) {
- exec := newOptExec(t, "test_count_restart_db")
- execSQL(exec, "DROP TABLE IF EXISTS r")
- if _, err := execSQL(exec, "CREATE TABLE r (id INTEGER PRIMARY KEY)"); err != nil {
- t.Fatalf("create: %v", err)
- }
- defer execSQL(exec, "DROP TABLE IF EXISTS r")
- for i := 1; i <= 7; i++ {
- if _, err := execSQL(exec, fmt.Sprintf("INSERT INTO r VALUES (%d)", i)); err != nil {
- t.Fatalf("insert: %v", err)
- }
- }
- // A brand-new executor over the same KV.
- exec2 := newOptExec(t, "test_count_restart_db")
- res, err := execSQL(exec2, "SELECT COUNT(*) FROM r")
- if err != nil {
- t.Fatalf("count after restart: %v", err)
- }
- if got := res.Rows[0][0].(int64); got != 7 {
- t.Fatalf("count after restart = %d, want 7", got)
- }
- }
- // TestCountFastPathUnsupportedShapesStillCorrect verifies shapes outside the
- // fast path fall through to the normal scan and produce correct results.
- func TestCountFastPathUnsupportedShapesStillCorrect(t *testing.T) {
- exec := newOptExec(t, "test_count_unsupported_db")
- execSQL(exec, "DROP TABLE IF EXISTS t2")
- execSQL(exec, "DROP TABLE IF EXISTS t1")
- if _, err := execSQL(exec, "CREATE TABLE t1 (id INTEGER PRIMARY KEY, x INTEGER)"); err != nil {
- t.Fatalf("create t1: %v", err)
- }
- if _, err := execSQL(exec, "CREATE TABLE t2 (id INTEGER PRIMARY KEY, y INTEGER)"); err != nil {
- t.Fatalf("create t2: %v", err)
- }
- defer execSQL(exec, "DROP TABLE IF EXISTS t2")
- defer execSQL(exec, "DROP TABLE IF EXISTS t1")
- for i := 1; i <= 4; i++ {
- execSQL(exec, fmt.Sprintf("INSERT INTO t1 VALUES (%d, %d)", i, i))
- execSQL(exec, fmt.Sprintf("INSERT INTO t2 VALUES (%d, %d)", i, i))
- }
- cases := []struct {
- q string
- want int64
- }{
- {"SELECT COUNT(*) FROM t1 WHERE x > 2", 2},
- {"SELECT COUNT(DISTINCT x) FROM t1", 4},
- {"SELECT COUNT(*) FROM t1 t1a, t2 t2b", 16},
- }
- for _, c := range cases {
- res, err := execSQL(exec, c.q)
- if err != nil {
- t.Fatalf("%s: %v", c.q, err)
- }
- if got := res.Rows[0][0].(int64); got != c.want {
- t.Fatalf("%s = %d, want %d", c.q, got, c.want)
- }
- }
- }
- func sliceRange(v []int64, offset, limit int) []int64 {
- if offset >= len(v) {
- return nil
- }
- end := offset + limit
- if end > len(v) {
- end = len(v)
- }
- return v[offset:end]
- }
- func equalInt64s(a, b []int64) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
- }
|