|
| 1 | +/* |
| 2 | +** Copyright (c) 2025 Oracle and/or its affiliates. |
| 3 | +** |
| 4 | +** The Universal Permissive License (UPL), Version 1.0 |
| 5 | +** |
| 6 | +** Subject to the condition set forth below, permission is hereby granted to any |
| 7 | +** person obtaining a copy of this software, associated documentation and/or data |
| 8 | +** (collectively the "Software"), free of charge and under any and all copyright |
| 9 | +** rights in the Software, and any and all patent rights owned or freely |
| 10 | +** licensable by each licensor hereunder covering either (i) the unmodified |
| 11 | +** Software as contributed to or provided by such licensor, or (ii) the Larger |
| 12 | +** Works (as defined below), to deal in both |
| 13 | +** |
| 14 | +** (a) the Software, and |
| 15 | +** (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if |
| 16 | +** one is included with the Software (each a "Larger Work" to which the Software |
| 17 | +** is contributed by such licensors), |
| 18 | +** |
| 19 | +** without restriction, including without limitation the rights to copy, create |
| 20 | +** derivative works of, display, perform, and distribute the Software and make, |
| 21 | +** use, sell, offer for sale, import, export, have made, and have sold the |
| 22 | +** Software and the Larger Work(s), and to sublicense the foregoing rights on |
| 23 | +** either these or other terms. |
| 24 | +** |
| 25 | +** This license is subject to the following condition: |
| 26 | +** The above copyright notice and either this complete permission notice or at |
| 27 | +** a minimum a reference to the UPL must be included in all copies or |
| 28 | +** substantial portions of the Software. |
| 29 | +** |
| 30 | +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 31 | +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 32 | +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 33 | +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 34 | +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 35 | +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 36 | +** SOFTWARE. |
| 37 | +*/ |
| 38 | + |
| 39 | +package tests |
| 40 | + |
| 41 | +import ( |
| 42 | + "database/sql" |
| 43 | + "math" |
| 44 | + "testing" |
| 45 | + "time" |
| 46 | + |
| 47 | + "gorm.io/gorm" |
| 48 | + "gorm.io/gorm/clause" |
| 49 | +) |
| 50 | + |
| 51 | +// IntegerTestModel covers basic integer data types. |
| 52 | +type IntegerTestModel struct { |
| 53 | + ID uint `gorm:"primaryKey;autoIncrement"` |
| 54 | + Int8Value int8 `gorm:"column:INT8_VALUE"` |
| 55 | + Int16Value int16 `gorm:"column:INT16_VALUE"` |
| 56 | + Int32Value int32 `gorm:"column:INT32_VALUE"` |
| 57 | + Int64Value int64 `gorm:"column:INT64_VALUE"` |
| 58 | + IntValue int `gorm:"column:INT_VALUE"` |
| 59 | + Uint8Value uint8 `gorm:"column:UINT8_VALUE"` |
| 60 | + Uint16Value uint16 `gorm:"column:UINT16_VALUE"` |
| 61 | + Uint32Value uint32 `gorm:"column:UINT32_VALUE"` |
| 62 | + Uint64Value uint64 `gorm:"column:UINT64_VALUE"` |
| 63 | + UintValue uint `gorm:"column:UINT_VALUE"` |
| 64 | + CreatedAt time.Time |
| 65 | + UpdatedAt time.Time |
| 66 | +} |
| 67 | + |
| 68 | +// NullableIntegerTestModel tests nullable and optional integer types. |
| 69 | +type NullableIntegerTestModel struct { |
| 70 | + ID uint `gorm:"primaryKey;autoIncrement"` |
| 71 | + NullInt32 sql.NullInt32 `gorm:"column:NULL_INT32"` |
| 72 | + NullInt64 sql.NullInt64 `gorm:"column:NULL_INT64"` |
| 73 | + OptionalInt32 *int32 `gorm:"column:OPTIONAL_INT32"` |
| 74 | + OptionalInt64 *int64 `gorm:"column:OPTIONAL_INT64"` |
| 75 | + OptionalUint32 *uint32 `gorm:"column:OPTIONAL_UINT32"` |
| 76 | + OptionalUint64 *uint64 `gorm:"column:OPTIONAL_UINT64"` |
| 77 | + CreatedAt time.Time |
| 78 | + UpdatedAt time.Time |
| 79 | +} |
| 80 | + |
| 81 | +// setupIntegerTestTables recreates the test tables before each test. |
| 82 | +func setupIntegerTestTables(t *testing.T) { |
| 83 | + t.Log("Setting up integer NUMBER test tables") |
| 84 | + |
| 85 | + DB.Migrator().DropTable(&IntegerTestModel{}) |
| 86 | + DB.Migrator().DropTable(&NullableIntegerTestModel{}) |
| 87 | + |
| 88 | + if err := DB.AutoMigrate(&IntegerTestModel{}, &NullableIntegerTestModel{}); err != nil { |
| 89 | + t.Fatalf("Failed to migrate integer test tables: %v", err) |
| 90 | + } |
| 91 | + |
| 92 | + t.Log("Integer NUMBER test tables created successfully") |
| 93 | +} |
| 94 | + |
| 95 | +func TestIntegerBasicCRUD(t *testing.T) { |
| 96 | + setupIntegerTestTables(t) |
| 97 | + |
| 98 | + model := &IntegerTestModel{ |
| 99 | + Int8Value: 127, |
| 100 | + Int16Value: 32767, |
| 101 | + Int32Value: 2147483647, |
| 102 | + Int64Value: 9223372036854775807, |
| 103 | + IntValue: 1000000, |
| 104 | + Uint8Value: 255, |
| 105 | + Uint16Value: 65535, |
| 106 | + Uint32Value: 4294967295, |
| 107 | + Uint64Value: 18446744073709551615, |
| 108 | + UintValue: 5000000, |
| 109 | + } |
| 110 | + |
| 111 | + if err := DB.Create(model).Error; err != nil { |
| 112 | + t.Fatalf("Failed to create integer record: %v", err) |
| 113 | + } |
| 114 | + |
| 115 | + if model.ID == 0 { |
| 116 | + t.Error("Expected auto-generated ID") |
| 117 | + } |
| 118 | + |
| 119 | + var retrieved IntegerTestModel |
| 120 | + if err := DB.First(&retrieved, model.ID).Error; err != nil { |
| 121 | + t.Fatalf("Failed to retrieve integer record: %v", err) |
| 122 | + } |
| 123 | + |
| 124 | + if retrieved.Int32Value != model.Int32Value { |
| 125 | + t.Errorf("Int32Value mismatch. Expected %d, got %d", model.Int32Value, retrieved.Int32Value) |
| 126 | + } |
| 127 | + |
| 128 | + // Update |
| 129 | + newInt32Value := int32(42) |
| 130 | + if err := DB.Model(&retrieved).Update("INT32_VALUE", newInt32Value).Error; err != nil { |
| 131 | + t.Fatalf("Failed to update integer record: %v", err) |
| 132 | + } |
| 133 | + |
| 134 | + var updated IntegerTestModel |
| 135 | + if err := DB.First(&updated, model.ID).Error; err != nil { |
| 136 | + t.Fatalf("Failed to retrieve updated integer record: %v", err) |
| 137 | + } |
| 138 | + if updated.Int32Value != newInt32Value { |
| 139 | + t.Errorf("Updated Int32Value mismatch. Expected %d, got %d", newInt32Value, updated.Int32Value) |
| 140 | + } |
| 141 | + |
| 142 | + // Delete |
| 143 | + if err := DB.Delete(&updated).Error; err != nil { |
| 144 | + t.Fatalf("Failed to delete integer record: %v", err) |
| 145 | + } |
| 146 | + |
| 147 | + var deleted IntegerTestModel |
| 148 | + err := DB.First(&deleted, model.ID).Error |
| 149 | + if err != gorm.ErrRecordNotFound { |
| 150 | + t.Errorf("Expected record not found, got: %v", err) |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +func TestIntegerEdgeCases(t *testing.T) { |
| 155 | + setupIntegerTestTables(t) |
| 156 | + |
| 157 | + testCases := []struct { |
| 158 | + name string |
| 159 | + model IntegerTestModel |
| 160 | + }{ |
| 161 | + { |
| 162 | + name: "Maximum positive values", |
| 163 | + model: IntegerTestModel{ |
| 164 | + Int8Value: math.MaxInt8, |
| 165 | + Int16Value: math.MaxInt16, |
| 166 | + Int32Value: math.MaxInt32, |
| 167 | + Int64Value: math.MaxInt64, |
| 168 | + IntValue: math.MaxInt, |
| 169 | + Uint8Value: math.MaxUint8, |
| 170 | + Uint16Value: math.MaxUint16, |
| 171 | + Uint32Value: math.MaxUint32, |
| 172 | + Uint64Value: math.MaxUint64, |
| 173 | + UintValue: math.MaxUint, |
| 174 | + }, |
| 175 | + }, |
| 176 | + } |
| 177 | + |
| 178 | + for _, tc := range testCases { |
| 179 | + t.Run(tc.name, func(t *testing.T) { |
| 180 | + if err := DB.Create(&tc.model).Error; err != nil { |
| 181 | + t.Fatalf("Failed to create record for %s: %v", tc.name, err) |
| 182 | + } |
| 183 | + |
| 184 | + var retrieved IntegerTestModel |
| 185 | + if err := DB.First(&retrieved, tc.model.ID).Error; err != nil { |
| 186 | + t.Fatalf("Failed to retrieve record for %s: %v", tc.name, err) |
| 187 | + } |
| 188 | + |
| 189 | + if retrieved.Int32Value != tc.model.Int32Value { |
| 190 | + t.Errorf("%s: Int32Value mismatch. Expected %d, got %d", |
| 191 | + tc.name, tc.model.Int32Value, retrieved.Int32Value) |
| 192 | + } |
| 193 | + }) |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | +func TestIntegerNullHandling(t *testing.T) { |
| 198 | + setupIntegerTestTables(t) |
| 199 | + |
| 200 | + model1 := &NullableIntegerTestModel{} |
| 201 | + if err := DB.Create(model1).Error; err != nil { |
| 202 | + t.Fatalf("Failed to create record with NULL values: %v", err) |
| 203 | + } |
| 204 | + |
| 205 | + var retrieved1 NullableIntegerTestModel |
| 206 | + if err := DB.First(&retrieved1, model1.ID).Error; err != nil { |
| 207 | + t.Fatalf("Failed to retrieve record with NULL values: %v", err) |
| 208 | + } |
| 209 | + |
| 210 | + if retrieved1.NullInt32.Valid || retrieved1.NullInt64.Valid { |
| 211 | + t.Error("Expected NULL values to remain invalid") |
| 212 | + } |
| 213 | + |
| 214 | + validInt32 := int32(42) |
| 215 | + validInt64 := int64(9999999) |
| 216 | + validUint32 := uint32(123) |
| 217 | + validUint64 := uint64(456789) |
| 218 | + |
| 219 | + model2 := &NullableIntegerTestModel{ |
| 220 | + NullInt32: sql.NullInt32{Int32: 100, Valid: true}, |
| 221 | + NullInt64: sql.NullInt64{Int64: 200, Valid: true}, |
| 222 | + OptionalInt32: &validInt32, |
| 223 | + OptionalInt64: &validInt64, |
| 224 | + OptionalUint32: &validUint32, |
| 225 | + OptionalUint64: &validUint64, |
| 226 | + } |
| 227 | + |
| 228 | + if err := DB.Create(model2).Error; err != nil { |
| 229 | + t.Fatalf("Failed to create record with valid nullable values: %v", err) |
| 230 | + } |
| 231 | + |
| 232 | + var retrieved2 NullableIntegerTestModel |
| 233 | + if err := DB.First(&retrieved2, model2.ID).Error; err != nil { |
| 234 | + t.Fatalf("Failed to retrieve record with valid nullable values: %v", err) |
| 235 | + } |
| 236 | + |
| 237 | + if !retrieved2.NullInt32.Valid || retrieved2.NullInt32.Int32 != 100 { |
| 238 | + t.Errorf("Expected NullInt32=100, got %v", retrieved2.NullInt32) |
| 239 | + } |
| 240 | + if !retrieved2.NullInt64.Valid || retrieved2.NullInt64.Int64 != 200 { |
| 241 | + t.Errorf("Expected NullInt64=200, got %v", retrieved2.NullInt64) |
| 242 | + } |
| 243 | + |
| 244 | + // Update NULL → value |
| 245 | + if err := DB.Model(&retrieved1).Updates(map[string]interface{}{ |
| 246 | + "NULL_INT32": sql.NullInt32{Int32: 500, Valid: true}, |
| 247 | + "NULL_INT64": sql.NullInt64{Int64: 600, Valid: true}, |
| 248 | + }).Error; err != nil { |
| 249 | + t.Fatalf("Failed to update NULL to value: %v", err) |
| 250 | + } |
| 251 | + |
| 252 | + var updated NullableIntegerTestModel |
| 253 | + if err := DB.First(&updated, model1.ID).Error; err != nil { |
| 254 | + t.Fatalf("Failed to retrieve updated record: %v", err) |
| 255 | + } |
| 256 | + |
| 257 | + if !updated.NullInt32.Valid || updated.NullInt32.Int32 != 500 { |
| 258 | + t.Error("Failed to update NullInt32 from NULL to value") |
| 259 | + } |
| 260 | + |
| 261 | + // Update value → NULL |
| 262 | + if err := DB.Model(&updated).Updates(map[string]interface{}{ |
| 263 | + "NULL_INT32": sql.NullInt32{Valid: false}, |
| 264 | + }).Error; err != nil { |
| 265 | + t.Fatalf("Failed to update value to NULL: %v", err) |
| 266 | + } |
| 267 | +} |
| 268 | + |
| 269 | +func TestIntegerQueryOperations(t *testing.T) { |
| 270 | + setupIntegerTestTables(t) |
| 271 | + |
| 272 | + data := []IntegerTestModel{ |
| 273 | + {Int32Value: 10, Int64Value: 100, UintValue: 1}, |
| 274 | + {Int32Value: 20, Int64Value: 200, UintValue: 2}, |
| 275 | + {Int32Value: 30, Int64Value: 300, UintValue: 3}, |
| 276 | + {Int32Value: 40, Int64Value: 400, UintValue: 4}, |
| 277 | + {Int32Value: 50, Int64Value: 500, UintValue: 5}, |
| 278 | + } |
| 279 | + |
| 280 | + if err := DB.Create(&data).Error; err != nil { |
| 281 | + t.Fatalf("Failed to insert test data: %v", err) |
| 282 | + } |
| 283 | + |
| 284 | + var result []IntegerTestModel |
| 285 | + if err := DB.Where("INT32_VALUE = ?", 30).Find(&result).Error; err != nil { |
| 286 | + t.Fatalf("Failed to query with equals: %v", err) |
| 287 | + } |
| 288 | + if len(result) != 1 { |
| 289 | + t.Errorf("Expected 1 record, got %d", len(result)) |
| 290 | + } |
| 291 | + |
| 292 | + var maxInt32 int32 |
| 293 | + if err := DB.Model(&IntegerTestModel{}).Select("MAX(INT32_VALUE)").Scan(&maxInt32).Error; err != nil { |
| 294 | + t.Fatalf("Failed to query MAX: %v", err) |
| 295 | + } |
| 296 | + if maxInt32 != 50 { |
| 297 | + t.Errorf("Expected MAX(INT32_VALUE)=50, got %d", maxInt32) |
| 298 | + } |
| 299 | +} |
| 300 | + |
| 301 | +func TestIntegerOverflowHandling(t *testing.T) { |
| 302 | + setupIntegerTestTables(t) |
| 303 | + |
| 304 | + t.Run("Int8 overflow", func(t *testing.T) { |
| 305 | + model := &IntegerTestModel{Int8Value: math.MaxInt8} |
| 306 | + if err := DB.Create(model).Error; err != nil { |
| 307 | + t.Fatalf("Failed to create record: %v", err) |
| 308 | + } |
| 309 | + |
| 310 | + err := DB.Model(&IntegerTestModel{}). |
| 311 | + Where("ID = ?", model.ID). |
| 312 | + Update("INT8_VALUE", gorm.Expr("INT8_VALUE + ?", 1)).Error |
| 313 | + |
| 314 | + if err != nil { |
| 315 | + t.Logf("Overflow prevented as expected: %v", err) |
| 316 | + } else { |
| 317 | + var updated IntegerTestModel |
| 318 | + DB.First(&updated, model.ID) |
| 319 | + t.Logf("Post-overflow value: %d", updated.Int8Value) |
| 320 | + } |
| 321 | + }) |
| 322 | + |
| 323 | + t.Run("Uint64 maximum", func(t *testing.T) { |
| 324 | + model := &IntegerTestModel{Uint64Value: math.MaxUint64} |
| 325 | + if err := DB.Create(model).Error; err != nil { |
| 326 | + t.Fatalf("Failed to create record: %v", err) |
| 327 | + } |
| 328 | + |
| 329 | + var retrieved IntegerTestModel |
| 330 | + if err := DB.First(&retrieved, model.ID).Error; err != nil { |
| 331 | + t.Fatalf("Failed to retrieve record: %v", err) |
| 332 | + } |
| 333 | + |
| 334 | + if retrieved.Uint64Value != math.MaxUint64 { |
| 335 | + t.Errorf("Expected %d, got %d", uint64(math.MaxUint64), retrieved.Uint64Value) |
| 336 | + } |
| 337 | + }) |
| 338 | +} |
| 339 | + |
| 340 | +func TestIntegerWithReturning(t *testing.T) { |
| 341 | + setupIntegerTestTables(t) |
| 342 | + |
| 343 | + models := []IntegerTestModel{ |
| 344 | + {Int32Value: 111, Int64Value: 1111}, |
| 345 | + {Int32Value: 222, Int64Value: 2222}, |
| 346 | + {Int32Value: 333, Int64Value: 3333}, |
| 347 | + } |
| 348 | + |
| 349 | + if err := DB.Create(&models).Error; err != nil { |
| 350 | + t.Fatalf("Failed to create records: %v", err) |
| 351 | + } |
| 352 | + |
| 353 | + for i, m := range models { |
| 354 | + if m.ID == 0 { |
| 355 | + t.Errorf("Record %d: expected ID populated, got 0", i) |
| 356 | + } |
| 357 | + } |
| 358 | + |
| 359 | + var updated []IntegerTestModel |
| 360 | + err := DB.Model(&IntegerTestModel{}). |
| 361 | + Clauses(clause.Returning{}). |
| 362 | + Where("INT32_VALUE > ?", 200). |
| 363 | + Update("INT32_VALUE", gorm.Expr("INT32_VALUE + ?", 1000)). |
| 364 | + Find(&updated).Error |
| 365 | + |
| 366 | + if err != nil { |
| 367 | + t.Logf("UPDATE with RETURNING not fully supported: %v", err) |
| 368 | + } else { |
| 369 | + t.Logf("Updated %d records via RETURNING", len(updated)) |
| 370 | + } |
| 371 | +} |
0 commit comments