arrays_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package analyzer
  2. import (
  3. "errors"
  4. "strings"
  5. "testing"
  6. "github.com/danfragoso/pizzasql-next/pkg/parser"
  7. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  8. )
  9. // arrayCatalog describes an array-typed table so expression analysis sees the
  10. // same declared types the executor resolves from storage.
  11. func arrayCatalog() *Catalog {
  12. catalog := NewCatalog()
  13. catalog.CreateTable(&TableInfo{
  14. Name: "documents",
  15. Columns: []ColumnInfo{
  16. {Name: "id", Type: TypeInteger, PrimaryKey: true},
  17. {Name: "tags", Type: TypeFromName("TEXT[]"), Nullable: true, Array: true},
  18. {Name: "nums", Type: TypeFromName("INTEGER[]"), Nullable: true, Array: true},
  19. },
  20. })
  21. return catalog
  22. }
  23. func TestArrayTypeNameKeepsElementAffinity(t *testing.T) {
  24. cases := map[string]Type{
  25. "TEXT[]": TypeText,
  26. "VARCHAR(10)[]": TypeText,
  27. "INTEGER[]": TypeInteger,
  28. "BIGINT[]": TypeInteger,
  29. "BOOLEAN[]": TypeBoolean,
  30. "DOUBLE PRECISION[]": TypeReal,
  31. "NUMERIC[]": TypeNumeric,
  32. }
  33. for name, want := range cases {
  34. if got := TypeFromName(name); got != want {
  35. t.Errorf("TypeFromName(%q) = %v, want %v", name, got, want)
  36. }
  37. }
  38. }
  39. func TestArrayColumnDDLAnalyzes(t *testing.T) {
  40. a := New(setupCatalog())
  41. if err := a.Analyze(parse(t, `CREATE TABLE tagged (id INTEGER PRIMARY KEY, tags TEXT[], nums INTEGER[])`)); err != nil {
  42. t.Fatalf("array column DDL: %v", err)
  43. }
  44. if err := a.Analyze(parse(t, `ALTER TABLE tagged ADD COLUMN flags BOOLEAN[]`)); err != nil {
  45. t.Fatalf("ALTER TABLE ADD array column: %v", err)
  46. }
  47. }
  48. func TestArrayExpressionsAnalyze(t *testing.T) {
  49. a := New(arrayCatalog())
  50. for _, sql := range []string{
  51. `SELECT tags FROM documents WHERE 'go' = ANY(tags)`,
  52. `SELECT tags FROM documents WHERE 'go' <> ALL(tags)`,
  53. `SELECT nums FROM documents WHERE id = ANY(ARRAY[1,2])`,
  54. `SELECT tags[1] FROM documents WHERE tags[1] = 'go'`,
  55. `SELECT array_length(tags, 1) FROM documents`,
  56. `SELECT ARRAY['a','b'] FROM documents`,
  57. `SELECT '{1,2}'::INTEGER[] FROM documents`,
  58. `SELECT nums FROM documents WHERE nums = '{3,4}'`,
  59. `SELECT nums FROM documents WHERE '{3,4}' = nums`,
  60. `SELECT nums FROM documents WHERE nums <> '{3,4}'`,
  61. } {
  62. if err := a.Analyze(parse(t, sql)); err != nil {
  63. t.Errorf("analyze %q: %v", sql, err)
  64. }
  65. }
  66. }
  67. func TestArrayExpressionRejections(t *testing.T) {
  68. a := New(arrayCatalog())
  69. cases := []struct {
  70. sql string
  71. want string
  72. }{
  73. {`SELECT missing[1] FROM documents`, "column not found"},
  74. {`SELECT tags['x'] FROM documents`, "array subscript must be an integer expression"},
  75. // A text literal is only coercible to an array side, not to a scalar
  76. // integer column.
  77. {`SELECT id FROM documents WHERE id = '{3,4}'`, "cannot compare INTEGER with TEXT"},
  78. // An array column still does not compare with a scalar number.
  79. {`SELECT nums FROM documents WHERE nums = 5`, "cannot compare INTEGER with INTEGER"},
  80. }
  81. for _, tc := range cases {
  82. err := a.Analyze(parse(t, tc.sql))
  83. if err == nil {
  84. t.Errorf("%q analyzed, want an error", tc.sql)
  85. continue
  86. }
  87. if !strings.Contains(err.Error(), tc.want) {
  88. t.Errorf("%q error = %v, want it to mention %q", tc.sql, err, tc.want)
  89. }
  90. }
  91. }
  92. func TestUnnestFromDefinesRelation(t *testing.T) {
  93. a := New(arrayCatalog())
  94. stmt := parse(t, `SELECT u.unnest FROM unnest('{1,2}'::INTEGER[]) AS u`).(*parser.SelectStmt)
  95. if err := a.Analyze(stmt); err != nil {
  96. t.Fatalf("unnest FROM analysis: %v", err)
  97. }
  98. }
  99. func TestTableFunctionColumnAliasesAnalyze(t *testing.T) {
  100. a := New(arrayCatalog())
  101. for _, sql := range []string{
  102. `SELECT u.x FROM unnest('{1,2}'::INTEGER[]) AS u(x)`,
  103. `SELECT x FROM unnest('{1,2}'::INTEGER[]) AS u(x) WHERE x > 1`,
  104. `SELECT e.k, e.v FROM jsonb_each('{"a":1}') AS e(k, v)`,
  105. } {
  106. if err := a.Analyze(parse(t, sql)); err != nil {
  107. t.Errorf("analyze %q: %v", sql, err)
  108. }
  109. }
  110. // A column alias list must match the function's output arity.
  111. err := a.Analyze(parse(t, `SELECT * FROM jsonb_each('{"a":1}') AS e(k)`))
  112. if !errors.Is(err, pgerror.ErrInvalidColumnReference) {
  113. t.Fatalf("arity error = %v, want ErrInvalidColumnReference", err)
  114. }
  115. }
  116. func TestArrayContainmentAnalyzes(t *testing.T) {
  117. a := New(arrayCatalog())
  118. for _, sql := range []string{
  119. `SELECT nums FROM documents WHERE nums @> ARRAY[1]`,
  120. `SELECT nums FROM documents WHERE ARRAY[1] <@ nums`,
  121. `SELECT nums FROM documents WHERE nums @> '{1}'`,
  122. `SELECT tags FROM documents WHERE tags @> ARRAY['go']`,
  123. `SELECT nums FROM documents WHERE nums @> NULL`,
  124. } {
  125. if err := a.Analyze(parse(t, sql)); err != nil {
  126. t.Errorf("analyze %q: %v", sql, err)
  127. }
  128. }
  129. for _, sql := range []string{
  130. `SELECT nums FROM documents WHERE nums @> 5`,
  131. `SELECT nums FROM documents WHERE 5 <@ nums`,
  132. } {
  133. if err := a.Analyze(parse(t, sql)); err == nil {
  134. t.Errorf("analyze %q succeeded, want an array containment type error", sql)
  135. }
  136. }
  137. }