372 lines
9.8 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|