Skip to content

Commit 69dcee4

Browse files
committed
Added unique constraints and primary keys. Also added COLLATE 'C' to order by where possible
1 parent 3510484 commit 69dcee4

8 files changed

Lines changed: 250 additions & 9 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Please note that this program never modifies any database directly, and you are solely responsible for verifying the generated SQL *before* running it against your database. Now that you know about that, it should give you confidence that it is safe to try out and see what SQL gets generated.
44

5-
Written in GoLang, this utility compares the schema between two PostgreSQL databases and generates alter statements to be *manually* run against the second database. Not everything in the schema is compared, but the things considered important (at the moment) are: tables, columns (and their default values), foreign keys... and soon constraints and user roles.
5+
Written in GoLang, this utility compares the schema between two PostgreSQL databases and generates alter statements to be *manually* run against the second database. Not everything in the schema is compared, but the things considered important (at the moment) are: sequences, tables, columns (and their default values), primary keys, unique constraints, foreign keys... and soon indexes and user roles.
66

77
It is written to be easy to add and improve the accuracy of the diff. Please let me know if you think this goal has not been met. I'm very interested in suggestions and contributions to improve this program. I'm not a GoLang expert yet, but each program I write gets me closer to that goal.
88

@@ -12,7 +12,7 @@ I'm a big fan of GoLang because of how easy it is to deliver a single executable
1212

1313
## usage
1414

15-
pgdiff [database flags] <schemaType>
15+
pgdiff [database flags] <schemaType>
1616

1717

1818
program flags | Explanation
@@ -28,4 +28,4 @@ I'm a big fan of GoLang because of how easy it is to deliver a single executable
2828
-p2 | second db port number. defaults to 5432
2929
-d2 | second db name
3030

31-
&lt;schemaType&gt; the type of objects in the schema to compare: TABLE, COLUMN, FOREIGN_KEY (soon: CONSTRAINT, ROLE)
31+
&lt;schemaType&gt; the type of objects in the schema to compare: ALL, SEQUENCE, TABLE, COLUMN, PRIMARY_KEY, UNIQUE, FOREIGN_KEY (soon: INDEX, ROLE)

column.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ SELECT table_name
131131
FROM information_schema.columns
132132
WHERE table_schema = 'public'
133133
AND is_updatable = 'YES'
134-
ORDER by table_name, column_name;`
134+
ORDER by table_name, column_name COLLATE "C" ASC;`
135135

136136
rowChan1, _ := pgutil.QueryStrings(conn1, sql)
137137
rowChan2, _ := pgutil.QueryStrings(conn2, sql)

foreignkey.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ FROM information_schema.table_constraints AS tc
8282
AND tc.constraint_schema = rc.constraint_schema
8383
AND tc.constraint_name = rc.constraint_name)
8484
WHERE tc.constraint_type = 'FOREIGN KEY'
85-
ORDER BY tc.table_name, tc.constraint_name; `
85+
ORDER BY tc.table_name, tc.constraint_name COLLATE "C" ASC; `
8686

8787
rowChan1, _ := pgutil.QueryStrings(conn1, sql)
8888
rowChan2, _ := pgutil.QueryStrings(conn2, sql)

pgdiff.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func init() {
3636
// Remaining args:
3737
args = flag.Args()
3838
if len(args) == 0 {
39-
fmt.Println("The required first argument is SchemaType: TABLE, COLUMN, FOREIGN_KEY, CONSTRAINT, ROLE")
39+
fmt.Println("The required first argument is SchemaType: SEQUENCE, TABLE, COLUMN, CONSTRAINT, ROLE")
4040
os.Exit(1)
4141
}
4242

@@ -57,12 +57,23 @@ func main() {
5757
// This section needs to be improved so that you do not need to choose the type
5858
// of alter statements to generate. Rather, all should be generated in the
5959
// proper order.
60-
if schemaType == "SEQUENCE" {
60+
if schemaType == "ALL" {
61+
compareSequences(conn1, conn2)
62+
compareTables(conn1, conn2)
63+
compareColumns(conn1, conn2)
64+
comparePrimaryKeys(conn1, conn2)
65+
compareUniqueConstraints(conn1, conn2)
66+
compareForeignKeys(conn1, conn2)
67+
} else if schemaType == "SEQUENCE" {
6168
compareSequences(conn1, conn2)
6269
} else if schemaType == "TABLE" {
6370
compareTables(conn1, conn2)
6471
} else if schemaType == "COLUMN" {
6572
compareColumns(conn1, conn2)
73+
} else if schemaType == "PRIMARY_KEY" {
74+
comparePrimaryKeys(conn1, conn2)
75+
} else if schemaType == "UNIQUE" {
76+
compareUniqueConstraints(conn1, conn2)
6677
} else if schemaType == "FOREIGN_KEY" {
6778
compareForeignKeys(conn1, conn2)
6879
} else {

primarykey.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package main
2+
3+
import "fmt"
4+
import "database/sql"
5+
import "github.com/joncrlsn/pgutil"
6+
7+
// PrimaryKeySchema holds a channel streaming foreign key data from one of the databases as well as
8+
// a reference to the current row of data we're viewing.
9+
//
10+
// PrimaryKeySchema implements the Schema interface defined in pgdiff.go
11+
type PrimaryKeySchema struct {
12+
channel chan map[string]string
13+
row map[string]string
14+
done bool
15+
}
16+
17+
// NextRow reads from the channel and tells you if there are (probably) more or not
18+
func (c *PrimaryKeySchema) NextRow() bool {
19+
c.row = <-c.channel
20+
if len(c.row) == 0 {
21+
c.done = true
22+
}
23+
return !c.done
24+
}
25+
26+
// Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row
27+
func (c *PrimaryKeySchema) Compare(obj interface{}) int {
28+
c2, ok := obj.(*PrimaryKeySchema)
29+
if !ok {
30+
fmt.Println("Error!!!, Change(...) needs a PrimaryKeySchema instance", c2)
31+
return +999
32+
}
33+
val := _compareString(c.row["table_name"], c2.row["table_name"])
34+
return val
35+
}
36+
37+
// Add returns SQL to add the primary key
38+
func (c PrimaryKeySchema) Add() {
39+
// ALTER TABLE ONLY t_product ADD CONSTRAINT t_product_pkey PRIMARY KEY (product_id, seq_no);
40+
// ALTER TABLE ONLY t_product ADD CONSTRAINT t_product_pkey UNIQUE (product_id);
41+
fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s);\n", c.row["table_name"], c.row["constraint_name"], c.primaryKeyString())
42+
}
43+
44+
// Drop returns SQL to drop the foreign key
45+
func (c PrimaryKeySchema) Drop() {
46+
fmt.Printf("ALTER TABLE %s DROP CONSTRAINT IF EXISTS %s;\n", c.row["table_name"], c.row["constraint_name"])
47+
}
48+
49+
// Change handles the case where the table name matches, but the details do not
50+
func (c PrimaryKeySchema) Change(obj interface{}) {
51+
c2, ok := obj.(*PrimaryKeySchema)
52+
if !ok {
53+
fmt.Println("Error!!!, change needs a PrimaryKeySchema instance", c2)
54+
}
55+
pk1 := c.primaryKeyString()
56+
pk2 := c.primaryKeyString()
57+
if pk1 != pk2 {
58+
fmt.Printf("-- Warning, primary key is different for table %s pk1:%s pk2:%s\n", c.row["table_name"], pk1, pk2)
59+
fmt.Printf("ALTER TABLE %s DROP CONSTRAINT %s;\n", c.row["table_name"], c2.row["constraint_name"])
60+
fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY (%s);\n", c.row["table_name"], c.row["constraint_name"], c.primaryKeyString())
61+
}
62+
}
63+
64+
// primaryKeyString concatenates the primary key column names into one string.
65+
// It's possible this could be done with SQL, I just haven't figured it out yet
66+
func (c PrimaryKeySchema) primaryKeyString() string {
67+
pkey := ""
68+
for i := 1; i <= 5; i++ {
69+
colName := fmt.Sprintf("col%d", i)
70+
col := c.row[colName]
71+
//fmt.Printf("-- colName: %s val:'%s'\n", colName, col)
72+
if len(col) > 0 {
73+
if len(pkey) > 0 {
74+
pkey = pkey + ","
75+
}
76+
pkey = pkey + col
77+
}
78+
}
79+
return pkey
80+
}
81+
82+
/*
83+
* Compare the primary keys in the two databases. This SQL can handle up to 5 columns
84+
* as part of the primary key
85+
*/
86+
func comparePrimaryKeys(conn1 *sql.DB, conn2 *sql.DB) {
87+
sql := `
88+
SELECT tc.table_name
89+
, kcu.constraint_name
90+
, MAX(CASE WHEN kcu.ordinal_position = 1 THEN kcu.column_name ELSE '' END) AS col1
91+
, MAX(CASE WHEN kcu.ordinal_position = 2 THEN kcu.column_name ELSE '' END) AS col2
92+
, MAX(CASE WHEN kcu.ordinal_position = 3 THEN kcu.column_name ELSE '' END) AS col3
93+
, MAX(CASE WHEN kcu.ordinal_position = 4 THEN kcu.column_name ELSE '' END) AS col4
94+
, MAX(CASE WHEN kcu.ordinal_position = 5 THEN kcu.column_name ELSE '' END) AS col5
95+
FROM information_schema.table_constraints AS tc
96+
LEFT JOIN information_schema.key_column_usage kcu
97+
ON tc.constraint_catalog = kcu.constraint_catalog
98+
AND tc.constraint_schema = kcu.constraint_schema
99+
AND tc.constraint_name = kcu.constraint_name
100+
WHERE tc.constraint_type = 'PRIMARY KEY'
101+
GROUP BY tc.table_name, kcu.constraint_name
102+
ORDER BY tc.table_name, kcu.constraint_name COLLATE "C" ASC;`
103+
104+
rowChan1, _ := pgutil.QueryStrings(conn1, sql)
105+
rowChan2, _ := pgutil.QueryStrings(conn2, sql)
106+
107+
// We have to explicitly type this as Schema for some unknown reason
108+
var schema1 Schema = &PrimaryKeySchema{channel: rowChan1}
109+
var schema2 Schema = &PrimaryKeySchema{channel: rowChan2}
110+
111+
// Compare the columns
112+
doDiff(schema1, schema2)
113+
}

sequence.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ SELECT sequence_name, data_type, start_value
6262
, increment, cycle_option
6363
FROM information_schema.sequences
6464
WHERE sequence_schema = 'public'
65-
ORDER BY sequence_name ASC;`
65+
ORDER BY sequence_name COLLATE "C" ASC;`
6666

6767
rowChan1, _ := pgutil.QueryStrings(conn1, sql)
6868
rowChan2, _ := pgutil.QueryStrings(conn2, sql)

table.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ SELECT table_name
6464
FROM information_schema.tables
6565
WHERE table_schema = 'public'
6666
AND table_type = 'BASE TABLE'
67-
ORDER BY table_name ASC;`
67+
ORDER BY table_name COLLATE "C" ASC;`
6868

6969
rowChan1, _ := pgutil.QueryStrings(conn1, sql)
7070
rowChan2, _ := pgutil.QueryStrings(conn2, sql)

unique.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package main
2+
3+
import "fmt"
4+
import "database/sql"
5+
import "github.com/joncrlsn/pgutil"
6+
7+
// UniqueSchema holds a channel streaming foreign key data from one of the databases as well as
8+
// a reference to the current row of data we're viewing.
9+
//
10+
// UniqueSchema implements the Schema interface defined in pgdiff.go
11+
type UniqueSchema struct {
12+
channel chan map[string]string
13+
row map[string]string
14+
done bool
15+
}
16+
17+
// NextRow reads from the channel and tells you if there are (probably) more or not
18+
func (c *UniqueSchema) NextRow() bool {
19+
c.row = <-c.channel
20+
if len(c.row) == 0 {
21+
c.done = true
22+
}
23+
return !c.done
24+
}
25+
26+
// Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row
27+
func (c *UniqueSchema) Compare(obj interface{}) int {
28+
c2, ok := obj.(*UniqueSchema)
29+
if !ok {
30+
fmt.Println("Error!!!, Change(...) needs a UniqueSchema instance", c2)
31+
return +999
32+
}
33+
val := _compareString(c.row["table_name"], c2.row["table_name"])
34+
if val != 0 {
35+
return val
36+
}
37+
val = _compareString(c.row["constraint_name"], c2.row["constraint_name"])
38+
return val
39+
}
40+
41+
// Add returns SQL to add the primary key
42+
func (c UniqueSchema) Add() {
43+
// ALTER TABLE ONLY t_product ADD CONSTRAINT t_product_pkey PRIMARY KEY (product_id, seq_no);
44+
// ALTER TABLE ONLY t_product ADD CONSTRAINT t_product_pkey UNIQUE (product_id);
45+
fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s);\n", c.row["table_name"], c.row["constraint_name"], c.uniqueColumnString())
46+
}
47+
48+
// Drop returns SQL to drop the foreign key
49+
func (c UniqueSchema) Drop() {
50+
fmt.Printf("ALTER TABLE %s DROP CONSTRAINT IF EXISTS %s;\n", c.row["table_name"], c.row["constraint_name"])
51+
}
52+
53+
// Change handles the case where the table name matches, but the details do not
54+
func (c UniqueSchema) Change(obj interface{}) {
55+
c2, ok := obj.(*UniqueSchema)
56+
if !ok {
57+
fmt.Println("Error!!!, change needs a UniqueSchema instance", c2)
58+
}
59+
pk1 := c.uniqueColumnString()
60+
pk2 := c.uniqueColumnString()
61+
if pk1 != pk2 {
62+
fmt.Printf("-- Warning, primary key is different for table %s pk1:%s pk2:%s\n", c.row["table_name"], pk1, pk2)
63+
fmt.Printf("ALTER TABLE %s DROP CONSTRAINT %s;\n", c.row["table_name"], c2.row["constraint_name"])
64+
fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE (%s);\n", c.row["table_name"], c.row["constraint_name"], c.uniqueColumnString())
65+
}
66+
}
67+
68+
// uniqueColumnString concatenates the primary key column names into one string.
69+
// It's possible this could be done with SQL, I just haven't figured it out yet
70+
func (c UniqueSchema) uniqueColumnString() string {
71+
pkey := ""
72+
for i := 1; i <= 5; i++ {
73+
colName := fmt.Sprintf("col%d", i)
74+
col := c.row[colName]
75+
//fmt.Printf("-- colName: %s val:'%s'\n", colName, col)
76+
if len(col) > 0 {
77+
if len(pkey) > 0 {
78+
pkey = pkey + ","
79+
}
80+
pkey = pkey + col
81+
}
82+
}
83+
return pkey
84+
}
85+
86+
/*
87+
* Compare the primary keys in the two databases. This SQL can handle up to 5 columns
88+
* as part of the primary key
89+
*/
90+
func compareUniqueConstraints(conn1 *sql.DB, conn2 *sql.DB) {
91+
sql := `
92+
SELECT tc.table_name
93+
, kcu.constraint_name
94+
, MAX(CASE WHEN kcu.ordinal_position = 1 THEN kcu.column_name ELSE '' END) AS col1
95+
, MAX(CASE WHEN kcu.ordinal_position = 2 THEN kcu.column_name ELSE '' END) AS col2
96+
, MAX(CASE WHEN kcu.ordinal_position = 3 THEN kcu.column_name ELSE '' END) AS col3
97+
, MAX(CASE WHEN kcu.ordinal_position = 4 THEN kcu.column_name ELSE '' END) AS col4
98+
, MAX(CASE WHEN kcu.ordinal_position = 5 THEN kcu.column_name ELSE '' END) AS col5
99+
FROM information_schema.table_constraints AS tc
100+
LEFT JOIN information_schema.key_column_usage kcu
101+
ON tc.constraint_catalog = kcu.constraint_catalog
102+
AND tc.constraint_schema = kcu.constraint_schema
103+
AND tc.constraint_name = kcu.constraint_name
104+
WHERE tc.constraint_type = 'UNIQUE'
105+
GROUP BY tc.table_name, kcu.constraint_name
106+
ORDER BY tc.table_name, kcu.constraint_name COLLATE "C" ASC;`
107+
108+
rowChan1, _ := pgutil.QueryStrings(conn1, sql)
109+
rowChan2, _ := pgutil.QueryStrings(conn2, sql)
110+
111+
// We have to explicitly type this as Schema for some unknown reason
112+
var schema1 Schema = &UniqueSchema{channel: rowChan1}
113+
var schema2 Schema = &UniqueSchema{channel: rowChan2}
114+
115+
// Compare the columns
116+
doDiff(schema1, schema2)
117+
}

0 commit comments

Comments
 (0)