Skip to content

Commit 314e6ca

Browse files
Support mssql (#20)
* Support mssql * Format code
1 parent 52e8e7e commit 314e6ca

File tree

5 files changed

+307
-24
lines changed

5 files changed

+307
-24
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/sijms/go-ora/v2 v2.8.21
1313
github.com/spf13/cobra v1.8.1
1414
github.com/stretchr/testify v1.9.0
15+
github.com/testcontainers/testcontainers-go/modules/mssql v0.33.0
1516
github.com/testcontainers/testcontainers-go/modules/mysql v0.33.0
1617
github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0
1718
github.com/xo/dburl v0.23.2

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
172172
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
173173
github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw=
174174
github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8=
175+
github.com/testcontainers/testcontainers-go/modules/mssql v0.33.0 h1:gD4pHUPnEm5Bwup8KFdVmwXJLpyVy1hsp6bOXHAUlTA=
176+
github.com/testcontainers/testcontainers-go/modules/mssql v0.33.0/go.mod h1:HdgR2Q9SsGqohT6nhtU3tnG56iNGUV1Tr5If0QypZl0=
175177
github.com/testcontainers/testcontainers-go/modules/mysql v0.33.0 h1:1JN7YEEepTMJmGI2hW678IiiYoLM5HDp3vbCPmUokJ8=
176178
github.com/testcontainers/testcontainers-go/modules/mysql v0.33.0/go.mod h1:9tZZwRW5s3RaI5X0Wnc+GXNJFXqbkKmob2nBHbfA/5E=
177179
github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 h1:c+Gt+XLJjqFAejgX4hSpnHIpC9eAhvgI/TFWL/PbrFI=

pkg/db/mssql.go

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package db
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"github.com/DanielLiu1123/gencoder/pkg/model"
8+
"slices"
9+
"sort"
10+
)
11+
12+
// GenMssqlTable generates an MSSQL table and fills the Table structure.
13+
func GenMssqlTable(ctx context.Context, db *sql.DB, schema string, name string, ignoreColumns []string) (*model.Table, error) {
14+
t, err := getMssqlTableInfo(ctx, db, schema, name)
15+
if err != nil {
16+
return nil, err
17+
}
18+
if t == nil {
19+
return nil, nil
20+
}
21+
22+
columns, err := getMssqlColumnsInfo(ctx, db, schema, name, ignoreColumns)
23+
if err != nil {
24+
return nil, err
25+
}
26+
t.Columns = columns
27+
28+
indexes, err := getMssqlIndexesInfo(ctx, db, schema, name)
29+
if err != nil {
30+
return nil, err
31+
}
32+
t.Indexes = indexes
33+
34+
return t, nil
35+
}
36+
37+
func getMssqlTableInfo(ctx context.Context, db *sql.DB, schema, name string) (*model.Table, error) {
38+
const tableSql = `
39+
SELECT s.name AS table_schema,
40+
t.name AS table_name,
41+
p.value AS table_comment
42+
FROM sys.tables t
43+
JOIN sys.schemas s ON t.schema_id = s.schema_id
44+
LEFT JOIN sys.extended_properties p ON p.major_id = t.object_id AND p.minor_id = 0 AND p.name = 'MS_Description'
45+
WHERE s.name = @p1
46+
AND t.name = @p2;
47+
`
48+
var t model.Table
49+
err := db.QueryRowContext(ctx, tableSql, schema, name).Scan(&t.Schema, &t.Name, &t.Comment)
50+
if err != nil {
51+
if errors.Is(err, sql.ErrNoRows) {
52+
return nil, nil
53+
}
54+
return nil, err
55+
}
56+
return &t, nil
57+
}
58+
59+
func getMssqlColumnsInfo(ctx context.Context, db *sql.DB, schema, name string, ignoreColumns []string) ([]*model.Column, error) {
60+
const columnsSql = `
61+
SELECT
62+
c.column_id AS ordinal,
63+
c.name AS column_name,
64+
tp.name AS data_type,
65+
c.is_nullable AS is_nullable,
66+
dc.definition AS default_value,
67+
CASE
68+
WHEN EXISTS (
69+
SELECT 1
70+
FROM sys.index_columns ic
71+
JOIN sys.indexes i ON ic.index_id = i.index_id
72+
WHERE ic.object_id = c.object_id AND ic.column_id = c.column_id AND i.is_primary_key = 1
73+
)
74+
THEN 1 ELSE 0
75+
END AS is_primary,
76+
ep.value AS comment
77+
FROM sys.columns c
78+
JOIN sys.types tp ON c.user_type_id = tp.user_type_id
79+
JOIN sys.tables t ON c.object_id = t.object_id
80+
JOIN sys.schemas s ON t.schema_id = s.schema_id
81+
LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id
82+
LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description'
83+
WHERE s.name = @p1
84+
AND t.name = @p2
85+
ORDER BY c.column_id;
86+
`
87+
rows, err := db.QueryContext(ctx, columnsSql, schema, name)
88+
if err != nil {
89+
return nil, err
90+
}
91+
defer rows.Close()
92+
93+
var columns []*model.Column
94+
for rows.Next() {
95+
var col model.Column
96+
if err := rows.Scan(&col.Ordinal, &col.Name, &col.Type, &col.IsNullable, &col.DefaultValue, &col.IsPrimaryKey, &col.Comment); err != nil {
97+
return nil, err
98+
}
99+
if !slices.Contains(ignoreColumns, col.Name) {
100+
columns = append(columns, &col)
101+
}
102+
}
103+
return columns, nil
104+
}
105+
106+
func getMssqlIndexesInfo(ctx context.Context, db *sql.DB, schema, name string) ([]*model.Index, error) {
107+
const indexesSql = `
108+
SELECT i.name AS index_name,
109+
i.is_unique AS is_unique,
110+
i.is_primary_key AS is_primary,
111+
ic.key_ordinal AS ordinal,
112+
c.name AS column_name
113+
FROM sys.indexes i
114+
JOIN sys.tables t ON i.object_id = t.object_id
115+
JOIN sys.schemas s ON t.schema_id = s.schema_id
116+
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
117+
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
118+
WHERE s.name = @p1
119+
AND t.name = @p2
120+
ORDER BY i.name, ic.key_ordinal;
121+
`
122+
rows, err := db.QueryContext(ctx, indexesSql, schema, name)
123+
if err != nil {
124+
return nil, err
125+
}
126+
defer rows.Close()
127+
128+
indexMap := make(map[string]*model.Index)
129+
for rows.Next() {
130+
var indexName, columnName string
131+
var isUnique, isPrimary bool
132+
var ordinal int
133+
134+
if err := rows.Scan(&indexName, &isUnique, &isPrimary, &ordinal, &columnName); err != nil {
135+
return nil, err
136+
}
137+
138+
if _, exists := indexMap[indexName]; !exists {
139+
indexMap[indexName] = &model.Index{
140+
Name: indexName,
141+
IsUnique: isUnique,
142+
IsPrimary: isPrimary,
143+
Columns: []*model.IndexColumn{},
144+
}
145+
}
146+
147+
indexMap[indexName].Columns = append(indexMap[indexName].Columns, &model.IndexColumn{
148+
Ordinal: ordinal,
149+
Name: columnName,
150+
})
151+
}
152+
153+
indexes := make([]*model.Index, 0, len(indexMap))
154+
for _, index := range indexMap {
155+
indexes = append(indexes, index)
156+
}
157+
158+
sort.Slice(indexes, func(i, j int) bool {
159+
if indexes[i].IsPrimary != indexes[j].IsPrimary {
160+
return indexes[i].IsPrimary
161+
}
162+
if indexes[i].IsUnique != indexes[j].IsUnique {
163+
return indexes[i].IsUnique
164+
}
165+
return indexes[i].Name < indexes[j].Name
166+
})
167+
168+
return indexes, nil
169+
}

pkg/db/mssql_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package db
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
"github.com/testcontainers/testcontainers-go/modules/mssql"
9+
"github.com/xo/dburl"
10+
"os/exec"
11+
"testing"
12+
13+
_ "github.com/microsoft/go-mssqldb"
14+
)
15+
16+
func TestGenMssqlTable(t *testing.T) {
17+
err := exec.Command("docker", "info").Run()
18+
if err != nil {
19+
t.Skip("Docker not available, skipping MSSQL tests")
20+
}
21+
22+
ctx := context.Background()
23+
mssqlContainer, err := mssql.Run(ctx,
24+
"mcr.microsoft.com/mssql/server:latest",
25+
mssql.WithAcceptEULA(),
26+
mssql.WithPassword("Sa123456.."),
27+
)
28+
require.NoError(t, err)
29+
defer func() {
30+
if err := mssqlContainer.Terminate(ctx); err != nil {
31+
t.Fatalf("failed to terminate container: %s", err)
32+
}
33+
}()
34+
35+
host, err := mssqlContainer.Host(ctx)
36+
require.NoError(t, err)
37+
port, err := mssqlContainer.MappedPort(ctx, "1433")
38+
require.NoError(t, err)
39+
40+
dsn := fmt.Sprintf("mssql://sa:Sa123456..@%s:%s/master", host, port.Port())
41+
db, err := dburl.Open(dsn)
42+
require.NoError(t, err)
43+
44+
// Create table and indexes in MSSQL
45+
_, err = db.Exec(`CREATE TABLE master.dbo.[user] (
46+
id INT IDENTITY (1,1) PRIMARY KEY,
47+
username NVARCHAR(64) NOT NULL,
48+
password NVARCHAR(128) NOT NULL,
49+
email NVARCHAR(128) NOT NULL DEFAULT '',
50+
first_name NVARCHAR(64),
51+
last_name NVARCHAR(64),
52+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
53+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
54+
status NVARCHAR(9) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')),
55+
deleted_at DATETIME NULL,
56+
CONSTRAINT unique_email UNIQUE (email)
57+
);
58+
CREATE INDEX idx_name ON master.dbo.[user] (username);
59+
CREATE INDEX idx_status_created ON master.dbo.[user] (status, created_at);
60+
CREATE INDEX idx_full_name ON master.dbo.[user] (first_name, last_name);
61+
62+
-- Add comments
63+
EXEC sp_addextendedproperty
64+
@name = N'MS_Description',
65+
@value = N'User account information',
66+
@level0type = N'SCHEMA', @level0name = 'dbo',
67+
@level1type = N'TABLE', @level1name = 'user';
68+
EXEC sp_addextendedproperty
69+
@name = N'MS_Description',
70+
@value = N'Username, required',
71+
@level0type = N'SCHEMA', @level0name = 'dbo',
72+
@level1type = N'TABLE', @level1name = 'user',
73+
@level2type = N'COLUMN', @level2name = 'username';
74+
EXEC sp_addextendedproperty
75+
@name = N'MS_Description',
76+
@value = N'User email, required',
77+
@level0type = N'SCHEMA', @level0name = 'dbo',
78+
@level1type = N'TABLE', @level1name = 'user',
79+
@level2type = N'COLUMN', @level2name = 'email';`)
80+
require.NoError(t, err)
81+
82+
// Call the GenMssqlTable function to generate the table model
83+
schema := "dbo"
84+
table := "user"
85+
86+
tb, err := GenMssqlTable(ctx, db, schema, table, []string{"deleted_at"})
87+
require.NoError(t, err)
88+
89+
// Assertions on the table structure
90+
assert.NotNil(t, tb)
91+
assert.Equal(t, "dbo", tb.Schema)
92+
assert.Equal(t, "user", tb.Name)
93+
assert.Equal(t, "User account information", tb.Comment)
94+
95+
// Assertions on the columns
96+
assert.Equal(t, 9, len(tb.Columns)) // Columns without deleted_at
97+
assert.Equal(t, true, tb.Columns[0].IsPrimaryKey)
98+
assert.Equal(t, false, tb.Columns[1].IsPrimaryKey)
99+
100+
// Assertions on the indexes
101+
assert.Equal(t, 5, len(tb.Indexes))
102+
assert.Contains(t, tb.Indexes[0].Name, "PK__user") // Primary key
103+
assert.Equal(t, "unique_email", tb.Indexes[1].Name) // Unique index
104+
assert.Equal(t, "idx_full_name", tb.Indexes[2].Name)
105+
assert.Equal(t, "first_name", tb.Indexes[2].Columns[0].Name)
106+
assert.Equal(t, "last_name", tb.Indexes[2].Columns[1].Name)
107+
assert.Equal(t, "idx_name", tb.Indexes[3].Name)
108+
assert.Equal(t, "idx_status_created", tb.Indexes[4].Name)
109+
assert.Equal(t, "status", tb.Indexes[4].Columns[0].Name)
110+
assert.Equal(t, "created_at", tb.Indexes[4].Columns[1].Name)
111+
}

pkg/db/postgres_test.go

+24-24
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,30 @@ func TestGenPostgresTable(t *testing.T) {
4444
require.NoError(t, err)
4545

4646
_, err = db.Exec(`CREATE TABLE testdb.public."user" (
47-
id SERIAL PRIMARY KEY,
48-
username VARCHAR(64) NOT NULL,
49-
password VARCHAR(128) NOT NULL,
50-
email VARCHAR(128) NOT NULL DEFAULT '',
51-
first_name VARCHAR(64),
52-
last_name VARCHAR(64),
53-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
54-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55-
status VARCHAR(9) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')),
56-
deleted_at TIMESTAMP,
57-
CONSTRAINT unique_email UNIQUE (email)
58-
);
59-
CREATE INDEX idx_name ON testdb.public."user" (username);
60-
CREATE INDEX idx_status_created ON testdb.public."user" (status, created_at);
61-
CREATE INDEX idx_full_name ON testdb.public."user" (first_name, last_name);
62-
COMMENT ON COLUMN testdb.public."user".username IS 'Username, required';
63-
COMMENT ON COLUMN testdb.public."user".email IS 'User email, required';
64-
COMMENT ON COLUMN testdb.public."user".first_name IS 'First name of the user';
65-
COMMENT ON COLUMN testdb.public."user".last_name IS 'Last name of the user';
66-
COMMENT ON COLUMN testdb.public."user".created_at IS 'Record creation timestamp';
67-
COMMENT ON COLUMN testdb.public."user".updated_at IS 'Record update timestamp';
68-
COMMENT ON COLUMN testdb.public."user".status IS 'Account status';
69-
COMMENT ON COLUMN testdb.public."user".deleted_at IS 'Record deletion timestamp';
70-
COMMENT ON TABLE testdb.public."user" IS 'User account information';`)
47+
id SERIAL PRIMARY KEY,
48+
username VARCHAR(64) NOT NULL,
49+
password VARCHAR(128) NOT NULL,
50+
email VARCHAR(128) NOT NULL DEFAULT '',
51+
first_name VARCHAR(64),
52+
last_name VARCHAR(64),
53+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
54+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55+
status VARCHAR(9) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')),
56+
deleted_at TIMESTAMP,
57+
CONSTRAINT unique_email UNIQUE (email)
58+
);
59+
CREATE INDEX idx_name ON testdb.public."user" (username);
60+
CREATE INDEX idx_status_created ON testdb.public."user" (status, created_at);
61+
CREATE INDEX idx_full_name ON testdb.public."user" (first_name, last_name);
62+
COMMENT ON COLUMN testdb.public."user".username IS 'Username, required';
63+
COMMENT ON COLUMN testdb.public."user".email IS 'User email, required';
64+
COMMENT ON COLUMN testdb.public."user".first_name IS 'First name of the user';
65+
COMMENT ON COLUMN testdb.public."user".last_name IS 'Last name of the user';
66+
COMMENT ON COLUMN testdb.public."user".created_at IS 'Record creation timestamp';
67+
COMMENT ON COLUMN testdb.public."user".updated_at IS 'Record update timestamp';
68+
COMMENT ON COLUMN testdb.public."user".status IS 'Account status';
69+
COMMENT ON COLUMN testdb.public."user".deleted_at IS 'Record deletion timestamp';
70+
COMMENT ON TABLE testdb.public."user" IS 'User account information';`)
7171
require.NoError(t, err)
7272

7373
schema := "public"

0 commit comments

Comments
 (0)