diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 148fe94..8ea5ddd 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -40,8 +40,18 @@ jobs: - name: Run compiler unit tests run: make test - - name: Run Integration Tests - env: - PG_DATABASE_URL: ${{ secrets.PG_DATABASE_URL }} - SQLITE_DATABASE_URL: ${{ secrets.SQLITE_DATABASE_URL }} + - name: Run SQLite Integration Tests run: make integration-test + + - name: Run Postgres Integration Tests + run: | + PG_URL="${{ secrets.PG_DATABASE_URL }}" + if [ -z "$PG_URL" ]; then + PG_URL="postgres://testuser:testpassword@localhost:5432/valkyrie_test?sslmode=disable" + fi + sed -i 's/provider = "sqlite"/provider = "postgres"/' integration/schema.prisma + rm -f integration/valkyrie/migrations/*.sql + make integration-gen + cd integration + ../bin/valkyrie migrate -u "$PG_URL" init_pg + PG_DATABASE_URL="$PG_URL" go test -v ./... diff --git a/integration/benchmark_test.go b/integration/benchmark_test.go index bd8109d..d8376a3 100644 --- a/integration/benchmark_test.go +++ b/integration/benchmark_test.go @@ -17,7 +17,7 @@ func generateCUID() string { buf := make([]byte, 1, 33) buf[0] = 'c' buf = strconv.AppendUint(buf, now, 16) - + const hextable = "0123456789abcdef" for _, v := range b { buf = append(buf, hextable[v>>4], hextable[v&0x0f]) @@ -35,14 +35,17 @@ func TestCreationBenchmark(t *testing.T) { // 1. Raw SQL Insert (Write Only) t.Logf("Running %d iterations of Raw SQL Insert (Write-only)...", iterations) startRawWrite := time.Now() - for i := 0; i < iterations; i++ { + for i := range iterations { id := "raw-w-" + strconv.Itoa(i) email := fmt.Sprintf("raw-w-%d@example.com", i) phone := fmt.Sprintf("+12345%d", i) role := "student" _, err := db.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), id, email, phone, role, ) if err != nil { @@ -55,10 +58,10 @@ func TestCreationBenchmark(t *testing.T) { // 2. ORM Create (which inserts and scans back the record) t.Logf("Running %d iterations of ORM Create...", iterations) startORM := time.Now() - for i := 0; i < iterations; i++ { + for i := range iterations { _, err := db.User.Create(valkyrie.UserCreateInput{ Email: fmt.Sprintf("orm-%d@example.com", i), - PhoneNum: fmt.Sprintf("+12345%d", i), + PhoneNum: fmt.Sprintf("+54321%d", i), }).Exec(ctx) if err != nil { t.Fatalf("ORM create failed: %v", err) @@ -73,11 +76,14 @@ func TestCreationBenchmark(t *testing.T) { for i := range iterations { id := "raw-r-" + strconv.Itoa(i) email := fmt.Sprintf("raw-r-%d@example.com", i) - phone := fmt.Sprintf("+12345%d", i) + phone := fmt.Sprintf("+99999%d", i) role := "student" _, err := db.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), id, email, phone, role, ) if err != nil { @@ -86,7 +92,10 @@ func TestCreationBenchmark(t *testing.T) { var res valkyrie.User err = db.Raw().QueryRowContext(ctx, - "SELECT id, email, phoneNum, role, referredById FROM User WHERE id = ?", + query( + `SELECT "id", "email", "phoneNum", "role", "referredById" FROM "User" WHERE "id" = ?`, + `SELECT "id", "email", "phoneNum", "role", "referredById" FROM "User" WHERE "id" = $1`, + ), id, ).Scan(&res.Id, &res.Email, &res.PhoneNum, &res.Role, &res.ReferredById) if err != nil { @@ -125,7 +134,10 @@ func BenchmarkRawSQLCreate(b *testing.B) { role := "student" _, err := db.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), id, email, phone, role, ) if err != nil { @@ -146,7 +158,10 @@ func BenchmarkRawSQLCreateWithScan(b *testing.B) { role := "student" _, err := db.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), id, email, phone, role, ) if err != nil { @@ -155,7 +170,10 @@ func BenchmarkRawSQLCreateWithScan(b *testing.B) { var res valkyrie.User err = db.Raw().QueryRowContext(ctx, - "SELECT id, email, phoneNum, role, referredById FROM User WHERE id = ?", + query( + `SELECT "id", "email", "phoneNum", "role", "referredById" FROM "User" WHERE "id" = ?`, + `SELECT "id", "email", "phoneNum", "role", "referredById" FROM "User" WHERE "id" = $1`, + ), id, ).Scan(&res.Id, &res.Email, &res.PhoneNum, &res.Role, &res.ReferredById) if err != nil { diff --git a/integration/create_test.go b/integration/create_test.go index ac3855b..76816a2 100644 --- a/integration/create_test.go +++ b/integration/create_test.go @@ -36,7 +36,10 @@ func TestCreateBasic(t *testing.T) { var dbEmail, dbPhone string var dbRole string - err = db.Raw().QueryRowContext(ctx, "SELECT email, phoneNum, role FROM User WHERE id = ?", u.Id).Scan(&dbEmail, &dbPhone, &dbRole) + err = db.Raw().QueryRowContext(ctx, query( + `SELECT "email", "phoneNum", "role" FROM "User" WHERE "id" = ?`, + `SELECT "email", "phoneNum", "role" FROM "User" WHERE "id" = $1`, + ), u.Id).Scan(&dbEmail, &dbPhone, &dbRole) if err != nil { t.Fatalf("failed to query database for created user: %v", err) } diff --git a/integration/go.mod b/integration/go.mod index cae3dca..1aa66f3 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -6,6 +6,7 @@ replace valkyrie => ../ require ( github.com/google/uuid v1.6.0 + github.com/lib/pq v1.12.3 github.com/pressly/goose/v3 v3.27.2 modernc.org/sqlite v1.53.0 ) diff --git a/integration/go.sum b/integration/go.sum index ddc91b0..a8ff022 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -8,6 +8,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= diff --git a/integration/schema.prisma b/integration/schema.prisma index f1a531a..0eccc03 100644 --- a/integration/schema.prisma +++ b/integration/schema.prisma @@ -35,7 +35,7 @@ model Profile { model Post { id String @id @default(cuid()) - title String @db.VarChar(22) + title String content String? published Boolean @default(false) authorId String diff --git a/integration/setup_test.go b/integration/setup_test.go index ba968c4..cd61675 100644 --- a/integration/setup_test.go +++ b/integration/setup_test.go @@ -2,20 +2,102 @@ package main import ( "context" + "database/sql" "integration/valkyrie" + "os" + "strings" "testing" + + _ "github.com/lib/pq" + _ "modernc.org/sqlite" ) +func query(sqlite, postgres string) string { + if getActiveProvider() == "postgres" { + return postgres + } + return sqlite +} + +func getActiveProvider() string { + content, err := os.ReadFile("schema.prisma") + if err != nil { + return "sqlite" + } + s := string(content) + if strings.Contains(s, `provider = "postgres"`) || strings.Contains(s, `provider = "postgresql"`) { + return "postgres" + } + return "sqlite" +} + +func getPostgresDSN() string { + if url := os.Getenv("PG_DATABASE_URL"); url != "" { + return url + } + + // Try local docker-compose default DSN first + localDSN := "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" + db, err := sql.Open("postgres", localDSN) + if err == nil { + err = db.Ping() + db.Close() + if err == nil { + return localDSN + } + } + + // Try CI default DSN + return "postgres://testuser:testpassword@localhost:5432/valkyrie_test?sslmode=disable" +} + func setupTestDB(t *testing.T) (*valkyrie.DB, func()) { ctx := context.Background() - db, err := valkyrie.Open("sqlite", "file::memory:?cache=shared&_pragma=foreign_keys(1)") + + provider := getActiveProvider() + var dsn string + + if provider == "postgres" { + dsn = getPostgresDSN() + + // Reset the postgres schema so we start fresh every time + resetDB, err := sql.Open("postgres", dsn) + if err != nil { + if t != nil { + t.Fatalf("failed to open database for reset: %v", err) + } else { + panic(err) + } + } + _, err = resetDB.Exec("DROP SCHEMA public CASCADE; CREATE SCHEMA public;") + resetDB.Close() + if err != nil { + if t != nil { + t.Fatalf("failed to reset postgres database: %v", err) + } else { + panic(err) + } + } + } else { + dsn = "file::memory:?cache=shared&_pragma=foreign_keys(1)" + } + + db, err := valkyrie.Open(provider, dsn) if err != nil { - t.Fatalf("failed to open database: %v", err) + if t != nil { + t.Fatalf("failed to open database (provider: %s, dsn: %s): %v", provider, dsn, err) + } else { + panic(err) + } } if err := db.RunMigrations(ctx); err != nil { db.Close() - t.Fatalf("failed to run migrations: %v", err) + if t != nil { + t.Fatalf("failed to run migrations: %v", err) + } else { + panic(err) + } } cleanup := func() { diff --git a/integration/transaction_test.go b/integration/transaction_test.go index a4c2146..e529b99 100644 --- a/integration/transaction_test.go +++ b/integration/transaction_test.go @@ -15,7 +15,10 @@ func TestTransactionCommit(t *testing.T) { err := db.Transaction(ctx, func(tx *valkyrie.Tx) error { _, err := tx.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), "u1", "user1@example.com", "123456789", "student", ) return err @@ -25,7 +28,10 @@ func TestTransactionCommit(t *testing.T) { } var email string - err = db.Raw().QueryRowContext(ctx, "SELECT email FROM User WHERE id = ?", "u1").Scan(&email) + err = db.Raw().QueryRowContext(ctx, query( + `SELECT "email" FROM "User" WHERE "id" = ?`, + `SELECT "email" FROM "User" WHERE "id" = $1`, + ), "u1").Scan(&email) if err != nil { t.Fatalf("failed to query committed user: %v", err) } @@ -42,7 +48,10 @@ func TestTransactionRollbackOnError(t *testing.T) { expectedErr := errors.New("something went wrong") err := db.Transaction(ctx, func(tx *valkyrie.Tx) error { _, err := tx.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), "u2", "user2@example.com", "987654321", "ADMIN", ) if err != nil { @@ -56,7 +65,10 @@ func TestTransactionRollbackOnError(t *testing.T) { } var count int - err = db.Raw().QueryRowContext(ctx, "SELECT COUNT(*) FROM User WHERE id = ?", "u2").Scan(&count) + err = db.Raw().QueryRowContext(ctx, query( + `SELECT COUNT(*) FROM "User" WHERE "id" = ?`, + `SELECT COUNT(*) FROM "User" WHERE "id" = $1`, + ), "u2").Scan(&count) if err != nil { t.Fatalf("failed to query database: %v", err) } @@ -81,7 +93,10 @@ func TestTransactionRollbackOnPanic(t *testing.T) { } var count int - err := db.Raw().QueryRowContext(ctx, "SELECT COUNT(*) FROM User WHERE id = ?", "u3").Scan(&count) + err := db.Raw().QueryRowContext(ctx, query( + `SELECT COUNT(*) FROM "User" WHERE "id" = ?`, + `SELECT COUNT(*) FROM "User" WHERE "id" = $1`, + ), "u3").Scan(&count) if err != nil { t.Fatalf("failed to query database after panic rollback: %v", err) } @@ -92,7 +107,10 @@ func TestTransactionRollbackOnPanic(t *testing.T) { _ = db.Transaction(ctx, func(tx *valkyrie.Tx) error { _, err := tx.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), "u3", "user3@example.com", "555555555", "TEACHER", ) if err != nil { @@ -112,7 +130,10 @@ func TestManualTransactionCommitAndRollback(t *testing.T) { t.Fatalf("failed to begin tx1: %v", err) } _, err = tx1.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), "u4", "user4@example.com", "444444444", "student", ) if err != nil { @@ -124,7 +145,10 @@ func TestManualTransactionCommitAndRollback(t *testing.T) { } var count1 int - err = db.Raw().QueryRowContext(ctx, "SELECT COUNT(*) FROM User WHERE id = ?", "u4").Scan(&count1) + err = db.Raw().QueryRowContext(ctx, query( + `SELECT COUNT(*) FROM "User" WHERE "id" = ?`, + `SELECT COUNT(*) FROM "User" WHERE "id" = $1`, + ), "u4").Scan(&count1) if err != nil || count1 != 1 { t.Errorf("expected user to be committed, count=%d, err=%v", count1, err) } @@ -134,7 +158,10 @@ func TestManualTransactionCommitAndRollback(t *testing.T) { t.Fatalf("failed to begin tx2: %v", err) } _, err = tx2.Raw().ExecContext(ctx, - "INSERT INTO User (id, email, phoneNum, role) VALUES (?, ?, ?, ?)", + query( + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES (?, ?, ?, ?)`, + `INSERT INTO "User" ("id", "email", "phoneNum", "role") VALUES ($1, $2, $3, $4)`, + ), "u5", "user5@example.com", "555555555", "student", ) if err != nil { @@ -146,7 +173,10 @@ func TestManualTransactionCommitAndRollback(t *testing.T) { } var count2 int - err = db.Raw().QueryRowContext(ctx, "SELECT COUNT(*) FROM User WHERE id = ?", "u5").Scan(&count2) + err = db.Raw().QueryRowContext(ctx, query( + `SELECT COUNT(*) FROM "User" WHERE "id" = ?`, + `SELECT COUNT(*) FROM "User" WHERE "id" = $1`, + ), "u5").Scan(&count2) if err != nil || count2 != 0 { t.Errorf("expected user to be rolled back, count=%d, err=%v", count2, err) }