package pgsql import ( "os" "testing" "git.warky.dev/wdevs/relspecgo/pkg/models" "git.warky.dev/wdevs/relspecgo/pkg/readers" ) // getTestConnectionString returns a PostgreSQL connection string from environment // or skips the test if not available func getTestConnectionString(t *testing.T) string { connStr := os.Getenv("RELSPEC_TEST_PG_CONN") if connStr == "" { t.Skip("Skipping PostgreSQL reader test: RELSPEC_TEST_PG_CONN environment variable not set") } return connStr } func TestReader_ReadDatabase(t *testing.T) { connStr := getTestConnectionString(t) options := &readers.ReaderOptions{ ConnectionString: connStr, } reader := NewReader(options) db, err := reader.ReadDatabase() if err != nil { t.Fatalf("Failed to read database: %v", err) } // Verify database properties if db.Name == "" { t.Error("Database name should not be empty") } if db.DatabaseType != models.PostgresqlDatabaseType { t.Errorf("Expected database type %s, got %s", models.PostgresqlDatabaseType, db.DatabaseType) } if db.SourceFormat != "pgsql" { t.Errorf("Expected source format 'pgsql', got %s", db.SourceFormat) } // Verify schemas if len(db.Schemas) == 0 { t.Error("Expected at least one schema, got none") } // Check that system schemas are excluded for _, schema := range db.Schemas { if schema.Name == "pg_catalog" || schema.Name == "information_schema" { t.Errorf("System schema %s should be excluded", schema.Name) } } t.Logf("Successfully read database '%s' with %d schemas", db.Name, len(db.Schemas)) // Log schema details for _, schema := range db.Schemas { t.Logf(" Schema: %s (Tables: %d, Views: %d, Sequences: %d)", schema.Name, len(schema.Tables), len(schema.Views), len(schema.Sequences)) // Verify tables have columns for _, table := range schema.Tables { if len(table.Columns) == 0 { t.Logf(" Warning: Table %s.%s has no columns", schema.Name, table.Name) } else { t.Logf(" Table: %s.%s (Columns: %d, Constraints: %d, Indexes: %d, Relationships: %d)", schema.Name, table.Name, len(table.Columns), len(table.Constraints), len(table.Indexes), len(table.Relationships)) } } // Verify views have columns and definitions for _, view := range schema.Views { if view.Definition == "" { t.Errorf("View %s.%s should have a definition", schema.Name, view.Name) } t.Logf(" View: %s.%s (Columns: %d)", schema.Name, view.Name, len(view.Columns)) } // Verify sequences for _, seq := range schema.Sequences { if seq.IncrementBy == 0 { t.Errorf("Sequence %s.%s should have non-zero increment", schema.Name, seq.Name) } t.Logf(" Sequence: %s.%s (Start: %d, Increment: %d)", schema.Name, seq.Name, seq.StartValue, seq.IncrementBy) } } } func TestReader_ReadSchema(t *testing.T) { connStr := getTestConnectionString(t) options := &readers.ReaderOptions{ ConnectionString: connStr, } reader := NewReader(options) schema, err := reader.ReadSchema() if err != nil { t.Fatalf("Failed to read schema: %v", err) } if schema.Name == "" { t.Error("Schema name should not be empty") } t.Logf("Successfully read schema '%s' with %d tables, %d views, %d sequences", schema.Name, len(schema.Tables), len(schema.Views), len(schema.Sequences)) } func TestReader_ReadTable(t *testing.T) { connStr := getTestConnectionString(t) options := &readers.ReaderOptions{ ConnectionString: connStr, } reader := NewReader(options) table, err := reader.ReadTable() if err != nil { t.Fatalf("Failed to read table: %v", err) } if table.Name == "" { t.Error("Table name should not be empty") } if table.Schema == "" { t.Error("Table schema should not be empty") } t.Logf("Successfully read table '%s.%s' with %d columns", table.Schema, table.Name, len(table.Columns)) } func TestReader_ReadDatabase_InvalidConnectionString(t *testing.T) { options := &readers.ReaderOptions{ ConnectionString: "invalid connection string", } reader := NewReader(options) _, err := reader.ReadDatabase() if err == nil { t.Error("Expected error with invalid connection string, got nil") } t.Logf("Correctly rejected invalid connection string: %v", err) } func TestReader_ReadDatabase_EmptyConnectionString(t *testing.T) { options := &readers.ReaderOptions{ ConnectionString: "", } reader := NewReader(options) _, err := reader.ReadDatabase() if err == nil { t.Error("Expected error with empty connection string, got nil") } expectedMsg := "connection string is required" if err.Error() != expectedMsg { t.Errorf("Expected error message '%s', got '%s'", expectedMsg, err.Error()) } } func TestMapDataType(t *testing.T) { reader := &Reader{} tests := []struct { pgType string udtName string expected string }{ {"integer", "int4", "int"}, {"bigint", "int8", "int64"}, {"smallint", "int2", "int16"}, {"character varying", "varchar", "string"}, {"text", "text", "string"}, {"boolean", "bool", "bool"}, {"timestamp without time zone", "timestamp", "timestamp"}, {"timestamp with time zone", "timestamptz", "timestamptz"}, {"json", "json", "json"}, {"jsonb", "jsonb", "jsonb"}, {"uuid", "uuid", "uuid"}, {"numeric", "numeric", "decimal"}, {"real", "float4", "float32"}, {"double precision", "float8", "float64"}, {"date", "date", "date"}, {"time without time zone", "time", "time"}, {"bytea", "bytea", "bytea"}, {"unknown_type", "custom", "custom"}, // Should return UDT name } for _, tt := range tests { t.Run(tt.pgType, func(t *testing.T) { result := reader.mapDataType(tt.pgType, tt.udtName) if result != tt.expected { t.Errorf("mapDataType(%s, %s) = %s, expected %s", tt.pgType, tt.udtName, result, tt.expected) } }) } } func TestParseIndexDefinition(t *testing.T) { reader := &Reader{} tests := []struct { name string indexName string tableName string schema string indexDef string wantType string wantUnique bool wantColumns int }{ { name: "simple btree index", indexName: "idx_users_email", tableName: "users", schema: "public", indexDef: "CREATE INDEX idx_users_email ON public.users USING btree (email)", wantType: "btree", wantUnique: false, wantColumns: 1, }, { name: "unique index", indexName: "idx_users_username", tableName: "users", schema: "public", indexDef: "CREATE UNIQUE INDEX idx_users_username ON public.users USING btree (username)", wantType: "btree", wantUnique: true, wantColumns: 1, }, { name: "composite index", indexName: "idx_users_name", tableName: "users", schema: "public", indexDef: "CREATE INDEX idx_users_name ON public.users USING btree (first_name, last_name)", wantType: "btree", wantUnique: false, wantColumns: 2, }, { name: "gin index", indexName: "idx_posts_tags", tableName: "posts", schema: "public", indexDef: "CREATE INDEX idx_posts_tags ON public.posts USING gin (tags)", wantType: "gin", wantUnique: false, wantColumns: 1, }, { name: "partial index with where clause", indexName: "idx_users_active", tableName: "users", schema: "public", indexDef: "CREATE INDEX idx_users_active ON public.users USING btree (id) WHERE (active = true)", wantType: "btree", wantUnique: false, wantColumns: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { index, err := reader.parseIndexDefinition(tt.indexName, tt.tableName, tt.schema, tt.indexDef) if err != nil { t.Fatalf("parseIndexDefinition() error = %v", err) } if index.Name != tt.indexName { t.Errorf("Name = %s, want %s", index.Name, tt.indexName) } if index.Type != tt.wantType { t.Errorf("Type = %s, want %s", index.Type, tt.wantType) } if index.Unique != tt.wantUnique { t.Errorf("Unique = %v, want %v", index.Unique, tt.wantUnique) } if len(index.Columns) != tt.wantColumns { t.Errorf("Columns count = %d, want %d (columns: %v)", len(index.Columns), tt.wantColumns, index.Columns) } }) } } func TestDeriveRelationship(t *testing.T) { table := models.InitTable("orders", "public") fk := models.InitConstraint("fk_orders_user_id", models.ForeignKeyConstraint) fk.Schema = "public" fk.Table = "orders" fk.Columns = []string{"user_id"} fk.ReferencedSchema = "public" fk.ReferencedTable = "users" fk.ReferencedColumns = []string{"id"} fk.OnDelete = "CASCADE" fk.OnUpdate = "RESTRICT" reader := &Reader{} reader.deriveRelationship(table, fk) if len(table.Relationships) != 1 { t.Fatalf("Expected 1 relationship, got %d", len(table.Relationships)) } relName := "orders_to_users" rel, exists := table.Relationships[relName] if !exists { t.Fatalf("Expected relationship '%s', not found", relName) } if rel.Type != models.OneToMany { t.Errorf("Expected relationship type %s, got %s", models.OneToMany, rel.Type) } if rel.FromTable != "users" { t.Errorf("Expected FromTable 'users', got '%s'", rel.FromTable) } if rel.ToTable != "orders" { t.Errorf("Expected ToTable 'orders', got '%s'", rel.ToTable) } if rel.ForeignKey != "fk_orders_user_id" { t.Errorf("Expected ForeignKey 'fk_orders_user_id', got '%s'", rel.ForeignKey) } if rel.Properties["on_delete"] != "CASCADE" { t.Errorf("Expected on_delete 'CASCADE', got '%s'", rel.Properties["on_delete"]) } if rel.Properties["on_update"] != "RESTRICT" { t.Errorf("Expected on_update 'RESTRICT', got '%s'", rel.Properties["on_update"]) } } // Benchmark tests func BenchmarkReader_ReadDatabase(b *testing.B) { connStr := os.Getenv("RELSPEC_TEST_PG_CONN") if connStr == "" { b.Skip("Skipping benchmark: RELSPEC_TEST_PG_CONN environment variable not set") } options := &readers.ReaderOptions{ ConnectionString: connStr, } b.ResetTimer() for i := 0; i < b.N; i++ { reader := NewReader(options) _, err := reader.ReadDatabase() if err != nil { b.Fatalf("Failed to read database: %v", err) } } }