|  | 
|  | 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