mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-13 17:10:36 +00:00
Added row PgSQLAdapter
Some checks are pending
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Waiting to run
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Waiting to run
Build , Vet Test, and Lint / Lint Code (push) Waiting to run
Build , Vet Test, and Lint / Build (push) Waiting to run
Tests / Integration Tests (push) Waiting to run
Tests / Unit Tests (push) Waiting to run
Some checks are pending
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Waiting to run
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Waiting to run
Build , Vet Test, and Lint / Lint Code (push) Waiting to run
Build , Vet Test, and Lint / Build (push) Waiting to run
Tests / Integration Tests (push) Waiting to run
Tests / Unit Tests (push) Waiting to run
This commit is contained in:
parent
68dee78a34
commit
4cc943b9d3
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@ -230,7 +230,17 @@
|
|||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"group": "test"
|
"group": "build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "go: lint workspace (fix)",
|
||||||
|
"command": "golangci-lint run --timeout=5m --fix",
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": [],
|
||||||
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
@ -275,4 +285,4 @@
|
|||||||
"command": "sh ${workspaceFolder}/make_release.sh"
|
"command": "sh ${workspaceFolder}/make_release.sh"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
1355
pkg/common/adapters/database/pgsql.go
Normal file
1355
pkg/common/adapters/database/pgsql.go
Normal file
File diff suppressed because it is too large
Load Diff
176
pkg/common/adapters/database/pgsql_example.go
Normal file
176
pkg/common/adapters/database/pgsql_example.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Example demonstrates how to use the PgSQL adapter
|
||||||
|
func ExamplePgSQLAdapter() error {
|
||||||
|
// Connect to PostgreSQL database
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Create the PgSQL adapter
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
|
||||||
|
// Enable query debugging (optional)
|
||||||
|
adapter.EnableQueryDebug()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Example 1: Simple SELECT query
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Where("age > ?", 18).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(10).
|
||||||
|
Scan(ctx, &results)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("select failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: INSERT query
|
||||||
|
result, err := adapter.NewInsert().
|
||||||
|
Table("users").
|
||||||
|
Value("name", "John Doe").
|
||||||
|
Value("email", "john@example.com").
|
||||||
|
Value("age", 25).
|
||||||
|
Returning("id").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("insert failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Rows affected: %d\n", result.RowsAffected())
|
||||||
|
|
||||||
|
// Example 3: UPDATE query
|
||||||
|
result, err = adapter.NewUpdate().
|
||||||
|
Table("users").
|
||||||
|
Set("name", "Jane Doe").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("update failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Rows updated: %d\n", result.RowsAffected())
|
||||||
|
|
||||||
|
// Example 4: DELETE query
|
||||||
|
result, err = adapter.NewDelete().
|
||||||
|
Table("users").
|
||||||
|
Where("age < ?", 18).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Rows deleted: %d\n", result.RowsAffected())
|
||||||
|
|
||||||
|
// Example 5: Using transactions
|
||||||
|
err = adapter.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
|
// Insert a new user
|
||||||
|
_, err := tx.NewInsert().
|
||||||
|
Table("users").
|
||||||
|
Value("name", "Transaction User").
|
||||||
|
Value("email", "tx@example.com").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update another user
|
||||||
|
_, err = tx.NewUpdate().
|
||||||
|
Table("users").
|
||||||
|
Set("verified", true).
|
||||||
|
Where("email = ?", "tx@example.com").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both operations succeed or both rollback
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("transaction failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 6: JOIN query
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Table("users u").
|
||||||
|
Column("u.id", "u.name", "p.title as post_title").
|
||||||
|
LeftJoin("posts p ON p.user_id = u.id").
|
||||||
|
Where("u.active = ?", true).
|
||||||
|
Scan(ctx, &results)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("join query failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 7: Aggregation query
|
||||||
|
count, err := adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Where("active = ?", true).
|
||||||
|
Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("count failed: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Active users: %d\n", count)
|
||||||
|
|
||||||
|
// Example 8: Raw SQL execution
|
||||||
|
_, err = adapter.Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("raw exec failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 9: Raw SQL query
|
||||||
|
var users []map[string]interface{}
|
||||||
|
err = adapter.Query(ctx, &users, "SELECT * FROM users WHERE age > $1 LIMIT $2", 18, 10)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("raw query failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is an example model
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName implements common.TableNameProvider
|
||||||
|
func (u User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleWithModel demonstrates using models with the PgSQL adapter
|
||||||
|
func ExampleWithModel() error {
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Use model with adapter
|
||||||
|
user := User{}
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&user).
|
||||||
|
Where("id = ?", 1).
|
||||||
|
Scan(ctx, &user)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
526
pkg/common/adapters/database/pgsql_integration_test.go
Normal file
526
pkg/common/adapters/database/pgsql_integration_test.go
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
// +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
|
||||||
|
}
|
||||||
275
pkg/common/adapters/database/pgsql_preload_example.go
Normal file
275
pkg/common/adapters/database/pgsql_preload_example.go
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Example models for demonstrating preload functionality
|
||||||
|
|
||||||
|
// Author model - has many Posts
|
||||||
|
type Author struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Email string `db:"email"`
|
||||||
|
Posts []*Post `bun:"rel:has-many,join:id=author_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Author) TableName() string {
|
||||||
|
return "authors"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post model - belongs to Author, has many Comments
|
||||||
|
type Post struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
Content string `db:"content"`
|
||||||
|
AuthorID int `db:"author_id"`
|
||||||
|
Author *Author `bun:"rel:belongs-to,join:author_id=id"`
|
||||||
|
Comments []*Comment `bun:"rel:has-many,join:id=post_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Post) TableName() string {
|
||||||
|
return "posts"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment model - belongs to Post
|
||||||
|
type Comment struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Content string `db:"content"`
|
||||||
|
PostID int `db:"post_id"`
|
||||||
|
Post *Post `bun:"rel:belongs-to,join:post_id=id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Comment) TableName() string {
|
||||||
|
return "comments"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamplePreload demonstrates the Preload functionality
|
||||||
|
func ExamplePreload() error {
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Example 1: Simple Preload (uses subquery for has-many)
|
||||||
|
var authors []*Author
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&Author{}).
|
||||||
|
Table("authors").
|
||||||
|
Preload("Posts"). // Load all posts for each author
|
||||||
|
Scan(ctx, &authors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now authors[i].Posts will be populated with their posts
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExamplePreloadRelation demonstrates smart PreloadRelation with auto-detection
|
||||||
|
func ExamplePreloadRelation() error {
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Example 1: PreloadRelation auto-detects has-many (uses subquery)
|
||||||
|
var authors []*Author
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&Author{}).
|
||||||
|
Table("authors").
|
||||||
|
PreloadRelation("Posts", func(q common.SelectQuery) common.SelectQuery {
|
||||||
|
return q.Where("published = ?", true).Order("created_at DESC")
|
||||||
|
}).
|
||||||
|
Where("active = ?", true).
|
||||||
|
Scan(ctx, &authors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: PreloadRelation auto-detects belongs-to (uses JOIN)
|
||||||
|
var posts []*Post
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&Post{}).
|
||||||
|
Table("posts").
|
||||||
|
PreloadRelation("Author"). // Will use JOIN because it's belongs-to
|
||||||
|
Scan(ctx, &posts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Nested preloads
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&Author{}).
|
||||||
|
Table("authors").
|
||||||
|
PreloadRelation("Posts", func(q common.SelectQuery) common.SelectQuery {
|
||||||
|
// First load posts, then preload comments for each post
|
||||||
|
return q.Limit(10)
|
||||||
|
}).
|
||||||
|
Scan(ctx, &authors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually load nested relationships (two-level preloading)
|
||||||
|
for _, author := range authors {
|
||||||
|
if author.Posts != nil {
|
||||||
|
for _, post := range author.Posts {
|
||||||
|
var comments []*Comment
|
||||||
|
err := adapter.NewSelect().
|
||||||
|
Table("comments").
|
||||||
|
Where("post_id = ?", post.ID).
|
||||||
|
Scan(ctx, &comments)
|
||||||
|
if err == nil {
|
||||||
|
post.Comments = comments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleJoinRelation demonstrates explicit JOIN loading
|
||||||
|
func ExampleJoinRelation() error {
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Example 1: Force JOIN for belongs-to relationship
|
||||||
|
var posts []*Post
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&Post{}).
|
||||||
|
Table("posts").
|
||||||
|
JoinRelation("Author", func(q common.SelectQuery) common.SelectQuery {
|
||||||
|
return q.Where("active = ?", true)
|
||||||
|
}).
|
||||||
|
Scan(ctx, &posts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Multiple JOINs
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&Post{}).
|
||||||
|
Table("posts p").
|
||||||
|
Column("p.*", "a.name as author_name", "a.email as author_email").
|
||||||
|
LeftJoin("authors a ON a.id = p.author_id").
|
||||||
|
Where("p.published = ?", true).
|
||||||
|
Scan(ctx, &posts)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleScanModel demonstrates ScanModel with struct destinations
|
||||||
|
func ExampleScanModel() error {
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Example 1: Scan single struct
|
||||||
|
author := Author{}
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&author).
|
||||||
|
Table("authors").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
ScanModel(ctx) // ScanModel automatically uses the model set with Model()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Scan slice of structs
|
||||||
|
authors := []*Author{}
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&authors).
|
||||||
|
Table("authors").
|
||||||
|
Where("active = ?", true).
|
||||||
|
Limit(10).
|
||||||
|
ScanModel(ctx)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleCompleteWorkflow demonstrates a complete workflow with preloading
|
||||||
|
func ExampleCompleteWorkflow() error {
|
||||||
|
dsn := "postgres://username:password@localhost:5432/dbname?sslmode=disable"
|
||||||
|
db, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
adapter.EnableQueryDebug() // Enable query logging
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Step 1: Create an author
|
||||||
|
author := &Author{
|
||||||
|
Name: "John Doe",
|
||||||
|
Email: "john@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := adapter.NewInsert().
|
||||||
|
Table("authors").
|
||||||
|
Value("name", author.Name).
|
||||||
|
Value("email", author.Email).
|
||||||
|
Returning("id").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = result
|
||||||
|
|
||||||
|
// Step 2: Load author with all their posts
|
||||||
|
var loadedAuthor Author
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(&loadedAuthor).
|
||||||
|
Table("authors").
|
||||||
|
PreloadRelation("Posts", func(q common.SelectQuery) common.SelectQuery {
|
||||||
|
return q.Order("created_at DESC").Limit(5)
|
||||||
|
}).
|
||||||
|
Where("id = ?", 1).
|
||||||
|
ScanModel(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Update author name
|
||||||
|
_, err = adapter.NewUpdate().
|
||||||
|
Table("authors").
|
||||||
|
Set("name", "Jane Doe").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
629
pkg/common/adapters/database/pgsql_test.go
Normal file
629
pkg/common/adapters/database/pgsql_test.go
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test models
|
||||||
|
type TestUser struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Email string `db:"email"`
|
||||||
|
Age int `db:"age"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u TestUser) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestPost struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Title string `db:"title"`
|
||||||
|
Content string `db:"content"`
|
||||||
|
UserID int `db:"user_id"`
|
||||||
|
User *TestUser `bun:"rel:belongs-to,join:user_id=id"`
|
||||||
|
Comments []TestComment `bun:"rel:has-many,join:id=post_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p TestPost) TableName() string {
|
||||||
|
return "posts"
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestComment struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Content string `db:"content"`
|
||||||
|
PostID int `db:"post_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c TestComment) TableName() string {
|
||||||
|
return "comments"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewPgSQLAdapter tests adapter creation
|
||||||
|
func TestNewPgSQLAdapter(t *testing.T) {
|
||||||
|
db, _, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
assert.NotNil(t, adapter)
|
||||||
|
assert.Equal(t, db, adapter.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLSelectQuery_BuildSQL tests SQL query building
|
||||||
|
func TestPgSQLSelectQuery_BuildSQL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func(*PgSQLSelectQuery)
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple select",
|
||||||
|
setup: func(q *PgSQLSelectQuery) {
|
||||||
|
q.tableName = "users"
|
||||||
|
},
|
||||||
|
expected: "SELECT * FROM users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "select with columns",
|
||||||
|
setup: func(q *PgSQLSelectQuery) {
|
||||||
|
q.tableName = "users"
|
||||||
|
q.columns = []string{"id", "name", "email"}
|
||||||
|
},
|
||||||
|
expected: "SELECT id, name, email FROM users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "select with where",
|
||||||
|
setup: func(q *PgSQLSelectQuery) {
|
||||||
|
q.tableName = "users"
|
||||||
|
q.whereClauses = []string{"age > $1"}
|
||||||
|
q.args = []interface{}{18}
|
||||||
|
},
|
||||||
|
expected: "SELECT * FROM users WHERE (age > $1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "select with order and limit",
|
||||||
|
setup: func(q *PgSQLSelectQuery) {
|
||||||
|
q.tableName = "users"
|
||||||
|
q.orderBy = []string{"created_at DESC"}
|
||||||
|
q.limit = 10
|
||||||
|
q.offset = 5
|
||||||
|
},
|
||||||
|
expected: "SELECT * FROM users ORDER BY created_at DESC LIMIT 10 OFFSET 5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "select with join",
|
||||||
|
setup: func(q *PgSQLSelectQuery) {
|
||||||
|
q.tableName = "users"
|
||||||
|
q.joins = []string{"LEFT JOIN posts ON posts.user_id = users.id"}
|
||||||
|
},
|
||||||
|
expected: "SELECT * FROM users LEFT JOIN posts ON posts.user_id = users.id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "select with group and having",
|
||||||
|
setup: func(q *PgSQLSelectQuery) {
|
||||||
|
q.tableName = "users"
|
||||||
|
q.groupBy = []string{"country"}
|
||||||
|
q.havingClauses = []string{"COUNT(*) > $1"}
|
||||||
|
q.args = []interface{}{5}
|
||||||
|
},
|
||||||
|
expected: "SELECT * FROM users GROUP BY country HAVING COUNT(*) > $1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
q := &PgSQLSelectQuery{
|
||||||
|
columns: []string{"*"},
|
||||||
|
}
|
||||||
|
tt.setup(q)
|
||||||
|
sql := q.buildSQL()
|
||||||
|
assert.Equal(t, tt.expected, sql)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLSelectQuery_ReplacePlaceholders tests placeholder replacement
|
||||||
|
func TestPgSQLSelectQuery_ReplacePlaceholders(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
query string
|
||||||
|
argCount int
|
||||||
|
paramCounter int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single placeholder",
|
||||||
|
query: "age > ?",
|
||||||
|
argCount: 1,
|
||||||
|
paramCounter: 0,
|
||||||
|
expected: "age > $1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple placeholders",
|
||||||
|
query: "age > ? AND status = ?",
|
||||||
|
argCount: 2,
|
||||||
|
paramCounter: 0,
|
||||||
|
expected: "age > $1 AND status = $2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with existing counter",
|
||||||
|
query: "name = ?",
|
||||||
|
argCount: 1,
|
||||||
|
paramCounter: 5,
|
||||||
|
expected: "name = $6",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
q := &PgSQLSelectQuery{paramCounter: tt.paramCounter}
|
||||||
|
result := q.replacePlaceholders(tt.query, tt.argCount)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLSelectQuery_Chaining tests method chaining
|
||||||
|
func TestPgSQLSelectQuery_Chaining(t *testing.T) {
|
||||||
|
db, _, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
query := adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Column("id", "name").
|
||||||
|
Where("age > ?", 18).
|
||||||
|
Order("name ASC").
|
||||||
|
Limit(10).
|
||||||
|
Offset(5)
|
||||||
|
|
||||||
|
pgQuery := query.(*PgSQLSelectQuery)
|
||||||
|
assert.Equal(t, "users", pgQuery.tableName)
|
||||||
|
assert.Equal(t, []string{"id", "name"}, pgQuery.columns)
|
||||||
|
assert.Len(t, pgQuery.whereClauses, 1)
|
||||||
|
assert.Equal(t, []string{"name ASC"}, pgQuery.orderBy)
|
||||||
|
assert.Equal(t, 10, pgQuery.limit)
|
||||||
|
assert.Equal(t, 5, pgQuery.offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLSelectQuery_Model tests model setting
|
||||||
|
func TestPgSQLSelectQuery_Model(t *testing.T) {
|
||||||
|
db, _, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
user := &TestUser{}
|
||||||
|
query := adapter.NewSelect().Model(user)
|
||||||
|
|
||||||
|
pgQuery := query.(*PgSQLSelectQuery)
|
||||||
|
assert.Equal(t, "users", pgQuery.tableName)
|
||||||
|
assert.Equal(t, user, pgQuery.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanRowsToStructSlice tests scanning rows into struct slice
|
||||||
|
func TestScanRowsToStructSlice(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name", "email", "age"}).
|
||||||
|
AddRow(1, "John Doe", "john@example.com", 25).
|
||||||
|
AddRow(2, "Jane Smith", "jane@example.com", 30)
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var users []TestUser
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Scan(ctx, &users)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, users, 2)
|
||||||
|
assert.Equal(t, "John Doe", users[0].Name)
|
||||||
|
assert.Equal(t, "jane@example.com", users[1].Email)
|
||||||
|
assert.Equal(t, 30, users[1].Age)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanRowsToStructSlicePointers tests scanning rows into pointer slice
|
||||||
|
func TestScanRowsToStructSlicePointers(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name", "email", "age"}).
|
||||||
|
AddRow(1, "John Doe", "john@example.com", 25)
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var users []*TestUser
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Scan(ctx, &users)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, users, 1)
|
||||||
|
assert.NotNil(t, users[0])
|
||||||
|
assert.Equal(t, "John Doe", users[0].Name)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanRowsToSingleStruct tests scanning a single row
|
||||||
|
func TestScanRowsToSingleStruct(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name", "email", "age"}).
|
||||||
|
AddRow(1, "John Doe", "john@example.com", 25)
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var user TestUser
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
Scan(ctx, &user)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, user.ID)
|
||||||
|
assert.Equal(t, "John Doe", user.Name)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanRowsToMapSlice tests scanning into map slice
|
||||||
|
func TestScanRowsToMapSlice(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
|
||||||
|
AddRow(1, "John Doe", "john@example.com").
|
||||||
|
AddRow(2, "Jane Smith", "jane@example.com")
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Scan(ctx, &results)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, results, 2)
|
||||||
|
assert.Equal(t, int64(1), results[0]["id"])
|
||||||
|
assert.Equal(t, "John Doe", results[0]["name"])
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLInsertQuery_Exec tests insert query execution
|
||||||
|
func TestPgSQLInsertQuery_Exec(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectExec("INSERT INTO users").
|
||||||
|
WithArgs("John Doe", "john@example.com", 25).
|
||||||
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
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.NotNil(t, result)
|
||||||
|
assert.Equal(t, int64(1), result.RowsAffected())
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLUpdateQuery_Exec tests update query execution
|
||||||
|
func TestPgSQLUpdateQuery_Exec(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Note: Args order is SET values first, then WHERE values
|
||||||
|
mock.ExpectExec("UPDATE users SET name = \\$1 WHERE id = \\$2").
|
||||||
|
WithArgs("Jane Doe", 1).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
result, err := adapter.NewUpdate().
|
||||||
|
Table("users").
|
||||||
|
Set("name", "Jane Doe").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, int64(1), result.RowsAffected())
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLDeleteQuery_Exec tests delete query execution
|
||||||
|
func TestPgSQLDeleteQuery_Exec(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectExec("DELETE FROM users WHERE id = \\$1").
|
||||||
|
WithArgs(1).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
result, err := adapter.NewDelete().
|
||||||
|
Table("users").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Equal(t, int64(1), result.RowsAffected())
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLSelectQuery_Count tests count query
|
||||||
|
func TestPgSQLSelectQuery_Count(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"count"}).AddRow(42)
|
||||||
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
count, err := adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Count(ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 42, count)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLSelectQuery_Exists tests exists query
|
||||||
|
func TestPgSQLSelectQuery_Exists(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"count"}).AddRow(1)
|
||||||
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
exists, err := adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Where("email = ?", "john@example.com").
|
||||||
|
Exists(ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLAdapter_Transaction tests transaction handling
|
||||||
|
func TestPgSQLAdapter_Transaction(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectExec("INSERT INTO users").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||||
|
mock.ExpectCommit()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err = adapter.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
|
_, err := tx.NewInsert().
|
||||||
|
Table("users").
|
||||||
|
Value("name", "John").
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPgSQLAdapter_TransactionRollback tests transaction rollback
|
||||||
|
func TestPgSQLAdapter_TransactionRollback(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectExec("INSERT INTO users").WillReturnError(sql.ErrConnDone)
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
err = adapter.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
|
_, err := tx.NewInsert().
|
||||||
|
Table("users").
|
||||||
|
Value("name", "John").
|
||||||
|
Exec(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuildFieldMap tests field mapping construction
|
||||||
|
func TestBuildFieldMap(t *testing.T) {
|
||||||
|
userType := reflect.TypeOf(TestUser{})
|
||||||
|
fieldMap := buildFieldMap(userType, nil)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, fieldMap)
|
||||||
|
|
||||||
|
// Check that fields are mapped
|
||||||
|
assert.Contains(t, fieldMap, "id")
|
||||||
|
assert.Contains(t, fieldMap, "name")
|
||||||
|
assert.Contains(t, fieldMap, "email")
|
||||||
|
assert.Contains(t, fieldMap, "age")
|
||||||
|
|
||||||
|
// Check field info
|
||||||
|
idInfo := fieldMap["id"]
|
||||||
|
assert.Equal(t, "ID", idInfo.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetRelationMetadata tests relationship metadata extraction
|
||||||
|
func TestGetRelationMetadata(t *testing.T) {
|
||||||
|
q := &PgSQLSelectQuery{
|
||||||
|
model: &TestPost{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test belongs-to relationship
|
||||||
|
meta := q.getRelationMetadata("User")
|
||||||
|
assert.NotNil(t, meta)
|
||||||
|
assert.Equal(t, "User", meta.fieldName)
|
||||||
|
|
||||||
|
// Test has-many relationship
|
||||||
|
meta = q.getRelationMetadata("Comments")
|
||||||
|
assert.NotNil(t, meta)
|
||||||
|
assert.Equal(t, "Comments", meta.fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPreloadConfiguration tests preload configuration
|
||||||
|
func TestPreloadConfiguration(t *testing.T) {
|
||||||
|
db, _, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
|
||||||
|
// Test Preload
|
||||||
|
query := adapter.NewSelect().
|
||||||
|
Model(&TestPost{}).
|
||||||
|
Table("posts").
|
||||||
|
Preload("User")
|
||||||
|
|
||||||
|
pgQuery := query.(*PgSQLSelectQuery)
|
||||||
|
assert.Len(t, pgQuery.preloads, 1)
|
||||||
|
assert.Equal(t, "User", pgQuery.preloads[0].relation)
|
||||||
|
assert.False(t, pgQuery.preloads[0].useJoin)
|
||||||
|
|
||||||
|
// Test PreloadRelation
|
||||||
|
query = adapter.NewSelect().
|
||||||
|
Model(&TestPost{}).
|
||||||
|
Table("posts").
|
||||||
|
PreloadRelation("Comments")
|
||||||
|
|
||||||
|
pgQuery = query.(*PgSQLSelectQuery)
|
||||||
|
assert.Len(t, pgQuery.preloads, 1)
|
||||||
|
assert.Equal(t, "Comments", pgQuery.preloads[0].relation)
|
||||||
|
|
||||||
|
// Test JoinRelation
|
||||||
|
query = adapter.NewSelect().
|
||||||
|
Model(&TestPost{}).
|
||||||
|
Table("posts").
|
||||||
|
JoinRelation("User")
|
||||||
|
|
||||||
|
pgQuery = query.(*PgSQLSelectQuery)
|
||||||
|
assert.Len(t, pgQuery.preloads, 1)
|
||||||
|
assert.Equal(t, "User", pgQuery.preloads[0].relation)
|
||||||
|
assert.True(t, pgQuery.preloads[0].useJoin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScanModel tests ScanModel functionality
|
||||||
|
func TestScanModel(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name", "email", "age"}).
|
||||||
|
AddRow(1, "John Doe", "john@example.com", 25)
|
||||||
|
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM users").WillReturnRows(rows)
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
user := &TestUser{}
|
||||||
|
err = adapter.NewSelect().
|
||||||
|
Model(user).
|
||||||
|
Table("users").
|
||||||
|
Where("id = ?", 1).
|
||||||
|
ScanModel(ctx)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, user.ID)
|
||||||
|
assert.Equal(t, "John Doe", user.Name)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRawSQL tests raw SQL execution
|
||||||
|
func TestRawSQL(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test Exec
|
||||||
|
mock.ExpectExec("CREATE TABLE test").WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
|
|
||||||
|
adapter := NewPgSQLAdapter(db)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, err = adapter.Exec(ctx, "CREATE TABLE test (id INT)")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test Query
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "Test")
|
||||||
|
mock.ExpectQuery("SELECT (.+) FROM test").WillReturnRows(rows)
|
||||||
|
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err = adapter.Query(ctx, &results, "SELECT * FROM test WHERE id = $1", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, results, 1)
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
132
pkg/common/adapters/database/test_helpers.go
Normal file
132
pkg/common/adapters/database/test_helpers.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHelper provides utilities for database testing
|
||||||
|
type TestHelper struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Adapter *PgSQLAdapter
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTestHelper creates a new test helper
|
||||||
|
func NewTestHelper(t *testing.T, db *sql.DB) *TestHelper {
|
||||||
|
return &TestHelper{
|
||||||
|
DB: db,
|
||||||
|
Adapter: NewPgSQLAdapter(db),
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupTables truncates all test tables
|
||||||
|
func (h *TestHelper) CleanupTables() {
|
||||||
|
ctx := context.Background()
|
||||||
|
tables := []string{"comments", "posts", "users"}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
_, err := h.DB.ExecContext(ctx, "TRUNCATE TABLE "+table+" CASCADE")
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertUser inserts a test user and returns the ID
|
||||||
|
func (h *TestHelper) InsertUser(name, email string, age int) int {
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := h.Adapter.NewInsert().
|
||||||
|
Table("users").
|
||||||
|
Value("name", name).
|
||||||
|
Value("email", email).
|
||||||
|
Value("age", age).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
id, _ := result.LastInsertId()
|
||||||
|
return int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertPost inserts a test post and returns the ID
|
||||||
|
func (h *TestHelper) InsertPost(userID int, title, content string, published bool) int {
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := h.Adapter.NewInsert().
|
||||||
|
Table("posts").
|
||||||
|
Value("user_id", userID).
|
||||||
|
Value("title", title).
|
||||||
|
Value("content", content).
|
||||||
|
Value("published", published).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
id, _ := result.LastInsertId()
|
||||||
|
return int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertComment inserts a test comment and returns the ID
|
||||||
|
func (h *TestHelper) InsertComment(postID int, content string) int {
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := h.Adapter.NewInsert().
|
||||||
|
Table("comments").
|
||||||
|
Value("post_id", postID).
|
||||||
|
Value("content", content).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
id, _ := result.LastInsertId()
|
||||||
|
return int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertUserExists checks if a user exists by email
|
||||||
|
func (h *TestHelper) AssertUserExists(email string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
exists, err := h.Adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Where("email = ?", email).
|
||||||
|
Exists(ctx)
|
||||||
|
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
require.True(h.t, exists, "User with email %s should exist", email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertUserCount asserts the number of users
|
||||||
|
func (h *TestHelper) AssertUserCount(expected int) {
|
||||||
|
ctx := context.Background()
|
||||||
|
count, err := h.Adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Count(ctx)
|
||||||
|
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
require.Equal(h.t, expected, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail retrieves a user by email
|
||||||
|
func (h *TestHelper) GetUserByEmail(email string) map[string]interface{} {
|
||||||
|
ctx := context.Background()
|
||||||
|
var results []map[string]interface{}
|
||||||
|
err := h.Adapter.NewSelect().
|
||||||
|
Table("users").
|
||||||
|
Where("email = ?", email).
|
||||||
|
Scan(ctx, &results)
|
||||||
|
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
require.Len(h.t, results, 1, "Expected exactly one user with email %s", email)
|
||||||
|
return results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeginTestTransaction starts a transaction for testing
|
||||||
|
func (h *TestHelper) BeginTestTransaction() (*PgSQLTxAdapter, func()) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tx, err := h.DB.BeginTx(ctx, nil)
|
||||||
|
require.NoError(h.t, err)
|
||||||
|
|
||||||
|
adapter := &PgSQLTxAdapter{tx: tx}
|
||||||
|
cleanup := func() {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter, cleanup
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user