Skip to content

Commit

Permalink
Support --textual flag, generating diff-style output
Browse files Browse the repository at this point in the history
Signed-off-by: Shlomi Noach <[email protected]>
  • Loading branch information
shlomi-noach committed Mar 24, 2024
1 parent e063bbd commit d888d97
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 23 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,33 @@ ALTER VIEW `v1` AS SELECT `id`, `name` FROM `t1`;

Consider that running `schemadiff diff` on the same views above results with validation error, because the referenced table `t1` does not appear in the schema definition. `diff-view` does not attempt to resolve dependencies.

### Textual diff format output

You may add `--textual` flag to get a diff-format output rather than semantic SQL output:

```sh
echo "create table t (id int primary key, i int); create view v as select id from t" > /tmp/schema_v1.sql
echo "create table t (id bigint primary key, i int, key (i)); create table t2 (id int primary key, name varchar(128) not null default '')" > /tmp/schema_v2.sql
schemadiff diff --source /tmp/schema_v1.sql --target /tmp/schema_v2.sql --textual
```

```diff
-CREATE VIEW `v` AS SELECT `id` FROM `t`;
CREATE TABLE `t` (
- `id` int,
+ `id` bigint,
`i` int,
PRIMARY KEY (`id`)
+ KEY `i` (`i`)
);
+CREATE TABLE `t2` (
+ `id` int,
+ `name` varchar(128) NOT NULL DEFAULT '',
+ PRIMARY KEY (`id`)
+);
```

The textual diff still works semantically under the hood, and it will ignore trailing comma changes, index reordering, cosntraint name changes, etc.

## Binaries

Expand Down
3 changes: 2 additions & 1 deletion cmd/schemadiff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ func main() {

source := flag.String("source", "", "Input source (file name / directory / empty for stdin)")
target := flag.String("target", "", "Input target (file name / directory / empty for stdin)")
textual := flag.Bool("textual", false, "Output textual diff rather than semantic SQL diff")
flag.Parse()

args := flag.Args()
if len(args) != 1 {
exitWithError(errors.New("one argument expected. Usage: schemadiff [flags...] <load|diff|ordered-diff|diff-table|diff-view>"))
}
command := args[0]
output, err := core.Exec(ctx, command, *source, *target)
output, err := core.Exec(ctx, command, *source, *target, *textual)
if err != nil {
exitWithError(err)
}
Expand Down
23 changes: 14 additions & 9 deletions pkg/core/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const mysqlVersion = "8.0.35"

// Exec is the main execution entry for this app, called by the main() function.
// Teh function returns a textual output, which is later send to standard output.
func Exec(ctx context.Context, command string, source string, target string) (output string, err error) {
func Exec(ctx context.Context, command string, source string, target string, textual bool) (output string, err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

Expand All @@ -35,6 +35,15 @@ func Exec(ctx context.Context, command string, source string, target string) (ou
}
env := schemadiff.NewEnv(vtenv, collEnv.DefaultConnectionCharset())
var bld strings.Builder
writeDiff := func(d schemadiff.EntityDiff) {
if textual {
_, _, unified := d.Annotated()
bld.WriteString(unified.Export())
} else {
bld.WriteString(d.CanonicalStatementString())
}
bld.WriteString(";\n")
}
getDiffs := func(ordered bool) (err error) {
if source == target {
return ErrIdenticalSourceTarget
Expand All @@ -54,8 +63,7 @@ func Exec(ctx context.Context, command string, source string, target string) (ou
diffs = diff.UnorderedDiffs()
}
for _, d := range diffs {
bld.WriteString(d.CanonicalStatementString())
bld.WriteString(";\n")
writeDiff(d)
}
return nil
}
Expand All @@ -66,8 +74,7 @@ func Exec(ctx context.Context, command string, source string, target string) (ou
return "", err
}
for _, e := range schema.Entities() {
bld.WriteString(e.Create().CanonicalStatementString())
bld.WriteString(";\n")
writeDiff(e.Create())
}
case "diff":
if err := getDiffs(false); err != nil {
Expand All @@ -86,8 +93,7 @@ func Exec(ctx context.Context, command string, source string, target string) (ou
return "", err
}
if !diff.IsEmpty() {
bld.WriteString(diff.CanonicalStatementString())
bld.WriteString(";\n")
writeDiff(diff)
}
case "diff-view":
if source == target {
Expand All @@ -98,8 +104,7 @@ func Exec(ctx context.Context, command string, source string, target string) (ou
return "", err
}
if !diff.IsEmpty() {
bld.WriteString(diff.CanonicalStatementString())
bld.WriteString(";\n")
writeDiff(diff)
}
case "apply":
default:
Expand Down
57 changes: 44 additions & 13 deletions pkg/core/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,33 @@ func TestExecLoad(t *testing.T) {
fileFrom := writeSchemaFile(t, schemaFrom)
require.NotEmpty(t, fileFrom)
defer os.RemoveAll(fileFrom)
schema, err := Exec(ctx, "load", fileFrom, "")
schema, err := Exec(ctx, "load", fileFrom, "", false)
assert.NoError(t, err)
assert.Equal(t, sqlsToMultiStatementText(loadFrom), schema)
})
t.Run("from-file-textual", func(t *testing.T) {
fileFrom := writeSchemaFile(t, schemaFrom)
require.NotEmpty(t, fileFrom)
defer os.RemoveAll(fileFrom)
schema, err := Exec(ctx, "load", fileFrom, "", true)
assert.NoError(t, err)
expect := []string{}
for _, sql := range loadFrom {
lines := strings.Split(sql, "\n")
for i := range lines {
lines[i] = "+" + lines[i]
}
sql = strings.Join(lines, "\n")
expect = append(expect, sql)
}
assert.Equal(t, sqlsToMultiStatementText(expect), schema)
})

t.Run("from-dir", func(t *testing.T) {
dirFrom := writeSchemaDir(t, schemaFrom)
require.NotEmpty(t, dirFrom)
defer os.RemoveAll(dirFrom)
schema, err := Exec(ctx, "load", dirFrom, "")
schema, err := Exec(ctx, "load", dirFrom, "", false)
assert.NoError(t, err)
assert.Equal(t, sqlsToMultiStatementText(loadFrom), schema)
})
Expand All @@ -119,7 +136,7 @@ func TestExecLoad(t *testing.T) {
fileTo := writeSchemaFile(t, schemaTo)
require.NotEmpty(t, fileTo)
defer os.RemoveAll(fileTo)
schema, err := Exec(ctx, "load", fileTo, "")
schema, err := Exec(ctx, "load", fileTo, "", false)
assert.NoError(t, err)
assert.Equal(t, sqlsToMultiStatementText(loadTo), schema)
})
Expand All @@ -128,7 +145,7 @@ func TestExecLoad(t *testing.T) {
dirTo := writeSchemaDir(t, schemaTo)
require.NotEmpty(t, dirTo)
defer os.RemoveAll(dirTo)
schema, err := Exec(ctx, "load", dirTo, "")
schema, err := Exec(ctx, "load", dirTo, "", false)
assert.NoError(t, err)
assert.Equal(t, sqlsToMultiStatementText(loadTo), schema)
})
Expand All @@ -137,7 +154,7 @@ func TestExecLoad(t *testing.T) {
require.NotEmpty(t, emptyFile) // testing that the *name* is not empty...
defer os.RemoveAll(emptyFile)

schema, err := Exec(ctx, "load", emptyFile, "")
schema, err := Exec(ctx, "load", emptyFile, "", false)
assert.NoError(t, err)
assert.Equal(t, "", schema)
})
Expand Down Expand Up @@ -169,6 +186,7 @@ func TestExecDiff(t *testing.T) {
name string
source string
target string
textual bool
expectDiff []string
expectError string
}{
Expand Down Expand Up @@ -262,7 +280,7 @@ func TestExecDiff(t *testing.T) {
t.Run(cmd, func(t *testing.T) {
for _, tcase := range tcases {
t.Run(tcase.name, func(t *testing.T) {
diff, err := Exec(ctx, cmd, tcase.source, tcase.target)
diff, err := Exec(ctx, cmd, tcase.source, tcase.target, tcase.textual)
if tcase.expectError == "" {
assert.NoError(t, err)
switch cmd {
Expand Down Expand Up @@ -314,7 +332,7 @@ func TestExecDiffTable(t *testing.T) {
require.NotEmpty(t, to)
defer os.RemoveAll(to)

diff, err := Exec(ctx, "diff-table", from, to)
diff, err := Exec(ctx, "diff-table", from, to, false)
assert.NoError(t, err)
assert.Equal(t, "ALTER TABLE `t1` MODIFY COLUMN `id` int unsigned;\n", diff)
})
Expand All @@ -327,10 +345,23 @@ func TestExecDiffTable(t *testing.T) {
require.NotEmpty(t, to)
defer os.RemoveAll(to)

diff, err := Exec(ctx, "diff-table", from, to)
diff, err := Exec(ctx, "diff-table", from, to, false)
assert.NoError(t, err)
assert.Equal(t, "ALTER TABLE `t1` ADD COLUMN `age` int unsigned;\n", diff)
})
t.Run("t1-t3-textual", func(t *testing.T) {
from := writeSchemaFile(t, schemaFrom[0:1])
require.NotEmpty(t, from)
defer os.RemoveAll(from)

to := writeSchemaFile(t, schemaTo[3:])
require.NotEmpty(t, to)
defer os.RemoveAll(to)

diff, err := Exec(ctx, "diff-table", from, to, true)
assert.NoError(t, err)
assert.Equal(t, " CREATE TABLE `t1` (\n \t`id` int,\n+\t`age` int unsigned,\n \tPRIMARY KEY (`id`)\n );\n", diff)
})
t.Run("t1-vone", func(t *testing.T) {
from := writeSchemaFile(t, schemaFrom[0:1])
require.NotEmpty(t, from)
Expand All @@ -340,7 +371,7 @@ func TestExecDiffTable(t *testing.T) {
require.NotEmpty(t, to)
defer os.RemoveAll(to)

_, err := Exec(ctx, "diff-table", from, to)
_, err := Exec(ctx, "diff-table", from, to, false)
assert.Error(t, err)
assert.ErrorIs(t, err, schemadiff.ErrExpectedCreateTable)
})
Expand All @@ -353,7 +384,7 @@ func TestExecDiffTable(t *testing.T) {
require.NotEmpty(t, to)
defer os.RemoveAll(to)

diff, err := Exec(ctx, "diff-view", from, to)
diff, err := Exec(ctx, "diff-view", from, to, false)
assert.NoError(t, err)
assert.Empty(t, diff)
})
Expand All @@ -366,7 +397,7 @@ func TestExecDiffTable(t *testing.T) {
require.NotEmpty(t, to)
defer os.RemoveAll(to)

diff, err := Exec(ctx, "diff-view", from, to)
diff, err := Exec(ctx, "diff-view", from, to, false)
assert.NoError(t, err)
assert.Equal(t, "ALTER VIEW `v1` AS SELECT `id`, 1 FROM `t1`;\n", diff)
})
Expand All @@ -380,12 +411,12 @@ func TestExecDiffTable(t *testing.T) {
defer os.RemoveAll(to)

{
_, err := Exec(ctx, "diff-table", from, to)
_, err := Exec(ctx, "diff-table", from, to, false)
assert.Error(t, err)
assert.ErrorContains(t, err, "expected one CREATE TABLE statement")
}
{
_, err := Exec(ctx, "diff-table", to, from)
_, err := Exec(ctx, "diff-table", to, from, false)
assert.Error(t, err)
assert.ErrorContains(t, err, "expected one CREATE TABLE statement")
}
Expand Down

0 comments on commit d888d97

Please sign in to comment.