feat: add MySQL reader and writer

This commit is contained in:
SG Command
2026-10-03 12:36:39 +02:00
parent b38f53c603
commit 7a9219b6e3
53 changed files with 12311 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
package mysql
import (
"context"
"database/sql"
"fmt"
"io"
"os"
"sort"
"strings"
"git.warky.dev/wdevs/relspecgo/pkg/mariadb"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
type Writer struct {
options *writers.WriterOptions
writer io.Writer
}
func NewWriter(options *writers.WriterOptions) *Writer { return &Writer{options: options} }
func (w *Writer) WriteDatabase(db *models.Database) error {
if w.options == nil {
return fmt.Errorf("writer options are required")
}
if conn, ok := w.options.Metadata["connection_string"].(string); ok && conn != "" {
return w.execute(db, conn)
}
if w.writer == nil {
if w.options.OutputPath != "" {
f, err := os.Create(w.options.OutputPath)
if err != nil {
return err
}
defer f.Close()
w.writer = f
} else {
w.writer = os.Stdout
}
}
fmt.Fprintf(w.writer, "-- MySQL Database Schema\n-- Database: %s\n-- Generated by RelSpec\n\n", db.Name)
for _, s := range db.Schemas {
if err := w.WriteSchema(s); err != nil {
return err
}
}
return nil
}
func (w *Writer) WriteSchema(s *models.Schema) error {
for _, t := range s.Tables {
if err := w.writeTable(s, t); err != nil {
return err
}
}
return nil
}
func (w *Writer) WriteTable(t *models.Table) error {
if w.writer == nil {
w.writer = os.Stdout
}
return w.writeTable(nil, t)
}
func (w *Writer) writeTable(s *models.Schema, t *models.Table) error {
name := t.Name
if s != nil {
name = fmt.Sprintf("%s.%s", quote(s.Name), quote(t.Name))
} else {
name = quote(name)
}
cols := make([]*models.Column, 0, len(t.Columns))
for _, c := range t.Columns {
cols = append(cols, c)
}
sort.Slice(cols, func(i, j int) bool { return cols[i].Sequence < cols[j].Sequence })
defs := []string{}
pk := []string{}
for _, c := range cols {
d := fmt.Sprintf(" %s %s", quote(c.Name), mariadb.ConvertCanonicalToMariaDB(c.Type))
if c.Length > 0 && strings.EqualFold(c.Type, "string") {
d = fmt.Sprintf(" %s VARCHAR(%d)", quote(c.Name), c.Length)
}
if c.NotNull {
d += " NOT NULL"
}
if c.AutoIncrement {
d += " AUTO_INCREMENT"
}
if c.Default != nil {
d += fmt.Sprintf(" DEFAULT %s", writers.QuoteDefaultValue(fmt.Sprint(c.Default), c.Type))
}
defs = append(defs, d)
if c.IsPrimaryKey {
pk = append(pk, quote(c.Name))
}
}
for _, c := range t.Constraints {
if c.Type == models.PrimaryKeyConstraint {
pk = nil
for _, n := range c.Columns {
pk = append(pk, quote(n))
}
break
}
}
if len(pk) > 0 {
defs = append(defs, " PRIMARY KEY ("+strings.Join(pk, ", ")+")")
}
for _, c := range t.Constraints {
if c.Type == models.UniqueConstraint {
defs = append(defs, fmt.Sprintf(" CONSTRAINT %s UNIQUE (%s)", quote(c.Name), quoted(c.Columns)))
}
}
sql := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n%s\n) ENGINE=InnoDB;\n\n", name, strings.Join(defs, ",\n"))
if _, err := io.WriteString(w.writer, sql); err != nil {
return err
}
return nil
}
func (w *Writer) execute(dbm *models.Database, conn string) error {
var b strings.Builder
old := w.writer
w.writer = &b
if err := w.WriteDatabase(dbm); err != nil {
w.writer = old
return err
}
w.writer = old
db, err := sql.Open("mysql", conn)
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
defer db.Close()
for _, stmt := range strings.Split(b.String(), ";\n") {
stmt = strings.TrimSpace(stmt)
if stmt == "" || strings.HasPrefix(stmt, "--") {
continue
}
if _, err := db.ExecContext(context.Background(), stmt); err != nil {
return fmt.Errorf("failed to execute SQL: %w", err)
}
}
return nil
}
func quote(s string) string { return "`" + strings.ReplaceAll(s, "`", "``") + "`" }
func quoted(xs []string) string {
out := make([]string, len(xs))
for i, x := range xs {
out[i] = quote(x)
}
return strings.Join(out, ", ")
}
+37
View File
@@ -0,0 +1,37 @@
package mysql
import (
"bytes"
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
func TestWriterGeneratesMySQLDDL(t *testing.T) {
db := models.InitDatabase("app")
s := models.InitSchema("app")
table := models.InitTable("users", "app")
table.Columns["id"] = models.InitColumn("id", "users", "app")
table.Columns["id"].Type = "int"
table.Columns["id"].IsPrimaryKey = true
table.Columns["id"].NotNull = true
table.Columns["name"] = models.InitColumn("name", "users", "app")
table.Columns["name"].Type = "string"
table.Columns["name"].Length = 80
s.Tables = append(s.Tables, table)
db.Schemas = append(db.Schemas, s)
var out bytes.Buffer
w := NewWriter(&writers.WriterOptions{Metadata: map[string]interface{}{}})
w.writer = &out
if err := w.WriteDatabase(db); err != nil {
t.Fatal(err)
}
got := out.String()
for _, want := range []string{"CREATE TABLE IF NOT EXISTS `app`.`users`", "`id` INT NOT NULL", "`name` VARCHAR(80)", "PRIMARY KEY (`id`)"} {
if !strings.Contains(got, want) {
t.Errorf("DDL missing %q:\n%s", want, got)
}
}
}