// +build integration package database import ( "context" "database/sql" "fmt" "testing" "time" _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) // Integration test models type IntegrationUser struct { ID int `db:"id"` Name string `db:"name"` Email string `db:"email"` Age int `db:"age"` CreatedAt time.Time `db:"created_at"` Posts []*IntegrationPost `bun:"rel:has-many,join:id=user_id"` } func (u IntegrationUser) TableName() string { return "users" } type IntegrationPost struct { ID int `db:"id"` Title string `db:"title"` Content string `db:"content"` UserID int `db:"user_id"` Published bool `db:"published"` CreatedAt time.Time `db:"created_at"` User *IntegrationUser `bun:"rel:belongs-to,join:user_id=id"` Comments []*IntegrationComment `bun:"rel:has-many,join:id=post_id"` } func (p IntegrationPost) TableName() string { return "posts" } type IntegrationComment struct { ID int `db:"id"` Content string `db:"content"` PostID int `db:"post_id"` CreatedAt time.Time `db:"created_at"` Post *IntegrationPost `bun:"rel:belongs-to,join:post_id=id"` } func (c IntegrationComment) TableName() string { return "comments" } // setupTestDB creates a PostgreSQL container and returns the connection func setupTestDB(t *testing.T) (*sql.DB, func()) { ctx := context.Background() req := testcontainers.ContainerRequest{ Image: "postgres:15-alpine", ExposedPorts: []string{"5432/tcp"}, Env: map[string]string{ "POSTGRES_USER": "testuser", "POSTGRES_PASSWORD": "testpass", "POSTGRES_DB": "testdb", }, WaitingFor: wait.ForLog("database system is ready to accept connections"). WithOccurrence(2). WithStartupTimeout(60 * time.Second), } postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) require.NoError(t, err) host, err := postgres.Host(ctx) require.NoError(t, err) port, err := postgres.MappedPort(ctx, "5432") require.NoError(t, err) dsn := fmt.Sprintf("postgres://testuser:testpass@%s:%s/testdb?sslmode=disable", host, port.Port()) db, err := sql.Open("pgx", dsn) require.NoError(t, err) // Wait for database to be ready err = db.Ping() require.NoError(t, err) // Create schema createSchema(t, db) cleanup := func() { db.Close() postgres.Terminate(ctx) } return db, cleanup } // createSchema creates test tables func createSchema(t *testing.T, db *sql.DB) { schema := ` DROP TABLE IF EXISTS comments CASCADE; DROP TABLE IF EXISTS posts CASCADE; DROP TABLE IF EXISTS users CASCADE; CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, age INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE, published BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE comments ( id SERIAL PRIMARY KEY, content TEXT NOT NULL, post_id INT NOT NULL REFERENCES posts(id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ` _, err := db.Exec(schema) require.NoError(t, err) } // TestIntegration_BasicCRUD tests basic CRUD operations func TestIntegration_BasicCRUD(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // CREATE result, err := adapter.NewInsert(). Table("users"). Value("name", "John Doe"). Value("email", "john@example.com"). Value("age", 25). Exec(ctx) require.NoError(t, err) assert.Equal(t, int64(1), result.RowsAffected()) // READ var users []IntegrationUser err = adapter.NewSelect(). Table("users"). Where("email = ?", "john@example.com"). Scan(ctx, &users) require.NoError(t, err) assert.Len(t, users, 1) assert.Equal(t, "John Doe", users[0].Name) assert.Equal(t, 25, users[0].Age) userID := users[0].ID // UPDATE result, err = adapter.NewUpdate(). Table("users"). Set("age", 26). Where("id = ?", userID). Exec(ctx) require.NoError(t, err) assert.Equal(t, int64(1), result.RowsAffected()) // Verify update var updatedUser IntegrationUser err = adapter.NewSelect(). Table("users"). Where("id = ?", userID). Scan(ctx, &updatedUser) require.NoError(t, err) assert.Equal(t, 26, updatedUser.Age) // DELETE result, err = adapter.NewDelete(). Table("users"). Where("id = ?", userID). Exec(ctx) require.NoError(t, err) assert.Equal(t, int64(1), result.RowsAffected()) // Verify delete count, err := adapter.NewSelect(). Table("users"). Where("id = ?", userID). Count(ctx) require.NoError(t, err) assert.Equal(t, 0, count) } // TestIntegration_ScanModel tests ScanModel functionality func TestIntegration_ScanModel(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Insert test data _, err := adapter.NewInsert(). Table("users"). Value("name", "Jane Smith"). Value("email", "jane@example.com"). Value("age", 30). Exec(ctx) require.NoError(t, err) // Test single struct scan user := &IntegrationUser{} err = adapter.NewSelect(). Model(user). Table("users"). Where("email = ?", "jane@example.com"). ScanModel(ctx) require.NoError(t, err) assert.Equal(t, "Jane Smith", user.Name) assert.Equal(t, 30, user.Age) // Test slice scan users := []*IntegrationUser{} err = adapter.NewSelect(). Model(&users). Table("users"). ScanModel(ctx) require.NoError(t, err) assert.Len(t, users, 1) } // TestIntegration_Transaction tests transaction handling func TestIntegration_Transaction(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Successful transaction err := adapter.RunInTransaction(ctx, func(tx common.Database) error { _, err := tx.NewInsert(). Table("users"). Value("name", "Alice"). Value("email", "alice@example.com"). Value("age", 28). Exec(ctx) if err != nil { return err } _, err = tx.NewInsert(). Table("users"). Value("name", "Bob"). Value("email", "bob@example.com"). Value("age", 32). Exec(ctx) return err }) require.NoError(t, err) // Verify both records exist count, err := adapter.NewSelect(). Table("users"). Count(ctx) require.NoError(t, err) assert.Equal(t, 2, count) // Failed transaction (should rollback) err = adapter.RunInTransaction(ctx, func(tx common.Database) error { _, err := tx.NewInsert(). Table("users"). Value("name", "Charlie"). Value("email", "charlie@example.com"). Value("age", 35). Exec(ctx) if err != nil { return err } // Intentional error - duplicate email _, err = tx.NewInsert(). Table("users"). Value("name", "David"). Value("email", "alice@example.com"). // Duplicate Value("age", 40). Exec(ctx) return err }) assert.Error(t, err) // Verify rollback - count should still be 2 count, err = adapter.NewSelect(). Table("users"). Count(ctx) require.NoError(t, err) assert.Equal(t, 2, count) } // TestIntegration_Preload tests basic preload functionality func TestIntegration_Preload(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Create test data userID := createTestUser(t, adapter, ctx, "John Doe", "john@example.com", 25) createTestPost(t, adapter, ctx, userID, "First Post", "Content 1", true) createTestPost(t, adapter, ctx, userID, "Second Post", "Content 2", false) // Test Preload var users []*IntegrationUser err := adapter.NewSelect(). Model(&IntegrationUser{}). Table("users"). Preload("Posts"). Scan(ctx, &users) require.NoError(t, err) assert.Len(t, users, 1) assert.NotNil(t, users[0].Posts) assert.Len(t, users[0].Posts, 2) } // TestIntegration_PreloadRelation tests smart PreloadRelation func TestIntegration_PreloadRelation(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Create test data userID := createTestUser(t, adapter, ctx, "Jane Smith", "jane@example.com", 30) postID := createTestPost(t, adapter, ctx, userID, "Test Post", "Test Content", true) createTestComment(t, adapter, ctx, postID, "Great post!") createTestComment(t, adapter, ctx, postID, "Thanks for sharing!") // Test PreloadRelation with belongs-to (should use JOIN) var posts []*IntegrationPost err := adapter.NewSelect(). Model(&IntegrationPost{}). Table("posts"). PreloadRelation("User"). Scan(ctx, &posts) require.NoError(t, err) assert.Len(t, posts, 1) // Note: JOIN preloading needs proper column selection to work // For now, we test that it doesn't error // Test PreloadRelation with has-many (should use subquery) posts = []*IntegrationPost{} err = adapter.NewSelect(). Model(&IntegrationPost{}). Table("posts"). PreloadRelation("Comments"). Scan(ctx, &posts) require.NoError(t, err) assert.Len(t, posts, 1) if posts[0].Comments != nil { assert.Len(t, posts[0].Comments, 2) } } // TestIntegration_JoinRelation tests explicit JoinRelation func TestIntegration_JoinRelation(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Create test data userID := createTestUser(t, adapter, ctx, "Bob Wilson", "bob@example.com", 35) createTestPost(t, adapter, ctx, userID, "Join Test", "Content", true) // Test JoinRelation var posts []*IntegrationPost err := adapter.NewSelect(). Model(&IntegrationPost{}). Table("posts"). JoinRelation("User"). Scan(ctx, &posts) require.NoError(t, err) assert.Len(t, posts, 1) } // TestIntegration_ComplexQuery tests complex queries func TestIntegration_ComplexQuery(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Create test data userID1 := createTestUser(t, adapter, ctx, "Alice", "alice@example.com", 25) userID2 := createTestUser(t, adapter, ctx, "Bob", "bob@example.com", 30) userID3 := createTestUser(t, adapter, ctx, "Charlie", "charlie@example.com", 35) createTestPost(t, adapter, ctx, userID1, "Post 1", "Content", true) createTestPost(t, adapter, ctx, userID2, "Post 2", "Content", true) createTestPost(t, adapter, ctx, userID3, "Post 3", "Content", false) // Complex query with joins, where, order, limit var results []map[string]interface{} err := adapter.NewSelect(). Table("posts p"). Column("p.title", "u.name as author_name", "u.age as author_age"). LeftJoin("users u ON u.id = p.user_id"). Where("p.published = ?", true). WhereOr("u.age > ?", 25). Order("u.age DESC"). Limit(2). Scan(ctx, &results) require.NoError(t, err) assert.LessOrEqual(t, len(results), 2) } // TestIntegration_Aggregation tests aggregation queries func TestIntegration_Aggregation(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() adapter := NewPgSQLAdapter(db) ctx := context.Background() // Create test data createTestUser(t, adapter, ctx, "User 1", "user1@example.com", 20) createTestUser(t, adapter, ctx, "User 2", "user2@example.com", 25) createTestUser(t, adapter, ctx, "User 3", "user3@example.com", 30) // Test Count count, err := adapter.NewSelect(). Table("users"). Where("age >= ?", 25). Count(ctx) require.NoError(t, err) assert.Equal(t, 2, count) // Test Exists exists, err := adapter.NewSelect(). Table("users"). Where("email = ?", "user1@example.com"). Exists(ctx) require.NoError(t, err) assert.True(t, exists) // Test Group By with aggregation var results []map[string]interface{} err = adapter.NewSelect(). Table("users"). Column("age", "COUNT(*) as count"). Group("age"). Having("COUNT(*) > ?", 0). Order("age ASC"). Scan(ctx, &results) require.NoError(t, err) assert.Len(t, results, 3) } // Helper functions func createTestUser(t *testing.T, adapter *PgSQLAdapter, ctx context.Context, name, email string, age int) int { var userID int err := adapter.Query(ctx, &userID, "INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING id", name, email, age) require.NoError(t, err) return userID } func createTestPost(t *testing.T, adapter *PgSQLAdapter, ctx context.Context, userID int, title, content string, published bool) int { var postID int err := adapter.Query(ctx, &postID, "INSERT INTO posts (title, content, user_id, published) VALUES ($1, $2, $3, $4) RETURNING id", title, content, userID, published) require.NoError(t, err) return postID } func createTestComment(t *testing.T, adapter *PgSQLAdapter, ctx context.Context, postID int, content string) int { var commentID int err := adapter.Query(ctx, &commentID, "INSERT INTO comments (content, post_id) VALUES ($1, $2) RETURNING id", content, postID) require.NoError(t, err) return commentID }