package testing import ( "context" "errors" "testing" "time" "b612.me/stardb" _ "modernc.org/sqlite" ) type TestUser struct { ID int64 `db:"id"` Name string `db:"name"` Email string `db:"email"` Age int `db:"age"` CreatedAt time.Time `db:"created_at"` } func setupBatchTestDB(t *testing.T) *stardb.StarDB { db := stardb.NewStarDB() err := db.Open("sqlite", ":memory:") if err != nil { t.Fatalf("Failed to open database: %v", err) } _, err = db.Exec(` CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL, age INTEGER, created_at DATETIME ) `) if err != nil { t.Fatalf("Failed to create table: %v", err) } return db } func TestStarDB_BatchInsert_Basic(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, {"Charlie", "charlie@example.com", 35}, } result, err := db.BatchInsert("users", columns, values) if err != nil { t.Fatalf("BatchInsert failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 3 { t.Errorf("Expected 3 rows affected, got %d", affected) } // Verify insertion rows, err := db.Query("SELECT COUNT(*) as count FROM users") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() count := rows.Row(0).MustInt("count") if count != 3 { t.Errorf("Expected 3 rows in database, got %d", count) } } func TestStarDB_BatchInsert_Single(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, } result, err := db.BatchInsert("users", columns, values) if err != nil { t.Fatalf("BatchInsert failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 1 { t.Errorf("Expected 1 row affected, got %d", affected) } } func TestStarDB_BatchInsert_Empty(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} values := [][]interface{}{} _, err := db.BatchInsert("users", columns, values) if !errors.Is(err, stardb.ErrNoInsertValues) { t.Errorf("Expected ErrNoInsertValues, got %v", err) } } func TestStarDB_BatchInsert_EmptyColumns(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() values := [][]interface{}{ {"Alice"}, } _, err := db.BatchInsert("users", nil, values) if !errors.Is(err, stardb.ErrNoInsertColumns) { t.Errorf("Expected ErrNoInsertColumns, got %v", err) } } func TestStarDB_BatchInsert_EmptyTableName(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, } _, err := db.BatchInsert("", columns, values) if !errors.Is(err, stardb.ErrTableNameEmpty) { t.Errorf("Expected ErrTableNameEmpty, got %v", err) } } func TestStarDB_BatchInsert_RowLengthMismatch(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com"}, } _, err := db.BatchInsert("users", columns, values) if !errors.Is(err, stardb.ErrBatchRowValueCountMismatch) { t.Errorf("Expected ErrBatchRowValueCountMismatch, got %v", err) } } func TestStarDB_BatchInsert_Large(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} var values [][]interface{} // Insert 100 rows for i := 0; i < 100; i++ { values = append(values, []interface{}{ "User" + string(rune(i)), "user" + string(rune(i)) + "@example.com", 20 + i%50, }) } result, err := db.BatchInsert("users", columns, values) if err != nil { t.Fatalf("BatchInsert failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 100 { t.Errorf("Expected 100 rows affected, got %d", affected) } // Verify rows, err := db.Query("SELECT COUNT(*) as count FROM users") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() count := rows.Row(0).MustInt("count") if count != 100 { t.Errorf("Expected 100 rows in database, got %d", count) } } func TestStarDB_BatchInsertContext(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() ctx := context.Background() columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, } result, err := db.BatchInsertContext(ctx, "users", columns, values) if err != nil { t.Fatalf("BatchInsertContext failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 2 { t.Errorf("Expected 2 rows affected, got %d", affected) } } func TestStarDB_BatchInsertContext_Timeout(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) defer cancel() time.Sleep(10 * time.Millisecond) // Ensure timeout columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, } _, err := db.BatchInsertContext(ctx, "users", columns, values) if err == nil { t.Error("Expected timeout error, got nil") } } func TestStarDB_BatchInsertMaxRows_Config(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() if got := db.BatchInsertMaxRows(); got != 0 { t.Fatalf("Expected default chunk size 0, got %d", got) } db.SetBatchInsertMaxRows(3) if got := db.BatchInsertMaxRows(); got != 3 { t.Fatalf("Expected chunk size 3, got %d", got) } db.SetBatchInsertMaxRows(-10) if got := db.BatchInsertMaxRows(); got != 0 { t.Fatalf("Expected chunk size reset to 0, got %d", got) } } func TestStarDB_BatchInsertMaxParams_Config(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() if got := db.BatchInsertMaxParams(); got != 0 { t.Fatalf("Expected default max params 0, got %d", got) } db.SetBatchInsertMaxParams(100) if got := db.BatchInsertMaxParams(); got != 100 { t.Fatalf("Expected max params 100, got %d", got) } db.SetBatchInsertMaxParams(-1) if got := db.BatchInsertMaxParams(); got != 0 { t.Fatalf("Expected max params reset to 0, got %d", got) } } func TestStarDB_BatchInsert_Chunked(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(2) columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, {"Charlie", "charlie@example.com", 35}, {"David", "david@example.com", 40}, {"Eva", "eva@example.com", 28}, } result, err := db.BatchInsert("users", columns, values) if err != nil { t.Fatalf("Chunked BatchInsert failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != int64(len(values)) { t.Fatalf("Expected %d affected rows, got %d", len(values), affected) } rows, err := db.Query("SELECT COUNT(*) as count FROM users") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() if count := rows.Row(0).MustInt("count"); count != len(values) { t.Fatalf("Expected %d rows in db, got %d", len(values), count) } } func TestStarDB_BatchInsert_ChunkedRollbackOnError(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(2) columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, {"Charlie", nil, 35}, // email NOT NULL, forces second chunk failure } if _, err := db.BatchInsert("users", columns, values); err == nil { t.Fatal("Expected chunked BatchInsert to fail, got nil") } rows, err := db.Query("SELECT COUNT(*) as count FROM users") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() if count := rows.Row(0).MustInt("count"); count != 0 { t.Fatalf("Expected rollback to keep table empty, got %d rows", count) } } func TestStarDB_BatchInsert_ChunkedByMaxParams(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(0) // disabled db.SetBatchInsertMaxParams(4) // 3 columns -> 1 row per chunk columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, {"Charlie", "charlie@example.com", 35}, } result, err := db.BatchInsert("users", columns, values) if err != nil { t.Fatalf("BatchInsert by max params failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != int64(len(values)) { t.Fatalf("Expected %d affected rows, got %d", len(values), affected) } } func TestStarDB_BatchInsert_MaxParamsTooLow(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(0) db.SetBatchInsertMaxParams(2) // columns=3 -> invalid columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, } _, err := db.BatchInsert("users", columns, values) if !errors.Is(err, stardb.ErrBatchInsertMaxParamsTooLow) { t.Fatalf("Expected ErrBatchInsertMaxParamsTooLow, got %v", err) } } func TestStarDB_BatchInsert_ChunkedHookMeta(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(2) db.SetBatchInsertMaxParams(0) var metas []stardb.BatchExecMeta db.SetSQLHooks(nil, func(ctx context.Context, query string, args []interface{}, d time.Duration, err error) { if meta, ok := stardb.BatchExecMetaFromContext(ctx); ok { metas = append(metas, meta) } }) columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, {"Charlie", "charlie@example.com", 35}, {"David", "david@example.com", 40}, {"Eva", "eva@example.com", 28}, } _, err := db.BatchInsertContext(context.Background(), "users", columns, values) if err != nil { t.Fatalf("BatchInsertContext failed: %v", err) } if len(metas) != 3 { t.Fatalf("Expected 3 chunk metas, got %d", len(metas)) } wantRows := []int{2, 2, 1} for i, meta := range metas { if meta.ChunkIndex != i+1 { t.Fatalf("Chunk %d: expected index %d, got %d", i, i+1, meta.ChunkIndex) } if meta.ChunkCount != 3 { t.Fatalf("Chunk %d: expected count 3, got %d", i, meta.ChunkCount) } if meta.ChunkRows != wantRows[i] { t.Fatalf("Chunk %d: expected rows %d, got %d", i, wantRows[i], meta.ChunkRows) } if meta.TotalRows != len(values) { t.Fatalf("Chunk %d: expected total rows %d, got %d", i, len(values), meta.TotalRows) } if meta.ColumnCount != len(columns) { t.Fatalf("Chunk %d: expected column count %d, got %d", i, len(columns), meta.ColumnCount) } } } func TestStarDB_BatchInsert_HookMetaAbsentWithoutChunking(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(0) db.SetBatchInsertMaxParams(0) metaFound := false db.SetSQLHooks(nil, func(ctx context.Context, query string, args []interface{}, d time.Duration, err error) { if _, ok := stardb.BatchExecMetaFromContext(ctx); ok { metaFound = true } }) columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, } _, err := db.BatchInsertContext(context.Background(), "users", columns, values) if err != nil { t.Fatalf("BatchInsertContext failed: %v", err) } if metaFound { t.Fatal("Expected no batch meta for non-chunked execution") } } func TestStarDB_BatchInsertStructs_Basic(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() users := []TestUser{ {Name: "Alice", Email: "alice@example.com", Age: 25, CreatedAt: time.Now()}, {Name: "Bob", Email: "bob@example.com", Age: 30, CreatedAt: time.Now()}, {Name: "Charlie", Email: "charlie@example.com", Age: 35, CreatedAt: time.Now()}, } result, err := db.BatchInsertStructs("users", users, "id") if err != nil { t.Fatalf("BatchInsertStructs failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 3 { t.Errorf("Expected 3 rows affected, got %d", affected) } // Verify insertion rows, err := db.Query("SELECT * FROM users ORDER BY name") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() if rows.Length() != 3 { t.Errorf("Expected 3 rows, got %d", rows.Length()) } // Verify first user name := rows.Row(0).MustString("name") if name != "Alice" { t.Errorf("Expected first user 'Alice', got '%s'", name) } } func TestStarDB_BatchInsertStructs_Chunked(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() db.SetBatchInsertMaxRows(2) users := []TestUser{ {Name: "Alice", Email: "alice@example.com", Age: 25, CreatedAt: time.Now()}, {Name: "Bob", Email: "bob@example.com", Age: 30, CreatedAt: time.Now()}, {Name: "Charlie", Email: "charlie@example.com", Age: 35, CreatedAt: time.Now()}, {Name: "David", Email: "david@example.com", Age: 40, CreatedAt: time.Now()}, {Name: "Eva", Email: "eva@example.com", Age: 28, CreatedAt: time.Now()}, } result, err := db.BatchInsertStructs("users", users, "id") if err != nil { t.Fatalf("Chunked BatchInsertStructs failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != int64(len(users)) { t.Fatalf("Expected %d affected rows, got %d", len(users), affected) } } func TestStarDB_BatchInsertStructs_Single(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() users := []TestUser{ {Name: "Alice", Email: "alice@example.com", Age: 25, CreatedAt: time.Now()}, } result, err := db.BatchInsertStructs("users", users, "id") if err != nil { t.Fatalf("BatchInsertStructs failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 1 { t.Errorf("Expected 1 row affected, got %d", affected) } } func TestStarDB_BatchInsertStructs_Empty(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() users := []TestUser{} _, err := db.BatchInsertStructs("users", users, "id") if !errors.Is(err, stardb.ErrNoStructsToInsert) { t.Errorf("Expected ErrNoStructsToInsert, got %v", err) } } func TestStarDB_BatchInsertStructs_NotSlice(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() user := TestUser{Name: "Alice", Email: "alice@example.com", Age: 25} _, err := db.BatchInsertStructs("users", user, "id") if !errors.Is(err, stardb.ErrStructsNotSlice) { t.Errorf("Expected ErrStructsNotSlice, got %v", err) } } func TestStarDB_BatchInsertStructs_Nil(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() _, err := db.BatchInsertStructs("users", nil, "id") if !errors.Is(err, stardb.ErrStructsNil) { t.Errorf("Expected ErrStructsNil, got %v", err) } } func TestStarDB_BatchInsertStructs_NilPointer(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() var users *[]TestUser _, err := db.BatchInsertStructs("users", users, "id") if !errors.Is(err, stardb.ErrStructsPointerNil) { t.Errorf("Expected ErrStructsPointerNil, got %v", err) } } func TestStarDB_BatchInsertStructs_Pointer(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() users := []TestUser{ {Name: "Alice", Email: "alice@example.com", Age: 25, CreatedAt: time.Now()}, {Name: "Bob", Email: "bob@example.com", Age: 30, CreatedAt: time.Now()}, } result, err := db.BatchInsertStructs("users", &users, "id") if err != nil { t.Fatalf("BatchInsertStructs with pointer failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 2 { t.Errorf("Expected 2 rows affected, got %d", affected) } } func TestStarDB_BatchInsertStructsContext(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() ctx := context.Background() users := []TestUser{ {Name: "Alice", Email: "alice@example.com", Age: 25, CreatedAt: time.Now()}, {Name: "Bob", Email: "bob@example.com", Age: 30, CreatedAt: time.Now()}, } result, err := db.BatchInsertStructsContext(ctx, "users", users, "id") if err != nil { t.Fatalf("BatchInsertStructsContext failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 2 { t.Errorf("Expected 2 rows affected, got %d", affected) } } func TestStarDB_BatchInsertStructs_Large(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() var users []TestUser for i := 0; i < 50; i++ { users = append(users, TestUser{ Name: "User" + string(rune(i)), Email: "user" + string(rune(i)) + "@example.com", Age: 20 + i%30, CreatedAt: time.Now(), }) } result, err := db.BatchInsertStructs("users", users, "id") if err != nil { t.Fatalf("BatchInsertStructs failed: %v", err) } affected, err := result.RowsAffected() if err != nil { t.Fatalf("RowsAffected failed: %v", err) } if affected != 50 { t.Errorf("Expected 50 rows affected, got %d", affected) } // Verify rows, err := db.Query("SELECT COUNT(*) as count FROM users") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() count := rows.Row(0).MustInt("count") if count != 50 { t.Errorf("Expected 50 rows in database, got %d", count) } } func TestStarDB_BatchInsert_VerifyData(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() columns := []string{"name", "email", "age"} values := [][]interface{}{ {"Alice", "alice@example.com", 25}, {"Bob", "bob@example.com", 30}, } _, err := db.BatchInsert("users", columns, values) if err != nil { t.Fatalf("BatchInsert failed: %v", err) } // Verify data integrity rows, err := db.Query("SELECT name, email, age FROM users ORDER BY name") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() // Check Alice row0 := rows.Row(0) if row0.MustString("name") != "Alice" { t.Errorf("Expected name 'Alice', got '%s'", row0.MustString("name")) } if row0.MustString("email") != "alice@example.com" { t.Errorf("Expected email 'alice@example.com', got '%s'", row0.MustString("email")) } if row0.MustInt("age") != 25 { t.Errorf("Expected age 25, got %d", row0.MustInt("age")) } // Check Bob row1 := rows.Row(1) if row1.MustString("name") != "Bob" { t.Errorf("Expected name 'Bob', got '%s'", row1.MustString("name")) } if row1.MustString("email") != "bob@example.com" { t.Errorf("Expected email 'bob@example.com', got '%s'", row1.MustString("email")) } if row1.MustInt("age") != 30 { t.Errorf("Expected age 30, got %d", row1.MustInt("age")) } } func TestStarDB_BatchInsertStructs_VerifyData(t *testing.T) { db := setupBatchTestDB(t) defer db.Close() now := time.Now() users := []TestUser{ {Name: "Alice", Email: "alice@example.com", Age: 25, CreatedAt: now}, {Name: "Bob", Email: "bob@example.com", Age: 30, CreatedAt: now}, } _, err := db.BatchInsertStructs("users", users, "id") if err != nil { t.Fatalf("BatchInsertStructs failed: %v", err) } // Query and verify with ORM rows, err := db.Query("SELECT * FROM users ORDER BY name") if err != nil { t.Fatalf("Query failed: %v", err) } defer rows.Close() var resultUsers []TestUser err = rows.Orm(&resultUsers) if err != nil { t.Fatalf("Orm failed: %v", err) } if len(resultUsers) != 2 { t.Fatalf("Expected 2 users, got %d", len(resultUsers)) } // Verify Alice if resultUsers[0].Name != "Alice" { t.Errorf("Expected name 'Alice', got '%s'", resultUsers[0].Name) } if resultUsers[0].Email != "alice@example.com" { t.Errorf("Expected email 'alice@example.com', got '%s'", resultUsers[0].Email) } if resultUsers[0].Age != 25 { t.Errorf("Expected age 25, got %d", resultUsers[0].Age) } // Verify Bob if resultUsers[1].Name != "Bob" { t.Errorf("Expected name 'Bob', got '%s'", resultUsers[1].Name) } if resultUsers[1].Email != "bob@example.com" { t.Errorf("Expected email 'bob@example.com', got '%s'", resultUsers[1].Email) } if resultUsers[1].Age != 30 { t.Errorf("Expected age 30, got %d", resultUsers[1].Age) } } // Benchmark tests func BenchmarkBatchInsert_10(b *testing.B) { db := setupBatchTestDB(&testing.T{}) defer db.Close() columns := []string{"name", "email", "age"} var values [][]interface{} for i := 0; i < 10; i++ { values = append(values, []interface{}{"User", "user@example.com", 25}) } b.ResetTimer() for i := 0; i < b.N; i++ { db.BatchInsert("users", columns, values) db.Exec("DELETE FROM users") } } func BenchmarkBatchInsert_100(b *testing.B) { db := setupBatchTestDB(&testing.T{}) defer db.Close() columns := []string{"name", "email", "age"} var values [][]interface{} for i := 0; i < 100; i++ { values = append(values, []interface{}{"User", "user@example.com", 25}) } b.ResetTimer() for i := 0; i < b.N; i++ { db.BatchInsert("users", columns, values) db.Exec("DELETE FROM users") } } func BenchmarkBatchInsertStructs_10(b *testing.B) { db := setupBatchTestDB(&testing.T{}) defer db.Close() var users []TestUser for i := 0; i < 10; i++ { users = append(users, TestUser{ Name: "User", Email: "user@example.com", Age: 25, CreatedAt: time.Now(), }) } b.ResetTimer() for i := 0; i < b.N; i++ { db.BatchInsertStructs("users", users, "id") db.Exec("DELETE FROM users") } } func BenchmarkBatchInsertStructs_100(b *testing.B) { db := setupBatchTestDB(&testing.T{}) defer db.Close() var users []TestUser for i := 0; i < 100; i++ { users = append(users, TestUser{ Name: "User", Email: "user@example.com", Age: 25, CreatedAt: time.Now(), }) } b.ResetTimer() for i := 0; i < b.N; i++ { db.BatchInsertStructs("users", users, "id") db.Exec("DELETE FROM users") } }