Files
relspecgo/pkg/readers/pgsql/reader_test.go
Hein db6cd21511
Some checks are pending
CI / Build (push) Waiting to run
CI / Test (1.23) (push) Waiting to run
CI / Test (1.24) (push) Waiting to run
CI / Test (1.25) (push) Waiting to run
CI / Lint (push) Waiting to run
Added more examples and pgsql reader
2025-12-17 10:08:50 +02:00

372 lines
9.8 KiB
Go

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)
}
}
}