expression_index_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package storage
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. )
  7. // registerLowerEvaluator installs a tiny expression evaluator that understands
  8. // the lower(col) form, mirroring what the SQL executor registers in production.
  9. func registerLowerEvaluator(t *testing.T, tables *TableManager) {
  10. t.Helper()
  11. tables.SetExpressionEvaluator(func(expression string, row Row) (interface{}, error) {
  12. if strings.HasPrefix(expression, "lower(") && strings.HasSuffix(expression, ")") {
  13. col := expression[len("lower(") : len(expression)-1]
  14. if v, ok := row[col]; ok && v != nil {
  15. return strings.ToLower(fmt.Sprintf("%v", v)), nil
  16. }
  17. return nil, nil
  18. }
  19. return nil, fmt.Errorf("unsupported expression %q", expression)
  20. })
  21. }
  22. func TestExpressionUniqueIndexEnforced(t *testing.T) {
  23. _, _, schemas, tables := newTestSession(t)
  24. createTestTable(t, schemas, "users", []Column{
  25. {Name: "id", Type: "INTEGER", PrimaryKey: true},
  26. {Name: "email", Type: "TEXT"},
  27. })
  28. registerLowerEvaluator(t, tables)
  29. if err := schemas.CreateIndex(&Index{
  30. Name: "users_email_lower",
  31. Table: "users",
  32. Unique: true,
  33. Columns: []IndexColumn{
  34. {Name: "lower(email)", Expression: "lower(email)"},
  35. },
  36. }); err != nil {
  37. t.Fatal(err)
  38. }
  39. if err := tables.Insert("users", Row{"id": int64(1), "email": "Alice@Example.com"}); err != nil {
  40. t.Fatalf("first insert: %v", err)
  41. }
  42. err := tables.Insert("users", Row{"id": int64(2), "email": "alice@example.com"})
  43. if err == nil || !strings.Contains(err.Error(), "UNIQUE constraint failed") {
  44. t.Fatalf("expected case-insensitive uniqueness violation, got %v", err)
  45. }
  46. if err := tables.Insert("users", Row{"id": int64(3), "email": "bob@example.com"}); err != nil {
  47. t.Fatalf("distinct insert: %v", err)
  48. }
  49. }
  50. func TestExpressionUniqueIndexAllowsNull(t *testing.T) {
  51. _, _, schemas, tables := newTestSession(t)
  52. createTestTable(t, schemas, "users", []Column{
  53. {Name: "id", Type: "INTEGER", PrimaryKey: true},
  54. {Name: "email", Type: "TEXT", Nullable: true},
  55. })
  56. registerLowerEvaluator(t, tables)
  57. if err := schemas.CreateIndex(&Index{
  58. Name: "users_email_lower", Table: "users", Unique: true,
  59. Columns: []IndexColumn{{Name: "lower(email)", Expression: "lower(email)"}},
  60. }); err != nil {
  61. t.Fatal(err)
  62. }
  63. if err := tables.Insert("users", Row{"id": int64(1)}); err != nil {
  64. t.Fatal(err)
  65. }
  66. if err := tables.Insert("users", Row{"id": int64(2)}); err != nil {
  67. t.Fatalf("NULL expression values must be exempt from uniqueness: %v", err)
  68. }
  69. }
  70. func TestGeneratedColumnMetadataRoundTrip(t *testing.T) {
  71. _, _, schemas, _ := newTestSession(t)
  72. schema := &Schema{
  73. Name: "t",
  74. Columns: []Column{
  75. {Name: "a", Type: "INTEGER", Nullable: true},
  76. {Name: "b", Type: "INTEGER", Nullable: true, GeneratedExpr: "a + 1", GeneratedStored: true},
  77. },
  78. }
  79. if err := schemas.CreateTable(schema); err != nil {
  80. t.Fatal(err)
  81. }
  82. got, err := schemas.GetSchema("t")
  83. if err != nil {
  84. t.Fatal(err)
  85. }
  86. b, ok := got.GetColumn("b")
  87. if !ok {
  88. t.Fatal("column b missing")
  89. }
  90. if b.GeneratedExpr != "a + 1" || !b.GeneratedStored {
  91. t.Fatalf("generated metadata not persisted: %#v", b)
  92. }
  93. }
  94. func TestIndexExpressionMetadataRoundTrip(t *testing.T) {
  95. _, _, schemas, _ := newTestSession(t)
  96. createTestTable(t, schemas, "users", []Column{{Name: "email", Type: "TEXT"}})
  97. if err := schemas.CreateIndex(&Index{
  98. Name: "users_email_lower", Table: "users", Unique: true,
  99. Columns: []IndexColumn{{Name: "lower(email)", Expression: "lower(email)"}},
  100. }); err != nil {
  101. t.Fatal(err)
  102. }
  103. idx, err := schemas.GetIndex("users_email_lower")
  104. if err != nil {
  105. t.Fatal(err)
  106. }
  107. if len(idx.Columns) != 1 || idx.Columns[0].Expression != "lower(email)" {
  108. t.Fatalf("expression metadata not persisted: %#v", idx.Columns)
  109. }
  110. }
  111. func TestExpressionIndexEvaluatorErrorPropagates(t *testing.T) {
  112. _, _, schemas, tables := newTestSession(t)
  113. createTestTable(t, schemas, "users", []Column{
  114. {Name: "id", Type: "INTEGER", PrimaryKey: true},
  115. {Name: "email", Type: "TEXT", Nullable: true},
  116. })
  117. registerLowerEvaluator(t, tables)
  118. if err := schemas.CreateIndex(&Index{
  119. Name: "users_email_lower", Table: "users",
  120. Columns: []IndexColumn{{Name: "lower(email)", Expression: "lower(email)"}},
  121. }); err != nil {
  122. t.Fatal(err)
  123. }
  124. if err := tables.BuildIndex("users_email_lower", "users", []string{"lower(email)"}); err != nil {
  125. t.Fatal(err)
  126. }
  127. // A broken evaluator must surface, not be skipped during index maintenance.
  128. tables.SetExpressionEvaluator(func(expression string, row Row) (interface{}, error) {
  129. return nil, fmt.Errorf("evaluator boom")
  130. })
  131. if err := tables.Insert("users", Row{"id": int64(1), "email": "a@x"}); err == nil {
  132. t.Fatal("expected evaluator error to propagate from Insert")
  133. }
  134. }