Skip to content

Commit df0bdf1

Browse files
committed
GH-4: mysql source
1 parent ef75af6 commit df0bdf1

7 files changed

Lines changed: 490 additions & 6 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ go install github.com/denchenko/dberd/cmd/dberd@latest
3333

3434
Currently, DBerd supports the following database sources:
3535

36-
- **CockroachDB**: Extract schema from CockroachDB databases using the `cockroach` source type.
37-
- **ClickHouse**: Extract schema from ClickHouse databases using the `clickhouse` source type.
36+
- **MySQL**: Extract schema from MySQL databases using the `mysql` source type;
37+
- **CockroachDB**: Extract schema from CockroachDB databases using the `cockroach` source type;
38+
- **ClickHouse**: Extract schema from ClickHouse databases using the `clickhouse` source type;
3839
- **MongoDB**: Extract collections from MongoDB databases using the `mongodb` source type.
3940

4041
## Supported Targets

cmd/dberd/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import (
1111
"github.com/denchenko/dberd/source/clickhouse"
1212
"github.com/denchenko/dberd/source/cockroach"
1313
"github.com/denchenko/dberd/source/mongodb"
14+
"github.com/denchenko/dberd/source/mysql"
1415
"github.com/denchenko/dberd/target/d2"
1516
"github.com/denchenko/dberd/target/json"
1617
"github.com/denchenko/dberd/target/mermaid"
1718
"github.com/denchenko/dberd/target/plantuml"
1819
)
1920

2021
func main() {
21-
sourceType := flag.String("source", "", "Source database type (cockroach, clickhouse, mongodb)")
22+
sourceType := flag.String("source", "", "Source database type (mysql, cockroach, clickhouse, mongodb)")
2223
targetType := flag.String("target", "", "Target type (d2, plantuml, json, mermaid)")
2324
formatToFile := flag.String("format-to-file", "", "Output file for the formatted schema")
2425
renderToFile := flag.String("render-to-file", "", "Output file for the rendered diagram")
@@ -101,6 +102,8 @@ func main() {
101102

102103
func pickSource(sourceType, sourceDSN string) (dberd.Source, error) {
103104
switch sourceType {
105+
case "mysql":
106+
return mysql.NewSource(sourceDSN)
104107
case "cockroach":
105108
return cockroach.NewSource(sourceDSN)
106109
case "clickhouse":

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@ go 1.23.1
44

55
require (
66
github.com/ClickHouse/clickhouse-go/v2 v2.35.0
7+
github.com/go-sql-driver/mysql v1.8.0
78
github.com/jackc/pgx/v5 v5.7.5
89
github.com/stretchr/testify v1.10.0
910
github.com/testcontainers/testcontainers-go v0.37.0
1011
github.com/testcontainers/testcontainers-go/modules/clickhouse v0.37.0
1112
github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.37.0
1213
github.com/testcontainers/testcontainers-go/modules/mongodb v0.37.0
14+
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0
1315
go.mongodb.org/mongo-driver v1.14.0
1416
oss.terrastruct.com/d2 v0.7.0
1517
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a
1618
)
1719

1820
require (
1921
dario.cat/mergo v1.0.1 // indirect
22+
filippo.io/edwards25519 v1.1.0 // indirect
2023
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
2124
github.com/ClickHouse/ch-go v0.66.0 // indirect
2225
github.com/Microsoft/go-winio v0.6.2 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
22
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
3+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
4+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
35
git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
46
git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
57
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
@@ -79,6 +81,8 @@ github.com/go-pdf/fpdf v0.8.0 h1:IJKpdaagnWUeSkUFUjTcSzTppFxmv8ucGQyNPQWxYOQ=
7981
github.com/go-pdf/fpdf v0.8.0/go.mod h1:gfqhcNwXrsd3XYKte9a7vM3smvU/jB4ZRDrmWSxpfdc=
8082
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
8183
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
84+
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
85+
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
8286
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
8387
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
8488
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
@@ -189,6 +193,8 @@ github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.37.0 h1:GdU/F
189193
github.com/testcontainers/testcontainers-go/modules/cockroachdb v0.37.0/go.mod h1:pnSBxvvRFsCyLBL/obJwtFbJ4xno54wlAcPsRAACB8c=
190194
github.com/testcontainers/testcontainers-go/modules/mongodb v0.37.0 h1:drGy4LJOVkIKpKGm1YKTfVzb1qRhN/konVpmuUphq0k=
191195
github.com/testcontainers/testcontainers-go/modules/mongodb v0.37.0/go.mod h1:e9/4dGJfSZW59/kXGf/ksrEvA+BqP/daax0Usp2cpsM=
196+
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI=
197+
github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc=
192198
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
193199
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
194200
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=

source/cockroach/cockroach.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// Package cockroach provides functionality for extracting database schema information
2-
// from CockroachDB databases. It implements the schema extraction interface for
3-
// CockroachDB, allowing users to retrieve table definitions, column information,
4-
// and foreign key relationships from a CockroachDB instance.
2+
// from CockroachDB databases.
53
package cockroach
64

75
import (

source/mysql/mysql.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Package mysql provides functionality for extracting database schema information
2+
// from MySQL databases.
3+
package mysql
4+
5+
import (
6+
"context"
7+
"database/sql"
8+
"fmt"
9+
"io"
10+
11+
"github.com/denchenko/dberd"
12+
_ "github.com/go-sql-driver/mysql" // import mysql driver
13+
)
14+
15+
// Ensure Source implements dberd interfaces.
16+
var (
17+
_ dberd.Source = (*Source)(nil)
18+
)
19+
20+
// Source represents a MySQL database source for schema extraction.
21+
type Source struct {
22+
db *sql.DB
23+
closer io.Closer
24+
}
25+
26+
// NewSource creates a new MySQL source from a connection string.
27+
func NewSource(connStr string) (*Source, error) {
28+
db, err := sql.Open("mysql", connStr)
29+
if err != nil {
30+
return nil, fmt.Errorf("opening mysql connection: %w", err)
31+
}
32+
33+
return &Source{
34+
db: db,
35+
closer: db,
36+
}, nil
37+
}
38+
39+
// NewSourceFromDB creates a new MySQL source from an existing database connection.
40+
// This is useful when you want to reuse an existing database connection
41+
// for schema extraction purposes.
42+
func NewSourceFromDB(db *sql.DB) *Source {
43+
return &Source{
44+
db: db,
45+
}
46+
}
47+
48+
// Close closes the database connection if it was created by NewSource.
49+
// If the connection was provided externally (via NewSourceFromDB), this is a no-op.
50+
func (s *Source) Close() error {
51+
if s.closer == nil {
52+
return nil
53+
}
54+
55+
return s.closer.Close()
56+
}
57+
58+
// ExtractSchema extracts the complete database schema including tables and their references.
59+
func (s *Source) ExtractSchema(ctx context.Context) (schema dberd.Schema, err error) {
60+
schema.Tables, err = s.extractTables(ctx)
61+
if err != nil {
62+
return dberd.Schema{}, fmt.Errorf("extracting tables: %w", err)
63+
}
64+
65+
schema.References, err = s.extractReferences(ctx)
66+
if err != nil {
67+
return dberd.Schema{}, fmt.Errorf("extracting references: %w", err)
68+
}
69+
70+
return schema, nil
71+
}
72+
73+
const extractTablesQuery = `
74+
SELECT
75+
TABLE_SCHEMA,
76+
TABLE_NAME,
77+
COLUMN_NAME,
78+
COLUMN_TYPE,
79+
IS_NULLABLE,
80+
COLUMN_DEFAULT,
81+
COLUMN_COMMENT,
82+
COLUMN_KEY = 'PRI' as is_primary
83+
FROM information_schema.COLUMNS
84+
WHERE TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
85+
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION;`
86+
87+
type tableRow struct {
88+
tableSchema string
89+
tableName string
90+
columnName string
91+
columnType string
92+
isNullable string
93+
columnDefault *string
94+
columnComment string
95+
isPrimary bool
96+
}
97+
98+
// extractTables queries the database for table and column information and converts it to dberd.Table format.
99+
func (s *Source) extractTables(ctx context.Context) ([]dberd.Table, error) {
100+
rows, err := s.db.QueryContext(ctx, extractTablesQuery)
101+
if err != nil {
102+
return nil, fmt.Errorf("querying tables: %w", err)
103+
}
104+
defer rows.Close()
105+
106+
tablesRows := make([]tableRow, 0, 100) // Assuming tables rows.
107+
108+
for rows.Next() {
109+
var r tableRow
110+
if err := rows.Scan(
111+
&r.tableSchema,
112+
&r.tableName,
113+
&r.columnName,
114+
&r.columnType,
115+
&r.isNullable,
116+
&r.columnDefault,
117+
&r.columnComment,
118+
&r.isPrimary,
119+
); err != nil {
120+
return nil, fmt.Errorf("scanning tables row: %w", err)
121+
}
122+
123+
tablesRows = append(tablesRows, r)
124+
}
125+
126+
if err := rows.Err(); err != nil {
127+
return nil, fmt.Errorf("tables rows error: %w", err)
128+
}
129+
130+
return tableRowsToSchemaTables(tablesRows), nil
131+
}
132+
133+
// tableRowsToSchemaTables converts a slice of tableRow into a slice of dberd.Table.
134+
func tableRowsToSchemaTables(tableRows []tableRow) []dberd.Table {
135+
// Pre-allocate map with estimated size
136+
tableMap := make(map[string]*dberd.Table, len(tableRows)/10) // Assuming average 10 columns per table
137+
138+
for _, row := range tableRows {
139+
tableKey := row.tableSchema + "." + row.tableName
140+
141+
table, exists := tableMap[tableKey]
142+
if !exists {
143+
table = &dberd.Table{
144+
Name: tableKey,
145+
Columns: make([]dberd.Column, 0, 10),
146+
}
147+
tableMap[tableKey] = table
148+
}
149+
150+
definition := row.columnType
151+
if row.isNullable == "NO" {
152+
definition += " NOT NULL"
153+
}
154+
if row.columnDefault != nil && *row.columnDefault != "" {
155+
definition += " DEFAULT " + *row.columnDefault
156+
}
157+
158+
column := dberd.Column{
159+
Name: row.columnName,
160+
Definition: definition,
161+
IsPrimary: row.isPrimary,
162+
}
163+
164+
if row.columnComment != "" {
165+
column.Comment = row.columnComment
166+
}
167+
168+
table.Columns = append(table.Columns, column)
169+
}
170+
171+
// Pre-allocate slice with exact size
172+
tables := make([]dberd.Table, 0, len(tableMap))
173+
for _, table := range tableMap {
174+
tables = append(tables, *table)
175+
}
176+
177+
return tables
178+
}
179+
180+
const extractReferencesQuery = `
181+
SELECT
182+
TABLE_SCHEMA,
183+
TABLE_NAME,
184+
COLUMN_NAME,
185+
REFERENCED_TABLE_SCHEMA,
186+
REFERENCED_TABLE_NAME,
187+
REFERENCED_COLUMN_NAME
188+
FROM information_schema.KEY_COLUMN_USAGE
189+
WHERE REFERENCED_TABLE_SCHEMA IS NOT NULL
190+
AND TABLE_SCHEMA NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
191+
ORDER BY TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME;`
192+
193+
type referenceRow struct {
194+
tableSchema string
195+
tableName string
196+
columnName string
197+
referencedSchema string
198+
referencedTableName string
199+
referencedColumn string
200+
}
201+
202+
// extractReferences queries the database for foreign key relationships and converts them to dberd.Reference format.
203+
func (s *Source) extractReferences(ctx context.Context) ([]dberd.Reference, error) {
204+
rows, err := s.db.QueryContext(ctx, extractReferencesQuery)
205+
if err != nil {
206+
return nil, fmt.Errorf("querying references: %w", err)
207+
}
208+
defer rows.Close()
209+
210+
referenceRows := make([]referenceRow, 0, 50) // Assuming reasonable number of references
211+
212+
for rows.Next() {
213+
var r referenceRow
214+
if err := rows.Scan(
215+
&r.tableSchema,
216+
&r.tableName,
217+
&r.columnName,
218+
&r.referencedSchema,
219+
&r.referencedTableName,
220+
&r.referencedColumn,
221+
); err != nil {
222+
return nil, fmt.Errorf("scanning references row: %w", err)
223+
}
224+
225+
referenceRows = append(referenceRows, r)
226+
}
227+
228+
if err := rows.Err(); err != nil {
229+
return nil, fmt.Errorf("references rows error: %w", err)
230+
}
231+
232+
return referenceRowsToSchemaReferences(referenceRows), nil
233+
}
234+
235+
// referenceRowsToSchemaReferences converts a slice of referenceRow into a slice of dberd.Reference.
236+
func referenceRowsToSchemaReferences(referenceRows []referenceRow) []dberd.Reference {
237+
references := make([]dberd.Reference, 0, len(referenceRows))
238+
239+
for _, row := range referenceRows {
240+
reference := dberd.Reference{
241+
Source: dberd.TableColumn{
242+
Table: row.tableSchema + "." + row.tableName,
243+
Column: row.columnName,
244+
},
245+
Target: dberd.TableColumn{
246+
Table: row.referencedSchema + "." + row.referencedTableName,
247+
Column: row.referencedColumn,
248+
},
249+
}
250+
references = append(references, reference)
251+
}
252+
253+
return references
254+
}

0 commit comments

Comments
 (0)