Skip to content

Commit 0737d0d

Browse files
committed
feat: multi-DB integration tests expansion and project cleanup
1 parent 9b9b888 commit 0737d0d

8 files changed

Lines changed: 382 additions & 11188 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package mssql_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/meru143/dbdiff/internal/db"
10+
_ "github.com/meru143/dbdiff/internal/db/mssql"
11+
"github.com/meru143/dbdiff/pkg/types"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"github.com/testcontainers/testcontainers-go"
15+
"github.com/testcontainers/testcontainers-go/wait"
16+
)
17+
18+
func TestMSSQLIntrospectionIntegration(t *testing.T) {
19+
if testing.Short() {
20+
t.Skip("Skipping integration test in short mode")
21+
}
22+
23+
ctx := context.Background()
24+
25+
// 1. Setup SQL Server Testcontainer
26+
// We use the official SQL Server 2022 image
27+
req := testcontainers.ContainerRequest{
28+
Image: "mcr.microsoft.com/mssql/server:2022-latest",
29+
ExposedPorts: []string{"1433/tcp"},
30+
Env: map[string]string{
31+
"ACCEPT_EULA": "Y",
32+
"MSSQL_SA_PASSWORD": "StrongPassword123!",
33+
},
34+
WaitingFor: wait.ForLog("SQL Server is now ready for client connections.").WithStartupTimeout(120 * time.Second),
35+
}
36+
37+
mssqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
38+
ContainerRequest: req,
39+
Started: true,
40+
})
41+
require.NoError(t, err)
42+
43+
defer func() {
44+
if err := mssqlC.Terminate(ctx); err != nil {
45+
t.Logf("failed to terminate container: %s", err)
46+
}
47+
}()
48+
49+
host, err := mssqlC.Host(ctx)
50+
require.NoError(t, err)
51+
port, err := mssqlC.MappedPort(ctx, "1433")
52+
require.NoError(t, err)
53+
54+
// DSN format: sqlserver://sa:Password@host:port?database=master&encrypt=disable
55+
connStr := fmt.Sprintf("sqlserver://sa:StrongPassword123!@%s:%s?database=master&encrypt=disable", host, port.Port())
56+
57+
// 2. Initialize dbdiff Driver
58+
driver, err := db.NewDriver(connStr)
59+
require.NoError(t, err)
60+
61+
err = driver.Connect(ctx, connStr, "disable", 30*time.Second)
62+
require.NoError(t, err)
63+
defer driver.Close()
64+
65+
// Create a test database and use it
66+
err = driver.Exec(ctx, "CREATE DATABASE testdb")
67+
require.NoError(t, err)
68+
69+
// Reconnect to the new database
70+
connStrWithDB := fmt.Sprintf("sqlserver://sa:StrongPassword123!@%s:%s?database=testdb&encrypt=disable", host, port.Port())
71+
driverWithDB, err := db.NewDriver(connStrWithDB)
72+
require.NoError(t, err)
73+
err = driverWithDB.Connect(ctx, connStrWithDB, "disable", 15*time.Second)
74+
require.NoError(t, err)
75+
defer driverWithDB.Close()
76+
77+
// 3. Setup dummy schema
78+
setupSQL := `
79+
CREATE TABLE employees (
80+
id INT IDENTITY(1,1) PRIMARY KEY,
81+
name NVARCHAR(100) NOT NULL,
82+
salary DECIMAL(18,2) NOT NULL,
83+
is_active BIT DEFAULT 1,
84+
hired_at DATETIME2 DEFAULT GETDATE()
85+
);
86+
87+
CREATE INDEX idx_employees_name ON employees(name);
88+
89+
EXEC('CREATE VIEW v_active_employees AS SELECT id, name FROM employees WHERE is_active = 1');
90+
`
91+
err = driverWithDB.Exec(ctx, setupSQL)
92+
require.NoError(t, err)
93+
94+
// 4. Test Introspection
95+
schemaObj, err := driverWithDB.Introspect(ctx, "dbo", []string{})
96+
require.NoError(t, err)
97+
98+
// 5. Verify parsed schema
99+
assert.NotNil(t, schemaObj)
100+
101+
var employeesTable *types.Table
102+
for i := range schemaObj.Tables {
103+
if schemaObj.Tables[i].Name == "employees" {
104+
employeesTable = &schemaObj.Tables[i]
105+
break
106+
}
107+
}
108+
require.NotNil(t, employeesTable, "Expected 'employees' table to be found")
109+
assert.Equal(t, "employees", employeesTable.Name)
110+
111+
// Verify columns
112+
colNames := []string{}
113+
for _, c := range employeesTable.Columns {
114+
colNames = append(colNames, c.Name)
115+
}
116+
assert.Contains(t, colNames, "id")
117+
assert.Contains(t, colNames, "name")
118+
assert.Contains(t, colNames, "salary")
119+
assert.Contains(t, colNames, "is_active")
120+
assert.Contains(t, colNames, "hired_at")
121+
122+
// Verify normalization
123+
for _, c := range employeesTable.Columns {
124+
normalized := driverWithDB.NormalizeType(c.DataType)
125+
if c.Name == "id" {
126+
assert.Equal(t, "integer", normalized)
127+
}
128+
if c.Name == "name" {
129+
assert.Equal(t, "varchar", normalized)
130+
}
131+
if c.Name == "salary" {
132+
assert.Equal(t, "numeric", normalized)
133+
}
134+
}
135+
136+
// Verify view
137+
assert.True(t, len(schemaObj.Views) > 0, "Expected at least one view")
138+
foundView := false
139+
for _, v := range schemaObj.Views {
140+
if v.Name == "v_active_employees" {
141+
foundView = true
142+
break
143+
}
144+
}
145+
assert.True(t, foundView, "Expected v_active_employees view to exist")
146+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package mysql_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/meru143/dbdiff/internal/db"
10+
_ "github.com/meru143/dbdiff/internal/db/mysql"
11+
"github.com/meru143/dbdiff/pkg/types"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"github.com/testcontainers/testcontainers-go"
15+
"github.com/testcontainers/testcontainers-go/wait"
16+
)
17+
18+
func TestMySQLIntrospectionIntegration(t *testing.T) {
19+
if testing.Short() {
20+
t.Skip("Skipping integration test in short mode")
21+
}
22+
23+
ctx := context.Background()
24+
25+
// 1. Setup MySQL Testcontainer
26+
// We use the official MySQL 8.0 image
27+
req := testcontainers.ContainerRequest{
28+
Image: "mysql:8.0",
29+
ExposedPorts: []string{"3306/tcp"},
30+
Env: map[string]string{
31+
"MYSQL_ROOT_PASSWORD": "rootpassword",
32+
"MYSQL_DATABASE": "testdb",
33+
"MYSQL_USER": "testuser",
34+
"MYSQL_PASSWORD": "testpassword",
35+
},
36+
WaitingFor: wait.ForLog("port: 3306 MySQL Community Server").WithStartupTimeout(90 * time.Second),
37+
}
38+
39+
mysqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
40+
ContainerRequest: req,
41+
Started: true,
42+
})
43+
require.NoError(t, err)
44+
45+
defer func() {
46+
if err := mysqlC.Terminate(ctx); err != nil {
47+
t.Logf("failed to terminate container: %s", err)
48+
}
49+
}()
50+
51+
host, err := mysqlC.Host(ctx)
52+
require.NoError(t, err)
53+
port, err := mysqlC.MappedPort(ctx, "3306")
54+
require.NoError(t, err)
55+
56+
// DSN format: user:password@tcp(host:port)/dbname?parseTime=true
57+
dsn := fmt.Sprintf("testuser:testpassword@tcp(%s:%s)/testdb?parseTime=true&multiStatements=true", host, port.Port())
58+
fullUrl := "mysql://" + dsn
59+
60+
// 2. Initialize dbdiff Driver
61+
driver, err := db.NewDriver(fullUrl)
62+
require.NoError(t, err)
63+
64+
err = driver.Connect(ctx, fullUrl, "disable", 20*time.Second)
65+
require.NoError(t, err)
66+
defer driver.Close()
67+
68+
// 3. Setup dummy schema
69+
setupSQL := `
70+
CREATE TABLE products (
71+
id INT AUTO_INCREMENT PRIMARY KEY,
72+
name VARCHAR(100) NOT NULL,
73+
price DECIMAL(10,2) NOT NULL,
74+
available BOOLEAN DEFAULT TRUE,
75+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
76+
);
77+
78+
CREATE INDEX idx_products_name ON products(name);
79+
80+
CREATE VIEW v_active_products AS
81+
SELECT id, name, price FROM products WHERE available = TRUE;
82+
`
83+
err = driver.Exec(ctx, setupSQL)
84+
require.NoError(t, err)
85+
86+
// 4. Test Introspection
87+
// MySQL Introspect uses current DB if schema is empty
88+
schemaObj, err := driver.Introspect(ctx, "testdb", []string{})
89+
require.NoError(t, err)
90+
91+
// 5. Verify parsed schema
92+
assert.NotNil(t, schemaObj)
93+
94+
var productsTable *types.Table
95+
for i := range schemaObj.Tables {
96+
if schemaObj.Tables[i].Name == "products" {
97+
productsTable = &schemaObj.Tables[i]
98+
break
99+
}
100+
}
101+
require.NotNil(t, productsTable, "Expected 'products' table to be found")
102+
assert.Equal(t, "products", productsTable.Name)
103+
104+
// Verify columns
105+
colNames := []string{}
106+
for _, c := range productsTable.Columns {
107+
colNames = append(colNames, c.Name)
108+
}
109+
assert.Contains(t, colNames, "id")
110+
assert.Contains(t, colNames, "name")
111+
assert.Contains(t, colNames, "price")
112+
assert.Contains(t, colNames, "available")
113+
assert.Contains(t, colNames, "created_at")
114+
115+
// Verify normalization via DiffEngine logic (or just direct NormalizeType)
116+
for _, c := range productsTable.Columns {
117+
if c.Name == "id" {
118+
assert.Equal(t, "integer", driver.NormalizeType(c.DataType))
119+
}
120+
if c.Name == "name" {
121+
assert.Equal(t, "varchar", driver.NormalizeType(c.DataType))
122+
}
123+
if c.Name == "price" {
124+
assert.Equal(t, "numeric", driver.NormalizeType(c.DataType))
125+
}
126+
}
127+
128+
// Verify view
129+
assert.True(t, len(schemaObj.Views) > 0, "Expected at least one view")
130+
foundView := false
131+
for _, v := range schemaObj.Views {
132+
if v.Name == "v_active_products" {
133+
foundView = true
134+
break
135+
}
136+
}
137+
assert.True(t, foundView, "Expected v_active_products view to exist")
138+
}

internal/db/schema_integration_test.go renamed to internal/db/postgres/postgres_integration_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
package db_test
1+
package postgres_test
22

33
import (
44
"context"
55
"testing"
66
"time"
77

88
"github.com/meru143/dbdiff/internal/db"
9+
_ "github.com/meru143/dbdiff/internal/db/postgres"
910
"github.com/meru143/dbdiff/pkg/types"
1011
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
@@ -14,7 +15,7 @@ import (
1415
"github.com/testcontainers/testcontainers-go/wait"
1516
)
1617

17-
func TestSchemaIntrospectionIntegration(t *testing.T) {
18+
func TestPostgresIntrospectionIntegration(t *testing.T) {
1819
if testing.Short() {
1920
t.Skip("Skipping integration test in short mode")
2021
}

internal/diff/cross_db_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package diff_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/meru143/dbdiff/internal/diff"
7+
"github.com/meru143/dbdiff/pkg/types"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestCrossDatabaseComparison(t *testing.T) {
12+
// Source: Postgres-like schema
13+
source := &types.Schema{
14+
Tables: []types.Table{
15+
{
16+
Name: "users",
17+
Columns: []types.Column{
18+
{Name: "id", DataType: "int4"},
19+
{Name: "name", DataType: "character varying"},
20+
{Name: "created_at", DataType: "timestamp with time zone"},
21+
},
22+
},
23+
},
24+
}
25+
26+
// Target: MySQL-like schema
27+
target := &types.Schema{
28+
Tables: []types.Table{
29+
{
30+
Name: "users",
31+
Columns: []types.Column{
32+
{Name: "id", DataType: "int"},
33+
{Name: "name", DataType: "varchar"},
34+
{Name: "created_at", DataType: "datetime"},
35+
},
36+
},
37+
},
38+
}
39+
40+
// Define a mock normalization function that maps both to standard generic types
41+
normalize := func(dt string) string {
42+
switch dt {
43+
case "int4", "int":
44+
return "integer"
45+
case "character varying", "varchar":
46+
return "varchar"
47+
case "timestamp with time zone", "datetime":
48+
return "timestamp"
49+
default:
50+
return dt
51+
}
52+
}
53+
54+
// Case 1: Without normalization - should have differences in data types
55+
engineNoNorm := diff.NewDiffEngine(source, target, nil)
56+
diffsNoNorm := engineNoNorm.Compare()
57+
assert.Greater(t, len(diffsNoNorm), 0, "Expected differences when comparing natively without normalization")
58+
59+
foundTypeDiff := false
60+
for _, d := range diffsNoNorm {
61+
if d.Object == types.ObjectColumn && d.Type == types.DiffAlter {
62+
foundTypeDiff = true
63+
}
64+
}
65+
assert.True(t, foundTypeDiff, "Expected at least one type alteration diff")
66+
67+
// Case 2: With normalization - should have 0 differences
68+
engineWithNorm := diff.NewDiffEngine(source, target, normalize)
69+
diffsWithNorm := engineWithNorm.Compare()
70+
assert.Equal(t, 0, len(diffsWithNorm), "Expected 0 differences after normalization, got: %v", diffsWithNorm)
71+
}

0 commit comments

Comments
 (0)