diff --git a/internal/enginetest/mysql/coverage_test.go b/internal/enginetest/mysql/coverage_test.go new file mode 100644 index 0000000000..bdd9f4ca96 --- /dev/null +++ b/internal/enginetest/mysql/coverage_test.go @@ -0,0 +1,122 @@ +package mysql + +import ( + "os" + "path/filepath" + "testing" + + "github.com/sqlc-dev/sqlc/internal/enginetest/testcases" +) + +// TestCoverage verifies that all required test cases are implemented +// for the MySQL engine. +func TestCoverage(t *testing.T) { + engine := Engine() + registry := testcases.DefaultRegistry + + // Get all tests this engine should implement + requiredTests := registry.RequiredTestsForEngine(engine) + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Find all implemented tests + implemented := make(map[string]bool) + err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" || info.Name() == "sqlc.json" { + dir := filepath.Dir(path) + testName := filepath.Base(dir) + implemented[testName] = true + return filepath.SkipDir + } + return nil + }) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + + // Check for missing tests + var missing []*testcases.TestCase + for _, tc := range requiredTests { + if !implemented[tc.Name] { + missing = append(missing, tc) + } + } + + // Report missing tests (informational, not a failure) + if len(missing) > 0 { + t.Logf("MySQL engine is missing %d required test cases (this is informational):", len(missing)) + for _, tc := range missing { + t.Logf(" - %s (%s): %s", tc.ID, tc.Name, tc.Description) + } + } + + // Report coverage statistics + total := len(requiredTests) + covered := total - len(missing) + percentage := float64(covered) / float64(total) * 100 + + t.Logf("MySQL test coverage: %d/%d (%.1f%%)", covered, total, percentage) +} + +// TestCoverageByCategory reports coverage broken down by category +func TestCoverageByCategory(t *testing.T) { + engine := Engine() + registry := testcases.DefaultRegistry + caps := testcases.DefaultCapabilities(engine) + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Find all implemented tests + implemented := make(map[string]bool) + _ = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" || info.Name() == "sqlc.json" { + dir := filepath.Dir(path) + testName := filepath.Base(dir) + implemented[testName] = true + return filepath.SkipDir + } + return nil + }) + + // Report by category + categories := testcases.RequiredCategories() + if caps.SupportsEnum { + categories = append(categories, testcases.CategoryEnum) + } + if caps.SupportsSchema { + categories = append(categories, testcases.CategorySchema) + } + if caps.SupportsArray { + categories = append(categories, testcases.CategoryArray) + } + if caps.SupportsJSON { + categories = append(categories, testcases.CategoryJSON) + } + + for _, cat := range categories { + tests := registry.GetByCategory(cat) + var covered, total int + for _, tc := range tests { + total++ + if implemented[tc.Name] { + covered++ + } + } + if total > 0 { + percentage := float64(covered) / float64(total) * 100 + t.Logf(" %s: %d/%d (%.1f%%)", cat, covered, total, percentage) + } + } +} diff --git a/internal/enginetest/mysql/endtoend_test.go b/internal/enginetest/mysql/endtoend_test.go new file mode 100644 index 0000000000..c631b6278e --- /dev/null +++ b/internal/enginetest/mysql/endtoend_test.go @@ -0,0 +1,174 @@ +// Package mysql contains end-to-end tests for the MySQL engine. +package mysql + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/sqlc-dev/sqlc/internal/cmd" + "github.com/sqlc-dev/sqlc/internal/enginetest/testcases" +) + +func TestEndToEnd(t *testing.T) { + t.Parallel() + ctx := context.Background() + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Walk through all test directories + err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Look for sqlc config files + if info.Name() != "sqlc.yaml" && info.Name() != "sqlc.json" { + return nil + } + + dir := filepath.Dir(path) + testName := strings.TrimPrefix(dir, testdataDir+string(filepath.Separator)) + + t.Run(testName, func(t *testing.T) { + t.Parallel() + runTest(ctx, t, dir) + }) + + return filepath.SkipDir + }) + + if err != nil { + t.Fatal(err) + } +} + +func runTest(ctx context.Context, t *testing.T, dir string) { + t.Helper() + + var stderr bytes.Buffer + opts := &cmd.Options{ + Env: cmd.Env{}, + Stderr: &stderr, + } + + // Check for expected stderr + expectedStderr := readExpectedStderr(t, dir) + + output, err := cmd.Generate(ctx, dir, "", opts) + + // If we expect an error, check stderr matches + if len(expectedStderr) > 0 { + if err == nil { + t.Fatalf("expected error but got none") + } + diff := cmp.Diff( + strings.TrimSpace(expectedStderr), + strings.TrimSpace(stderr.String()), + stderrTransformer(), + ) + if diff != "" { + t.Fatalf("stderr differed (-want +got):\n%s", diff) + } + return + } + + if err != nil { + t.Fatalf("sqlc generate failed: %s", stderr.String()) + } + + cmpDirectory(t, dir, output) +} + +func readExpectedStderr(t *testing.T, dir string) string { + t.Helper() + + paths := []string{ + filepath.Join(dir, "stderr.txt"), + } + + for _, path := range paths { + if _, err := os.Stat(path); !os.IsNotExist(err) { + blob, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(blob) + } + } + return "" +} + +func stderrTransformer() cmp.Option { + return cmp.Transformer("Stderr", func(in string) string { + s := strings.Replace(in, "\r", "", -1) + return strings.Replace(s, "\\", "/", -1) + }) +} + +func lineEndings() cmp.Option { + return cmp.Transformer("LineEndings", func(in string) string { + return strings.Replace(in, "\r\n", "\n", -1) + }) +} + +func cmpDirectory(t *testing.T, dir string, actual map[string]string) { + t.Helper() + + expected := map[string]string{} + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".go") { + return nil + } + if strings.HasSuffix(path, "_test.go") { + return nil + } + blob, err := os.ReadFile(path) + if err != nil { + return err + } + expected[path] = string(blob) + return nil + }) + if err != nil { + t.Fatal(err) + } + + opts := []cmp.Option{ + cmpopts.EquateEmpty(), + lineEndings(), + } + + if !cmp.Equal(expected, actual, opts...) { + t.Errorf("%s contents differ", dir) + for name, contents := range expected { + if actual[name] == "" { + t.Errorf("%s is empty", name) + continue + } + if diff := cmp.Diff(contents, actual[name], opts...); diff != "" { + t.Errorf("%s differed (-want +got):\n%s", name, diff) + } + } + } +} + +// Engine returns the engine type for this package +func Engine() testcases.Engine { + return testcases.EngineMySQL +} diff --git a/internal/enginetest/mysql/testdata/join_inner/go/db.go b/internal/enginetest/mysql/testdata/join_inner/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/mysql/testdata/join_inner/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/mysql/testdata/join_inner/go/models.go b/internal/enginetest/mysql/testdata/join_inner/go/models.go new file mode 100644 index 0000000000..7cd96c8c87 --- /dev/null +++ b/internal/enginetest/mysql/testdata/join_inner/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/mysql/testdata/join_inner/go/query.sql.go b/internal/enginetest/mysql/testdata/join_inner/go/query.sql.go new file mode 100644 index 0000000000..1962e9f34e --- /dev/null +++ b/internal/enginetest/mysql/testdata/join_inner/go/query.sql.go @@ -0,0 +1,88 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getOrderItemsWithProduct = `-- name: GetOrderItemsWithProduct :many +SELECT oi.quantity, oi.unit_price, p.name AS product_name +FROM order_items oi +INNER JOIN products p ON oi.product_id = p.id +` + +type GetOrderItemsWithProductRow struct { + Quantity int32 + UnitPrice string + ProductName string +} + +func (q *Queries) GetOrderItemsWithProduct(ctx context.Context) ([]GetOrderItemsWithProductRow, error) { + rows, err := q.db.QueryContext(ctx, getOrderItemsWithProduct) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrderItemsWithProductRow + for rows.Next() { + var i GetOrderItemsWithProductRow + if err := rows.Scan(&i.Quantity, &i.UnitPrice, &i.ProductName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOrdersWithUser = `-- name: GetOrdersWithUser :many +SELECT o.id, o.status, o.total_amount, u.username, u.email +FROM orders o +INNER JOIN users u ON o.user_id = u.id +` + +type GetOrdersWithUserRow struct { + ID int32 + Status string + TotalAmount string + Username string + Email string +} + +func (q *Queries) GetOrdersWithUser(ctx context.Context) ([]GetOrdersWithUserRow, error) { + rows, err := q.db.QueryContext(ctx, getOrdersWithUser) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrdersWithUserRow + for rows.Next() { + var i GetOrdersWithUserRow + if err := rows.Scan( + &i.ID, + &i.Status, + &i.TotalAmount, + &i.Username, + &i.Email, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/enginetest/mysql/testdata/join_inner/query.sql b/internal/enginetest/mysql/testdata/join_inner/query.sql new file mode 100644 index 0000000000..d13c84bc37 --- /dev/null +++ b/internal/enginetest/mysql/testdata/join_inner/query.sql @@ -0,0 +1,9 @@ +-- name: GetOrdersWithUser :many +SELECT o.id, o.status, o.total_amount, u.username, u.email +FROM orders o +INNER JOIN users u ON o.user_id = u.id; + +-- name: GetOrderItemsWithProduct :many +SELECT oi.quantity, oi.unit_price, p.name AS product_name +FROM order_items oi +INNER JOIN products p ON oi.product_id = p.id; diff --git a/internal/enginetest/mysql/testdata/join_inner/sqlc.yaml b/internal/enginetest/mysql/testdata/join_inner/sqlc.yaml new file mode 100644 index 0000000000..87911b9166 --- /dev/null +++ b/internal/enginetest/mysql/testdata/join_inner/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "mysql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/mysql/testdata/schema.sql b/internal/enginetest/mysql/testdata/schema.sql new file mode 100644 index 0000000000..d1c8fe0dd9 --- /dev/null +++ b/internal/enginetest/mysql/testdata/schema.sql @@ -0,0 +1,163 @@ +-- ============================================================================= +-- MySQL Core Schema +-- This schema is used by all MySQL end-to-end tests +-- ============================================================================= + +-- Users table: core entity with various column types +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL, + full_name TEXT, + age INT, + balance DECIMAL(10,2) NOT NULL DEFAULT 0.00, + is_active BOOLEAN NOT NULL DEFAULT true, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + role VARCHAR(20) NOT NULL DEFAULT 'user', + bio TEXT, + metadata BLOB, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL +); + +-- Categories table: self-referential for tree structures +CREATE TABLE categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + parent_id INT, + name VARCHAR(255) NOT NULL, + description TEXT, + sort_order INT NOT NULL DEFAULT 0, + is_visible BOOLEAN NOT NULL DEFAULT true, + FOREIGN KEY (parent_id) REFERENCES categories(id) +); + +-- Products table: many-to-one with categories +CREATE TABLE products ( + id INT AUTO_INCREMENT PRIMARY KEY, + category_id INT, + name VARCHAR(255) NOT NULL, + description TEXT, + price DECIMAL(10,2) NOT NULL, + quantity INT NOT NULL DEFAULT 0, + weight FLOAT, + is_available BOOLEAN NOT NULL DEFAULT true, + tags TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL, + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +-- Orders table: many-to-one with users +CREATE TABLE orders ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'draft', + total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, + notes TEXT, + shipped_at TIMESTAMP NULL, + delivered_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- Order items table: many-to-one with orders and products +CREATE TABLE order_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + order_id INT NOT NULL, + product_id INT NOT NULL, + quantity INT NOT NULL DEFAULT 1, + unit_price DECIMAL(10,2) NOT NULL, + discount DECIMAL(10,2) NOT NULL DEFAULT 0.00, + UNIQUE KEY (order_id, product_id), + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) +); + +-- Tags table: for many-to-many relationships +CREATE TABLE tags ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + color VARCHAR(7), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Product tags junction table: many-to-many +CREATE TABLE product_tags ( + product_id INT NOT NULL, + tag_id INT NOT NULL, + PRIMARY KEY (product_id, tag_id), + FOREIGN KEY (product_id) REFERENCES products(id), + FOREIGN KEY (tag_id) REFERENCES tags(id) +); + +-- Audit log table +CREATE TABLE audit_logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + table_name VARCHAR(100) NOT NULL, + record_id INT NOT NULL, + action VARCHAR(20) NOT NULL, + old_values TEXT, + new_values TEXT, + user_id INT, + ip_address VARCHAR(45), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- Settings table: key-value store +CREATE TABLE settings ( + `key` VARCHAR(255) PRIMARY KEY, + value TEXT NOT NULL, + value_type VARCHAR(20) NOT NULL DEFAULT 'string', + description TEXT, + updated_at TIMESTAMP NULL +); + +-- Tasks table +CREATE TABLE tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + priority VARCHAR(20) NOT NULL DEFAULT 'medium', + is_completed BOOLEAN NOT NULL DEFAULT false, + due_date DATE, + started_at TIMESTAMP NULL, + completed_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- ============================================================================= +-- Views +-- ============================================================================= + +CREATE VIEW active_users AS + SELECT id, username, email, full_name, created_at + FROM users + WHERE is_active = true AND deleted_at IS NULL; + +CREATE VIEW order_summaries AS + SELECT + o.id AS order_id, + o.user_id, + u.username, + o.status, + o.total_amount, + COUNT(oi.id) AS item_count, + o.created_at + FROM orders o + JOIN users u ON o.user_id = u.id + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY o.id, o.user_id, u.username, o.status, o.total_amount, o.created_at; + +CREATE VIEW category_tree AS + SELECT + c.id, + c.name, + c.parent_id, + p.name AS parent_name + FROM categories c + LEFT JOIN categories p ON c.parent_id = p.id; diff --git a/internal/enginetest/mysql/testdata/select_star/go/db.go b/internal/enginetest/mysql/testdata/select_star/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_star/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/mysql/testdata/select_star/go/models.go b/internal/enginetest/mysql/testdata/select_star/go/models.go new file mode 100644 index 0000000000..7cd96c8c87 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_star/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/mysql/testdata/select_star/go/query.sql.go b/internal/enginetest/mysql/testdata/select_star/go/query.sql.go new file mode 100644 index 0000000000..78ec4ce1fc --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_star/go/query.sql.go @@ -0,0 +1,123 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getAllFromSubquery = `-- name: GetAllFromSubquery :many +SELECT id, username FROM (SELECT id, username FROM users) t +` + +type GetAllFromSubqueryRow struct { + ID int32 + Username string +} + +func (q *Queries) GetAllFromSubquery(ctx context.Context) ([]GetAllFromSubqueryRow, error) { + rows, err := q.db.QueryContext(ctx, getAllFromSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllFromSubqueryRow + for rows.Next() { + var i GetAllFromSubqueryRow + if err := rows.Scan(&i.ID, &i.Username); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllProducts = `-- name: GetAllProducts :many +SELECT id, category_id, name, description, price, quantity, weight, is_available, tags, created_at, updated_at FROM products +` + +func (q *Queries) GetAllProducts(ctx context.Context) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, getAllProducts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Product + for rows.Next() { + var i Product + if err := rows.Scan( + &i.ID, + &i.CategoryID, + &i.Name, + &i.Description, + &i.Price, + &i.Quantity, + &i.Weight, + &i.IsAvailable, + &i.Tags, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllUsers = `-- name: GetAllUsers :many +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users +` + +func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getAllUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/enginetest/mysql/testdata/select_star/query.sql b/internal/enginetest/mysql/testdata/select_star/query.sql new file mode 100644 index 0000000000..e5d8686d1a --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_star/query.sql @@ -0,0 +1,8 @@ +-- name: GetAllUsers :many +SELECT * FROM users; + +-- name: GetAllProducts :many +SELECT * FROM products; + +-- name: GetAllFromSubquery :many +SELECT * FROM (SELECT id, username FROM users) t; diff --git a/internal/enginetest/mysql/testdata/select_star/sqlc.yaml b/internal/enginetest/mysql/testdata/select_star/sqlc.yaml new file mode 100644 index 0000000000..87911b9166 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_star/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "mysql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/mysql/testdata/select_where/go/db.go b/internal/enginetest/mysql/testdata/select_where/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_where/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/mysql/testdata/select_where/go/models.go b/internal/enginetest/mysql/testdata/select_where/go/models.go new file mode 100644 index 0000000000..7cd96c8c87 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_where/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/mysql/testdata/select_where/go/query.sql.go b/internal/enginetest/mysql/testdata/select_where/go/query.sql.go new file mode 100644 index 0000000000..9836f244ac --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_where/go/query.sql.go @@ -0,0 +1,104 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getActiveUsers = `-- name: GetActiveUsers :many +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE is_active = ? +` + +func (q *Queries) GetActiveUsers(ctx context.Context, isActive bool) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getActiveUsers, isActive) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE id = ? +` + +func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const getUserByUsername = `-- name: GetUserByUsername :one +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE username = ? +` + +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByUsername, username) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/internal/enginetest/mysql/testdata/select_where/query.sql b/internal/enginetest/mysql/testdata/select_where/query.sql new file mode 100644 index 0000000000..bb9a73e718 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_where/query.sql @@ -0,0 +1,8 @@ +-- name: GetUserByID :one +SELECT * FROM users WHERE id = ?; + +-- name: GetUserByUsername :one +SELECT * FROM users WHERE username = ?; + +-- name: GetActiveUsers :many +SELECT * FROM users WHERE is_active = ?; diff --git a/internal/enginetest/mysql/testdata/select_where/sqlc.yaml b/internal/enginetest/mysql/testdata/select_where/sqlc.yaml new file mode 100644 index 0000000000..87911b9166 --- /dev/null +++ b/internal/enginetest/mysql/testdata/select_where/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "mysql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/postgresql/coverage_test.go b/internal/enginetest/postgresql/coverage_test.go new file mode 100644 index 0000000000..909a43a1a5 --- /dev/null +++ b/internal/enginetest/postgresql/coverage_test.go @@ -0,0 +1,122 @@ +package postgresql + +import ( + "os" + "path/filepath" + "testing" + + "github.com/sqlc-dev/sqlc/internal/enginetest/testcases" +) + +// TestCoverage verifies that all required test cases are implemented +// for the PostgreSQL engine. +func TestCoverage(t *testing.T) { + engine := Engine() + registry := testcases.DefaultRegistry + + // Get all tests this engine should implement + requiredTests := registry.RequiredTestsForEngine(engine) + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Find all implemented tests + implemented := make(map[string]bool) + err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" || info.Name() == "sqlc.json" { + dir := filepath.Dir(path) + testName := filepath.Base(dir) + implemented[testName] = true + return filepath.SkipDir + } + return nil + }) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + + // Check for missing tests + var missing []*testcases.TestCase + for _, tc := range requiredTests { + if !implemented[tc.Name] { + missing = append(missing, tc) + } + } + + // Report missing tests (informational, not a failure) + if len(missing) > 0 { + t.Logf("PostgreSQL engine is missing %d required test cases (this is informational):", len(missing)) + for _, tc := range missing { + t.Logf(" - %s (%s): %s", tc.ID, tc.Name, tc.Description) + } + } + + // Report coverage statistics + total := len(requiredTests) + covered := total - len(missing) + percentage := float64(covered) / float64(total) * 100 + + t.Logf("PostgreSQL test coverage: %d/%d (%.1f%%)", covered, total, percentage) +} + +// TestCoverageByCategory reports coverage broken down by category +func TestCoverageByCategory(t *testing.T) { + engine := Engine() + registry := testcases.DefaultRegistry + caps := testcases.DefaultCapabilities(engine) + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Find all implemented tests + implemented := make(map[string]bool) + _ = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" || info.Name() == "sqlc.json" { + dir := filepath.Dir(path) + testName := filepath.Base(dir) + implemented[testName] = true + return filepath.SkipDir + } + return nil + }) + + // Report by category + categories := testcases.RequiredCategories() + if caps.SupportsEnum { + categories = append(categories, testcases.CategoryEnum) + } + if caps.SupportsSchema { + categories = append(categories, testcases.CategorySchema) + } + if caps.SupportsArray { + categories = append(categories, testcases.CategoryArray) + } + if caps.SupportsJSON { + categories = append(categories, testcases.CategoryJSON) + } + + for _, cat := range categories { + tests := registry.GetByCategory(cat) + var covered, total int + for _, tc := range tests { + total++ + if implemented[tc.Name] { + covered++ + } + } + if total > 0 { + percentage := float64(covered) / float64(total) * 100 + t.Logf(" %s: %d/%d (%.1f%%)", cat, covered, total, percentage) + } + } +} diff --git a/internal/enginetest/postgresql/endtoend_test.go b/internal/enginetest/postgresql/endtoend_test.go new file mode 100644 index 0000000000..72fc5a3216 --- /dev/null +++ b/internal/enginetest/postgresql/endtoend_test.go @@ -0,0 +1,174 @@ +// Package postgresql contains end-to-end tests for the PostgreSQL engine. +package postgresql + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/sqlc-dev/sqlc/internal/cmd" + "github.com/sqlc-dev/sqlc/internal/enginetest/testcases" +) + +func TestEndToEnd(t *testing.T) { + t.Parallel() + ctx := context.Background() + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Walk through all test directories + err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Look for sqlc config files + if info.Name() != "sqlc.yaml" && info.Name() != "sqlc.json" { + return nil + } + + dir := filepath.Dir(path) + testName := strings.TrimPrefix(dir, testdataDir+string(filepath.Separator)) + + t.Run(testName, func(t *testing.T) { + t.Parallel() + runTest(ctx, t, dir) + }) + + return filepath.SkipDir + }) + + if err != nil { + t.Fatal(err) + } +} + +func runTest(ctx context.Context, t *testing.T, dir string) { + t.Helper() + + var stderr bytes.Buffer + opts := &cmd.Options{ + Env: cmd.Env{}, + Stderr: &stderr, + } + + // Check for expected stderr + expectedStderr := readExpectedStderr(t, dir) + + output, err := cmd.Generate(ctx, dir, "", opts) + + // If we expect an error, check stderr matches + if len(expectedStderr) > 0 { + if err == nil { + t.Fatalf("expected error but got none") + } + diff := cmp.Diff( + strings.TrimSpace(expectedStderr), + strings.TrimSpace(stderr.String()), + stderrTransformer(), + ) + if diff != "" { + t.Fatalf("stderr differed (-want +got):\n%s", diff) + } + return + } + + if err != nil { + t.Fatalf("sqlc generate failed: %s", stderr.String()) + } + + cmpDirectory(t, dir, output) +} + +func readExpectedStderr(t *testing.T, dir string) string { + t.Helper() + + paths := []string{ + filepath.Join(dir, "stderr.txt"), + } + + for _, path := range paths { + if _, err := os.Stat(path); !os.IsNotExist(err) { + blob, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(blob) + } + } + return "" +} + +func stderrTransformer() cmp.Option { + return cmp.Transformer("Stderr", func(in string) string { + s := strings.Replace(in, "\r", "", -1) + return strings.Replace(s, "\\", "/", -1) + }) +} + +func lineEndings() cmp.Option { + return cmp.Transformer("LineEndings", func(in string) string { + return strings.Replace(in, "\r\n", "\n", -1) + }) +} + +func cmpDirectory(t *testing.T, dir string, actual map[string]string) { + t.Helper() + + expected := map[string]string{} + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".go") { + return nil + } + if strings.HasSuffix(path, "_test.go") { + return nil + } + blob, err := os.ReadFile(path) + if err != nil { + return err + } + expected[path] = string(blob) + return nil + }) + if err != nil { + t.Fatal(err) + } + + opts := []cmp.Option{ + cmpopts.EquateEmpty(), + lineEndings(), + } + + if !cmp.Equal(expected, actual, opts...) { + t.Errorf("%s contents differ", dir) + for name, contents := range expected { + if actual[name] == "" { + t.Errorf("%s is empty", name) + continue + } + if diff := cmp.Diff(contents, actual[name], opts...); diff != "" { + t.Errorf("%s differed (-want +got):\n%s", name, diff) + } + } + } +} + +// Engine returns the engine type for this package +func Engine() testcases.Engine { + return testcases.EnginePostgreSQL +} diff --git a/internal/enginetest/postgresql/testdata/insert_returning_star/go/db.go b/internal/enginetest/postgresql/testdata/insert_returning_star/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/insert_returning_star/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/postgresql/testdata/insert_returning_star/go/models.go b/internal/enginetest/postgresql/testdata/insert_returning_star/go/models.go new file mode 100644 index 0000000000..ebcbf1076f --- /dev/null +++ b/internal/enginetest/postgresql/testdata/insert_returning_star/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/postgresql/testdata/insert_returning_star/go/query.sql.go b/internal/enginetest/postgresql/testdata/insert_returning_star/go/query.sql.go new file mode 100644 index 0000000000..a3e1fa69f4 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/insert_returning_star/go/query.sql.go @@ -0,0 +1,73 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const createProduct = `-- name: CreateProduct :one +INSERT INTO products (name, price, created_at) +VALUES ($1, $2, NOW()) +RETURNING id, category_id, name, description, price, quantity, weight, is_available, tags, created_at, updated_at +` + +type CreateProductParams struct { + Name string + Price string +} + +func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) (Product, error) { + row := q.db.QueryRowContext(ctx, createProduct, arg.Name, arg.Price) + var i Product + err := row.Scan( + &i.ID, + &i.CategoryID, + &i.Name, + &i.Description, + &i.Price, + &i.Quantity, + &i.Weight, + &i.IsAvailable, + &i.Tags, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const createUser = `-- name: CreateUser :one +INSERT INTO users (username, email, created_at) +VALUES ($1, $2, NOW()) +RETURNING id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at +` + +type CreateUserParams struct { + Username string + Email string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.Email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/internal/enginetest/postgresql/testdata/insert_returning_star/query.sql b/internal/enginetest/postgresql/testdata/insert_returning_star/query.sql new file mode 100644 index 0000000000..c3d8a2f335 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/insert_returning_star/query.sql @@ -0,0 +1,9 @@ +-- name: CreateUser :one +INSERT INTO users (username, email, created_at) +VALUES ($1, $2, NOW()) +RETURNING *; + +-- name: CreateProduct :one +INSERT INTO products (name, price, created_at) +VALUES ($1, $2, NOW()) +RETURNING *; diff --git a/internal/enginetest/postgresql/testdata/insert_returning_star/sqlc.yaml b/internal/enginetest/postgresql/testdata/insert_returning_star/sqlc.yaml new file mode 100644 index 0000000000..756220e6b7 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/insert_returning_star/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/postgresql/testdata/join_inner/go/db.go b/internal/enginetest/postgresql/testdata/join_inner/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/join_inner/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/postgresql/testdata/join_inner/go/models.go b/internal/enginetest/postgresql/testdata/join_inner/go/models.go new file mode 100644 index 0000000000..ebcbf1076f --- /dev/null +++ b/internal/enginetest/postgresql/testdata/join_inner/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/postgresql/testdata/join_inner/go/query.sql.go b/internal/enginetest/postgresql/testdata/join_inner/go/query.sql.go new file mode 100644 index 0000000000..1962e9f34e --- /dev/null +++ b/internal/enginetest/postgresql/testdata/join_inner/go/query.sql.go @@ -0,0 +1,88 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getOrderItemsWithProduct = `-- name: GetOrderItemsWithProduct :many +SELECT oi.quantity, oi.unit_price, p.name AS product_name +FROM order_items oi +INNER JOIN products p ON oi.product_id = p.id +` + +type GetOrderItemsWithProductRow struct { + Quantity int32 + UnitPrice string + ProductName string +} + +func (q *Queries) GetOrderItemsWithProduct(ctx context.Context) ([]GetOrderItemsWithProductRow, error) { + rows, err := q.db.QueryContext(ctx, getOrderItemsWithProduct) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrderItemsWithProductRow + for rows.Next() { + var i GetOrderItemsWithProductRow + if err := rows.Scan(&i.Quantity, &i.UnitPrice, &i.ProductName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOrdersWithUser = `-- name: GetOrdersWithUser :many +SELECT o.id, o.status, o.total_amount, u.username, u.email +FROM orders o +INNER JOIN users u ON o.user_id = u.id +` + +type GetOrdersWithUserRow struct { + ID int32 + Status string + TotalAmount string + Username string + Email string +} + +func (q *Queries) GetOrdersWithUser(ctx context.Context) ([]GetOrdersWithUserRow, error) { + rows, err := q.db.QueryContext(ctx, getOrdersWithUser) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrdersWithUserRow + for rows.Next() { + var i GetOrdersWithUserRow + if err := rows.Scan( + &i.ID, + &i.Status, + &i.TotalAmount, + &i.Username, + &i.Email, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/enginetest/postgresql/testdata/join_inner/query.sql b/internal/enginetest/postgresql/testdata/join_inner/query.sql new file mode 100644 index 0000000000..d13c84bc37 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/join_inner/query.sql @@ -0,0 +1,9 @@ +-- name: GetOrdersWithUser :many +SELECT o.id, o.status, o.total_amount, u.username, u.email +FROM orders o +INNER JOIN users u ON o.user_id = u.id; + +-- name: GetOrderItemsWithProduct :many +SELECT oi.quantity, oi.unit_price, p.name AS product_name +FROM order_items oi +INNER JOIN products p ON oi.product_id = p.id; diff --git a/internal/enginetest/postgresql/testdata/join_inner/sqlc.yaml b/internal/enginetest/postgresql/testdata/join_inner/sqlc.yaml new file mode 100644 index 0000000000..756220e6b7 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/join_inner/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/postgresql/testdata/schema.sql b/internal/enginetest/postgresql/testdata/schema.sql new file mode 100644 index 0000000000..9ee3bc38f7 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/schema.sql @@ -0,0 +1,154 @@ +-- ============================================================================= +-- PostgreSQL Core Schema +-- This schema is used by all PostgreSQL end-to-end tests +-- ============================================================================= + +-- Users table: core entity with various column types +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL, + full_name TEXT, + age INT, + balance DECIMAL(10,2) NOT NULL DEFAULT 0.00, + is_active BOOLEAN NOT NULL DEFAULT true, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + role VARCHAR(20) NOT NULL DEFAULT 'user', + bio TEXT, + metadata BYTEA, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP, + deleted_at TIMESTAMP +); + +-- Categories table: self-referential for tree structures +CREATE TABLE categories ( + id SERIAL PRIMARY KEY, + parent_id INT REFERENCES categories(id), + name VARCHAR(255) NOT NULL, + description TEXT, + sort_order INT NOT NULL DEFAULT 0, + is_visible BOOLEAN NOT NULL DEFAULT true +); + +-- Products table: many-to-one with categories +CREATE TABLE products ( + id SERIAL PRIMARY KEY, + category_id INT REFERENCES categories(id), + name VARCHAR(255) NOT NULL, + description TEXT, + price DECIMAL(10,2) NOT NULL, + quantity INT NOT NULL DEFAULT 0, + weight REAL, + is_available BOOLEAN NOT NULL DEFAULT true, + tags TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP +); + +-- Orders table: many-to-one with users +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES users(id), + status VARCHAR(20) NOT NULL DEFAULT 'draft', + total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, + notes TEXT, + shipped_at TIMESTAMP, + delivered_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP +); + +-- Order items table: many-to-one with orders and products +CREATE TABLE order_items ( + id SERIAL PRIMARY KEY, + order_id INT NOT NULL REFERENCES orders(id), + product_id INT NOT NULL REFERENCES products(id), + quantity INT NOT NULL DEFAULT 1, + unit_price DECIMAL(10,2) NOT NULL, + discount DECIMAL(10,2) NOT NULL DEFAULT 0.00, + UNIQUE(order_id, product_id) +); + +-- Tags table: for many-to-many relationships +CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + color VARCHAR(7), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Product tags junction table: many-to-many +CREATE TABLE product_tags ( + product_id INT NOT NULL REFERENCES products(id), + tag_id INT NOT NULL REFERENCES tags(id), + PRIMARY KEY (product_id, tag_id) +); + +-- Audit log table +CREATE TABLE audit_logs ( + id SERIAL PRIMARY KEY, + table_name VARCHAR(100) NOT NULL, + record_id INT NOT NULL, + action VARCHAR(20) NOT NULL, + old_values TEXT, + new_values TEXT, + user_id INT REFERENCES users(id), + ip_address VARCHAR(45), + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- Settings table: key-value store +CREATE TABLE settings ( + key VARCHAR(255) PRIMARY KEY, + value TEXT NOT NULL, + value_type VARCHAR(20) NOT NULL DEFAULT 'string', + description TEXT, + updated_at TIMESTAMP +); + +-- Tasks table +CREATE TABLE tasks ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES users(id), + title VARCHAR(255) NOT NULL, + description TEXT, + priority VARCHAR(20) NOT NULL DEFAULT 'medium', + is_completed BOOLEAN NOT NULL DEFAULT false, + due_date DATE, + started_at TIMESTAMP, + completed_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- ============================================================================= +-- Views +-- ============================================================================= + +CREATE VIEW active_users AS + SELECT id, username, email, full_name, created_at + FROM users + WHERE is_active = true AND deleted_at IS NULL; + +CREATE VIEW order_summaries AS + SELECT + o.id AS order_id, + o.user_id, + u.username, + o.status, + o.total_amount, + COUNT(oi.id) AS item_count, + o.created_at + FROM orders o + JOIN users u ON o.user_id = u.id + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY o.id, o.user_id, u.username, o.status, o.total_amount, o.created_at; + +CREATE VIEW category_tree AS + SELECT + c.id, + c.name, + c.parent_id, + p.name AS parent_name + FROM categories c + LEFT JOIN categories p ON c.parent_id = p.id; diff --git a/internal/enginetest/postgresql/testdata/select_star/go/db.go b/internal/enginetest/postgresql/testdata/select_star/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_star/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/postgresql/testdata/select_star/go/models.go b/internal/enginetest/postgresql/testdata/select_star/go/models.go new file mode 100644 index 0000000000..ebcbf1076f --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_star/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/postgresql/testdata/select_star/go/query.sql.go b/internal/enginetest/postgresql/testdata/select_star/go/query.sql.go new file mode 100644 index 0000000000..78ec4ce1fc --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_star/go/query.sql.go @@ -0,0 +1,123 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getAllFromSubquery = `-- name: GetAllFromSubquery :many +SELECT id, username FROM (SELECT id, username FROM users) t +` + +type GetAllFromSubqueryRow struct { + ID int32 + Username string +} + +func (q *Queries) GetAllFromSubquery(ctx context.Context) ([]GetAllFromSubqueryRow, error) { + rows, err := q.db.QueryContext(ctx, getAllFromSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllFromSubqueryRow + for rows.Next() { + var i GetAllFromSubqueryRow + if err := rows.Scan(&i.ID, &i.Username); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllProducts = `-- name: GetAllProducts :many +SELECT id, category_id, name, description, price, quantity, weight, is_available, tags, created_at, updated_at FROM products +` + +func (q *Queries) GetAllProducts(ctx context.Context) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, getAllProducts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Product + for rows.Next() { + var i Product + if err := rows.Scan( + &i.ID, + &i.CategoryID, + &i.Name, + &i.Description, + &i.Price, + &i.Quantity, + &i.Weight, + &i.IsAvailable, + &i.Tags, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllUsers = `-- name: GetAllUsers :many +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users +` + +func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getAllUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/enginetest/postgresql/testdata/select_star/query.sql b/internal/enginetest/postgresql/testdata/select_star/query.sql new file mode 100644 index 0000000000..e5d8686d1a --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_star/query.sql @@ -0,0 +1,8 @@ +-- name: GetAllUsers :many +SELECT * FROM users; + +-- name: GetAllProducts :many +SELECT * FROM products; + +-- name: GetAllFromSubquery :many +SELECT * FROM (SELECT id, username FROM users) t; diff --git a/internal/enginetest/postgresql/testdata/select_star/sqlc.yaml b/internal/enginetest/postgresql/testdata/select_star/sqlc.yaml new file mode 100644 index 0000000000..756220e6b7 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_star/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/postgresql/testdata/select_where/go/db.go b/internal/enginetest/postgresql/testdata/select_where/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_where/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/postgresql/testdata/select_where/go/models.go b/internal/enginetest/postgresql/testdata/select_where/go/models.go new file mode 100644 index 0000000000..ebcbf1076f --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_where/go/models.go @@ -0,0 +1,141 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" + "time" +) + +type ActiveUser struct { + ID int32 + Username string + Email string + FullName sql.NullString + CreatedAt time.Time +} + +type AuditLog struct { + ID int32 + TableName string + RecordID int32 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt32 + IpAddress sql.NullString + CreatedAt time.Time +} + +type Category struct { + ID int32 + ParentID sql.NullInt32 + Name string + Description sql.NullString + SortOrder int32 + IsVisible bool +} + +type CategoryTree struct { + ID int32 + Name string + ParentID sql.NullInt32 + ParentName sql.NullString +} + +type Order struct { + ID int32 + UserID int32 + Status string + TotalAmount string + Notes sql.NullString + ShippedAt sql.NullTime + DeliveredAt sql.NullTime + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type OrderItem struct { + ID int32 + OrderID int32 + ProductID int32 + Quantity int32 + UnitPrice string + Discount string +} + +type OrderSummary struct { + OrderID int32 + UserID int32 + Username string + Status string + TotalAmount string + ItemCount int64 + CreatedAt time.Time +} + +type Product struct { + ID int32 + CategoryID sql.NullInt32 + Name string + Description sql.NullString + Price string + Quantity int32 + Weight sql.NullFloat64 + IsAvailable bool + Tags sql.NullString + CreatedAt time.Time + UpdatedAt sql.NullTime +} + +type ProductTag struct { + ProductID int32 + TagID int32 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullTime +} + +type Tag struct { + ID int32 + Name string + Color sql.NullString + CreatedAt time.Time +} + +type Task struct { + ID int32 + UserID int32 + Title string + Description sql.NullString + Priority string + IsCompleted bool + DueDate sql.NullTime + StartedAt sql.NullTime + CompletedAt sql.NullTime + CreatedAt time.Time +} + +type User struct { + ID int32 + Username string + Email string + FullName sql.NullString + Age sql.NullInt32 + Balance string + IsActive bool + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt time.Time + UpdatedAt sql.NullTime + DeletedAt sql.NullTime +} diff --git a/internal/enginetest/postgresql/testdata/select_where/go/query.sql.go b/internal/enginetest/postgresql/testdata/select_where/go/query.sql.go new file mode 100644 index 0000000000..af08987040 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_where/go/query.sql.go @@ -0,0 +1,104 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getActiveUsers = `-- name: GetActiveUsers :many +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE is_active = $1 +` + +func (q *Queries) GetActiveUsers(ctx context.Context, isActive bool) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getActiveUsers, isActive) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE id = $1 +` + +func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const getUserByUsername = `-- name: GetUserByUsername :one +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE username = $1 +` + +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByUsername, username) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/internal/enginetest/postgresql/testdata/select_where/query.sql b/internal/enginetest/postgresql/testdata/select_where/query.sql new file mode 100644 index 0000000000..18a18becff --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_where/query.sql @@ -0,0 +1,8 @@ +-- name: GetUserByID :one +SELECT * FROM users WHERE id = $1; + +-- name: GetUserByUsername :one +SELECT * FROM users WHERE username = $1; + +-- name: GetActiveUsers :many +SELECT * FROM users WHERE is_active = $1; diff --git a/internal/enginetest/postgresql/testdata/select_where/sqlc.yaml b/internal/enginetest/postgresql/testdata/select_where/sqlc.yaml new file mode 100644 index 0000000000..756220e6b7 --- /dev/null +++ b/internal/enginetest/postgresql/testdata/select_where/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "postgresql" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/sqlite/coverage_test.go b/internal/enginetest/sqlite/coverage_test.go new file mode 100644 index 0000000000..a1a60177b6 --- /dev/null +++ b/internal/enginetest/sqlite/coverage_test.go @@ -0,0 +1,122 @@ +package sqlite + +import ( + "os" + "path/filepath" + "testing" + + "github.com/sqlc-dev/sqlc/internal/enginetest/testcases" +) + +// TestCoverage verifies that all required test cases are implemented +// for the SQLite engine. +func TestCoverage(t *testing.T) { + engine := Engine() + registry := testcases.DefaultRegistry + + // Get all tests this engine should implement + requiredTests := registry.RequiredTestsForEngine(engine) + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Find all implemented tests + implemented := make(map[string]bool) + err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" || info.Name() == "sqlc.json" { + dir := filepath.Dir(path) + testName := filepath.Base(dir) + implemented[testName] = true + return filepath.SkipDir + } + return nil + }) + if err != nil && !os.IsNotExist(err) { + t.Fatal(err) + } + + // Check for missing tests + var missing []*testcases.TestCase + for _, tc := range requiredTests { + if !implemented[tc.Name] { + missing = append(missing, tc) + } + } + + // Report missing tests (informational, not a failure) + if len(missing) > 0 { + t.Logf("SQLite engine is missing %d required test cases (this is informational):", len(missing)) + for _, tc := range missing { + t.Logf(" - %s (%s): %s", tc.ID, tc.Name, tc.Description) + } + } + + // Report coverage statistics + total := len(requiredTests) + covered := total - len(missing) + percentage := float64(covered) / float64(total) * 100 + + t.Logf("SQLite test coverage: %d/%d (%.1f%%)", covered, total, percentage) +} + +// TestCoverageByCategory reports coverage broken down by category +func TestCoverageByCategory(t *testing.T) { + engine := Engine() + registry := testcases.DefaultRegistry + caps := testcases.DefaultCapabilities(engine) + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Find all implemented tests + implemented := make(map[string]bool) + _ = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" || info.Name() == "sqlc.json" { + dir := filepath.Dir(path) + testName := filepath.Base(dir) + implemented[testName] = true + return filepath.SkipDir + } + return nil + }) + + // Report by category + categories := testcases.RequiredCategories() + if caps.SupportsEnum { + categories = append(categories, testcases.CategoryEnum) + } + if caps.SupportsSchema { + categories = append(categories, testcases.CategorySchema) + } + if caps.SupportsArray { + categories = append(categories, testcases.CategoryArray) + } + if caps.SupportsJSON { + categories = append(categories, testcases.CategoryJSON) + } + + for _, cat := range categories { + tests := registry.GetByCategory(cat) + var covered, total int + for _, tc := range tests { + total++ + if implemented[tc.Name] { + covered++ + } + } + if total > 0 { + percentage := float64(covered) / float64(total) * 100 + t.Logf(" %s: %d/%d (%.1f%%)", cat, covered, total, percentage) + } + } +} diff --git a/internal/enginetest/sqlite/endtoend_test.go b/internal/enginetest/sqlite/endtoend_test.go new file mode 100644 index 0000000000..5887d08a87 --- /dev/null +++ b/internal/enginetest/sqlite/endtoend_test.go @@ -0,0 +1,174 @@ +// Package sqlite contains end-to-end tests for the SQLite engine. +package sqlite + +import ( + "bytes" + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/sqlc-dev/sqlc/internal/cmd" + "github.com/sqlc-dev/sqlc/internal/enginetest/testcases" +) + +func TestEndToEnd(t *testing.T) { + t.Parallel() + ctx := context.Background() + + testdataDir, err := filepath.Abs("testdata") + if err != nil { + t.Fatal(err) + } + + // Walk through all test directories + err = filepath.Walk(testdataDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Look for sqlc config files + if info.Name() != "sqlc.yaml" && info.Name() != "sqlc.json" { + return nil + } + + dir := filepath.Dir(path) + testName := strings.TrimPrefix(dir, testdataDir+string(filepath.Separator)) + + t.Run(testName, func(t *testing.T) { + t.Parallel() + runTest(ctx, t, dir) + }) + + return filepath.SkipDir + }) + + if err != nil { + t.Fatal(err) + } +} + +func runTest(ctx context.Context, t *testing.T, dir string) { + t.Helper() + + var stderr bytes.Buffer + opts := &cmd.Options{ + Env: cmd.Env{}, + Stderr: &stderr, + } + + // Check for expected stderr + expectedStderr := readExpectedStderr(t, dir) + + output, err := cmd.Generate(ctx, dir, "", opts) + + // If we expect an error, check stderr matches + if len(expectedStderr) > 0 { + if err == nil { + t.Fatalf("expected error but got none") + } + diff := cmp.Diff( + strings.TrimSpace(expectedStderr), + strings.TrimSpace(stderr.String()), + stderrTransformer(), + ) + if diff != "" { + t.Fatalf("stderr differed (-want +got):\n%s", diff) + } + return + } + + if err != nil { + t.Fatalf("sqlc generate failed: %s", stderr.String()) + } + + cmpDirectory(t, dir, output) +} + +func readExpectedStderr(t *testing.T, dir string) string { + t.Helper() + + paths := []string{ + filepath.Join(dir, "stderr.txt"), + } + + for _, path := range paths { + if _, err := os.Stat(path); !os.IsNotExist(err) { + blob, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return string(blob) + } + } + return "" +} + +func stderrTransformer() cmp.Option { + return cmp.Transformer("Stderr", func(in string) string { + s := strings.Replace(in, "\r", "", -1) + return strings.Replace(s, "\\", "/", -1) + }) +} + +func lineEndings() cmp.Option { + return cmp.Transformer("LineEndings", func(in string) string { + return strings.Replace(in, "\r\n", "\n", -1) + }) +} + +func cmpDirectory(t *testing.T, dir string, actual map[string]string) { + t.Helper() + + expected := map[string]string{} + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".go") { + return nil + } + if strings.HasSuffix(path, "_test.go") { + return nil + } + blob, err := os.ReadFile(path) + if err != nil { + return err + } + expected[path] = string(blob) + return nil + }) + if err != nil { + t.Fatal(err) + } + + opts := []cmp.Option{ + cmpopts.EquateEmpty(), + lineEndings(), + } + + if !cmp.Equal(expected, actual, opts...) { + t.Errorf("%s contents differ", dir) + for name, contents := range expected { + if actual[name] == "" { + t.Errorf("%s is empty", name) + continue + } + if diff := cmp.Diff(contents, actual[name], opts...); diff != "" { + t.Errorf("%s differed (-want +got):\n%s", name, diff) + } + } + } +} + +// Engine returns the engine type for this package +func Engine() testcases.Engine { + return testcases.EngineSQLite +} diff --git a/internal/enginetest/sqlite/testdata/insert_returning_star/go/db.go b/internal/enginetest/sqlite/testdata/insert_returning_star/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/insert_returning_star/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/sqlite/testdata/insert_returning_star/go/models.go b/internal/enginetest/sqlite/testdata/insert_returning_star/go/models.go new file mode 100644 index 0000000000..a80e7d12cf --- /dev/null +++ b/internal/enginetest/sqlite/testdata/insert_returning_star/go/models.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" +) + +type ActiveUser struct { + ID int64 + Username string + Email string + FullName sql.NullString + CreatedAt string +} + +type AuditLog struct { + ID int64 + TableName string + RecordID int64 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt64 + IpAddress sql.NullString + CreatedAt string +} + +type Category struct { + ID int64 + ParentID sql.NullInt64 + Name string + Description sql.NullString + SortOrder int64 + IsVisible int64 +} + +type CategoryTree struct { + ID int64 + Name string + ParentID sql.NullInt64 + ParentName sql.NullString +} + +type Order struct { + ID int64 + UserID int64 + Status string + TotalAmount float64 + Notes sql.NullString + ShippedAt sql.NullString + DeliveredAt sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type OrderItem struct { + ID int64 + OrderID int64 + ProductID int64 + Quantity int64 + UnitPrice float64 + Discount float64 +} + +type OrderSummary struct { + OrderID int64 + UserID int64 + Username string + Status string + TotalAmount float64 + ItemCount int64 + CreatedAt string +} + +type Product struct { + ID int64 + CategoryID sql.NullInt64 + Name string + Description sql.NullString + Price float64 + Quantity int64 + Weight sql.NullFloat64 + IsAvailable int64 + Tags sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type ProductTag struct { + ProductID int64 + TagID int64 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullString +} + +type Tag struct { + ID int64 + Name string + Color sql.NullString + CreatedAt string +} + +type Task struct { + ID int64 + UserID int64 + Title string + Description sql.NullString + Priority string + IsCompleted int64 + DueDate sql.NullString + StartedAt sql.NullString + CompletedAt sql.NullString + CreatedAt string +} + +type User struct { + ID int64 + Username string + Email string + FullName sql.NullString + Age sql.NullInt64 + Balance float64 + IsActive int64 + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt string + UpdatedAt sql.NullString + DeletedAt sql.NullString +} diff --git a/internal/enginetest/sqlite/testdata/insert_returning_star/go/query.sql.go b/internal/enginetest/sqlite/testdata/insert_returning_star/go/query.sql.go new file mode 100644 index 0000000000..ae6404412e --- /dev/null +++ b/internal/enginetest/sqlite/testdata/insert_returning_star/go/query.sql.go @@ -0,0 +1,73 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const createProduct = `-- name: CreateProduct :one +INSERT INTO products (name, price) +VALUES (?, ?) +RETURNING id, category_id, name, description, price, quantity, weight, is_available, tags, created_at, updated_at +` + +type CreateProductParams struct { + Name string + Price float64 +} + +func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) (Product, error) { + row := q.db.QueryRowContext(ctx, createProduct, arg.Name, arg.Price) + var i Product + err := row.Scan( + &i.ID, + &i.CategoryID, + &i.Name, + &i.Description, + &i.Price, + &i.Quantity, + &i.Weight, + &i.IsAvailable, + &i.Tags, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const createUser = `-- name: CreateUser :one +INSERT INTO users (username, email) +VALUES (?, ?) +RETURNING id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at +` + +type CreateUserParams struct { + Username string + Email string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.Email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/internal/enginetest/sqlite/testdata/insert_returning_star/query.sql b/internal/enginetest/sqlite/testdata/insert_returning_star/query.sql new file mode 100644 index 0000000000..99fc391d94 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/insert_returning_star/query.sql @@ -0,0 +1,9 @@ +-- name: CreateUser :one +INSERT INTO users (username, email) +VALUES (?, ?) +RETURNING *; + +-- name: CreateProduct :one +INSERT INTO products (name, price) +VALUES (?, ?) +RETURNING *; diff --git a/internal/enginetest/sqlite/testdata/insert_returning_star/sqlc.yaml b/internal/enginetest/sqlite/testdata/insert_returning_star/sqlc.yaml new file mode 100644 index 0000000000..99caeb1b4e --- /dev/null +++ b/internal/enginetest/sqlite/testdata/insert_returning_star/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/sqlite/testdata/join_inner/go/db.go b/internal/enginetest/sqlite/testdata/join_inner/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/join_inner/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/sqlite/testdata/join_inner/go/models.go b/internal/enginetest/sqlite/testdata/join_inner/go/models.go new file mode 100644 index 0000000000..a80e7d12cf --- /dev/null +++ b/internal/enginetest/sqlite/testdata/join_inner/go/models.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" +) + +type ActiveUser struct { + ID int64 + Username string + Email string + FullName sql.NullString + CreatedAt string +} + +type AuditLog struct { + ID int64 + TableName string + RecordID int64 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt64 + IpAddress sql.NullString + CreatedAt string +} + +type Category struct { + ID int64 + ParentID sql.NullInt64 + Name string + Description sql.NullString + SortOrder int64 + IsVisible int64 +} + +type CategoryTree struct { + ID int64 + Name string + ParentID sql.NullInt64 + ParentName sql.NullString +} + +type Order struct { + ID int64 + UserID int64 + Status string + TotalAmount float64 + Notes sql.NullString + ShippedAt sql.NullString + DeliveredAt sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type OrderItem struct { + ID int64 + OrderID int64 + ProductID int64 + Quantity int64 + UnitPrice float64 + Discount float64 +} + +type OrderSummary struct { + OrderID int64 + UserID int64 + Username string + Status string + TotalAmount float64 + ItemCount int64 + CreatedAt string +} + +type Product struct { + ID int64 + CategoryID sql.NullInt64 + Name string + Description sql.NullString + Price float64 + Quantity int64 + Weight sql.NullFloat64 + IsAvailable int64 + Tags sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type ProductTag struct { + ProductID int64 + TagID int64 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullString +} + +type Tag struct { + ID int64 + Name string + Color sql.NullString + CreatedAt string +} + +type Task struct { + ID int64 + UserID int64 + Title string + Description sql.NullString + Priority string + IsCompleted int64 + DueDate sql.NullString + StartedAt sql.NullString + CompletedAt sql.NullString + CreatedAt string +} + +type User struct { + ID int64 + Username string + Email string + FullName sql.NullString + Age sql.NullInt64 + Balance float64 + IsActive int64 + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt string + UpdatedAt sql.NullString + DeletedAt sql.NullString +} diff --git a/internal/enginetest/sqlite/testdata/join_inner/go/query.sql.go b/internal/enginetest/sqlite/testdata/join_inner/go/query.sql.go new file mode 100644 index 0000000000..e57624162a --- /dev/null +++ b/internal/enginetest/sqlite/testdata/join_inner/go/query.sql.go @@ -0,0 +1,88 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getOrderItemsWithProduct = `-- name: GetOrderItemsWithProduct :many +SELECT oi.quantity, oi.unit_price, p.name AS product_name +FROM order_items oi +INNER JOIN products p ON oi.product_id = p.id +` + +type GetOrderItemsWithProductRow struct { + Quantity int64 + UnitPrice float64 + ProductName string +} + +func (q *Queries) GetOrderItemsWithProduct(ctx context.Context) ([]GetOrderItemsWithProductRow, error) { + rows, err := q.db.QueryContext(ctx, getOrderItemsWithProduct) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrderItemsWithProductRow + for rows.Next() { + var i GetOrderItemsWithProductRow + if err := rows.Scan(&i.Quantity, &i.UnitPrice, &i.ProductName); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getOrdersWithUser = `-- name: GetOrdersWithUser :many +SELECT o.id, o.status, o.total_amount, u.username, u.email +FROM orders o +INNER JOIN users u ON o.user_id = u.id +` + +type GetOrdersWithUserRow struct { + ID int64 + Status string + TotalAmount float64 + Username string + Email string +} + +func (q *Queries) GetOrdersWithUser(ctx context.Context) ([]GetOrdersWithUserRow, error) { + rows, err := q.db.QueryContext(ctx, getOrdersWithUser) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetOrdersWithUserRow + for rows.Next() { + var i GetOrdersWithUserRow + if err := rows.Scan( + &i.ID, + &i.Status, + &i.TotalAmount, + &i.Username, + &i.Email, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/enginetest/sqlite/testdata/join_inner/query.sql b/internal/enginetest/sqlite/testdata/join_inner/query.sql new file mode 100644 index 0000000000..d13c84bc37 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/join_inner/query.sql @@ -0,0 +1,9 @@ +-- name: GetOrdersWithUser :many +SELECT o.id, o.status, o.total_amount, u.username, u.email +FROM orders o +INNER JOIN users u ON o.user_id = u.id; + +-- name: GetOrderItemsWithProduct :many +SELECT oi.quantity, oi.unit_price, p.name AS product_name +FROM order_items oi +INNER JOIN products p ON oi.product_id = p.id; diff --git a/internal/enginetest/sqlite/testdata/join_inner/sqlc.yaml b/internal/enginetest/sqlite/testdata/join_inner/sqlc.yaml new file mode 100644 index 0000000000..99caeb1b4e --- /dev/null +++ b/internal/enginetest/sqlite/testdata/join_inner/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/sqlite/testdata/schema.sql b/internal/enginetest/sqlite/testdata/schema.sql new file mode 100644 index 0000000000..185b3e8638 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/schema.sql @@ -0,0 +1,154 @@ +-- ============================================================================= +-- SQLite Core Schema +-- This schema is used by all SQLite end-to-end tests +-- ============================================================================= + +-- Users table: core entity with various column types +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL, + full_name TEXT, + age INTEGER, + balance REAL NOT NULL DEFAULT 0.00, + is_active INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'pending', + role TEXT NOT NULL DEFAULT 'user', + bio TEXT, + metadata BLOB, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT, + deleted_at TEXT +); + +-- Categories table: self-referential for tree structures +CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_id INTEGER REFERENCES categories(id), + name TEXT NOT NULL, + description TEXT, + sort_order INTEGER NOT NULL DEFAULT 0, + is_visible INTEGER NOT NULL DEFAULT 1 +); + +-- Products table: many-to-one with categories +CREATE TABLE products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER REFERENCES categories(id), + name TEXT NOT NULL, + description TEXT, + price REAL NOT NULL, + quantity INTEGER NOT NULL DEFAULT 0, + weight REAL, + is_available INTEGER NOT NULL DEFAULT 1, + tags TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT +); + +-- Orders table: many-to-one with users +CREATE TABLE orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + status TEXT NOT NULL DEFAULT 'draft', + total_amount REAL NOT NULL DEFAULT 0.00, + notes TEXT, + shipped_at TEXT, + delivered_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT +); + +-- Order items table: many-to-one with orders and products +CREATE TABLE order_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + order_id INTEGER NOT NULL REFERENCES orders(id), + product_id INTEGER NOT NULL REFERENCES products(id), + quantity INTEGER NOT NULL DEFAULT 1, + unit_price REAL NOT NULL, + discount REAL NOT NULL DEFAULT 0.00, + UNIQUE(order_id, product_id) +); + +-- Tags table: for many-to-many relationships +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + color TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Product tags junction table: many-to-many +CREATE TABLE product_tags ( + product_id INTEGER NOT NULL REFERENCES products(id), + tag_id INTEGER NOT NULL REFERENCES tags(id), + PRIMARY KEY (product_id, tag_id) +); + +-- Audit log table +CREATE TABLE audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + table_name TEXT NOT NULL, + record_id INTEGER NOT NULL, + action TEXT NOT NULL, + old_values TEXT, + new_values TEXT, + user_id INTEGER REFERENCES users(id), + ip_address TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Settings table: key-value store +CREATE TABLE settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + value_type TEXT NOT NULL DEFAULT 'string', + description TEXT, + updated_at TEXT +); + +-- Tasks table +CREATE TABLE tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL REFERENCES users(id), + title TEXT NOT NULL, + description TEXT, + priority TEXT NOT NULL DEFAULT 'medium', + is_completed INTEGER NOT NULL DEFAULT 0, + due_date TEXT, + started_at TEXT, + completed_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- ============================================================================= +-- Views +-- ============================================================================= + +CREATE VIEW active_users AS + SELECT id, username, email, full_name, created_at + FROM users + WHERE is_active = 1 AND deleted_at IS NULL; + +CREATE VIEW order_summaries AS + SELECT + o.id AS order_id, + o.user_id, + u.username, + o.status, + o.total_amount, + COUNT(oi.id) AS item_count, + o.created_at + FROM orders o + JOIN users u ON o.user_id = u.id + LEFT JOIN order_items oi ON o.id = oi.order_id + GROUP BY o.id, o.user_id, u.username, o.status, o.total_amount, o.created_at; + +CREATE VIEW category_tree AS + SELECT + c.id, + c.name, + c.parent_id, + p.name AS parent_name + FROM categories c + LEFT JOIN categories p ON c.parent_id = p.id; diff --git a/internal/enginetest/sqlite/testdata/select_star/go/db.go b/internal/enginetest/sqlite/testdata/select_star/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_star/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/sqlite/testdata/select_star/go/models.go b/internal/enginetest/sqlite/testdata/select_star/go/models.go new file mode 100644 index 0000000000..a80e7d12cf --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_star/go/models.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" +) + +type ActiveUser struct { + ID int64 + Username string + Email string + FullName sql.NullString + CreatedAt string +} + +type AuditLog struct { + ID int64 + TableName string + RecordID int64 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt64 + IpAddress sql.NullString + CreatedAt string +} + +type Category struct { + ID int64 + ParentID sql.NullInt64 + Name string + Description sql.NullString + SortOrder int64 + IsVisible int64 +} + +type CategoryTree struct { + ID int64 + Name string + ParentID sql.NullInt64 + ParentName sql.NullString +} + +type Order struct { + ID int64 + UserID int64 + Status string + TotalAmount float64 + Notes sql.NullString + ShippedAt sql.NullString + DeliveredAt sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type OrderItem struct { + ID int64 + OrderID int64 + ProductID int64 + Quantity int64 + UnitPrice float64 + Discount float64 +} + +type OrderSummary struct { + OrderID int64 + UserID int64 + Username string + Status string + TotalAmount float64 + ItemCount int64 + CreatedAt string +} + +type Product struct { + ID int64 + CategoryID sql.NullInt64 + Name string + Description sql.NullString + Price float64 + Quantity int64 + Weight sql.NullFloat64 + IsAvailable int64 + Tags sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type ProductTag struct { + ProductID int64 + TagID int64 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullString +} + +type Tag struct { + ID int64 + Name string + Color sql.NullString + CreatedAt string +} + +type Task struct { + ID int64 + UserID int64 + Title string + Description sql.NullString + Priority string + IsCompleted int64 + DueDate sql.NullString + StartedAt sql.NullString + CompletedAt sql.NullString + CreatedAt string +} + +type User struct { + ID int64 + Username string + Email string + FullName sql.NullString + Age sql.NullInt64 + Balance float64 + IsActive int64 + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt string + UpdatedAt sql.NullString + DeletedAt sql.NullString +} diff --git a/internal/enginetest/sqlite/testdata/select_star/go/query.sql.go b/internal/enginetest/sqlite/testdata/select_star/go/query.sql.go new file mode 100644 index 0000000000..625de0d155 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_star/go/query.sql.go @@ -0,0 +1,123 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getAllFromSubquery = `-- name: GetAllFromSubquery :many +SELECT id, username FROM (SELECT id, username FROM users) t +` + +type GetAllFromSubqueryRow struct { + ID int64 + Username string +} + +func (q *Queries) GetAllFromSubquery(ctx context.Context) ([]GetAllFromSubqueryRow, error) { + rows, err := q.db.QueryContext(ctx, getAllFromSubquery) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllFromSubqueryRow + for rows.Next() { + var i GetAllFromSubqueryRow + if err := rows.Scan(&i.ID, &i.Username); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllProducts = `-- name: GetAllProducts :many +SELECT id, category_id, name, description, price, quantity, weight, is_available, tags, created_at, updated_at FROM products +` + +func (q *Queries) GetAllProducts(ctx context.Context) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, getAllProducts) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Product + for rows.Next() { + var i Product + if err := rows.Scan( + &i.ID, + &i.CategoryID, + &i.Name, + &i.Description, + &i.Price, + &i.Quantity, + &i.Weight, + &i.IsAvailable, + &i.Tags, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllUsers = `-- name: GetAllUsers :many +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users +` + +func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getAllUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/enginetest/sqlite/testdata/select_star/query.sql b/internal/enginetest/sqlite/testdata/select_star/query.sql new file mode 100644 index 0000000000..e5d8686d1a --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_star/query.sql @@ -0,0 +1,8 @@ +-- name: GetAllUsers :many +SELECT * FROM users; + +-- name: GetAllProducts :many +SELECT * FROM products; + +-- name: GetAllFromSubquery :many +SELECT * FROM (SELECT id, username FROM users) t; diff --git a/internal/enginetest/sqlite/testdata/select_star/sqlc.yaml b/internal/enginetest/sqlite/testdata/select_star/sqlc.yaml new file mode 100644 index 0000000000..99caeb1b4e --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_star/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/sqlite/testdata/select_where/go/db.go b/internal/enginetest/sqlite/testdata/select_where/go/db.go new file mode 100644 index 0000000000..3b320aa168 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_where/go/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/enginetest/sqlite/testdata/select_where/go/models.go b/internal/enginetest/sqlite/testdata/select_where/go/models.go new file mode 100644 index 0000000000..a80e7d12cf --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_where/go/models.go @@ -0,0 +1,140 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 + +package querytest + +import ( + "database/sql" +) + +type ActiveUser struct { + ID int64 + Username string + Email string + FullName sql.NullString + CreatedAt string +} + +type AuditLog struct { + ID int64 + TableName string + RecordID int64 + Action string + OldValues sql.NullString + NewValues sql.NullString + UserID sql.NullInt64 + IpAddress sql.NullString + CreatedAt string +} + +type Category struct { + ID int64 + ParentID sql.NullInt64 + Name string + Description sql.NullString + SortOrder int64 + IsVisible int64 +} + +type CategoryTree struct { + ID int64 + Name string + ParentID sql.NullInt64 + ParentName sql.NullString +} + +type Order struct { + ID int64 + UserID int64 + Status string + TotalAmount float64 + Notes sql.NullString + ShippedAt sql.NullString + DeliveredAt sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type OrderItem struct { + ID int64 + OrderID int64 + ProductID int64 + Quantity int64 + UnitPrice float64 + Discount float64 +} + +type OrderSummary struct { + OrderID int64 + UserID int64 + Username string + Status string + TotalAmount float64 + ItemCount int64 + CreatedAt string +} + +type Product struct { + ID int64 + CategoryID sql.NullInt64 + Name string + Description sql.NullString + Price float64 + Quantity int64 + Weight sql.NullFloat64 + IsAvailable int64 + Tags sql.NullString + CreatedAt string + UpdatedAt sql.NullString +} + +type ProductTag struct { + ProductID int64 + TagID int64 +} + +type Setting struct { + Key string + Value string + ValueType string + Description sql.NullString + UpdatedAt sql.NullString +} + +type Tag struct { + ID int64 + Name string + Color sql.NullString + CreatedAt string +} + +type Task struct { + ID int64 + UserID int64 + Title string + Description sql.NullString + Priority string + IsCompleted int64 + DueDate sql.NullString + StartedAt sql.NullString + CompletedAt sql.NullString + CreatedAt string +} + +type User struct { + ID int64 + Username string + Email string + FullName sql.NullString + Age sql.NullInt64 + Balance float64 + IsActive int64 + Status string + Role string + Bio sql.NullString + Metadata []byte + CreatedAt string + UpdatedAt sql.NullString + DeletedAt sql.NullString +} diff --git a/internal/enginetest/sqlite/testdata/select_where/go/query.sql.go b/internal/enginetest/sqlite/testdata/select_where/go/query.sql.go new file mode 100644 index 0000000000..46732f52f5 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_where/go/query.sql.go @@ -0,0 +1,104 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: query.sql + +package querytest + +import ( + "context" +) + +const getActiveUsers = `-- name: GetActiveUsers :many +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE is_active = ? +` + +func (q *Queries) GetActiveUsers(ctx context.Context, isActive int64) ([]User, error) { + rows, err := q.db.QueryContext(ctx, getActiveUsers, isActive) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE id = ? +` + +func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const getUserByUsername = `-- name: GetUserByUsername :one +SELECT id, username, email, full_name, age, balance, is_active, status, role, bio, metadata, created_at, updated_at, deleted_at FROM users WHERE username = ? +` + +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByUsername, username) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.FullName, + &i.Age, + &i.Balance, + &i.IsActive, + &i.Status, + &i.Role, + &i.Bio, + &i.Metadata, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} diff --git a/internal/enginetest/sqlite/testdata/select_where/query.sql b/internal/enginetest/sqlite/testdata/select_where/query.sql new file mode 100644 index 0000000000..bb9a73e718 --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_where/query.sql @@ -0,0 +1,8 @@ +-- name: GetUserByID :one +SELECT * FROM users WHERE id = ?; + +-- name: GetUserByUsername :one +SELECT * FROM users WHERE username = ?; + +-- name: GetActiveUsers :many +SELECT * FROM users WHERE is_active = ?; diff --git a/internal/enginetest/sqlite/testdata/select_where/sqlc.yaml b/internal/enginetest/sqlite/testdata/select_where/sqlc.yaml new file mode 100644 index 0000000000..99caeb1b4e --- /dev/null +++ b/internal/enginetest/sqlite/testdata/select_where/sqlc.yaml @@ -0,0 +1,9 @@ +version: "2" +sql: + - engine: "sqlite" + schema: "../schema.sql" + queries: "query.sql" + gen: + go: + package: "querytest" + out: "go" diff --git a/internal/enginetest/testcases/cases.go b/internal/enginetest/testcases/cases.go new file mode 100644 index 0000000000..842c20fa05 --- /dev/null +++ b/internal/enginetest/testcases/cases.go @@ -0,0 +1,401 @@ +package testcases + +// DefaultRegistry is the global registry of all standard test cases +var DefaultRegistry = NewRegistry() + +func init() { + registerSelectTests() + registerInsertTests() + registerUpdateTests() + registerDeleteTests() + registerJoinTests() + registerCTETests() + registerSubqueryTests() + registerUnionTests() + registerAggregateTests() + registerOperatorTests() + registerCaseTests() + registerNullTests() + registerCastTests() + registerFunctionTests() + registerDataTypeTests() + registerDDLTests() + registerViewTests() + registerUpsertTests() + registerParamTests() + registerResultTests() + registerErrorTests() + + // Extension tests + registerEnumTests() + registerSchemaTests() + registerArrayTests() + registerJSONTests() +} + +func registerSelectTests() { + tests := []*TestCase{ + {ID: "S01", Name: "select_star", Category: CategorySelect, Description: "Star expansion returns all columns", Required: true}, + {ID: "S02", Name: "select_columns", Category: CategorySelect, Description: "Specific column selection", Required: true}, + {ID: "S03", Name: "select_column_alias", Category: CategorySelect, Description: "Column aliasing with AS", Required: true}, + {ID: "S04", Name: "select_table_alias", Category: CategorySelect, Description: "Table aliasing", Required: true}, + {ID: "S05", Name: "select_distinct", Category: CategorySelect, Description: "DISTINCT keyword", Required: true}, + {ID: "S06", Name: "select_where", Category: CategorySelect, Description: "WHERE with parameter", Required: true}, + {ID: "S07", Name: "select_where_multiple", Category: CategorySelect, Description: "Multiple WHERE conditions", Required: true}, + {ID: "S08", Name: "select_order_by", Category: CategorySelect, Description: "ORDER BY clause", Required: true}, + {ID: "S09", Name: "select_order_by_desc", Category: CategorySelect, Description: "ORDER BY with DESC", Required: true}, + {ID: "S10", Name: "select_order_by_multiple", Category: CategorySelect, Description: "Multiple ORDER BY columns", Required: true}, + {ID: "S11", Name: "select_limit", Category: CategorySelect, Description: "LIMIT with parameter", Required: true}, + {ID: "S12", Name: "select_limit_offset", Category: CategorySelect, Description: "LIMIT and OFFSET", Required: true}, + {ID: "S13", Name: "select_qualified_star", Category: CategorySelect, Description: "Table-qualified star", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerInsertTests() { + tests := []*TestCase{ + {ID: "I01", Name: "insert_single_row", Category: CategoryInsert, Description: "Single row insert", Required: true}, + {ID: "I02", Name: "insert_multiple_rows", Category: CategoryInsert, Description: "Multi-row insert", Required: true}, + {ID: "I03", Name: "insert_partial_columns", Category: CategoryInsert, Description: "Partial column insert", Required: true}, + {ID: "I04", Name: "insert_returning_id", Category: CategoryInsert, Description: "RETURNING specific column", Required: true}, + {ID: "I05", Name: "insert_returning_star", Category: CategoryInsert, Description: "RETURNING all columns", Required: true}, + {ID: "I06", Name: "insert_select", Category: CategoryInsert, Description: "INSERT...SELECT", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerUpdateTests() { + tests := []*TestCase{ + {ID: "U01", Name: "update_single_column", Category: CategoryUpdate, Description: "Single column update", Required: true}, + {ID: "U02", Name: "update_multiple_columns", Category: CategoryUpdate, Description: "Multiple column update", Required: true}, + {ID: "U03", Name: "update_expression", Category: CategoryUpdate, Description: "Expression in SET", Required: true}, + {ID: "U04", Name: "update_all_rows", Category: CategoryUpdate, Description: "Update without WHERE", Required: true}, + {ID: "U05", Name: "update_returning", Category: CategoryUpdate, Description: "RETURNING with UPDATE", Required: true}, + {ID: "U06", Name: "update_returning_columns", Category: CategoryUpdate, Description: "RETURNING specific columns", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerDeleteTests() { + tests := []*TestCase{ + {ID: "D01", Name: "delete_where", Category: CategoryDelete, Description: "Conditional delete", Required: true}, + {ID: "D02", Name: "delete_all", Category: CategoryDelete, Description: "Delete all rows", Required: true}, + {ID: "D03", Name: "delete_returning", Category: CategoryDelete, Description: "RETURNING with DELETE", Required: true}, + {ID: "D04", Name: "delete_returning_columns", Category: CategoryDelete, Description: "RETURNING specific columns", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerJoinTests() { + tests := []*TestCase{ + {ID: "J01", Name: "join_inner", Category: CategoryJoin, Description: "INNER JOIN", Required: true}, + {ID: "J02", Name: "join_left", Category: CategoryJoin, Description: "LEFT JOIN with nullable result", Required: true}, + {ID: "J03", Name: "join_right", Category: CategoryJoin, Description: "RIGHT JOIN", Required: true}, + {ID: "J04", Name: "join_full", Category: CategoryJoin, Description: "FULL OUTER JOIN", Required: true}, + {ID: "J05", Name: "join_cross", Category: CategoryJoin, Description: "CROSS JOIN", Required: true}, + {ID: "J06", Name: "join_implicit", Category: CategoryJoin, Description: "Implicit join with comma", Required: true}, + {ID: "J07", Name: "join_self", Category: CategoryJoin, Description: "Self-join", Required: true}, + {ID: "J08", Name: "join_multiple", Category: CategoryJoin, Description: "Multiple joins", Required: true}, + {ID: "J09", Name: "join_star_expansion", Category: CategoryJoin, Description: "Star with JOIN", Required: true}, + {ID: "J10", Name: "join_qualified_star", Category: CategoryJoin, Description: "Qualified star with JOIN", Required: true}, + {ID: "J11", Name: "join_many_to_many", Category: CategoryJoin, Description: "Many-to-many JOIN", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerCTETests() { + tests := []*TestCase{ + {ID: "C01", Name: "cte_basic", Category: CategoryCTE, Description: "Basic CTE", Required: true}, + {ID: "C02", Name: "cte_multiple", Category: CategoryCTE, Description: "Multiple CTEs", Required: true}, + {ID: "C03", Name: "cte_chained", Category: CategoryCTE, Description: "CTEs referencing CTEs", Required: true}, + {ID: "C04", Name: "cte_recursive", Category: CategoryCTE, Description: "Recursive CTE", Required: true}, + {ID: "C05", Name: "cte_insert", Category: CategoryCTE, Description: "CTE with INSERT", Required: true}, + {ID: "C06", Name: "cte_update", Category: CategoryCTE, Description: "CTE with UPDATE", Required: true}, + {ID: "C07", Name: "cte_delete", Category: CategoryCTE, Description: "CTE with DELETE", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerSubqueryTests() { + tests := []*TestCase{ + {ID: "Q01", Name: "subquery_from", Category: CategorySubquery, Description: "Subquery in FROM", Required: true}, + {ID: "Q02", Name: "subquery_where_in", Category: CategorySubquery, Description: "Subquery in WHERE IN", Required: true}, + {ID: "Q03", Name: "subquery_scalar", Category: CategorySubquery, Description: "Scalar subquery", Required: true}, + {ID: "Q04", Name: "subquery_exists", Category: CategorySubquery, Description: "EXISTS subquery", Required: true}, + {ID: "Q05", Name: "subquery_not_exists", Category: CategorySubquery, Description: "NOT EXISTS", Required: true}, + {ID: "Q06", Name: "subquery_select_list", Category: CategorySubquery, Description: "Subquery in SELECT", Required: true}, + {ID: "Q07", Name: "subquery_correlated", Category: CategorySubquery, Description: "Correlated subquery", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerUnionTests() { + tests := []*TestCase{ + {ID: "N01", Name: "union_basic", Category: CategoryUnion, Description: "UNION deduplicates", Required: true}, + {ID: "N02", Name: "union_all", Category: CategoryUnion, Description: "UNION ALL keeps dupes", Required: true}, + {ID: "N03", Name: "union_order_by", Category: CategoryUnion, Description: "UNION with ORDER BY", Required: true}, + {ID: "N04", Name: "intersect", Category: CategoryUnion, Description: "INTERSECT", Required: true}, + {ID: "N05", Name: "except", Category: CategoryUnion, Description: "EXCEPT", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerAggregateTests() { + tests := []*TestCase{ + {ID: "A01", Name: "agg_count_star", Category: CategoryAggregate, Description: "COUNT(*)", Required: true}, + {ID: "A02", Name: "agg_count_column", Category: CategoryAggregate, Description: "COUNT(column)", Required: true}, + {ID: "A03", Name: "agg_count_distinct", Category: CategoryAggregate, Description: "COUNT(DISTINCT)", Required: true}, + {ID: "A04", Name: "agg_sum", Category: CategoryAggregate, Description: "SUM", Required: true}, + {ID: "A05", Name: "agg_avg", Category: CategoryAggregate, Description: "AVG", Required: true}, + {ID: "A06", Name: "agg_min_max", Category: CategoryAggregate, Description: "MIN/MAX", Required: true}, + {ID: "A07", Name: "agg_group_by", Category: CategoryAggregate, Description: "GROUP BY", Required: true}, + {ID: "A08", Name: "agg_group_by_multiple", Category: CategoryAggregate, Description: "Multiple GROUP BY", Required: true}, + {ID: "A09", Name: "agg_having", Category: CategoryAggregate, Description: "HAVING clause", Required: true}, + {ID: "A10", Name: "agg_having_aggregate", Category: CategoryAggregate, Description: "HAVING with aggregate", Required: true}, + {ID: "A11", Name: "agg_mixed", Category: CategoryAggregate, Description: "Multiple aggregates", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerOperatorTests() { + tests := []*TestCase{ + {ID: "O01", Name: "op_equal", Category: CategoryOperator, Description: "Equality", Required: true}, + {ID: "O02", Name: "op_not_equal", Category: CategoryOperator, Description: "Not equal", Required: true}, + {ID: "O03", Name: "op_less_than", Category: CategoryOperator, Description: "Less than", Required: true}, + {ID: "O04", Name: "op_less_equal", Category: CategoryOperator, Description: "Less or equal", Required: true}, + {ID: "O05", Name: "op_greater_than", Category: CategoryOperator, Description: "Greater than", Required: true}, + {ID: "O06", Name: "op_greater_equal", Category: CategoryOperator, Description: "Greater or equal", Required: true}, + {ID: "O07", Name: "op_between", Category: CategoryOperator, Description: "BETWEEN", Required: true}, + {ID: "O08", Name: "op_in_list", Category: CategoryOperator, Description: "IN with values", Required: true}, + {ID: "O09", Name: "op_is_null", Category: CategoryOperator, Description: "IS NULL", Required: true}, + {ID: "O10", Name: "op_is_not_null", Category: CategoryOperator, Description: "IS NOT NULL", Required: true}, + {ID: "O11", Name: "op_and", Category: CategoryOperator, Description: "AND", Required: true}, + {ID: "O12", Name: "op_or", Category: CategoryOperator, Description: "OR", Required: true}, + {ID: "O13", Name: "op_not", Category: CategoryOperator, Description: "NOT", Required: true}, + {ID: "O14", Name: "op_precedence", Category: CategoryOperator, Description: "Operator precedence", Required: true}, + {ID: "O15", Name: "op_like", Category: CategoryOperator, Description: "LIKE", Required: true}, + {ID: "O16", Name: "op_arithmetic", Category: CategoryOperator, Description: "Arithmetic operators", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerCaseTests() { + tests := []*TestCase{ + {ID: "E01", Name: "case_simple", Category: CategoryCase, Description: "Simple CASE", Required: true}, + {ID: "E02", Name: "case_searched", Category: CategoryCase, Description: "Searched CASE", Required: true}, + {ID: "E03", Name: "case_no_else", Category: CategoryCase, Description: "CASE without ELSE", Required: true}, + {ID: "E04", Name: "case_in_where", Category: CategoryCase, Description: "CASE in WHERE", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerNullTests() { + tests := []*TestCase{ + {ID: "F01", Name: "null_coalesce_two", Category: CategoryNull, Description: "COALESCE with 2 args", Required: true}, + {ID: "F02", Name: "null_coalesce_multiple", Category: CategoryNull, Description: "COALESCE with multiple", Required: true}, + {ID: "F03", Name: "null_coalesce_literal", Category: CategoryNull, Description: "COALESCE with literal", Required: true}, + {ID: "F04", Name: "null_nullif", Category: CategoryNull, Description: "NULLIF", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerCastTests() { + tests := []*TestCase{ + {ID: "T01", Name: "cast_to_int", Category: CategoryCast, Description: "CAST to integer", Required: true}, + {ID: "T02", Name: "cast_to_text", Category: CategoryCast, Description: "CAST to text", Required: true}, + {ID: "T03", Name: "cast_param", Category: CategoryCast, Description: "CAST parameter", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerFunctionTests() { + tests := []*TestCase{ + {ID: "B01", Name: "func_upper", Category: CategoryFunction, Description: "UPPER", Required: true}, + {ID: "B02", Name: "func_lower", Category: CategoryFunction, Description: "LOWER", Required: true}, + {ID: "B03", Name: "func_length", Category: CategoryFunction, Description: "LENGTH", Required: true}, + {ID: "B04", Name: "func_trim", Category: CategoryFunction, Description: "TRIM", Required: true}, + {ID: "B05", Name: "func_substring", Category: CategoryFunction, Description: "SUBSTRING", Required: true}, + {ID: "B06", Name: "func_replace", Category: CategoryFunction, Description: "REPLACE", Required: true}, + {ID: "B07", Name: "func_abs", Category: CategoryFunction, Description: "ABS", Required: true}, + {ID: "B08", Name: "func_round", Category: CategoryFunction, Description: "ROUND", Required: true}, + {ID: "B09", Name: "func_now", Category: CategoryFunction, Description: "NOW", Required: true}, + {ID: "B10", Name: "func_current_date", Category: CategoryFunction, Description: "CURRENT_DATE", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerDataTypeTests() { + tests := []*TestCase{ + {ID: "DT01", Name: "datatype_int", Category: CategoryDataType, Description: "INT type", Required: true}, + {ID: "DT02", Name: "datatype_int_nullable", Category: CategoryDataType, Description: "Nullable INT", Required: true}, + {ID: "DT03", Name: "datatype_bigint", Category: CategoryDataType, Description: "BIGINT type", Required: true}, + {ID: "DT04", Name: "datatype_float", Category: CategoryDataType, Description: "FLOAT type", Required: true}, + {ID: "DT05", Name: "datatype_decimal", Category: CategoryDataType, Description: "DECIMAL type", Required: true}, + {ID: "DT06", Name: "datatype_text", Category: CategoryDataType, Description: "TEXT type", Required: true}, + {ID: "DT07", Name: "datatype_varchar", Category: CategoryDataType, Description: "VARCHAR type", Required: true}, + {ID: "DT08", Name: "datatype_boolean", Category: CategoryDataType, Description: "BOOLEAN type", Required: true}, + {ID: "DT09", Name: "datatype_timestamp", Category: CategoryDataType, Description: "TIMESTAMP type", Required: true}, + {ID: "DT10", Name: "datatype_date", Category: CategoryDataType, Description: "DATE type", Required: true}, + {ID: "DT11", Name: "datatype_blob", Category: CategoryDataType, Description: "BLOB type", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerDDLTests() { + tests := []*TestCase{ + {ID: "DDL01", Name: "ddl_create_basic", Category: CategoryDDL, Description: "Basic table creation", Required: true}, + {ID: "DDL02", Name: "ddl_create_not_null", Category: CategoryDDL, Description: "NOT NULL constraint", Required: true}, + {ID: "DDL03", Name: "ddl_create_primary_key", Category: CategoryDDL, Description: "Primary key", Required: true}, + {ID: "DDL04", Name: "ddl_create_composite_pk", Category: CategoryDDL, Description: "Composite primary key", Required: true}, + {ID: "DDL05", Name: "ddl_create_unique", Category: CategoryDDL, Description: "UNIQUE constraint", Required: true}, + {ID: "DDL06", Name: "ddl_create_default", Category: CategoryDDL, Description: "Default value", Required: true}, + {ID: "DDL07", Name: "ddl_create_foreign_key", Category: CategoryDDL, Description: "Foreign key", Required: true}, + {ID: "DDL08", Name: "ddl_alter_add_column", Category: CategoryDDL, Description: "Add column", Required: true}, + {ID: "DDL09", Name: "ddl_alter_drop_column", Category: CategoryDDL, Description: "Drop column", Required: true}, + {ID: "DDL10", Name: "ddl_drop_table", Category: CategoryDDL, Description: "Drop table", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerViewTests() { + tests := []*TestCase{ + {ID: "V01", Name: "view_select", Category: CategoryView, Description: "Query view", Required: true}, + {ID: "V02", Name: "view_filter", Category: CategoryView, Description: "Filter view", Required: true}, + {ID: "V03", Name: "view_complex", Category: CategoryView, Description: "Complex view with joins", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerUpsertTests() { + tests := []*TestCase{ + {ID: "UP01", Name: "upsert_do_nothing", Category: CategoryUpsert, Description: "ON CONFLICT DO NOTHING", Required: true}, + {ID: "UP02", Name: "upsert_do_update", Category: CategoryUpsert, Description: "ON CONFLICT DO UPDATE", Required: true}, + {ID: "UP03", Name: "upsert_excluded", Category: CategoryUpsert, Description: "excluded pseudo-table", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerParamTests() { + tests := []*TestCase{ + {ID: "P01", Name: "param_single", Category: CategoryParam, Description: "Single parameter", Required: true}, + {ID: "P02", Name: "param_multiple_same_type", Category: CategoryParam, Description: "Multiple same-type params", Required: true}, + {ID: "P03", Name: "param_multiple_different_type", Category: CategoryParam, Description: "Different type params", Required: true}, + {ID: "P04", Name: "param_repeated", Category: CategoryParam, Description: "Same param used twice", Required: true}, + {ID: "P05", Name: "param_in_function", Category: CategoryParam, Description: "Param inside function", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerResultTests() { + tests := []*TestCase{ + {ID: "R01", Name: "result_one", Category: CategoryResult, Description: ":one annotation", Required: true}, + {ID: "R02", Name: "result_many", Category: CategoryResult, Description: ":many annotation", Required: true}, + {ID: "R03", Name: "result_exec", Category: CategoryResult, Description: ":exec annotation", Required: true}, + {ID: "R04", Name: "result_execrows", Category: CategoryResult, Description: ":execrows annotation", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerErrorTests() { + tests := []*TestCase{ + {ID: "ER01", Name: "error_unknown_table", Category: CategoryError, Description: "Unknown table error", Required: true}, + {ID: "ER02", Name: "error_unknown_column_select", Category: CategoryError, Description: "Unknown column in SELECT", Required: true}, + {ID: "ER03", Name: "error_unknown_column_where", Category: CategoryError, Description: "Unknown column in WHERE", Required: true}, + {ID: "ER04", Name: "error_unknown_column_insert", Category: CategoryError, Description: "Unknown column in INSERT", Required: true}, + {ID: "ER05", Name: "error_unknown_column_update", Category: CategoryError, Description: "Unknown column in UPDATE", Required: true}, + {ID: "ER06", Name: "error_syntax", Category: CategoryError, Description: "Syntax error", Required: true}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +// Extension tests - not required for all engines + +func registerEnumTests() { + tests := []*TestCase{ + {ID: "EN01", Name: "enum_select", Category: CategoryEnum, Description: "Select with enum column", Required: false}, + {ID: "EN02", Name: "enum_filter", Category: CategoryEnum, Description: "Filter by enum", Required: false}, + {ID: "EN03", Name: "enum_insert", Category: CategoryEnum, Description: "Insert enum value", Required: false}, + {ID: "EN04", Name: "enum_update", Category: CategoryEnum, Description: "Update enum value", Required: false}, + {ID: "EN05", Name: "enum_nullable", Category: CategoryEnum, Description: "Nullable enum", Required: false}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerSchemaTests() { + tests := []*TestCase{ + {ID: "SC01", Name: "schema_select", Category: CategorySchema, Description: "Schema-qualified SELECT", Required: false}, + {ID: "SC02", Name: "schema_insert", Category: CategorySchema, Description: "Schema-qualified INSERT", Required: false}, + {ID: "SC03", Name: "schema_update", Category: CategorySchema, Description: "Schema-qualified UPDATE", Required: false}, + {ID: "SC04", Name: "schema_delete", Category: CategorySchema, Description: "Schema-qualified DELETE", Required: false}, + {ID: "SC05", Name: "schema_join", Category: CategorySchema, Description: "Cross-schema JOIN", Required: false}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerArrayTests() { + tests := []*TestCase{ + {ID: "AR01", Name: "array_select", Category: CategoryArray, Description: "Select with array columns", Required: false}, + {ID: "AR02", Name: "array_insert", Category: CategoryArray, Description: "Insert array value", Required: false}, + {ID: "AR03", Name: "array_any", Category: CategoryArray, Description: "ANY with array", Required: false}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} + +func registerJSONTests() { + tests := []*TestCase{ + {ID: "JS01", Name: "json_select", Category: CategoryJSON, Description: "Select with JSON columns", Required: false}, + {ID: "JS02", Name: "json_insert", Category: CategoryJSON, Description: "Insert JSON value", Required: false}, + {ID: "JS03", Name: "json_nullable", Category: CategoryJSON, Description: "Nullable JSON", Required: false}, + } + for _, tc := range tests { + DefaultRegistry.Register(tc) + } +} diff --git a/internal/enginetest/testcases/registry.go b/internal/enginetest/testcases/registry.go new file mode 100644 index 0000000000..67c8db2b9d --- /dev/null +++ b/internal/enginetest/testcases/registry.go @@ -0,0 +1,326 @@ +// Package testcases defines the standard set of end-to-end test cases +// that each SQL engine must implement. +// +// Each engine (PostgreSQL, MySQL, SQLite) provides its own implementation +// of these test cases with engine-specific SQL syntax. The registry only +// defines WHAT to test, not HOW - each engine's testdata directory contains +// the actual SQL files. +// +// Directory structure for each engine: +// +// internal/endtoend/{engine}/testdata/ +// ├── core/ # Core tests (required) +// │ ├── select_star/ +// │ │ ├── sqlc.yaml +// │ │ ├── query.sql +// │ │ └── go/ # Expected output +// │ └── ... +// ├── enum/ # Enum extension (optional) +// │ └── ... +// ├── schema/ # Schema namespace extension (optional) +// │ └── ... +// └── schema.sql # Shared schema for this engine +package testcases + +// Category represents a category of test cases +type Category string + +const ( + // Core categories - all engines must implement these + CategorySelect Category = "select" + CategoryInsert Category = "insert" + CategoryUpdate Category = "update" + CategoryDelete Category = "delete" + CategoryJoin Category = "join" + CategoryCTE Category = "cte" + CategorySubquery Category = "subquery" + CategoryUnion Category = "union" + CategoryAggregate Category = "aggregate" + CategoryOperator Category = "operator" + CategoryCase Category = "case" + CategoryNull Category = "null" + CategoryCast Category = "cast" + CategoryFunction Category = "function" + CategoryDataType Category = "datatype" + CategoryDDL Category = "ddl" + CategoryView Category = "view" + CategoryUpsert Category = "upsert" + CategoryParam Category = "param" + CategoryResult Category = "result" + CategoryError Category = "error" + + // Extension categories - optional based on engine capabilities + CategoryEnum Category = "enum" + CategorySchema Category = "schema" + CategoryArray Category = "array" + CategoryJSON Category = "json" +) + +// TestCase defines a single end-to-end test case +type TestCase struct { + // ID is the unique identifier for this test case (e.g., "S01") + ID string + + // Name is the test case name used in the filesystem (e.g., "select_star") + Name string + + // Category is the category this test belongs to + Category Category + + // Description explains what this test validates + Description string + + // Required indicates if this test is mandatory for all engines + Required bool +} + +// Registry holds all registered test cases +type Registry struct { + cases map[string]*TestCase + byCategory map[Category][]*TestCase +} + +// NewRegistry creates a new test case registry +func NewRegistry() *Registry { + return &Registry{ + cases: make(map[string]*TestCase), + byCategory: make(map[Category][]*TestCase), + } +} + +// Register adds a test case to the registry +func (r *Registry) Register(tc *TestCase) { + r.cases[tc.ID] = tc + r.byCategory[tc.Category] = append(r.byCategory[tc.Category], tc) +} + +// Get returns a test case by ID +func (r *Registry) Get(id string) *TestCase { + return r.cases[id] +} + +// GetByCategory returns all test cases in a category +func (r *Registry) GetByCategory(cat Category) []*TestCase { + return r.byCategory[cat] +} + +// All returns all registered test cases +func (r *Registry) All() []*TestCase { + result := make([]*TestCase, 0, len(r.cases)) + for _, tc := range r.cases { + result = append(result, tc) + } + return result +} + +// Required returns all required test cases +func (r *Registry) Required() []*TestCase { + var result []*TestCase + for _, tc := range r.cases { + if tc.Required { + result = append(result, tc) + } + } + return result +} + +// RequiredCategories returns the categories that all engines must implement +func RequiredCategories() []Category { + return []Category{ + CategorySelect, + CategoryInsert, + CategoryUpdate, + CategoryDelete, + CategoryJoin, + CategoryCTE, + CategorySubquery, + CategoryUnion, + CategoryAggregate, + CategoryOperator, + CategoryCase, + CategoryNull, + CategoryCast, + CategoryFunction, + CategoryDataType, + CategoryDDL, + CategoryView, + CategoryUpsert, + CategoryParam, + CategoryResult, + CategoryError, + } +} + +// ExtensionCategories returns optional extension categories +func ExtensionCategories() []Category { + return []Category{ + CategoryEnum, + CategorySchema, + CategoryArray, + CategoryJSON, + } +} + +// Engine represents a SQL database engine +type Engine string + +const ( + EnginePostgreSQL Engine = "postgresql" + EngineMySQL Engine = "mysql" + EngineSQLite Engine = "sqlite" +) + +// EngineCapabilities defines what features an engine supports +type EngineCapabilities struct { + // SupportsReturning indicates if the engine supports RETURNING clause + SupportsReturning bool + + // SupportsFullOuterJoin indicates if the engine supports FULL OUTER JOIN + SupportsFullOuterJoin bool + + // SupportsRightJoin indicates if the engine supports RIGHT JOIN + SupportsRightJoin bool + + // SupportsCTE indicates if the engine supports Common Table Expressions + SupportsCTE bool + + // SupportsRecursiveCTE indicates if the engine supports recursive CTEs + SupportsRecursiveCTE bool + + // SupportsUpsert indicates if the engine supports upsert operations + SupportsUpsert bool + + // SupportsEnum indicates if the engine supports ENUM types + SupportsEnum bool + + // SupportsSchema indicates if the engine supports schema namespaces + SupportsSchema bool + + // SupportsArray indicates if the engine supports array types + SupportsArray bool + + // SupportsJSON indicates if the engine supports native JSON types + SupportsJSON bool + + // SupportsIntersect indicates if the engine supports INTERSECT + SupportsIntersect bool + + // SupportsExcept indicates if the engine supports EXCEPT + SupportsExcept bool +} + +// DefaultCapabilities returns the default capabilities for each engine +func DefaultCapabilities(engine Engine) EngineCapabilities { + switch engine { + case EnginePostgreSQL: + return EngineCapabilities{ + SupportsReturning: true, + SupportsFullOuterJoin: true, + SupportsRightJoin: true, + SupportsCTE: true, + SupportsRecursiveCTE: true, + SupportsUpsert: true, + SupportsEnum: true, + SupportsSchema: true, + SupportsArray: true, + SupportsJSON: true, + SupportsIntersect: true, + SupportsExcept: true, + } + case EngineMySQL: + return EngineCapabilities{ + SupportsReturning: false, // MySQL 8.0.21+ has limited support + SupportsFullOuterJoin: false, + SupportsRightJoin: true, + SupportsCTE: true, // MySQL 8.0+ + SupportsRecursiveCTE: true, // MySQL 8.0+ + SupportsUpsert: true, // ON DUPLICATE KEY UPDATE + SupportsEnum: true, + SupportsSchema: true, // databases act as schemas + SupportsArray: false, + SupportsJSON: true, + SupportsIntersect: true, // MySQL 8.0.31+ + SupportsExcept: true, // MySQL 8.0.31+ + } + case EngineSQLite: + return EngineCapabilities{ + SupportsReturning: true, // SQLite 3.35+ + SupportsFullOuterJoin: false, + SupportsRightJoin: true, // SQLite 3.39+ + SupportsCTE: true, + SupportsRecursiveCTE: true, + SupportsUpsert: true, // ON CONFLICT + SupportsEnum: false, + SupportsSchema: false, // attached databases only + SupportsArray: false, + SupportsJSON: true, // json1 extension + SupportsIntersect: true, + SupportsExcept: true, + } + default: + return EngineCapabilities{} + } +} + +// TestsForEngine returns all test cases that an engine should implement +// based on its capabilities +func (r *Registry) TestsForEngine(engine Engine) []*TestCase { + caps := DefaultCapabilities(engine) + var result []*TestCase + + for _, tc := range r.cases { + if shouldIncludeTest(tc, caps) { + result = append(result, tc) + } + } + return result +} + +// RequiredTestsForEngine returns required test cases for an engine +func (r *Registry) RequiredTestsForEngine(engine Engine) []*TestCase { + caps := DefaultCapabilities(engine) + var result []*TestCase + + for _, tc := range r.cases { + if tc.Required && shouldIncludeTest(tc, caps) { + result = append(result, tc) + } + } + return result +} + +func shouldIncludeTest(tc *TestCase, caps EngineCapabilities) bool { + // Extension categories depend on capabilities + switch tc.Category { + case CategoryEnum: + return caps.SupportsEnum + case CategorySchema: + return caps.SupportsSchema + case CategoryArray: + return caps.SupportsArray + case CategoryJSON: + return caps.SupportsJSON + } + + // Some specific tests depend on capabilities + switch tc.ID { + case "J03": // join_right + return caps.SupportsRightJoin + case "J04": // join_full + return caps.SupportsFullOuterJoin + case "I04", "I05", "U05", "U06", "D03", "D04": // RETURNING tests + return caps.SupportsReturning + case "C04": // cte_recursive + return caps.SupportsRecursiveCTE + case "C01", "C02", "C03", "C05", "C06", "C07": // CTE tests + return caps.SupportsCTE + case "UP01", "UP02", "UP03": // upsert tests + return caps.SupportsUpsert + case "N04": // intersect + return caps.SupportsIntersect + case "N05": // except + return caps.SupportsExcept + } + + return true +}