Skip to content

Commit 5fe8259

Browse files
Improve dir schema source errors (#172)
1 parent 6937348 commit 5fe8259

File tree

9 files changed

+165
-45
lines changed

9 files changed

+165
-45
lines changed

cmd/pg-schema-diff/plan_cmd.go

+4-37
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"database/sql"
66
"fmt"
77
"io"
8-
"os"
9-
"path/filepath"
108
"regexp"
119
"strconv"
1210
"strings"
@@ -223,19 +221,12 @@ func parsePlanConfig(p planFlags) (planConfig, error) {
223221

224222
func parseSchemaSource(p schemaSourceFlags) (schemaSourceFactory, error) {
225223
if len(p.schemaDirs) > 0 {
226-
var ddl []string
227-
// Ordering of execution of schema SQL can be guaranteed by:
228-
// - Splitting across multiple directories and using multiple schema dir flags
229-
// - Relying on lexical order of SQL files
230-
for _, schemaDir := range p.schemaDirs {
231-
stmts, err := getDDLFromPath(schemaDir)
224+
return func() (diff.SchemaSource, io.Closer, error) {
225+
schemaSource, err := diff.DirSchemaSource(p.schemaDirs)
232226
if err != nil {
233-
return nil, fmt.Errorf("getting DDL from path %q: %w", schemaDir, err)
227+
return nil, nil, err
234228
}
235-
ddl = append(ddl, stmts...)
236-
}
237-
return func() (diff.SchemaSource, io.Closer, error) {
238-
return diff.DDLSchemaSource(ddl), nil, nil
229+
return schemaSource, nil, nil
239230
}, nil
240231
}
241232

@@ -434,30 +425,6 @@ func applyPlanModifiers(
434425
return plan, nil
435426
}
436427

437-
// getDDLFromPath reads all .sql files under the given path (including sub-directories) and returns the DDL
438-
// in lexical order.
439-
func getDDLFromPath(path string) ([]string, error) {
440-
var ddl []string
441-
if err := filepath.Walk(path, func(path string, entry os.FileInfo, err error) error {
442-
if err != nil {
443-
return fmt.Errorf("walking path %q: %w", path, err)
444-
}
445-
if strings.ToLower(filepath.Ext(entry.Name())) != ".sql" {
446-
return nil
447-
}
448-
449-
if stmts, err := os.ReadFile(path); err != nil {
450-
return fmt.Errorf("reading file %q: %w", entry.Name(), err)
451-
} else {
452-
ddl = append(ddl, string(stmts))
453-
}
454-
return nil
455-
}); err != nil {
456-
return nil, err
457-
}
458-
return ddl, nil
459-
}
460-
461428
func planToPrettyS(plan diff.Plan) string {
462429
sb := strings.Builder{}
463430

internal/migration_acceptance_tests/database_schema_source_cases_test.go

+63-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/stripe/pg-schema-diff/pkg/tempdb"
1010
)
1111

12-
func databaseSchemaSourcePlanFactory(ctx context.Context, connPool sqldb.Queryable, tempDbFactory tempdb.Factory, newSchemaDDL []string, opts ...diff.PlanOpt) (_ diff.Plan, retErr error) {
12+
func databaseSchemaSourcePlan(ctx context.Context, connPool sqldb.Queryable, tempDbFactory tempdb.Factory, newSchemaDDL []string, opts ...diff.PlanOpt) (_ diff.Plan, retErr error) {
1313
newSchemaDb, err := tempDbFactory.Create(ctx)
1414
if err != nil {
1515
return diff.Plan{}, fmt.Errorf("creating temp database: %w", err)
@@ -38,8 +38,29 @@ func databaseSchemaSourcePlanFactory(ctx context.Context, connPool sqldb.Queryab
3838
return diff.Generate(ctx, connPool, diff.DBSchemaSource(newSchemaDb.ConnPool), opts...)
3939
}
4040

41+
func dirSchemaSourcePlanFactory(schemaDirs []string) planFactory {
42+
return func(ctx context.Context, connPool sqldb.Queryable, tempDbFactory tempdb.Factory, newSchemaDDL []string, opts ...diff.PlanOpt) (_ diff.Plan, retErr error) {
43+
// Clone the opts so we don't modify the original.
44+
opts = append([]diff.PlanOpt(nil), opts...)
45+
opts = append(opts, diff.WithTempDbFactory(tempDbFactory))
46+
47+
if len(newSchemaDDL) != 0 {
48+
panic("newSchemaDDL should be empty for dir schema sources")
49+
}
50+
51+
schemaSource, err := diff.DirSchemaSource(schemaDirs)
52+
if err != nil {
53+
return diff.Plan{}, fmt.Errorf("creating schema source: %w", err)
54+
}
55+
56+
return diff.Generate(ctx, connPool, schemaSource, opts...)
57+
}
58+
}
59+
4160
var databaseSchemaSourceTestCases = []acceptanceTestCase{
4261
{
62+
planFactory: databaseSchemaSourcePlan,
63+
4364
name: "Drop partitioned table, Add partitioned table with local keys",
4465
oldSchemaDDL: []string{
4566
`
@@ -119,8 +140,48 @@ var databaseSchemaSourceTestCases = []acceptanceTestCase{
119140
expectedHazardTypes: []diff.MigrationHazardType{
120141
diff.MigrationHazardTypeDeletesData,
121142
},
143+
},
144+
{
145+
planFactory: dirSchemaSourcePlanFactory([]string{"testdata/dirsrc_happy_path/schema_0", "testdata/dirsrc_happy_path/schema_1"}),
146+
147+
name: "Dir src - happy path",
148+
oldSchemaDDL: []string{
149+
`
150+
CREATE TABLE foobar( );
151+
`,
152+
},
153+
154+
expectedHazardTypes: []diff.MigrationHazardType{
155+
diff.MigrationHazardTypeIndexBuild,
156+
},
157+
expectedDBSchemaDDL: []string{
158+
`
159+
CREATE TYPE color AS ENUM ('red', 'green', 'blue');
160+
CREATE TABLE foobar(
161+
color color,
162+
id varchar(255) PRIMARY KEY
163+
);
164+
CREATE TABLE foobar_fk(
165+
id TEXT REFERENCES foobar(id)
166+
);
167+
CREATE TABLE fizzbuzz(
168+
id TEXT,
169+
primary_color color,
170+
other_color color
171+
); `,
172+
},
173+
},
174+
{
175+
planFactory: dirSchemaSourcePlanFactory([]string{"testdata/dirsrc_invalid_sql/schema_0"}),
176+
177+
name: "Dir src - invalid sql",
178+
oldSchemaDDL: []string{
179+
`
180+
CREATE TABLE foobar( );
181+
`,
182+
},
122183

123-
planFactory: databaseSchemaSourcePlanFactory,
184+
expectedPlanErrorContains: "testdata/dirsrc_invalid_sql/schema_0/1.sql",
124185
},
125186
}
126187

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE TYPE color AS ENUM ('red', 'green', 'blue');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE foobar (
2+
id varchar(255) PRIMARY KEY,
3+
color color
4+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE foobar_fk (
2+
id TEXT REFERENCES foobar (id)
3+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
CREATE TABLE fizzbuzz (
2+
id TEXT,
3+
primary_color COLOR,
4+
other_color COLOR
5+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE foobar (
2+
id SERIAL PRIMARY KEY
3+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE fizzbuzz (
2+
id TEXT REFERENCES non_existent_table (id)
3+
);

pkg/diff/schema_source.go

+79-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package diff
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
69

710
"github.com/stripe/pg-schema-diff/internal/schema"
811
"github.com/stripe/pg-schema-diff/pkg/log"
@@ -20,13 +23,79 @@ type SchemaSource interface {
2023
GetSchema(ctx context.Context, deps schemaSourcePlanDeps) (schema.Schema, error)
2124
}
2225

23-
type ddlSchemaSource struct {
24-
ddl []string
26+
type (
27+
ddlStatement struct {
28+
// stmt is the DDL statement to run.
29+
stmt string
30+
// file is an optional field that can be used to store the file name from which the DDL was read.
31+
file string
32+
}
33+
34+
ddlSchemaSource struct {
35+
ddl []ddlStatement
36+
}
37+
)
38+
39+
// DirSchemaSource returns a SchemaSource that returns a schema based on the provided directories. You must provide a tempDBFactory
40+
// via the WithTempDbFactory option.
41+
func DirSchemaSource(dirs []string) (SchemaSource, error) {
42+
var ddl []ddlStatement
43+
for _, dir := range dirs {
44+
stmts, err := getDDLFromPath(dir)
45+
if err != nil {
46+
return &ddlSchemaSource{}, err
47+
}
48+
ddl = append(ddl, stmts...)
49+
50+
}
51+
return &ddlSchemaSource{
52+
ddl: ddl,
53+
}, nil
54+
}
55+
56+
// getDDLFromPath reads all .sql files under the given path (including sub-directories) and returns the DDL
57+
// in lexical order.
58+
func getDDLFromPath(path string) ([]ddlStatement, error) {
59+
var ddl []ddlStatement
60+
if err := filepath.Walk(path, func(path string, entry os.FileInfo, err error) error {
61+
if err != nil {
62+
return fmt.Errorf("walking path %q: %w", path, err)
63+
}
64+
if strings.ToLower(filepath.Ext(entry.Name())) != ".sql" {
65+
return nil
66+
}
67+
68+
fileContents, err := os.ReadFile(path)
69+
if err != nil {
70+
return fmt.Errorf("reading file %q: %w", entry.Name(), err)
71+
}
72+
73+
// In the future, it would make sense to split the file contents into individual DDL statements; however,
74+
// that would require fully parsing the SQL. Naively splitting on `;` would not work because `;` can be
75+
// used in comments, strings, and escaped identifiers.
76+
ddl = append(ddl, ddlStatement{
77+
stmt: string(fileContents),
78+
file: path,
79+
})
80+
return nil
81+
}); err != nil {
82+
return nil, err
83+
}
84+
return ddl, nil
2585
}
2686

2787
// DDLSchemaSource returns a SchemaSource that returns a schema based on the provided DDL. You must provide a tempDBFactory
2888
// via the WithTempDbFactory option.
29-
func DDLSchemaSource(ddl []string) SchemaSource {
89+
func DDLSchemaSource(stmts []string) SchemaSource {
90+
var ddl []ddlStatement
91+
for _, stmt := range stmts {
92+
ddl = append(ddl, ddlStatement{
93+
stmt: stmt,
94+
// There is no file name associated with the DDL statement.
95+
file: ""},
96+
)
97+
}
98+
3099
return &ddlSchemaSource{ddl: ddl}
31100
}
32101

@@ -45,9 +114,13 @@ func (s *ddlSchemaSource) GetSchema(ctx context.Context, deps schemaSourcePlanDe
45114
}
46115
}(tempDb.ContextualCloser)
47116

48-
for _, stmt := range s.ddl {
49-
if _, err := tempDb.ConnPool.ExecContext(ctx, stmt); err != nil {
50-
return schema.Schema{}, fmt.Errorf("running DDL: %w", err)
117+
for _, ddlStmt := range s.ddl {
118+
if _, err := tempDb.ConnPool.ExecContext(ctx, ddlStmt.stmt); err != nil {
119+
debugInfo := ""
120+
if ddlStmt.file != "" {
121+
debugInfo = fmt.Sprintf(" (from %s)", ddlStmt.file)
122+
}
123+
return schema.Schema{}, fmt.Errorf("running DDL%s: %w", debugInfo, err)
51124
}
52125
}
53126

0 commit comments

Comments
 (0)