2
0

dashboard_repro_test.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package executor
  2. import "testing"
  3. // TestSelectColumnTypesGroupBy verifies that GROUP BY projections keep their
  4. // schema types, so protocol clients can decode timestamp/date columns even
  5. // though the grouped projection is not a plain table scan.
  6. func TestSelectColumnTypesGroupBy(t *testing.T) {
  7. _, schema, table := newTestDB(t)
  8. e := newExec(schema, table)
  9. execMust(t, e, `CREATE TABLE hit_counts (
  10. site_id INTEGER NOT NULL,
  11. path_id INTEGER NOT NULL,
  12. hour TIMESTAMP NOT NULL,
  13. total INTEGER NOT NULL
  14. )`)
  15. execMust(t, e, `CREATE TABLE hit_stats (
  16. site_id INTEGER NOT NULL,
  17. path_id INTEGER NOT NULL,
  18. day DATE NOT NULL,
  19. stats TEXT
  20. )`)
  21. execMust(t, e, `INSERT INTO hit_counts VALUES (1, 1, '2024-05-06 07:00:00', 3)`)
  22. execMust(t, e, `INSERT INTO hit_stats VALUES (1, 1, '2024-05-06', '[]')`)
  23. res := execMust(t, e, `SELECT hour, sum(total) FROM hit_counts GROUP BY hour ORDER BY hour`)
  24. want := []string{"TIMESTAMP", "TEXT"}
  25. if len(res.ColumnTypes) != len(want) {
  26. t.Fatalf("group-by column types = %v, want %v", res.ColumnTypes, want)
  27. }
  28. for i := range want {
  29. if res.ColumnTypes[i] != want[i] {
  30. t.Fatalf("group-by column types = %v, want %v", res.ColumnTypes, want)
  31. }
  32. }
  33. res = execMust(t, e, `SELECT path_id, day, stats FROM hit_stats ORDER BY day`)
  34. want = []string{"INTEGER", "DATE", "TEXT"}
  35. if len(res.ColumnTypes) != len(want) {
  36. t.Fatalf("date column types = %v, want %v", res.ColumnTypes, want)
  37. }
  38. for i := range want {
  39. if res.ColumnTypes[i] != want[i] {
  40. t.Fatalf("date column types = %v, want %v", res.ColumnTypes, want)
  41. }
  42. }
  43. }
  44. // TestSubstrSQLiteSemantics pins SQLite's zero/negative start behavior, which
  45. // GoatCounter relies on to derive a country code from a region code.
  46. func TestSubstrSQLiteSemantics(t *testing.T) {
  47. _, schema, table := newTestDB(t)
  48. e := newExec(schema, table)
  49. cases := []struct {
  50. sql string
  51. want string
  52. }{
  53. {`SELECT substr('US-NY', 0, 3)`, "US"},
  54. {`SELECT substr('US-NY', 1, 3)`, "US-"},
  55. {`SELECT substr('US-NY', 2, 3)`, "S-N"},
  56. {`SELECT substr('US-NY', -2, 2)`, "NY"},
  57. {`SELECT substr('US-NY', 4)`, "NY"},
  58. {`SELECT substr('US-NY', 0)`, "US-NY"},
  59. }
  60. for _, tc := range cases {
  61. res := execMust(t, e, tc.sql)
  62. if len(res.Rows) != 1 || res.Rows[0][0] != tc.want {
  63. t.Fatalf("%s = %#v, want %q", tc.sql, res.Rows, tc.want)
  64. }
  65. }
  66. }
  67. // TestCTEGroupByAlias reproduces the GoatCounter locations query: a CTE that
  68. // groups by a SELECT alias, with the outer query joining on that alias column.
  69. func TestCTEGroupByAlias(t *testing.T) {
  70. _, schema, table := newTestDB(t)
  71. e := newExec(schema, table)
  72. execMust(t, e, `CREATE TABLE location_stats (
  73. site_id INTEGER NOT NULL,
  74. path_id INTEGER NOT NULL,
  75. day DATE NOT NULL,
  76. location TEXT NOT NULL,
  77. count INTEGER NOT NULL
  78. )`)
  79. execMust(t, e, `CREATE TABLE locations (
  80. iso_3166_2 TEXT NOT NULL,
  81. country_name TEXT NOT NULL
  82. )`)
  83. execMust(t, e, `INSERT INTO location_stats VALUES (1, 1, '2024-05-06', 'US-NY', 4)`)
  84. execMust(t, e, `INSERT INTO locations VALUES ('US', 'United States')`)
  85. res := execMust(t, e, `WITH x AS (
  86. SELECT substr(location, 0, 3) AS loc, sum(count) AS count
  87. FROM location_stats
  88. WHERE site_id = 1 AND day >= '2024-01-01' AND day <= '2024-12-31'
  89. GROUP BY loc
  90. ORDER BY count DESC, loc
  91. LIMIT 5
  92. )
  93. SELECT locations.iso_3166_2 AS id, locations.country_name AS name, x.count AS count
  94. FROM x
  95. JOIN locations ON locations.iso_3166_2 = x.loc
  96. ORDER BY count DESC, name ASC`)
  97. if res.RowCount != 1 {
  98. t.Fatalf("expected 1 row, got %d (%v)", res.RowCount, res.Rows)
  99. }
  100. if res.Rows[0][0] != "US" || res.Rows[0][2].(int64) != 4 {
  101. t.Fatalf("unexpected row %#v", res.Rows[0])
  102. }
  103. }