2
0

qualified_wildcard_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. package executor
  2. import (
  3. "os"
  4. "os/exec"
  5. "path/filepath"
  6. "reflect"
  7. "sync"
  8. "testing"
  9. "time"
  10. "github.com/danfragoso/pizzasql-next/pkg/storage"
  11. )
  12. // startPizzaKV launches a local PizzaKV for a test and returns a pool connected
  13. // to it plus a cleanup func. Skips when PIZZAKV_BIN is not set.
  14. func startPizzaKV(t *testing.T) (*storage.KVPool, func()) {
  15. t.Helper()
  16. binary := os.Getenv("PIZZAKV_BIN")
  17. if binary == "" {
  18. t.Skip("PIZZAKV_BIN is not set")
  19. }
  20. dir := t.TempDir()
  21. socket := filepath.Join(dir, "kv.sock")
  22. database := filepath.Join(dir, "test.pkvdb")
  23. cmd := exec.Command(binary, "-unix="+socket, "-path="+database)
  24. cmd.Stdout = os.Stderr
  25. cmd.Stderr = os.Stderr
  26. if err := cmd.Start(); err != nil {
  27. t.Fatalf("start PizzaKV: %v", err)
  28. }
  29. var once sync.Once
  30. stop := func() {
  31. once.Do(func() {
  32. _ = cmd.Process.Kill()
  33. _ = cmd.Wait()
  34. })
  35. }
  36. t.Cleanup(stop)
  37. addr := "unix:" + socket
  38. deadline := time.Now().Add(10 * time.Second)
  39. for time.Now().Before(deadline) {
  40. pool, err := storage.NewKVPool(addr, 2, 5*time.Second)
  41. if err == nil {
  42. return pool, stop
  43. }
  44. time.Sleep(20 * time.Millisecond)
  45. }
  46. t.Fatal("PizzaKV did not become ready")
  47. return nil, nil
  48. }
  49. func TestQualifiedWildcardProjection(t *testing.T) {
  50. pool, stop := startPizzaKV(t)
  51. if pool == nil {
  52. return
  53. }
  54. defer stop()
  55. defer pool.Close()
  56. schema := storage.NewSchemaManager(pool, "gogs")
  57. table := storage.NewTableManager(pool, schema, "gogs")
  58. exec := New(schema, table)
  59. mustExec := func(sql string) *Result {
  60. t.Helper()
  61. res, err := execSQL(exec, sql)
  62. if err != nil {
  63. t.Fatalf("exec %q: %v", sql, err)
  64. }
  65. return res
  66. }
  67. mustExec(`CREATE TABLE repository (id INTEGER PRIMARY KEY, owner_id INTEGER, name TEXT)`)
  68. mustExec(`CREATE TABLE access (id INTEGER PRIMARY KEY, user_id INTEGER, repo_id INTEGER, mode INTEGER)`)
  69. mustExec(`INSERT INTO repository VALUES (1, 100, 'gogs')`)
  70. mustExec(`INSERT INTO repository VALUES (2, 100, 'pizza')`)
  71. mustExec(`INSERT INTO repository VALUES (3, 200, 'shared')`)
  72. mustExec(`INSERT INTO access VALUES (1, 1, 1, 2)`)
  73. mustExec(`INSERT INTO access VALUES (2, 1, 2, 1)`)
  74. mustExec(`INSERT INTO access VALUES (3, 2, 1, 1)`)
  75. t.Run("single_table_alias", func(t *testing.T) {
  76. res := mustExec(`SELECT repo.* FROM repository AS repo ORDER BY repo.id`)
  77. wantCols := []string{"id", "owner_id", "name"}
  78. if !reflect.DeepEqual(res.Columns, wantCols) {
  79. t.Fatalf("columns = %v, want %v", res.Columns, wantCols)
  80. }
  81. if res.RowCount != 3 {
  82. t.Fatalf("row count = %d, want 3", res.RowCount)
  83. }
  84. if res.Rows[0][0] != int64(1) || res.Rows[0][1] != int64(100) || res.Rows[0][2] != "gogs" {
  85. t.Fatalf("row 0 = %v", res.Rows[0])
  86. }
  87. })
  88. t.Run("distinct_left_join_only_repo", func(t *testing.T) {
  89. // The Gogs SearchRepositoryByName shape: qualified wildcard on the left
  90. // table of a LEFT JOIN must not leak joined-table columns.
  91. res := mustExec(`SELECT DISTINCT repo.* FROM repository AS repo LEFT JOIN access ON access.repo_id = repo.id WHERE repo.owner_id = 100 ORDER BY repo.id`)
  92. wantCols := []string{"id", "owner_id", "name"}
  93. if !reflect.DeepEqual(res.Columns, wantCols) {
  94. t.Fatalf("columns = %v, want %v", res.Columns, wantCols)
  95. }
  96. if res.RowCount != 2 {
  97. t.Fatalf("row count = %d, want 2 (DISTINCT dedupe)", res.RowCount)
  98. }
  99. if res.Rows[0][0] != int64(1) || res.Rows[0][2] != "gogs" {
  100. t.Fatalf("row 0 = %v", res.Rows[0])
  101. }
  102. if res.Rows[1][0] != int64(2) || res.Rows[1][2] != "pizza" {
  103. t.Fatalf("row 1 = %v", res.Rows[1])
  104. }
  105. })
  106. t.Run("mixed_wildcard_and_qualified_column", func(t *testing.T) {
  107. res := mustExec(`SELECT repo.*, access.mode FROM repository AS repo LEFT JOIN access ON access.repo_id = repo.id WHERE repo.id = 1 ORDER BY access.mode`)
  108. wantCols := []string{"id", "owner_id", "name", "mode"}
  109. if !reflect.DeepEqual(res.Columns, wantCols) {
  110. t.Fatalf("columns = %v, want %v", res.Columns, wantCols)
  111. }
  112. if res.RowCount != 2 {
  113. t.Fatalf("row count = %d, want 2", res.RowCount)
  114. }
  115. if res.Rows[0][3] != int64(1) || res.Rows[1][3] != int64(2) {
  116. t.Fatalf("modes = %v, want [1 2]", [][]interface{}{res.Rows[0], res.Rows[1]})
  117. }
  118. })
  119. t.Run("unknown_qualifier_errors", func(t *testing.T) {
  120. _, err := execSQL(exec, `SELECT nope.* FROM repository AS repo`)
  121. if err == nil {
  122. t.Fatal("expected error for unknown wildcard qualifier, got nil")
  123. }
  124. })
  125. t.Run("unaliased_table_wildcard", func(t *testing.T) {
  126. res := mustExec(`SELECT repository.* FROM repository ORDER BY id`)
  127. wantCols := []string{"id", "owner_id", "name"}
  128. if !reflect.DeepEqual(res.Columns, wantCols) {
  129. t.Fatalf("columns = %v, want %v", res.Columns, wantCols)
  130. }
  131. wantTypes := []string{"INTEGER", "INTEGER", "TEXT"}
  132. if !reflect.DeepEqual(res.ColumnTypes, wantTypes) {
  133. t.Fatalf("column types = %v, want %v", res.ColumnTypes, wantTypes)
  134. }
  135. })
  136. }