| 1 | // vtest build: present_sqlite3? && !sanitize-memory-clang |
| 2 | // vtest vflags: -prod |
| 3 | // Tests for generic ORM with WHERE clause variable shadowing field names. |
| 4 | // This tests the fix for the bug where `select from T where status == status` |
| 5 | // (where `status` is both a field name and a local variable) was incorrectly |
| 6 | // optimized to `true` by the transformer, resulting in empty WHERE clauses. |
| 7 | import db.sqlite |
| 8 | |
| 9 | pub enum TestStatus as u8 { |
| 10 | pending |
| 11 | active |
| 12 | completed |
| 13 | } |
| 14 | |
| 15 | pub struct GenericRecord[T] { |
| 16 | pub: |
| 17 | id int @[primary; sql: serial] |
| 18 | status TestStatus |
| 19 | payload T @[sql: '-'] |
| 20 | } |
| 21 | |
| 22 | struct TestPayload { |
| 23 | value int |
| 24 | } |
| 25 | |
| 26 | // Test that WHERE clause works when variable name shadows field name |
| 27 | fn test_generic_where_variable_shadows_field() { |
| 28 | mut db := sqlite.connect(':memory:') or { panic(err) } |
| 29 | defer { db.close() or {} } |
| 30 | |
| 31 | sql db { |
| 32 | create table GenericRecord[TestPayload] |
| 33 | } or { panic(err) } |
| 34 | |
| 35 | // Insert records with different statuses |
| 36 | r1 := GenericRecord[TestPayload]{ |
| 37 | status: .pending |
| 38 | payload: TestPayload{ |
| 39 | value: 1 |
| 40 | } |
| 41 | } |
| 42 | r2 := GenericRecord[TestPayload]{ |
| 43 | status: .active |
| 44 | payload: TestPayload{ |
| 45 | value: 2 |
| 46 | } |
| 47 | } |
| 48 | r3 := GenericRecord[TestPayload]{ |
| 49 | status: .active |
| 50 | payload: TestPayload{ |
| 51 | value: 3 |
| 52 | } |
| 53 | } |
| 54 | r4 := GenericRecord[TestPayload]{ |
| 55 | status: .completed |
| 56 | payload: TestPayload{ |
| 57 | value: 4 |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | sql db { |
| 62 | insert r1 into GenericRecord[TestPayload] |
| 63 | insert r2 into GenericRecord[TestPayload] |
| 64 | insert r3 into GenericRecord[TestPayload] |
| 65 | insert r4 into GenericRecord[TestPayload] |
| 66 | } or { panic(err) } |
| 67 | |
| 68 | // Test: variable `status` shadows field `status` |
| 69 | // Before the fix, this would return all rows or error because |
| 70 | // `status == status` was optimized to `true` |
| 71 | status := TestStatus.active |
| 72 | results := sql db { |
| 73 | select from GenericRecord[TestPayload] where status == status |
| 74 | } or { panic(err) } |
| 75 | |
| 76 | assert results.len == 2, 'Expected 2 active records, got ${results.len}' |
| 77 | |
| 78 | // Test with different status values |
| 79 | status2 := TestStatus.pending |
| 80 | pending_results := sql db { |
| 81 | select from GenericRecord[TestPayload] where status == status2 |
| 82 | } or { panic(err) } |
| 83 | assert pending_results.len == 1, 'Expected 1 pending record, got ${pending_results.len}' |
| 84 | |
| 85 | status3 := TestStatus.completed |
| 86 | completed_results := sql db { |
| 87 | select from GenericRecord[TestPayload] where status == status3 |
| 88 | } or { panic(err) } |
| 89 | assert completed_results.len == 1, 'Expected 1 completed record, got ${completed_results.len}' |
| 90 | } |
| 91 | |
| 92 | // Test generic select helper function with shadowed variable |
| 93 | fn generic_select_by_status[T](db &sqlite.DB, status TestStatus) ![]GenericRecord[T] { |
| 94 | return sql db { |
| 95 | select from GenericRecord[T] where status == status |
| 96 | }! |
| 97 | } |
| 98 | |
| 99 | fn test_generic_function_where_shadow() { |
| 100 | mut db := sqlite.connect(':memory:') or { panic(err) } |
| 101 | defer { db.close() or {} } |
| 102 | |
| 103 | sql db { |
| 104 | create table GenericRecord[TestPayload] |
| 105 | } or { panic(err) } |
| 106 | |
| 107 | r1 := GenericRecord[TestPayload]{ |
| 108 | status: .pending |
| 109 | payload: TestPayload{ |
| 110 | value: 10 |
| 111 | } |
| 112 | } |
| 113 | r2 := GenericRecord[TestPayload]{ |
| 114 | status: .active |
| 115 | payload: TestPayload{ |
| 116 | value: 20 |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | sql db { |
| 121 | insert r1 into GenericRecord[TestPayload] |
| 122 | insert r2 into GenericRecord[TestPayload] |
| 123 | } or { panic(err) } |
| 124 | |
| 125 | // Call generic function where parameter shadows field name |
| 126 | active_records := generic_select_by_status[TestPayload](&db, .active) or { panic(err) } |
| 127 | assert active_records.len == 1, 'Expected 1 active record from generic function' |
| 128 | assert active_records[0].status == .active |
| 129 | |
| 130 | pending_records := generic_select_by_status[TestPayload](&db, .pending) or { panic(err) } |
| 131 | assert pending_records.len == 1, 'Expected 1 pending record from generic function' |
| 132 | assert pending_records[0].status == .pending |
| 133 | } |
| 134 | |
| 135 | // Test count with shadowed variable |
| 136 | fn test_generic_count_where_shadow() { |
| 137 | mut db := sqlite.connect(':memory:') or { panic(err) } |
| 138 | defer { db.close() or {} } |
| 139 | |
| 140 | sql db { |
| 141 | create table GenericRecord[TestPayload] |
| 142 | } or { panic(err) } |
| 143 | |
| 144 | for i in 0 .. 5 { |
| 145 | r := GenericRecord[TestPayload]{ |
| 146 | status: if i < 3 { .active } else { .completed } |
| 147 | payload: TestPayload{ |
| 148 | value: i |
| 149 | } |
| 150 | } |
| 151 | sql db { |
| 152 | insert r into GenericRecord[TestPayload] |
| 153 | } or { panic(err) } |
| 154 | } |
| 155 | |
| 156 | status := TestStatus.active |
| 157 | count := sql db { |
| 158 | select count from GenericRecord[TestPayload] where status == status |
| 159 | } or { panic(err) } |
| 160 | |
| 161 | assert count == 3, 'Expected count 3, got ${count}' |
| 162 | } |
| 163 | |
| 164 | // Test that table name doesn't include generic brackets |
| 165 | fn test_generic_table_name() { |
| 166 | mut db := sqlite.connect(':memory:') or { panic(err) } |
| 167 | defer { db.close() or {} } |
| 168 | |
| 169 | // This should create table named "GenericRecord", not "GenericRecord[TestPayload]" |
| 170 | sql db { |
| 171 | create table GenericRecord[TestPayload] |
| 172 | } or { panic(err) } |
| 173 | |
| 174 | // Insert and select should work without SQL syntax errors |
| 175 | r := GenericRecord[TestPayload]{ |
| 176 | status: .pending |
| 177 | payload: TestPayload{ |
| 178 | value: 42 |
| 179 | } |
| 180 | } |
| 181 | sql db { |
| 182 | insert r into GenericRecord[TestPayload] |
| 183 | } or { panic(err) } |
| 184 | |
| 185 | results := sql db { |
| 186 | select from GenericRecord[TestPayload] |
| 187 | } or { panic(err) } |
| 188 | |
| 189 | assert results.len == 1 |
| 190 | } |
| 191 | |