├── .gitignore ├── LICENSE ├── README-linux.md ├── README-macos-arm64.md ├── README-osx.md ├── README-win.md ├── README.md ├── build.sh ├── column.go ├── flags.go ├── foreignkey.go ├── function.go ├── go.mod ├── go.sum ├── grant-attribute.go ├── grant-relationship.go ├── grant.go ├── grant_test.go ├── index.go ├── mat_view.go ├── owner.go ├── pgdiff.go ├── pgdiff.sh ├── role.go ├── schemata.go ├── sequence.go ├── table.go ├── test ├── README.md ├── example.dump ├── load-example.sh ├── mypsql ├── populate-db.sh ├── start-fresh.sh ├── test-column ├── test-foreignkey ├── test-function ├── test-grant-attribute ├── test-grant-relationship ├── test-identity-column ├── test-index ├── test-owner ├── test-schemata ├── test-sequence ├── test-table ├── test-table-column └── test-trigger ├── trigger.go ├── vendor └── github.com │ └── lib │ └── pq │ └── .gitignore └── view.go /.gitignore: -------------------------------------------------------------------------------- 1 | pgdiff 2 | vendor/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jon Carlson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-linux.md: -------------------------------------------------------------------------------- 1 | ## Linux pgdiff instructions 2 | 3 | These instructions will guide you through the process of generating SQL, reviewing it, and optionally running it on the target database. It requires a familiarity with a Linux command-line shell. 4 | 5 | 1. download pgdiff-linux-\.tar.gz to your machine 6 | 1. untar it (a new directory will be created: called pgdiff) 7 | 1. cd into the new pgdiff directory 8 | 1. edit pgdiff.sh to change the db access values... or set them at runtime (i.e. USER1=joe NAME1=mydb USER2=joe NAME2=myotherdb ./pgdiff.sh) 9 | 1. run pgdiff.sh 10 | 11 | ## tar contents 12 | * pgdiff - a linux executable 13 | * pgrun - a linux executable for running SQL 14 | * pgdiff.sh - a bash shell script to coordinate your interaction with pgdiff and pgrun 15 | 16 | If you write a Go version of pgdiff.sh, please share it and I'll include it for others to use (with your copyright information intact). 17 | -------------------------------------------------------------------------------- /README-macos-arm64.md: -------------------------------------------------------------------------------- 1 | ## OSX / Mac / ARM64 pgdiff instructions 2 | 3 | These instructions will guide you through the process of generating SQL, reviewing it, and optionally running it on the target database. It requires a familiarity with the bash shell in OSX. 4 | 5 | 1. download pgdiff-arm64-\.tar.gz to your machine 6 | 1. untar it (a new directory will be created: called pgdiff) 7 | 1. cd into the new pgdiff directory 8 | 1. edit pgdiff.sh to change the db access values... or set them at runtime (i.e. USER1=joe NAME1=mydb USER2=joe NAME2=myotherdb ./pgdiff.sh) 9 | 1. run pgdiff.sh 10 | 11 | ## tar contents 12 | * pgdiff - an OSX executable 13 | * pgrun - an OSX executable for running SQL 14 | * pgdiff.sh - a bash shell script to coordinate your interaction with pgdiff and pgrun 15 | 16 | If you write a Go version of pgdiff.sh, please share it so we can include it or link to it for others to use. 17 | -------------------------------------------------------------------------------- /README-osx.md: -------------------------------------------------------------------------------- 1 | ## OSX / Mac pgdiff instructions 2 | 3 | These instructions will guide you through the process of generating SQL, reviewing it, and optionally running it on the target database. It requires a familiarity with the bash shell in OSX. 4 | 5 | 1. download pgdiff-mac-\.tar.gz to your machine 6 | 1. untar it (a new directory will be created: called pgdiff) 7 | 1. cd into the new pgdiff directory 8 | 1. edit pgdiff.sh to change the db access values... or set them at runtime (i.e. USER1=joe NAME1=mydb USER2=joe NAME2=myotherdb ./pgdiff.sh) 9 | 1. run pgdiff.sh 10 | 11 | ## tar contents 12 | * pgdiff - an OSX executable 13 | * pgrun - an OSX executable for running SQL 14 | * pgdiff.sh - a bash shell script to coordinate your interaction with pgdiff and pgrun 15 | 16 | If you write a Go version of pgdiff.sh, please share it so we can include it or link to it for others to use. 17 | -------------------------------------------------------------------------------- /README-win.md: -------------------------------------------------------------------------------- 1 | ### getting started on windows 2 | 3 | 1. download pgdiff.exe from the release page on github 4 | 1. either install cygwin so you can run pgdiff.sh or... 5 | 1. manually run pgdiff.exe for each schema type listed in the usage section above 6 | 1. review the SQL output and, if you want to make them match, run it against the second db 7 | 8 | This project works on Windows, just not as nicely as it does for Linux and Mac. If you are inclined to write a Windows complement to the pgdiff.sh script, feel free to contribute it or we can link to it. Even better would be a replacement written in Go. 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pgdiff - PostgreSQL schema diff 2 | 3 | pgdiff compares the schema between two PostgreSQL 9 databases and generates alter statements to be *manually* run against the second database to make them match. The provided pgdiff.sh script helps automate the process. 4 | 5 | pgdiff is transparent in what it does, so it never modifies a database directly. You alone are responsible for verifying the generated SQL before running it against your database. Go ahead and see what SQL gets generated. 6 | 7 | pgdiff is written to be easy to expand and improve the accuracy of the diff. 8 | 9 | 10 | ### download 1.0 beta 1 11 | [osx](https://github.com/joncrlsn/pgdiff/releases/download/v1.0-beta.1/pgdiff-osx-1.0b1.tar.gz "OSX version")   [linux](https://github.com/joncrlsn/pgdiff/files/1480823/pgdiff-linux-1.0b1.tar.gz "Linux version")   [windows](https://github.com/joncrlsn/pgdiff/releases/download/v1.0-beta.1/pgdiff-win-1.0b1.zip "Windows version") 12 | 13 | 14 | ### usage 15 | pgdiff [options] 16 | 17 | (where options and <schemaType> are listed below) 18 | 19 | There seems to be an ideal order for running the different schema types. This order should minimize the problems you encounter. For example, you will always want to add new tables before you add new columns. 20 | 21 | In addition, some types can have dependencies which are not in the right order. A classic case is views which depend on other views. The missing view SQL is generated in alphabetical order so if a view create fails due to a missing view, just run the views SQL file over again. The pgdiff.sh script will prompt you about running it again. 22 | 23 | Schema type ordering: 24 | 25 | 1. SCHEMA 26 | 1. ROLE 27 | 1. SEQUENCE 28 | 1. TABLE 29 | 1. COLUMN 30 | 1. INDEX 31 | 1. VIEW 32 | 1. FOREIGN\_KEY 33 | 1. FUNCTION 34 | 1. TRIGGER 35 | 1. OWNER 36 | 1. GRANT\_RELATIONSHIP 37 | 1. GRANT\_ATTRIBUTE 38 | 1. ALL (all above in one run) 39 | 40 | 41 | ### example 42 | I have found it helpful to take ```--schema-only``` dumps of the databases in question, load them into a local postgres, then do my sql generation and testing there before running the SQL against a more official database. Your local postgres instance will need the correct users/roles populated because db dumps do not copy that information. 43 | 44 | ``` 45 | pgdiff -U dbuser -H localhost -D refDB -O "sslmode=disable" -S public \ 46 | -u dbuser -h localhost -d compDB -o "sslmode=disable" -s public \ 47 | TABLE 48 | ``` 49 | 50 | 51 | ### options 52 | 53 | options | explanation 54 | ----------------: | ------------------------------------ 55 | -V, --version | prints the version of pgdiff being used 56 | -?, --help | displays helpful usage information 57 | -U, --user1 | first postgres user 58 | -u, --user2 | second postgres user 59 | -W, --password1 | first db password 60 | -w, --password2 | second db password 61 | -H, --host1 | first db host. default is localhost 62 | -h, --host2 | second db host. default is localhost 63 | -P, --port1 | first db port number. default is 5432 64 | -p, --port2 | second db port number. default is 5432 65 | -D, --dbname1 | first db name 66 | -d, --dbname2 | second db name 67 | -S, --schema1 | first schema name. default is * (all non-system schemas) 68 | -s, --schema2 | second schema name. default is * (all non-system schemas) 69 | -O, --option1 | first db options. example: sslmode=disable 70 | -o, --option2 | second db options. example: sslmode=disable 71 | 72 | 73 | ### getting started on linux and osx 74 | 75 | linux and osx binaries are packaged with an extra, optional bash script and pgrun program that helps speed the diffing process. 76 | 77 | 1. download the tgz file for your OS 78 | 1. untar it: ```tar -xzvf pgdiff.tgz``` 79 | 1. cd to the new pgdiff directory 80 | 1. edit the db connection defaults in pgdiff.sh 81 | 1. ...or manually run pgdiff for each schema type listed in the usage section above 82 | 1. review the SQL output for each schema type and, if you want to make them match, run it against the second db 83 | 84 | 85 | ### getting started on windows 86 | 87 | 1. download pgdiff.exe from the bin-win directory on github 88 | 1. either install cygwin so you can run pgdiff.sh or... 89 | 1. manually run pgdiff.exe for each schema type listed in the usage section above 90 | 1. review the SQL output and, if you want to make them match, run it against the second db 91 | 92 | This project works on Windows, just not as nicely as it does for Linux and Mac. If you are inclined to write a Windows complement to the pgdiff.sh script, feel free to contribute it or we can link to it. Even better would be a replacement written in Go. 93 | 94 | 95 | ### version history 96 | * 0.9.0 - Implemented ROLE, SEQUENCE, TABLE, COLUMN, INDEX, FOREIGN\_KEY, OWNER, GRANT\_RELATIONSHIP, GRANT\_ATTRIBUTE 97 | * 0.9.1 - Added VIEW, FUNCTION, and TRIGGER (Thank you, Shawn Carroll AKA SparkeyG) 98 | * 0.9.2 - Fixed bug when using the non-default port 99 | * 0.9.3 - Fixed VARCHAR bug when no max length specified 100 | * 1.0.0 - Adding support for comparing two different schemas (same or different db), one schema between databases, or all schemas between databases. (Also removed binaries from git repository) 101 | 102 | ### getting help 103 | If you think you found a bug, it might help replicate it if you find the appropriate test script (in the test directory) and modify it to show the problem. Attach the script to an Issue request. 104 | 105 | ### todo 106 | * fix SQL for adding an array column 107 | * create windows version of pgdiff.sh (or even better: re-write it all in Go) 108 | * allow editing of individual SQL lines after failure (this would probably be done in the script pgdiff.sh) 109 | * store failed SQL statements in an error file for later fixing and rerunning? 110 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Builds executable and downloadable bundle for 3 platforms 4 | # 5 | # For OSX and Linux: 6 | # * builds pgdiff 7 | # * downloads pgrun 8 | # * combines them, a README, and pgdiff.sh into a tgz file 9 | # 10 | # For Windows: 11 | # * builds pgdiff.exe 12 | # * downloads pgrun.exe 13 | # * combines them, a README, and pgdiff.sh into a zip file 14 | # 15 | 16 | SCRIPT_DIR="$(dirname `ls -l $0 | awk '{ print $NF }'`)" 17 | 18 | [[ -z $APPNAME ]] && APPNAME=pgdiff 19 | [[ -z $VERSION ]] && read -p "Enter version number: " VERSION 20 | 21 | LINUX_README=README-linux.md 22 | LINUX_FILE="${APPNAME}-linux-${VERSION}.tar.gz" 23 | 24 | OSX_README=README-osx.md 25 | OSX_FILE="${APPNAME}-osx-${VERSION}.tar.gz" 26 | 27 | ARM64_README=README-macos-arm64.md 28 | ARM64_FILE="${APPNAME}-macos-arm64-${VERSION}.tar.gz" 29 | 30 | WIN_README=README-win.md 31 | WIN_FILE="${APPNAME}-win-${VERSION}.zip" 32 | 33 | if [[ -f $LINUX_README ]]; then 34 | echo " ==== Building Linux ====" 35 | tempdir="$(mktemp -d)" 36 | workdir="$tempdir/$APPNAME" 37 | echo $workdir 38 | mkdir -p $workdir 39 | # Build the executable 40 | GOOS=linux GOARCH=386 go build -o "$workdir/$APPNAME" 41 | # Download pgrun to the temp directory 42 | wget -O "$workdir/pgrun" "https://github.com/joncrlsn/pgrun/raw/master/bin-linux/pgrun" 43 | # Copy the bash runtime script to the temp directory 44 | cp pgdiff.sh "$workdir/" 45 | cp "${SCRIPT_DIR}/${LINUX_README}" "$workdir/README.md" 46 | cd "$tempdir" 47 | # Make everything executable 48 | chmod -v ugo+x $APPNAME/* 49 | tarName="${tempdir}/${LINUX_FILE}" 50 | COPYFILE_DISABLE=true tar -cvzf "$tarName" $APPNAME 51 | cd - 52 | mv "$tarName" "${SCRIPT_DIR}/" 53 | echo "Built linux." 54 | else 55 | echo "Skipping linux. No $LINUX_README file." 56 | fi 57 | 58 | if [[ -f $OSX_README ]]; then 59 | echo " ==== Building OSX ====" 60 | tempdir="$(mktemp -d)" 61 | workdir="$tempdir/$APPNAME" 62 | echo $workdir 63 | mkdir -p $workdir 64 | # Build the executable 65 | GOOS=darwin GOARCH=386 go build -o "$workdir/$APPNAME" 66 | # Download pgrun to the work directory 67 | wget -O "$workdir/pgrun" "https://github.com/joncrlsn/pgrun/raw/master/bin-osx/pgrun" 68 | # Copy the bash runtime script to the temp directory 69 | cp pgdiff.sh "$workdir/" 70 | cp "${SCRIPT_DIR}/${OSX_README}" "$workdir/README.md" 71 | cd "$tempdir" 72 | # Make everything executable 73 | chmod -v ugo+x $APPNAME/* 74 | tarName="${tempdir}/${OSX_FILE}" 75 | COPYFILE_DISABLE=true tar -cvzf "$tarName" $APPNAME 76 | cd - 77 | mv "$tarName" "${SCRIPT_DIR}/" 78 | echo "Built osx." 79 | else 80 | echo "Skipping osx. No $OSX_README file." 81 | fi 82 | 83 | if [[ -f $ARM64_README ]]; then 84 | echo " ==== Building macOS-arm64 ====" 85 | tempdir="$(mktemp -d)" 86 | workdir="$tempdir/$APPNAME" 87 | echo $workdir 88 | mkdir -p $workdir 89 | # Build the executable 90 | GOOS=darwin GOARCH=arm64 go build -o "$workdir/$APPNAME" 91 | # Download pgrun to the work directory 92 | wget -O "$workdir/pgrun" "https://github.com/feverxai/pgrun/raw/master/bin-arm64/pgrun" # once PR is accepted change to "https://github.com/joncrlsn/pgrun/raw/master/bin-arm64/pgrun" 93 | # Copy the bash runtime script to the temp directory 94 | cp pgdiff.sh "$workdir/" 95 | cp "${SCRIPT_DIR}/${ARM64_README}" "$workdir/README.md" 96 | cd "$tempdir" 97 | # Make everything executable 98 | chmod -v ugo+x $APPNAME/* 99 | tarName="${tempdir}/${ARM64_FILE}" 100 | COPYFILE_DISABLE=true tar -cvzf "$tarName" $APPNAME 101 | cd - 102 | mv "$tarName" "${SCRIPT_DIR}/" 103 | echo "Built macOS-arm64." 104 | else 105 | echo "Skipping macOS-arm64. No $ARM64_README file." 106 | fi 107 | 108 | if [[ -f $WIN_README ]]; then 109 | echo " ==== Building Windows ====" 110 | tempdir="$(mktemp -d)" 111 | workdir="$tempdir/$APPNAME" 112 | echo $workdir 113 | mkdir -p $workdir 114 | GOOS=windows GOARCH=386 go build -o "${workdir}/${APPNAME}.exe" 115 | # Download pgrun to the work directory 116 | # Copy the bash runtime script to the temp directory 117 | cp "${SCRIPT_DIR}/${WIN_README}" "$workdir/README.md" 118 | cd "$tempdir" 119 | # Make everything executable 120 | chmod -v ugo+x $APPNAME/* 121 | wget -O "${workdir}/pgrun.exe" "https://github.com/joncrlsn/pgrun/raw/master/bin-win/pgrun.exe" 122 | zipName="${tempdir}/${WIN_FILE}" 123 | zip -r "$zipName" $APPNAME 124 | cd - 125 | mv "$zipName" "${SCRIPT_DIR}/" 126 | echo "Built win." 127 | else 128 | echo "Skipping win. No $WIN_README file." 129 | fi 130 | -------------------------------------------------------------------------------- /column.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "strconv" 17 | "strings" 18 | "text/template" 19 | ) 20 | 21 | var ( 22 | columnSqlTemplate = initColumnSqlTemplate() 23 | ) 24 | 25 | // Initializes the Sql template 26 | func initColumnSqlTemplate() *template.Template { 27 | sql := ` 28 | SELECT table_schema 29 | , {{if eq $.DbSchema "*" }}table_schema || '.' || {{end}}table_name || '.' ||lpad(cast (ordinal_position as varchar), 5, '0')|| column_name AS compare_name 30 | , table_name 31 | , column_name 32 | , data_type 33 | , is_nullable 34 | , column_default 35 | , character_maximum_length 36 | , is_identity 37 | , identity_generation 38 | , substring(udt_name from 2) AS array_type 39 | FROM information_schema.columns 40 | WHERE is_updatable = 'YES' 41 | {{if eq $.DbSchema "*" }} 42 | AND table_schema NOT LIKE 'pg_%' 43 | AND table_schema <> 'information_schema' 44 | {{else}} 45 | AND table_schema = '{{$.DbSchema}}' 46 | {{end}} 47 | ORDER BY compare_name ASC; 48 | ` 49 | t := template.New("ColumnSqlTmpl") 50 | template.Must(t.Parse(sql)) 51 | return t 52 | } 53 | 54 | var ( 55 | tableColumnSqlTemplate = initTableColumnSqlTemplate() 56 | ) 57 | 58 | // Initializes the Sql template 59 | func initTableColumnSqlTemplate() *template.Template { 60 | sql := ` 61 | SELECT a.table_schema 62 | , {{if eq $.DbSchema "*" }}a.table_schema || '.' || {{end}}a.table_name || '.' || column_name AS compare_name 63 | , a.table_name 64 | , column_name 65 | , data_type 66 | , is_nullable 67 | , column_default 68 | , character_maximum_length 69 | FROM information_schema.columns a 70 | INNER JOIN information_schema.tables b 71 | ON a.table_schema = b.table_schema AND 72 | a.table_name = b.table_name AND 73 | b.table_type = 'BASE TABLE' 74 | WHERE is_updatable = 'YES' 75 | {{if eq $.DbSchema "*" }} 76 | AND a.table_schema NOT LIKE 'pg_%' 77 | AND a.table_schema <> 'information_schema' 78 | {{else}} 79 | AND a.table_schema = '{{$.DbSchema}}' 80 | {{end}} 81 | {{ if $.TableType }} 82 | AND b.table_type = '{{ $.TableType }}' 83 | {{ end }} 84 | ORDER BY compare_name ASC; 85 | ` 86 | t := template.New("ColumnSqlTmpl") 87 | template.Must(t.Parse(sql)) 88 | return t 89 | } 90 | 91 | // ================================== 92 | // Column Rows definition 93 | // ================================== 94 | 95 | // ColumnRows is a sortable slice of string maps 96 | type ColumnRows []map[string]string 97 | 98 | func (slice ColumnRows) Len() int { 99 | return len(slice) 100 | } 101 | 102 | func (slice ColumnRows) Less(i, j int) bool { 103 | return slice[i]["compare_name"] < slice[j]["compare_name"] 104 | } 105 | 106 | func (slice ColumnRows) Swap(i, j int) { 107 | slice[i], slice[j] = slice[j], slice[i] 108 | } 109 | 110 | // ================================== 111 | // ColumnSchema definition 112 | // (implements Schema -- defined in pgdiff.go) 113 | // ================================== 114 | 115 | // ColumnSchema holds a slice of rows from one of the databases as well as 116 | // a reference to the current row of data we're viewing. 117 | type ColumnSchema struct { 118 | rows ColumnRows 119 | rowNum int 120 | done bool 121 | } 122 | 123 | // get returns the value from the current row for the given key 124 | func (c *ColumnSchema) get(key string) string { 125 | if c.rowNum >= len(c.rows) { 126 | return "" 127 | } 128 | return c.rows[c.rowNum][key] 129 | } 130 | 131 | // NextRow increments the rowNum and tells you whether or not there are more 132 | func (c *ColumnSchema) NextRow() bool { 133 | if c.rowNum >= len(c.rows)-1 { 134 | c.done = true 135 | } 136 | c.rowNum = c.rowNum + 1 137 | return !c.done 138 | } 139 | 140 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 141 | func (c *ColumnSchema) Compare(obj interface{}) int { 142 | c2, ok := obj.(*ColumnSchema) 143 | if !ok { 144 | fmt.Println("Error!!!, Compare needs a ColumnSchema instance", c2) 145 | } 146 | 147 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 148 | return val 149 | } 150 | 151 | // Add prints SQL to add the column 152 | func (c *ColumnSchema) Add() { 153 | 154 | schema := dbInfo2.DbSchema 155 | if schema == "*" { 156 | schema = c.get("table_schema") 157 | } 158 | 159 | // Knowing the version of db2 would eliminate the need for this warning 160 | if c.get("is_identity") == "YES" { 161 | fmt.Println("-- WARNING: identity columns are not supported in PostgreSQL versions < 10.") 162 | fmt.Println("-- Attempting to create identity columns in earlier versions will probably result in errors.") 163 | } 164 | 165 | if c.get("data_type") == "character varying" { 166 | maxLength, valid := getMaxLength(c.get("character_maximum_length")) 167 | if !valid { 168 | fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s character varying", schema, c.get("table_name"), c.get("column_name")) 169 | } else { 170 | fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s character varying(%s)", schema, c.get("table_name"), c.get("column_name"), maxLength) 171 | } 172 | } else { 173 | dataType := c.get("data_type") 174 | //if c.get("data_type") == "ARRAY" { 175 | //fmt.Println("-- Note that adding of array data types are not yet generated properly.") 176 | //} 177 | if dataType == "ARRAY" { 178 | dataType = c.get("array_type")+"[]" 179 | } 180 | //fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s %s", schema, c.get("table_name"), c.get("column_name"), c.get("data_type")) 181 | fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s %s", schema, c.get("table_name"), c.get("column_name"), dataType) 182 | } 183 | 184 | if c.get("is_nullable") == "NO" { 185 | fmt.Printf(" NOT NULL") 186 | } 187 | if c.get("column_default") != "null" { 188 | fmt.Printf(" DEFAULT %s", c.get("column_default")) 189 | } 190 | // NOTE: there are more identity column sequence options according to the PostgreSQL 191 | // CREATE TABLE docs, but these do not appear to be available as of version 10.1 192 | if c.get("is_identity") == "YES" { 193 | fmt.Printf(" GENERATED %s AS IDENTITY", c.get("identity_generation")) 194 | } 195 | fmt.Printf(";\n") 196 | } 197 | 198 | // Drop prints SQL to drop the column 199 | func (c *ColumnSchema) Drop() { 200 | // if dropping column 201 | fmt.Printf("ALTER TABLE %s.%s DROP COLUMN IF EXISTS %s;\n", c.get("table_schema"), c.get("table_name"), c.get("column_name")) 202 | } 203 | 204 | // Change handles the case where the table and column match, but the details do not 205 | func (c *ColumnSchema) Change(obj interface{}) { 206 | c2, ok := obj.(*ColumnSchema) 207 | if !ok { 208 | fmt.Println("Error!!!, ColumnSchema.Change(obj) needs a ColumnSchema instance", c2) 209 | } 210 | 211 | // Adjust data type for array columns 212 | dataType1 := c.get("data_type") 213 | if dataType1 == "ARRAY" { 214 | dataType1 = c.get("array_type")+"[]" 215 | } 216 | dataType2 := c2.get("data_type") 217 | if dataType2 == "ARRAY" { 218 | dataType2 = c2.get("array_type")+"[]" 219 | } 220 | 221 | // Detect column type change (mostly varchar length, or number size increase) 222 | // (integer to/from bigint is OK) 223 | if dataType1 == dataType2 { 224 | if dataType1 == "character varying" { 225 | max1, max1Valid := getMaxLength(c.get("character_maximum_length")) 226 | max2, max2Valid := getMaxLength(c2.get("character_maximum_length")) 227 | if !max1Valid && !max2Valid { 228 | // Leave them alone, they both have undefined max lengths 229 | } else if (max1Valid || !max2Valid) && (max1 != c2.get("character_maximum_length")) { 230 | //if !max1Valid { 231 | // fmt.Println("-- WARNING: varchar column has no maximum length. Setting to 1024, which may result in data loss.") 232 | //} 233 | max1Int, err1 := strconv.Atoi(max1) 234 | check("converting string to int", err1) 235 | max2Int, err2 := strconv.Atoi(max2) 236 | check("converting string to int", err2) 237 | if max1Int < max2Int { 238 | fmt.Println("-- WARNING: The next statement will shorten a character varying column, which may result in data loss.") 239 | } 240 | fmt.Printf("-- max1Valid: %v max2Valid: %v \n", max1Valid, max2Valid) 241 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s TYPE character varying(%s);\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name"), max1) 242 | } 243 | } 244 | } 245 | 246 | // Code and test a column change from integer to bigint 247 | if dataType1 != dataType2 { 248 | fmt.Printf("-- WARNING: This type change may not work well: (%s to %s).\n", dataType2, dataType1) 249 | if strings.HasPrefix(dataType1, "character") { 250 | max1, max1Valid := getMaxLength(c.get("character_maximum_length")) 251 | if !max1Valid { 252 | fmt.Println("-- WARNING: varchar column has no maximum length. Setting to 1024") 253 | } 254 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s TYPE %s(%s);\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name"), dataType1, max1) 255 | } else { 256 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s TYPE %s;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name"), dataType1) 257 | } 258 | } 259 | 260 | // Detect column default change (or added, dropped) 261 | if c.get("column_default") == "null" { 262 | if c2.get("column_default") != "null" { 263 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s DROP DEFAULT;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name")) 264 | } 265 | } else if c.get("column_default") != c2.get("column_default") { 266 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s SET DEFAULT %s;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name"), c.get("column_default")) 267 | } 268 | 269 | // Detect identity column change 270 | // Save result to variable instead of printing because order for adding/removing 271 | // is_nullable affects identity columns 272 | var identitySql string 273 | if c.get("is_identity") != c2.get("is_identity") { 274 | // Knowing the version of db2 would eliminate the need for this warning 275 | fmt.Println("-- WARNING: identity columns are not supported in PostgreSQL versions < 10.") 276 | fmt.Println("-- Attempting to create identity columns in earlier versions will probably result in errors.") 277 | if c.get("is_identity") == "YES" { 278 | identitySql = fmt.Sprintf("ALTER TABLE \"%s\".\"%s\" ALTER COLUMN \"%s\" ADD GENERATED %s AS IDENTITY;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name"), c.get("identity_generation")) 279 | } else { 280 | identitySql = fmt.Sprintf("ALTER TABLE \"%s\".\"%s\" ALTER COLUMN \"%s\" DROP IDENTITY;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name")) 281 | } 282 | } 283 | 284 | // Detect not-null and nullable change 285 | if c.get("is_nullable") != c2.get("is_nullable") { 286 | if c.get("is_nullable") == "YES" { 287 | if identitySql != "" { 288 | fmt.Printf(identitySql) 289 | } 290 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s DROP NOT NULL;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name")) 291 | } else { 292 | fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s SET NOT NULL;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name")) 293 | if identitySql != "" { 294 | fmt.Printf(identitySql) 295 | } 296 | } 297 | } else { 298 | if identitySql != "" { 299 | fmt.Printf(identitySql) 300 | } 301 | } 302 | } 303 | 304 | // ================================== 305 | // Standalone Functions 306 | // ================================== 307 | 308 | // compare outputs SQL to make the columns match between two databases or schemas 309 | func compare(conn1 *sql.DB, conn2 *sql.DB, tpl *template.Template) { 310 | buf1 := new(bytes.Buffer) 311 | tpl.Execute(buf1, dbInfo1) 312 | 313 | buf2 := new(bytes.Buffer) 314 | tpl.Execute(buf2, dbInfo2) 315 | 316 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 317 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 318 | 319 | //rows1 := make([]map[string]string, 500) 320 | rows1 := make(ColumnRows, 0) 321 | for row := range rowChan1 { 322 | rows1 = append(rows1, row) 323 | } 324 | sort.Sort(rows1) 325 | 326 | //rows2 := make([]map[string]string, 500) 327 | rows2 := make(ColumnRows, 0) 328 | for row := range rowChan2 { 329 | rows2 = append(rows2, row) 330 | } 331 | sort.Sort(&rows2) 332 | 333 | // We have to explicitly type this as Schema here for some unknown reason 334 | var schema1 Schema = &ColumnSchema{rows: rows1, rowNum: -1} 335 | var schema2 Schema = &ColumnSchema{rows: rows2, rowNum: -1} 336 | 337 | // Compare the columns 338 | doDiff(schema1, schema2) 339 | 340 | } 341 | 342 | // compareColumns outputs SQL to make the columns match between two databases or schemas 343 | func compareColumns(conn1 *sql.DB, conn2 *sql.DB) { 344 | 345 | compare(conn1, conn2, columnSqlTemplate) 346 | 347 | } 348 | 349 | // compareColumns outputs SQL to make the tables columns (without views columns) match between two databases or schemas 350 | func compareTableColumns(conn1 *sql.DB, conn2 *sql.DB) { 351 | 352 | compare(conn1, conn2, tableColumnSqlTemplate) 353 | 354 | } 355 | 356 | // getMaxLength returns the maximum length and whether or not it is valid 357 | func getMaxLength(maxLength string) (string, bool) { 358 | 359 | if maxLength == "null" { 360 | // default to 1024 361 | return "1024", false 362 | } 363 | return maxLength, true 364 | } 365 | 366 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2014 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by the MIT 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "github.com/joncrlsn/pgutil" 11 | flag "github.com/ogier/pflag" 12 | ) 13 | 14 | func parseFlags() (pgutil.DbInfo, pgutil.DbInfo) { 15 | 16 | var dbUser1 = flag.StringP("user1", "U", "", "db user") 17 | var dbPass1 = flag.StringP("password1", "W", "", "db password") 18 | var dbHost1 = flag.StringP("host1", "H", "localhost", "db host") 19 | var dbPort1 = flag.IntP("port1", "P", 5432, "db port") 20 | var dbName1 = flag.StringP("dbname1", "D", "", "db name") 21 | var dbSchema1 = flag.StringP("schema1", "S", "*", "schema name or * for all schemas") 22 | var dbOptions1 = flag.StringP("options1", "O", "", "db options (eg. sslmode=disable)") 23 | 24 | var dbUser2 = flag.StringP("user2", "u", "", "db user") 25 | var dbPass2 = flag.StringP("password2", "w", "", "db password") 26 | var dbHost2 = flag.StringP("host2", "h", "localhost", "db host") 27 | var dbPort2 = flag.IntP("port2", "p", 5432, "db port") 28 | var dbName2 = flag.StringP("dbname2", "d", "", "db name") 29 | var dbSchema2 = flag.StringP("schema2", "s", "*", "schema name or * for all schemas") 30 | var dbOptions2 = flag.StringP("options2", "o", "", "db options (eg. sslmode=disable)") 31 | 32 | flag.Parse() 33 | 34 | dbInfo1 := pgutil.DbInfo{DbName: *dbName1, DbHost: *dbHost1, DbPort: int32(*dbPort1), DbUser: *dbUser1, DbPass: *dbPass1, DbSchema: *dbSchema1, DbOptions: *dbOptions1} 35 | 36 | dbInfo2 := pgutil.DbInfo{DbName: *dbName2, DbHost: *dbHost2, DbPort: int32(*dbPort2), DbUser: *dbUser2, DbPass: *dbPass2, DbSchema: *dbSchema2, DbOptions: *dbOptions2} 37 | 38 | return dbInfo1, dbInfo2 39 | } 40 | -------------------------------------------------------------------------------- /foreignkey.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "text/template" 17 | ) 18 | 19 | var ( 20 | foreignKeySqlTemplate = initForeignKeySqlTemplate() 21 | ) 22 | 23 | // Initializes the Sql template 24 | func initForeignKeySqlTemplate() *template.Template { 25 | sql := ` 26 | SELECT {{if eq $.DbSchema "*" }}ns.nspname || '.' || {{end}}cl.relname || '.' || c.conname AS compare_name 27 | , ns.nspname AS schema_name 28 | , cl.relname AS table_name 29 | , c.conname AS fk_name 30 | , pg_catalog.pg_get_constraintdef(c.oid, true) as constraint_def 31 | FROM pg_catalog.pg_constraint c 32 | INNER JOIN pg_class AS cl ON (c.conrelid = cl.oid) 33 | INNER JOIN pg_namespace AS ns ON (ns.oid = c.connamespace) 34 | WHERE c.contype = 'f' 35 | {{if eq $.DbSchema "*"}} 36 | AND ns.nspname NOT LIKE 'pg_%' 37 | AND ns.nspname <> 'information_schema' 38 | {{else}} 39 | AND ns.nspname = '{{$.DbSchema}}' 40 | {{end}} 41 | ` 42 | t := template.New("ForeignKeySqlTmpl") 43 | template.Must(t.Parse(sql)) 44 | return t 45 | } 46 | 47 | // ================================== 48 | // ForeignKeyRows definition 49 | // ================================== 50 | 51 | // ForeignKeyRows is a sortable string map 52 | type ForeignKeyRows []map[string]string 53 | 54 | func (slice ForeignKeyRows) Len() int { 55 | return len(slice) 56 | } 57 | 58 | func (slice ForeignKeyRows) Less(i, j int) bool { 59 | if slice[i]["compare_name"] != slice[j]["compare_name"] { 60 | return slice[i]["compare_name"] < slice[j]["compare_name"] 61 | } 62 | return slice[i]["constraint_def"] < slice[j]["constraint_def"] 63 | } 64 | 65 | func (slice ForeignKeyRows) Swap(i, j int) { 66 | slice[i], slice[j] = slice[j], slice[i] 67 | } 68 | 69 | // ================================== 70 | // ForeignKeySchema definition 71 | // (implements Schema -- defined in pgdiff.go) 72 | // ================================== 73 | 74 | // ForeignKeySchema holds a slice of rows from one of the databases as well as 75 | // a reference to the current row of data we're viewing. 76 | type ForeignKeySchema struct { 77 | rows ForeignKeyRows 78 | rowNum int 79 | done bool 80 | } 81 | 82 | // get returns the value from the current row for the given key 83 | func (c *ForeignKeySchema) get(key string) string { 84 | if c.rowNum >= len(c.rows) { 85 | return "" 86 | } 87 | return c.rows[c.rowNum][key] 88 | } 89 | 90 | // get returns the current row for the given key 91 | func (c *ForeignKeySchema) getRow() map[string]string { 92 | if c.rowNum >= len(c.rows) { 93 | return make(map[string]string) 94 | } 95 | return c.rows[c.rowNum] 96 | } 97 | 98 | // NextRow reads from the channel and tells you if there are (probably) more or not 99 | func (c *ForeignKeySchema) NextRow() bool { 100 | if c.rowNum >= len(c.rows)-1 { 101 | c.done = true 102 | } 103 | c.rowNum = c.rowNum + 1 104 | return !c.done 105 | } 106 | 107 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 108 | func (c *ForeignKeySchema) Compare(obj interface{}) int { 109 | c2, ok := obj.(*ForeignKeySchema) 110 | if !ok { 111 | fmt.Println("Error!!!, Compare(obj) needs a ForeignKeySchema instance", c2) 112 | return +999 113 | } 114 | 115 | //fmt.Printf("Comparing %s with %s", c.get("table_name"), c2.get("table_name")) 116 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 117 | if val != 0 { 118 | return val 119 | } 120 | 121 | val = misc.CompareStrings(c.get("constraint_def"), c2.get("constraint_def")) 122 | return val 123 | } 124 | 125 | // Add returns SQL to add the foreign key 126 | func (c *ForeignKeySchema) Add() { 127 | schema := dbInfo2.DbSchema 128 | if schema == "*" { 129 | schema = c.get("schema_name") 130 | } 131 | fmt.Printf("ALTER TABLE %s.%s ADD CONSTRAINT %s %s;\n", schema, c.get("table_name"), c.get("fk_name"), c.get("constraint_def")) 132 | } 133 | 134 | // Drop returns SQL to drop the foreign key 135 | func (c ForeignKeySchema) Drop() { 136 | fmt.Printf("ALTER TABLE %s.%s DROP CONSTRAINT %s; -- %s\n", c.get("schema_name"), c.get("table_name"), c.get("fk_name"), c.get("constraint_def")) 137 | } 138 | 139 | // Change handles the case where the table and foreign key name, but the details do not 140 | func (c *ForeignKeySchema) Change(obj interface{}) { 141 | c2, ok := obj.(*ForeignKeySchema) 142 | if !ok { 143 | fmt.Println("Error!!!, ForeignKeySchema.Change(obj) needs a ForeignKeySchema instance", c2) 144 | } 145 | // There is no "changing" a foreign key. It either gets created or dropped (or left as-is). 146 | } 147 | 148 | /* 149 | * Compare the foreign keys in the two databases. 150 | */ 151 | func compareForeignKeys(conn1 *sql.DB, conn2 *sql.DB) { 152 | 153 | buf1 := new(bytes.Buffer) 154 | foreignKeySqlTemplate.Execute(buf1, dbInfo1) 155 | 156 | buf2 := new(bytes.Buffer) 157 | foreignKeySqlTemplate.Execute(buf2, dbInfo2) 158 | 159 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 160 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 161 | 162 | rows1 := make(ForeignKeyRows, 0) 163 | for row := range rowChan1 { 164 | rows1 = append(rows1, row) 165 | } 166 | sort.Sort(rows1) 167 | 168 | rows2 := make(ForeignKeyRows, 0) 169 | for row := range rowChan2 { 170 | rows2 = append(rows2, row) 171 | } 172 | sort.Sort(rows2) 173 | 174 | // We have to explicitly type this as Schema here for some unknown reason 175 | var schema1 Schema = &ForeignKeySchema{rows: rows1, rowNum: -1} 176 | var schema2 Schema = &ForeignKeySchema{rows: rows2, rowNum: -1} 177 | 178 | // Compare the foreign keys 179 | doDiff(schema1, schema2) 180 | } 181 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "strings" 17 | "text/template" 18 | ) 19 | 20 | var ( 21 | functionSqlTemplate = initFunctionSqlTemplate() 22 | ) 23 | 24 | // Initializes the Sql template 25 | func initFunctionSqlTemplate() *template.Template { 26 | sql := ` 27 | SELECT n.nspname AS schema_name 28 | , {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}p.proname AS compare_name 29 | , p.proname AS function_name 30 | , p.oid::regprocedure AS fancy 31 | , t.typname AS return_type 32 | , pg_get_functiondef(p.oid) AS definition 33 | FROM pg_proc AS p 34 | JOIN pg_type t ON (p.prorettype = t.oid) 35 | JOIN pg_namespace n ON (n.oid = p.pronamespace) 36 | JOIN pg_language l ON (p.prolang = l.oid AND l.lanname IN ('c','plpgsql', 'sql')) 37 | WHERE true 38 | {{if eq $.DbSchema "*" }} 39 | AND n.nspname NOT LIKE 'pg_%' 40 | AND n.nspname <> 'information_schema' 41 | {{else}} 42 | AND n.nspname = '{{$.DbSchema}}' 43 | {{end}}; 44 | ` 45 | t := template.New("FunctionSqlTmpl") 46 | template.Must(t.Parse(sql)) 47 | return t 48 | } 49 | 50 | // ================================== 51 | // FunctionRows definition 52 | // ================================== 53 | 54 | // FunctionRows is a sortable slice of string maps 55 | type FunctionRows []map[string]string 56 | 57 | func (slice FunctionRows) Len() int { 58 | return len(slice) 59 | } 60 | 61 | func (slice FunctionRows) Less(i, j int) bool { 62 | return slice[i]["compare_name"] < slice[j]["compare_name"] 63 | } 64 | 65 | func (slice FunctionRows) Swap(i, j int) { 66 | slice[i], slice[j] = slice[j], slice[i] 67 | } 68 | 69 | // FunctionSchema holds a channel streaming function information from one of the databases as well as 70 | // a reference to the current row of data we're viewing. 71 | // 72 | // FunctionSchema implements the Schema interface defined in pgdiff.go 73 | type FunctionSchema struct { 74 | rows FunctionRows 75 | rowNum int 76 | done bool 77 | } 78 | 79 | // get returns the value from the current row for the given key 80 | func (c *FunctionSchema) get(key string) string { 81 | if c.rowNum >= len(c.rows) { 82 | return "" 83 | } 84 | return c.rows[c.rowNum][key] 85 | } 86 | 87 | // NextRow increments the rowNum and tells you whether or not there are more 88 | func (c *FunctionSchema) NextRow() bool { 89 | if c.rowNum >= len(c.rows)-1 { 90 | c.done = true 91 | } 92 | c.rowNum = c.rowNum + 1 93 | return !c.done 94 | } 95 | 96 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 97 | func (c *FunctionSchema) Compare(obj interface{}) int { 98 | c2, ok := obj.(*FunctionSchema) 99 | if !ok { 100 | fmt.Println("Error!!!, Compare(obj) needs a FunctionSchema instance", c2) 101 | return +999 102 | } 103 | 104 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 105 | //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("function_name"), c2.get("function_name")) 106 | return val 107 | } 108 | 109 | // Add returns SQL to create the function 110 | func (c FunctionSchema) Add() { 111 | // If we are comparing two different schemas against each other, we need to do some 112 | // modification of the first function definition so we create it in the right schema 113 | functionDef := c.get("definition") 114 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 115 | functionDef = strings.Replace( 116 | functionDef, 117 | fmt.Sprintf("FUNCTION %s.%s(", c.get("schema_name"), c.get("function_name")), 118 | fmt.Sprintf("FUNCTION %s.%s(", dbInfo2.DbSchema, c.get("function_name")), 119 | -1) 120 | } 121 | 122 | fmt.Println("-- STATEMENT-BEGIN") 123 | fmt.Println(functionDef, ";") 124 | fmt.Println("-- STATEMENT-END") 125 | } 126 | 127 | // Drop returns SQL to drop the function 128 | func (c FunctionSchema) Drop() { 129 | fmt.Println("-- Note that CASCADE in the statement below will also drop any triggers depending on this function.") 130 | fmt.Println("-- Also, if there are two functions with this name, you will want to add arguments to identify the correct one to drop.") 131 | fmt.Println("-- (See http://www.postgresql.org/docs/9.4/interactive/sql-dropfunction.html) ") 132 | fmt.Printf("DROP FUNCTION %s.%s CASCADE;\n", c.get("schema_name"), c.get("function_name")) 133 | } 134 | 135 | // Change handles the case where the function names match, but the definition does not 136 | func (c FunctionSchema) Change(obj interface{}) { 137 | c2, ok := obj.(*FunctionSchema) 138 | if !ok { 139 | fmt.Println("Error!!!, Change needs a FunctionSchema instance", c2) 140 | } 141 | if c.get("definition") != c2.get("definition") { 142 | fmt.Println("-- This function is different so we'll recreate it:") 143 | 144 | // If we are comparing two different schemas against each other, we need to do some 145 | // modification of the first function definition so we create it in the right schema 146 | functionDef := c.get("definition") 147 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 148 | functionDef = strings.Replace( 149 | functionDef, 150 | fmt.Sprintf("FUNCTION %s.%s(", c.get("schema_name"), c.get("function_name")), 151 | fmt.Sprintf("FUNCTION %s.%s(", dbInfo2.DbSchema, c.get("function_name")), 152 | -1) 153 | } 154 | 155 | // The definition column has everything needed to rebuild the function 156 | fmt.Println("-- STATEMENT-BEGIN") 157 | fmt.Printf("%s;\n", functionDef) 158 | fmt.Println("-- STATEMENT-END") 159 | } 160 | } 161 | 162 | // ================================== 163 | // Functions 164 | // ================================== 165 | 166 | // compareFunctions outputs SQL to make the functions match between DBs 167 | func compareFunctions(conn1 *sql.DB, conn2 *sql.DB) { 168 | 169 | buf1 := new(bytes.Buffer) 170 | functionSqlTemplate.Execute(buf1, dbInfo1) 171 | 172 | buf2 := new(bytes.Buffer) 173 | functionSqlTemplate.Execute(buf2, dbInfo2) 174 | 175 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 176 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 177 | 178 | rows1 := make(FunctionRows, 0) 179 | for row := range rowChan1 { 180 | rows1 = append(rows1, row) 181 | } 182 | sort.Sort(rows1) 183 | 184 | rows2 := make(FunctionRows, 0) 185 | for row := range rowChan2 { 186 | rows2 = append(rows2, row) 187 | } 188 | sort.Sort(rows2) 189 | 190 | // We must explicitly type this as Schema here 191 | var schema1 Schema = &FunctionSchema{rows: rows1, rowNum: -1} 192 | var schema2 Schema = &FunctionSchema{rows: rows2, rowNum: -1} 193 | 194 | // Compare the functions 195 | doDiff(schema1, schema2) 196 | } 197 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pgdiff 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/joncrlsn/fileutil v0.0.0-20150212043926-71757336e569 // indirect 7 | github.com/joncrlsn/misc v0.0.0-20160408024000-193a3fcec166 8 | github.com/joncrlsn/pgutil v0.0.0-20171213024902-4c8aab9306b4 9 | github.com/kr/pretty v0.2.1 // indirect 10 | github.com/lib/pq v1.10.9 11 | github.com/ogier/pflag v0.0.1 12 | github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b // indirect 13 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/joncrlsn/fileutil v0.0.0-20150212043926-71757336e569 h1:bGOVwE4GrUyU3Pz22yNL7mML4tc/8b8zSjUZzofLpFA= 2 | github.com/joncrlsn/fileutil v0.0.0-20150212043926-71757336e569/go.mod h1:YFE9T2vDUoqBSIywxQRZi1FWDcLsBgo+KDbLSw7HDNM= 3 | github.com/joncrlsn/misc v0.0.0-20160408024000-193a3fcec166 h1:urNZ026xorI3t6Nzivkd8KSNACPjHxeeuxG1FGQXBD8= 4 | github.com/joncrlsn/misc v0.0.0-20160408024000-193a3fcec166/go.mod h1:ZnHyWkKQ3JfdVYRo3PrjvB4RMdLs7SaQqTyUmk/rjIg= 5 | github.com/joncrlsn/pgutil v0.0.0-20171213024902-4c8aab9306b4 h1:wTFs1uYdQfopjUVlbpJj0k2pHqKGa4M6D6gxGSH54Z8= 6 | github.com/joncrlsn/pgutil v0.0.0-20171213024902-4c8aab9306b4/go.mod h1:iKyJCP0yj+Z+jEAobsEBh+t6WrUFxMQiClq3r7yI4z8= 7 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 8 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 11 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 12 | github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= 13 | github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 14 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 15 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 16 | github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= 17 | github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= 18 | github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b h1:GlTM/aMVIwU3luIuSN2SIVRuTqGPt1P97YxAi514ulw= 19 | github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b/go.mod h1:CC7OXV9IjEZRA+znA6/Kz5vbSwh69QioernOHeDCatU= 20 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 21 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 22 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 23 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 26 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= 28 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 29 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 30 | -------------------------------------------------------------------------------- /grant-attribute.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "strings" 17 | "text/template" 18 | ) 19 | 20 | var ( 21 | grantAttributeSqlTemplate = initGrantAttributeSqlTemplate() 22 | ) 23 | 24 | // Initializes the Sql template 25 | func initGrantAttributeSqlTemplate() *template.Template { 26 | sql := ` 27 | -- Attribute/Column ACL only 28 | SELECT 29 | n.nspname AS schema_name 30 | , {{ if eq $.DbSchema "*" }}n.nspname::text || '.' || {{ end }}c.relkind::text || '.' || c.relname::text || '.' || a.attname AS compare_name 31 | , CASE c.relkind 32 | WHEN 'r' THEN 'TABLE' 33 | WHEN 'v' THEN 'VIEW' 34 | WHEN 'f' THEN 'FOREIGN TABLE' 35 | END as type 36 | , c.relname AS relationship_name 37 | , a.attname AS attribute_name 38 | , a.attacl AS attribute_acl 39 | FROM pg_catalog.pg_class c 40 | LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) 41 | INNER JOIN (SELECT attname, unnest(attacl) AS attacl, attrelid 42 | FROM pg_catalog.pg_attribute 43 | WHERE NOT attisdropped AND attacl IS NOT NULL) 44 | AS a ON (a.attrelid = c.oid) 45 | WHERE c.relkind IN ('r', 'v', 'f') 46 | --AND pg_catalog.pg_table_is_visible(c.oid) 47 | {{ if eq $.DbSchema "*" }} 48 | AND n.nspname NOT LIKE 'pg_%' 49 | AND n.nspname <> 'information_schema' 50 | {{ else }} 51 | AND n.nspname = '{{ $.DbSchema }}' 52 | {{ end }}; 53 | ` 54 | 55 | t := template.New("GrantAttributeSqlTmpl") 56 | template.Must(t.Parse(sql)) 57 | return t 58 | } 59 | 60 | // ================================== 61 | // GrantAttributeRows definition 62 | // ================================== 63 | 64 | // GrantAttributeRows is a sortable slice of string maps 65 | type GrantAttributeRows []map[string]string 66 | 67 | func (slice GrantAttributeRows) Len() int { 68 | return len(slice) 69 | } 70 | 71 | func (slice GrantAttributeRows) Less(i, j int) bool { 72 | if slice[i]["compare_name"] != slice[j]["compare_name"] { 73 | return slice[i]["compare_name"] < slice[j]["compare_name"] 74 | } 75 | 76 | // Only compare the role part of the ACL 77 | // Not yet sure if this is absolutely necessary 78 | // (or if we could just compare the entire ACL string) 79 | role1, _ := parseAcl(slice[i]["attribute_acl"]) 80 | role2, _ := parseAcl(slice[j]["attribute_acl"]) 81 | if role1 != role2 { 82 | return role1 < role2 83 | } 84 | 85 | return false 86 | } 87 | 88 | func (slice GrantAttributeRows) Swap(i, j int) { 89 | slice[i], slice[j] = slice[j], slice[i] 90 | } 91 | 92 | // ================================== 93 | // GrantAttributeSchema definition 94 | // (implements Schema -- defined in pgdiff.go) 95 | // ================================== 96 | 97 | // GrantAttributeSchema holds a slice of rows from one of the databases as well as 98 | // a reference to the current row of data we're viewing. 99 | type GrantAttributeSchema struct { 100 | rows GrantAttributeRows 101 | rowNum int 102 | done bool 103 | } 104 | 105 | // get returns the value from the current row for the given key 106 | func (c *GrantAttributeSchema) get(key string) string { 107 | if c.rowNum >= len(c.rows) { 108 | return "" 109 | } 110 | return c.rows[c.rowNum][key] 111 | } 112 | 113 | // get returns the current row for the given key 114 | func (c *GrantAttributeSchema) getRow() map[string]string { 115 | if c.rowNum >= len(c.rows) { 116 | return make(map[string]string) 117 | } 118 | return c.rows[c.rowNum] 119 | } 120 | 121 | // NextRow increments the rowNum and tells you whether or not there are more 122 | func (c *GrantAttributeSchema) NextRow() bool { 123 | if c.rowNum >= len(c.rows)-1 { 124 | c.done = true 125 | } 126 | c.rowNum = c.rowNum + 1 127 | return !c.done 128 | } 129 | 130 | // Compare tells you, in one pass, whether or not the first row matches, is less than, 131 | // or greater than the second row. 132 | func (c *GrantAttributeSchema) Compare(obj interface{}) int { 133 | c2, ok := obj.(*GrantAttributeSchema) 134 | if !ok { 135 | fmt.Println("Error!!!, Compare needs a GrantAttributeSchema instance", c2) 136 | return +999 137 | } 138 | 139 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 140 | if val != 0 { 141 | return val 142 | } 143 | 144 | role1, _ := parseAcl(c.get("attribute_acl")) 145 | role2, _ := parseAcl(c2.get("attribute_acl")) 146 | val = misc.CompareStrings(role1, role2) 147 | return val 148 | } 149 | 150 | // Add prints SQL to add the grant 151 | func (c *GrantAttributeSchema) Add() { 152 | schema := dbInfo2.DbSchema 153 | if schema == "*" { 154 | schema = c.get("schema_name") 155 | } 156 | 157 | role, grants := parseGrants(c.get("attribute_acl")) 158 | fmt.Printf("GRANT %s (%s) ON %s.%s TO %s; -- Add\n", strings.Join(grants, ", "), c.get("attribute_name"), schema, c.get("relationship_name"), role) 159 | } 160 | 161 | // Drop prints SQL to drop the grant 162 | func (c *GrantAttributeSchema) Drop() { 163 | role, grants := parseGrants(c.get("attribute_acl")) 164 | fmt.Printf("REVOKE %s (%s) ON %s.%s FROM %s; -- Drop\n", strings.Join(grants, ", "), c.get("attribute_name"), c.get("schema_name"), c.get("relationship_name"), role) 165 | } 166 | 167 | // Change handles the case where the relationship and column match, but the grant does not 168 | func (c *GrantAttributeSchema) Change(obj interface{}) { 169 | c2, ok := obj.(*GrantAttributeSchema) 170 | if !ok { 171 | fmt.Println("-- Error!!!, Change needs a GrantAttributeSchema instance", c2) 172 | } 173 | 174 | role, grants1 := parseGrants(c.get("attribute_acl")) 175 | _, grants2 := parseGrants(c2.get("attribute_acl")) 176 | 177 | // Find grants in the first db that are not in the second 178 | // (for this relationship and owner) 179 | var grantList []string 180 | for _, g := range grants1 { 181 | if !misc.ContainsString(grants2, g) { 182 | grantList = append(grantList, g) 183 | } 184 | } 185 | if len(grantList) > 0 { 186 | fmt.Printf("GRANT %s (%s) ON %s.%s TO %s; -- Change\n", strings.Join(grantList, ", "), 187 | c.get("attribute_name"), c2.get("schema_name"), c.get("relationship_name"), role) 188 | } 189 | 190 | // Find grants in the second db that are not in the first 191 | // (for this relationship and owner) 192 | var revokeList []string 193 | for _, g := range grants2 { 194 | if !misc.ContainsString(grants1, g) { 195 | revokeList = append(revokeList, g) 196 | } 197 | } 198 | if len(revokeList) > 0 { 199 | fmt.Printf("REVOKE %s (%s) ON %s.%s FROM %s; -- Change\n", strings.Join(revokeList, ", "), c.get("attribute_name"), c2.get("schema_name"), c.get("relationship_name"), role) 200 | } 201 | 202 | //fmt.Printf("--1 rel:%s, relAcl:%s, col:%s, colAcl:%s\n", c.get("attribute_name"), c.get("attribute_acl"), c.get("attribute_name"), c.get("attribute_acl")) 203 | //fmt.Printf("--2 rel:%s, relAcl:%s, col:%s, colAcl:%s\n", c2.get("attribute_name"), c2.get("attribute_acl"), c2.get("attribute_name"), c2.get("attribute_acl")) 204 | } 205 | 206 | // ================================== 207 | // Functions 208 | // ================================== 209 | 210 | // compareGrantAttributes outputs SQL to make the granted permissions match between DBs or schemas 211 | func compareGrantAttributes(conn1 *sql.DB, conn2 *sql.DB) { 212 | 213 | buf1 := new(bytes.Buffer) 214 | grantAttributeSqlTemplate.Execute(buf1, dbInfo1) 215 | 216 | buf2 := new(bytes.Buffer) 217 | grantAttributeSqlTemplate.Execute(buf2, dbInfo2) 218 | 219 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 220 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 221 | 222 | rows1 := make(GrantAttributeRows, 0) 223 | for row := range rowChan1 { 224 | rows1 = append(rows1, row) 225 | } 226 | sort.Sort(rows1) 227 | //for _, row := range rows1 { 228 | //fmt.Printf("--1b compare:%s, col:%s, colAcl:%s\n", row["compare_name"], row["attribute_name"], row["attribute_acl"]) 229 | //} 230 | 231 | rows2 := make(GrantAttributeRows, 0) 232 | for row := range rowChan2 { 233 | rows2 = append(rows2, row) 234 | } 235 | sort.Sort(rows2) 236 | //for _, row := range rows2 { 237 | //fmt.Printf("--2b compare:%s, col:%s, colAcl:%s\n", row["compare_name"], row["attribute_name"], row["attribute_acl"]) 238 | //} 239 | 240 | // We have to explicitly type this as Schema here for some unknown reason 241 | var schema1 Schema = &GrantAttributeSchema{rows: rows1, rowNum: -1} 242 | var schema2 Schema = &GrantAttributeSchema{rows: rows2, rowNum: -1} 243 | 244 | doDiff(schema1, schema2) 245 | } 246 | -------------------------------------------------------------------------------- /grant-relationship.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "strings" 17 | "text/template" 18 | ) 19 | 20 | var ( 21 | grantRelationshipSqlTemplate = initGrantRelationshipSqlTemplate() 22 | ) 23 | 24 | // Initializes the Sql template 25 | func initGrantRelationshipSqlTemplate() *template.Template { 26 | sql := ` 27 | SELECT n.nspname AS schema_name 28 | , {{ if eq $.DbSchema "*" }}n.nspname::text || '.' || {{ end }}c.relkind::text || '.' || c.relname::text AS compare_name 29 | , CASE c.relkind 30 | WHEN 'r' THEN 'TABLE' 31 | WHEN 'v' THEN 'VIEW' 32 | WHEN 'S' THEN 'SEQUENCE' 33 | WHEN 'f' THEN 'FOREIGN TABLE' 34 | END as type 35 | , c.relname AS relationship_name 36 | , unnest(c.relacl) AS relationship_acl 37 | FROM pg_catalog.pg_class c 38 | LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) 39 | WHERE c.relkind IN ('r', 'v', 'S', 'f') 40 | --AND pg_catalog.pg_table_is_visible(c.oid) 41 | {{ if eq $.DbSchema "*" }} 42 | AND n.nspname NOT LIKE 'pg_%' 43 | AND n.nspname <> 'information_schema' 44 | {{ else }} 45 | AND n.nspname = '{{ $.DbSchema }}' 46 | {{ end }}; 47 | ` 48 | 49 | t := template.New("GrantRelationshipSqlTmpl") 50 | template.Must(t.Parse(sql)) 51 | return t 52 | } 53 | 54 | // ================================== 55 | // GrantRelationshipRows definition 56 | // ================================== 57 | 58 | // GrantRelationshipRows is a sortable slice of string maps 59 | type GrantRelationshipRows []map[string]string 60 | 61 | func (slice GrantRelationshipRows) Len() int { 62 | return len(slice) 63 | } 64 | 65 | func (slice GrantRelationshipRows) Less(i, j int) bool { 66 | if slice[i]["compare_name"] != slice[j]["compare_name"] { 67 | return slice[i]["compare_name"] < slice[j]["compare_name"] 68 | } 69 | 70 | // Only compare the role part of the ACL 71 | // Not yet sure if this is absolutely necessary 72 | // (or if we could just compare the entire ACL string) 73 | relRole1, _ := parseAcl(slice[i]["relationship_acl"]) 74 | relRole2, _ := parseAcl(slice[j]["relationship_acl"]) 75 | if relRole1 != relRole2 { 76 | return relRole1 < relRole2 77 | } 78 | 79 | return false 80 | } 81 | 82 | func (slice GrantRelationshipRows) Swap(i, j int) { 83 | slice[i], slice[j] = slice[j], slice[i] 84 | } 85 | 86 | // ================================== 87 | // GrantRelationshipSchema definition 88 | // (implements Schema -- defined in pgdiff.go) 89 | // ================================== 90 | 91 | // GrantRelationshipSchema holds a slice of rows from one of the databases as well as 92 | // a reference to the current row of data we're viewing. 93 | type GrantRelationshipSchema struct { 94 | rows GrantRelationshipRows 95 | rowNum int 96 | done bool 97 | } 98 | 99 | // get returns the value from the current row for the given key 100 | func (c *GrantRelationshipSchema) get(key string) string { 101 | if c.rowNum >= len(c.rows) { 102 | return "" 103 | } 104 | return c.rows[c.rowNum][key] 105 | } 106 | 107 | // get returns the current row for the given key 108 | func (c *GrantRelationshipSchema) getRow() map[string]string { 109 | if c.rowNum >= len(c.rows) { 110 | return make(map[string]string) 111 | } 112 | return c.rows[c.rowNum] 113 | } 114 | 115 | // NextRow increments the rowNum and tells you whether or not there are more 116 | func (c *GrantRelationshipSchema) NextRow() bool { 117 | if c.rowNum >= len(c.rows)-1 { 118 | c.done = true 119 | } 120 | c.rowNum = c.rowNum + 1 121 | return !c.done 122 | } 123 | 124 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 125 | func (c *GrantRelationshipSchema) Compare(obj interface{}) int { 126 | c2, ok := obj.(*GrantRelationshipSchema) 127 | if !ok { 128 | fmt.Println("Error!!!, Compare needs a GrantRelationshipSchema instance", c2) 129 | return +999 130 | } 131 | 132 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 133 | if val != 0 { 134 | return val 135 | } 136 | 137 | relRole1, _ := parseAcl(c.get("relationship_acl")) 138 | relRole2, _ := parseAcl(c2.get("relationship_acl")) 139 | val = misc.CompareStrings(relRole1, relRole2) 140 | return val 141 | } 142 | 143 | // Add prints SQL to add the grant 144 | func (c *GrantRelationshipSchema) Add() { 145 | schema := dbInfo2.DbSchema 146 | if schema == "*" { 147 | schema = c.get("schema_name") 148 | } 149 | 150 | role, grants := parseGrants(c.get("relationship_acl")) 151 | fmt.Printf("GRANT %s ON %s.%s TO %s; -- Add\n", strings.Join(grants, ", "), schema, c.get("relationship_name"), role) 152 | } 153 | 154 | // Drop prints SQL to drop the grant 155 | func (c *GrantRelationshipSchema) Drop() { 156 | role, grants := parseGrants(c.get("relationship_acl")) 157 | fmt.Printf("REVOKE %s ON %s.%s FROM %s; -- Drop\n", strings.Join(grants, ", "), c.get("schema_name"), c.get("relationship_name"), role) 158 | } 159 | 160 | // Change handles the case where the relationship and column match, but the grant does not 161 | func (c *GrantRelationshipSchema) Change(obj interface{}) { 162 | c2, ok := obj.(*GrantRelationshipSchema) 163 | if !ok { 164 | fmt.Println("-- Error!!!, Change needs a GrantRelationshipSchema instance", c2) 165 | } 166 | 167 | role, grants1 := parseGrants(c.get("relationship_acl")) 168 | _, grants2 := parseGrants(c2.get("relationship_acl")) 169 | 170 | // Find grants in the first db that are not in the second 171 | // (for this relationship and owner) 172 | var grantList []string 173 | for _, g := range grants1 { 174 | if !misc.ContainsString(grants2, g) { 175 | grantList = append(grantList, g) 176 | } 177 | } 178 | if len(grantList) > 0 { 179 | fmt.Printf("GRANT %s ON %s.%s TO %s; -- Change\n", strings.Join(grantList, ", "), c2.get("schema_name"), c.get("relationship_name"), role) 180 | } 181 | 182 | // Find grants in the second db that are not in the first 183 | // (for this relationship and owner) 184 | var revokeList []string 185 | for _, g := range grants2 { 186 | if !misc.ContainsString(grants1, g) { 187 | revokeList = append(revokeList, g) 188 | } 189 | } 190 | if len(revokeList) > 0 { 191 | fmt.Printf("REVOKE %s ON %s.%s FROM %s; -- Change\n", strings.Join(revokeList, ", "), c2.get("schema_name"), c.get("relationship_name"), role) 192 | } 193 | 194 | // fmt.Printf("--1 rel:%s, relAcl:%s, col:%s, colAcl:%s\n", c.get("relationship_name"), c.get("relationship_acl"), c.get("column_name"), c.get("column_acl")) 195 | // fmt.Printf("--2 rel:%s, relAcl:%s, col:%s, colAcl:%s\n", c2.get("relationship_name"), c2.get("relationship_acl"), c2.get("column_name"), c2.get("column_acl")) 196 | } 197 | 198 | // ================================== 199 | // Functions 200 | // ================================== 201 | 202 | // compareGrantRelationships outputs SQL to make the granted permissions match between DBs or schemas 203 | func compareGrantRelationships(conn1 *sql.DB, conn2 *sql.DB) { 204 | 205 | buf1 := new(bytes.Buffer) 206 | grantRelationshipSqlTemplate.Execute(buf1, dbInfo1) 207 | 208 | buf2 := new(bytes.Buffer) 209 | grantRelationshipSqlTemplate.Execute(buf2, dbInfo2) 210 | 211 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 212 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 213 | 214 | rows1 := make(GrantRelationshipRows, 0) 215 | for row := range rowChan1 { 216 | rows1 = append(rows1, row) 217 | } 218 | sort.Sort(rows1) 219 | 220 | rows2 := make(GrantRelationshipRows, 0) 221 | for row := range rowChan2 { 222 | rows2 = append(rows2, row) 223 | } 224 | sort.Sort(rows2) 225 | 226 | // We have to explicitly type this as Schema here for some unknown (to me) reason 227 | var schema1 Schema = &GrantRelationshipSchema{rows: rows1, rowNum: -1} 228 | var schema2 Schema = &GrantRelationshipSchema{rows: rows2, rowNum: -1} 229 | 230 | doDiff(schema1, schema2) 231 | } 232 | -------------------------------------------------------------------------------- /grant.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2014 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | // grant.go provides functions and structures that are common to grant-relationships and grant-attributes 7 | // 8 | 9 | package main 10 | 11 | import ( 12 | "sort" 13 | "fmt" 14 | "strings" 15 | "regexp" 16 | ) 17 | 18 | var aclRegex = regexp.MustCompile(`([a-zA-Z0-9_]+)*=([rwadDxtXUCcT]+)/([a-zA-Z0-9_]+)$`) 19 | 20 | var permMap = map[string]string{ 21 | "a": "INSERT", 22 | "r": "SELECT", 23 | "w": "UPDATE", 24 | "d": "DELETE", 25 | "D": "TRUNCATE", 26 | "x": "REFERENCES", 27 | "t": "TRIGGER", 28 | "X": "EXECUTE", 29 | "U": "USAGE", 30 | "C": "CREATE", 31 | "c": "CONNECT", 32 | "T": "TEMPORARY", 33 | } 34 | 35 | /* 36 | parseGrants converts an ACL (access control list) line into a role and a slice of permission strings 37 | 38 | Example of an ACL: user1=rwa/c42 39 | 40 | rolename=xxxx -- privileges granted to a role 41 | =xxxx -- privileges granted to PUBLIC 42 | r -- SELECT ("read") 43 | w -- UPDATE ("write") 44 | a -- INSERT ("append") 45 | d -- DELETE 46 | D -- TRUNCATE 47 | x -- REFERENCES 48 | t -- TRIGGER 49 | X -- EXECUTE 50 | U -- USAGE 51 | C -- CREATE 52 | c -- CONNECT 53 | T -- TEMPORARY 54 | arwdDxt -- ALL PRIVILEGES (for tables, varies for other objects) 55 | * -- grant option for preceding privilege 56 | /yyyy -- role that granted this privilege 57 | */ 58 | func parseGrants(acl string) (string, []string) { 59 | role, perms := parseAcl(acl) 60 | if len(role) == 0 && len(acl) == 0 { 61 | return role, make([]string, 0) 62 | } 63 | // For each character in perms, convert it to a word found in permMap 64 | // e.g. 'a' maps to 'INSERT' 65 | permWords := make(sort.StringSlice, 0) 66 | for _, c := range strings.Split(perms, "") { 67 | permWord := permMap[c] 68 | if len(permWord) > 0 { 69 | permWords = append(permWords, permWord) 70 | } else { 71 | fmt.Printf("-- Error, found permission character we haven't coded for: %s", c) 72 | } 73 | } 74 | permWords.Sort() 75 | return role, permWords 76 | } 77 | 78 | // parseAcl parses an ACL (access control list) string (e.g. 'c42=aur/postgres') into a role and 79 | // a string made up of one-character permissions 80 | func parseAcl(acl string) (role string, perms string) { 81 | role, perms = "", "" 82 | matches := aclRegex.FindStringSubmatch(acl) 83 | if matches != nil { 84 | role = matches[1] 85 | perms = matches[2] 86 | if len(role) == 0 { 87 | role = "public" 88 | } 89 | } 90 | return role, perms 91 | } 92 | -------------------------------------------------------------------------------- /grant_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_parseAcls(t *testing.T) { 9 | doParseAcls(t, "user1=rwa/c42", "user1", 3) 10 | doParseAcls(t, "=arwdDxt/c42", "public", 7) // first of two lines 11 | doParseAcls(t, "u3=rwad/postgres", "u3", 4) // second of two lines 12 | doParseAcls(t, "user2=arwxt/postgres", "user2", 5) 13 | doParseAcls(t, "", "", 0) 14 | } 15 | 16 | func doParseAcls(t *testing.T, acl string, expectedRole string, expectedPermCount int) { 17 | fmt.Println("Testing", acl) 18 | role, perms := parseAcl(acl) 19 | if role != expectedRole { 20 | t.Error("Wrong role parsed: " + role + " instead of " + expectedRole) 21 | } 22 | if len(perms) != expectedPermCount { 23 | t.Errorf("Incorrect number of permissions parsed: %d instead of %d", len(perms), expectedPermCount) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /index.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "strings" 17 | "text/template" 18 | ) 19 | 20 | var ( 21 | indexSqlTemplate = initIndexSqlTemplate() 22 | ) 23 | 24 | // Initializes the Sql template 25 | func initIndexSqlTemplate() *template.Template { 26 | sql := ` 27 | SELECT {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}c.relname || '.' || c2.relname AS compare_name 28 | , n.nspname AS schema_name 29 | , c.relname AS table_name 30 | , c2.relname AS index_name 31 | , i.indisprimary AS pk 32 | , i.indisunique AS uq 33 | , pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS index_def 34 | , pg_catalog.pg_get_constraintdef(con.oid, true) AS constraint_def 35 | , con.contype AS typ 36 | FROM pg_catalog.pg_index AS i 37 | INNER JOIN pg_catalog.pg_class AS c ON (c.oid = i.indrelid) 38 | INNER JOIN pg_catalog.pg_class AS c2 ON (c2.oid = i.indexrelid) 39 | LEFT OUTER JOIN pg_catalog.pg_constraint con 40 | ON (con.conrelid = i.indrelid AND con.conindid = i.indexrelid AND con.contype IN ('p','u','x')) 41 | INNER JOIN pg_catalog.pg_namespace AS n ON (c2.relnamespace = n.oid) 42 | WHERE true 43 | {{if eq $.DbSchema "*"}} 44 | AND n.nspname NOT LIKE 'pg_%' 45 | AND n.nspname <> 'information_schema' 46 | {{else}} 47 | AND n.nspname = '{{$.DbSchema}}' 48 | {{end}} 49 | ` 50 | t := template.New("IndexSqlTmpl") 51 | template.Must(t.Parse(sql)) 52 | return t 53 | } 54 | 55 | // ================================== 56 | // IndexRows definition 57 | // ================================== 58 | 59 | // IndexRows is a sortable slice of string maps 60 | type IndexRows []map[string]string 61 | 62 | func (slice IndexRows) Len() int { 63 | return len(slice) 64 | } 65 | 66 | func (slice IndexRows) Less(i, j int) bool { 67 | //fmt.Printf("--Less %s:%s with %s:%s", slice[i]["table_name"], slice[i]["column_name"], slice[j]["table_name"], slice[j]["column_name"]) 68 | return slice[i]["compare_name"] < slice[j]["compare_name"] 69 | } 70 | 71 | func (slice IndexRows) Swap(i, j int) { 72 | //fmt.Printf("--Swapping %d/%s:%s with %d/%s:%s \n", i, slice[i]["table_name"], slice[i]["index_name"], j, slice[j]["table_name"], slice[j]["index_name"]) 73 | slice[i], slice[j] = slice[j], slice[i] 74 | } 75 | 76 | // ================================== 77 | // IndexSchema definition 78 | // (implements Schema -- defined in pgdiff.go) 79 | // ================================== 80 | 81 | // IndexSchema holds a slice of rows from one of the databases as well as 82 | // a reference to the current row of data we're viewing. 83 | type IndexSchema struct { 84 | rows IndexRows 85 | rowNum int 86 | done bool 87 | } 88 | 89 | // get returns the value from the current row for the given key 90 | func (c *IndexSchema) get(key string) string { 91 | if c.rowNum >= len(c.rows) { 92 | return "" 93 | } 94 | return c.rows[c.rowNum][key] 95 | } 96 | 97 | // get returns the current row for the given key 98 | func (c *IndexSchema) getRow() map[string]string { 99 | if c.rowNum >= len(c.rows) { 100 | return make(map[string]string) 101 | } 102 | return c.rows[c.rowNum] 103 | } 104 | 105 | // NextRow increments the rowNum and tells you whether or not there are more 106 | func (c *IndexSchema) NextRow() bool { 107 | if c.rowNum >= len(c.rows)-1 { 108 | c.done = true 109 | } 110 | c.rowNum = c.rowNum + 1 111 | return !c.done 112 | } 113 | 114 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 115 | func (c *IndexSchema) Compare(obj interface{}) int { 116 | c2, ok := obj.(*IndexSchema) 117 | if !ok { 118 | fmt.Println("Error!!!, change needs a IndexSchema instance", c2) 119 | return +999 120 | } 121 | 122 | if len(c.get("table_name")) == 0 || len(c.get("index_name")) == 0 { 123 | fmt.Printf("--Comparing (table_name and/or index_name is empty): %v\n", c.getRow()) 124 | fmt.Printf("-- %v\n", c2.getRow()) 125 | } 126 | 127 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 128 | return val 129 | } 130 | 131 | // Add prints SQL to add the index 132 | func (c *IndexSchema) Add() { 133 | schema := dbInfo2.DbSchema 134 | if schema == "*" { 135 | schema = c.get("schema_name") 136 | } 137 | 138 | // Assertion 139 | if c.get("index_def") == "null" || len(c.get("index_def")) == 0 { 140 | fmt.Printf("-- Add Unexpected situation in index.go: there is no index_def for %s.%s %s\n", schema, c.get("table_name"), c.get("index_name")) 141 | return 142 | } 143 | 144 | // If we are comparing two different schemas against each other, we need to do some 145 | // modification of the first index_def so we create the index in the write schema 146 | indexDef := c.get("index_def") 147 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 148 | indexDef = strings.Replace( 149 | indexDef, 150 | fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), 151 | fmt.Sprintf(" %s.%s ", dbInfo2.DbSchema, c.get("table_name")), 152 | -1) 153 | } 154 | 155 | fmt.Printf("%v;\n", indexDef) 156 | 157 | if c.get("constraint_def") != "null" { 158 | // Create the constraint using the index we just created 159 | if c.get("pk") == "true" { 160 | // Add primary key using the index 161 | fmt.Printf("ALTER TABLE %s.%s ADD CONSTRAINT %s PRIMARY KEY USING INDEX %s; -- (1)\n", schema, c.get("table_name"), c.get("index_name"), c.get("index_name")) 162 | } else if c.get("uq") == "true" { 163 | // Add unique constraint using the index 164 | fmt.Printf("ALTER TABLE %s.%s ADD CONSTRAINT %s UNIQUE USING INDEX %s; -- (2)\n", schema, c.get("table_name"), c.get("index_name"), c.get("index_name")) 165 | } 166 | } 167 | } 168 | 169 | // Drop prints SQL to drop the index 170 | func (c *IndexSchema) Drop() { 171 | if c.get("constraint_def") != "null" { 172 | fmt.Println("-- Warning, this may drop foreign keys pointing at this column. Make sure you re-run the FOREIGN_KEY diff after running this SQL.") 173 | fmt.Printf("ALTER TABLE %s.%s DROP CONSTRAINT %s CASCADE; -- %s\n", c.get("schema_name"), c.get("table_name"), c.get("index_name"), c.get("constraint_def")) 174 | } 175 | fmt.Printf("DROP INDEX %s.%s;\n", c.get("schema_name"), c.get("index_name")) 176 | } 177 | 178 | // Change handles the case where the table and column match, but the details do not 179 | func (c *IndexSchema) Change(obj interface{}) { 180 | c2, ok := obj.(*IndexSchema) 181 | if !ok { 182 | fmt.Println("-- Error!!!, Change needs an IndexSchema instance", c2) 183 | } 184 | 185 | // Table and constraint name matches... We need to make sure the details match 186 | 187 | // NOTE that there should always be an index_def for both c and c2 (but we're checking below anyway) 188 | if len(c.get("index_def")) == 0 { 189 | fmt.Printf("-- Change: Unexpected situation in index.go: index_def is empty for 1: %v 2:%v\n", c.getRow(), c2.getRow()) 190 | return 191 | } 192 | if len(c2.get("index_def")) == 0 { 193 | fmt.Printf("-- Change: Unexpected situation in index.go: index_def is empty for 2: %v 1: %v\n", c2.getRow(), c.getRow()) 194 | return 195 | } 196 | 197 | if c.get("constraint_def") != c2.get("constraint_def") { 198 | // c1.constraint and c2.constraint are just different 199 | fmt.Printf("-- CHANGE: Different defs on %s:\n-- %s\n-- %s\n", c.get("table_name"), c.get("constraint_def"), c2.get("constraint_def")) 200 | if c.get("constraint_def") == "null" { 201 | // c1.constraint does not exist, c2.constraint does, so 202 | // Drop constraint 203 | fmt.Printf("DROP INDEX %s; -- %s \n", c2.get("index_name"), c2.get("index_def")) 204 | } else if c2.get("constraint_def") == "null" { 205 | // c1.constraint exists, c2.constraint does not, so 206 | // Add constraint 207 | if c.get("index_def") == c2.get("index_def") { 208 | // Indexes match, so 209 | // Add constraint using the index 210 | if c.get("pk") == "true" { 211 | // Add primary key using the index 212 | fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY USING INDEX %s; -- (3)\n", c.get("table_name"), c.get("index_name"), c.get("index_name")) 213 | } else if c.get("uq") == "true" { 214 | // Add unique constraint using the index 215 | fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE USING INDEX %s; -- (4)\n", c.get("table_name"), c.get("index_name"), c.get("index_name")) 216 | } else { 217 | 218 | } 219 | } else { 220 | // Drop the c2 index, create a copy of the c1 index 221 | fmt.Printf("DROP INDEX %s; -- %s \n", c2.get("index_name"), c2.get("index_def")) 222 | } 223 | // WIP 224 | //fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s %s;\n", c.get("table_name"), c.get("index_name"), c.get("constraint_def")) 225 | 226 | } else if c.get("index_def") != c2.get("index_def") { 227 | // The constraints match 228 | } 229 | 230 | return 231 | } 232 | 233 | // At this point, we know that the constraint_def matches. Compare the index_def 234 | 235 | indexDef1 := c.get("index_def") 236 | indexDef2 := c2.get("index_def") 237 | 238 | // If we are comparing two different schemas against each other, we need to do 239 | // some modification of the first index_def so it looks more like the second 240 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 241 | indexDef1 = strings.Replace( 242 | indexDef1, 243 | fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), 244 | fmt.Sprintf(" %s.%s ", c2.get("schema_name"), c2.get("table_name")), 245 | -1, 246 | ) 247 | } 248 | 249 | if indexDef1 != indexDef2 { 250 | // Notice that, if we are here, then the two constraint_defs match (both may be empty) 251 | // The indexes do not match, but the constraints do 252 | if !strings.HasPrefix(c.get("index_def"), c2.get("index_def")) && 253 | !strings.HasPrefix(c2.get("index_def"), c.get("index_def")) { 254 | fmt.Println("--\n--CHANGE: index defs are different for identical constraint defs:") 255 | fmt.Printf("-- %s\n-- %s\n", c.get("index_def"), c2.get("index_def")) 256 | 257 | // Drop the index (and maybe the constraint) so we can recreate the index 258 | c.Drop() 259 | 260 | // Recreate the index (and a constraint if specified) 261 | c.Add() 262 | } 263 | } 264 | 265 | } 266 | 267 | // compareIndexes outputs Sql to make the indexes match between to DBs or schemas 268 | func compareIndexes(conn1 *sql.DB, conn2 *sql.DB) { 269 | 270 | buf1 := new(bytes.Buffer) 271 | indexSqlTemplate.Execute(buf1, dbInfo1) 272 | 273 | buf2 := new(bytes.Buffer) 274 | indexSqlTemplate.Execute(buf2, dbInfo2) 275 | 276 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 277 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 278 | 279 | rows1 := make(IndexRows, 0) 280 | for row := range rowChan1 { 281 | rows1 = append(rows1, row) 282 | } 283 | sort.Sort(rows1) 284 | 285 | rows2 := make(IndexRows, 0) 286 | for row := range rowChan2 { 287 | rows2 = append(rows2, row) 288 | } 289 | sort.Sort(rows2) 290 | 291 | // We have to explicitly type this as Schema here for some unknown reason 292 | var schema1 Schema = &IndexSchema{rows: rows1, rowNum: -1} 293 | var schema2 Schema = &IndexSchema{rows: rows2, rowNum: -1} 294 | 295 | // Compare the indexes 296 | doDiff(schema1, schema2) 297 | } 298 | -------------------------------------------------------------------------------- /mat_view.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | "sort" 13 | 14 | "github.com/joncrlsn/misc" 15 | "github.com/joncrlsn/pgutil" 16 | ) 17 | 18 | // ================================== 19 | // MatViewRows definition 20 | // ================================== 21 | 22 | // MatViewRows is a sortable slice of string maps 23 | type MatViewRows []map[string]string 24 | 25 | func (slice MatViewRows) Len() int { 26 | return len(slice) 27 | } 28 | 29 | func (slice MatViewRows) Less(i, j int) bool { 30 | return slice[i]["matviewname"] < slice[j]["matviewname"] 31 | } 32 | 33 | func (slice MatViewRows) Swap(i, j int) { 34 | slice[i], slice[j] = slice[j], slice[i] 35 | } 36 | 37 | // MatViewSchema holds a channel streaming matview information from one of the databases as well as 38 | // a reference to the current row of data we're matviewing. 39 | // 40 | // MatViewSchema implements the Schema interface defined in pgdiff.go 41 | type MatViewSchema struct { 42 | rows MatViewRows 43 | rowNum int 44 | done bool 45 | } 46 | 47 | // get returns the value from the current row for the given key 48 | func (c *MatViewSchema) get(key string) string { 49 | if c.rowNum >= len(c.rows) { 50 | return "" 51 | } 52 | return c.rows[c.rowNum][key] 53 | } 54 | 55 | // NextRow increments the rowNum and tells you whether or not there are more 56 | func (c *MatViewSchema) NextRow() bool { 57 | if c.rowNum >= len(c.rows)-1 { 58 | c.done = true 59 | } 60 | c.rowNum = c.rowNum + 1 61 | return !c.done 62 | } 63 | 64 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 65 | func (c *MatViewSchema) Compare(obj interface{}) int { 66 | c2, ok := obj.(*MatViewSchema) 67 | if !ok { 68 | fmt.Println("Error!!!, Compare(obj) needs a MatViewSchema instance", c2) 69 | return +999 70 | } 71 | 72 | val := misc.CompareStrings(c.get("matviewname"), c2.get("matviewname")) 73 | //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("matviewname"), c2.get("matviewname")) 74 | return val 75 | } 76 | 77 | // Add returns SQL to create the matview 78 | func (c MatViewSchema) Add() { 79 | fmt.Printf("CREATE MATERIALIZED VIEW %s AS %s \n\n%s \n\n", c.get("matviewname"), c.get("definition"), c.get("indexdef")) 80 | } 81 | 82 | // Drop returns SQL to drop the matview 83 | func (c MatViewSchema) Drop() { 84 | fmt.Printf("DROP MATERIALIZED VIEW %s;\n\n", c.get("matviewname")) 85 | } 86 | 87 | // Change handles the case where the names match, but the definition does not 88 | func (c MatViewSchema) Change(obj interface{}) { 89 | c2, ok := obj.(*MatViewSchema) 90 | if !ok { 91 | fmt.Println("Error!!!, Change needs a MatViewSchema instance", c2) 92 | } 93 | if c.get("definition") != c2.get("definition") { 94 | fmt.Printf("DROP MATERIALIZED VIEW %s;\n\n", c.get("matviewname")) 95 | fmt.Printf("CREATE MATERIALIZED VIEW %s AS %s \n\n%s \n\n", c.get("matviewname"), c.get("definition"), c.get("indexdef")) 96 | } 97 | } 98 | 99 | // compareMatViews outputs SQL to make the matviews match between DBs 100 | func compareMatViews(conn1 *sql.DB, conn2 *sql.DB) { 101 | sql := ` 102 | WITH matviews as ( SELECT schemaname || '.' || matviewname AS matviewname, 103 | definition 104 | FROM pg_catalog.pg_matviews 105 | WHERE schemaname NOT LIKE 'pg_%' 106 | ) 107 | SELECT 108 | matviewname, 109 | definition, 110 | COALESCE(string_agg(indexdef, ';' || E'\n\n') || ';', '') as indexdef 111 | FROM matviews 112 | LEFT JOIN pg_catalog.pg_indexes on matviewname = schemaname || '.' || tablename 113 | group by matviewname, definition 114 | ORDER BY 115 | matviewname; 116 | ` 117 | 118 | rowChan1, _ := pgutil.QueryStrings(conn1, sql) 119 | rowChan2, _ := pgutil.QueryStrings(conn2, sql) 120 | 121 | rows1 := make(MatViewRows, 0) 122 | for row := range rowChan1 { 123 | rows1 = append(rows1, row) 124 | } 125 | sort.Sort(rows1) 126 | 127 | rows2 := make(MatViewRows, 0) 128 | for row := range rowChan2 { 129 | rows2 = append(rows2, row) 130 | } 131 | sort.Sort(rows2) 132 | 133 | // We have to explicitly type this as Schema here 134 | var schema1 Schema = &MatViewSchema{rows: rows1, rowNum: -1} 135 | var schema2 Schema = &MatViewSchema{rows: rows2, rowNum: -1} 136 | 137 | // Compare the matviews 138 | doDiff(schema1, schema2) 139 | } 140 | -------------------------------------------------------------------------------- /owner.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "text/template" 17 | ) 18 | 19 | var ( 20 | ownerSqlTemplate = initOwnerSqlTemplate() 21 | ) 22 | 23 | // Initializes the Sql template 24 | func initOwnerSqlTemplate() *template.Template { 25 | sql := ` 26 | SELECT n.nspname AS schema_name 27 | , {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}c.relname || '.' || c.relname AS compare_name 28 | , c.relname AS relationship_name 29 | , a.rolname AS owner 30 | , CASE WHEN c.relkind = 'r' THEN 'TABLE' 31 | WHEN c.relkind = 'S' THEN 'SEQUENCE' 32 | WHEN c.relkind = 'v' THEN 'VIEW' 33 | ELSE c.relkind::varchar END AS type 34 | FROM pg_class AS c 35 | INNER JOIN pg_roles AS a ON (a.oid = c.relowner) 36 | INNER JOIN pg_namespace AS n ON (n.oid = c.relnamespace) 37 | WHERE c.relkind IN ('r', 'S', 'v') 38 | {{if eq $.DbSchema "*" }} 39 | AND n.nspname NOT LIKE 'pg_%' 40 | AND n.nspname <> 'information_schema' 41 | {{else}} 42 | AND n.nspname = '{{$.DbSchema}}' 43 | {{end}} 44 | ;` 45 | 46 | t := template.New("OwnerSqlTmpl") 47 | template.Must(t.Parse(sql)) 48 | return t 49 | } 50 | 51 | // ================================== 52 | // OwnerRows definition 53 | // ================================== 54 | 55 | // OwnerRows is a sortable slice of string maps 56 | type OwnerRows []map[string]string 57 | 58 | func (slice OwnerRows) Len() int { 59 | return len(slice) 60 | } 61 | 62 | func (slice OwnerRows) Less(i, j int) bool { 63 | return slice[i]["compare_name"] < slice[j]["compare_name"] 64 | } 65 | 66 | func (slice OwnerRows) Swap(i, j int) { 67 | slice[i], slice[j] = slice[j], slice[i] 68 | } 69 | 70 | // ================================== 71 | // OwnerSchema definition 72 | // (implements Schema -- defined in pgdiff.go) 73 | // ================================== 74 | 75 | // OwnerSchema holds a slice of rows from one of the databases as well as 76 | // a reference to the current row of data we're viewing. 77 | type OwnerSchema struct { 78 | rows OwnerRows 79 | rowNum int 80 | done bool 81 | } 82 | 83 | // get returns the value from the current row for the given key 84 | func (c *OwnerSchema) get(key string) string { 85 | if c.rowNum >= len(c.rows) { 86 | return "" 87 | } 88 | return c.rows[c.rowNum][key] 89 | } 90 | 91 | // get returns the current row for the given key 92 | func (c *OwnerSchema) getRow() map[string]string { 93 | if c.rowNum >= len(c.rows) { 94 | return make(map[string]string) 95 | } 96 | return c.rows[c.rowNum] 97 | } 98 | 99 | // NextRow increments the rowNum and tells you whether or not there are more 100 | func (c *OwnerSchema) NextRow() bool { 101 | if c.rowNum >= len(c.rows)-1 { 102 | c.done = true 103 | } 104 | c.rowNum = c.rowNum + 1 105 | return !c.done 106 | } 107 | 108 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 109 | func (c *OwnerSchema) Compare(obj interface{}) int { 110 | c2, ok := obj.(*OwnerSchema) 111 | if !ok { 112 | fmt.Println("Error!!!, Compare needs a OwnerSchema instance", c2) 113 | return +999 114 | } 115 | 116 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 117 | return val 118 | } 119 | 120 | // Add generates SQL to add the table/view owner 121 | func (c OwnerSchema) Add() { 122 | fmt.Printf("-- Notice!, db2 has no %s named %s. First, run pgdiff with the %s option.\n", c.get("type"), c.get("relationship_name"), c.get("type")) 123 | } 124 | 125 | // Drop generates SQL to drop the owner 126 | func (c OwnerSchema) Drop() { 127 | fmt.Printf("-- Notice!, db2 has a %s that db1 does not: %s. First, run pgdiff with the %s option.\n", c.get("type"), c.get("relationship_name"), c.get("type")) 128 | } 129 | 130 | // Change handles the case where the relationship name matches, but the owner does not 131 | func (c OwnerSchema) Change(obj interface{}) { 132 | c2, ok := obj.(*OwnerSchema) 133 | if !ok { 134 | fmt.Println("-- Error!!!, Change needs a OwnerSchema instance", c2) 135 | } 136 | 137 | if c.get("owner") != c2.get("owner") { 138 | fmt.Printf("ALTER %s %s.%s OWNER TO %s; \n", c.get("type"), c2.get("schema_name"), c.get("relationship_name"), c.get("owner")) 139 | } 140 | } 141 | 142 | // compareOwners compares the ownership of tables, sequences, and views between two databases or schemas 143 | func compareOwners(conn1 *sql.DB, conn2 *sql.DB) { 144 | 145 | buf1 := new(bytes.Buffer) 146 | ownerSqlTemplate.Execute(buf1, dbInfo1) 147 | 148 | buf2 := new(bytes.Buffer) 149 | ownerSqlTemplate.Execute(buf2, dbInfo2) 150 | 151 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 152 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 153 | 154 | rows1 := make(OwnerRows, 0) 155 | for row := range rowChan1 { 156 | rows1 = append(rows1, row) 157 | } 158 | sort.Sort(rows1) 159 | 160 | rows2 := make(OwnerRows, 0) 161 | for row := range rowChan2 { 162 | rows2 = append(rows2, row) 163 | } 164 | sort.Sort(rows2) 165 | 166 | // We have to explicitly type this as Schema here for some unknown reason 167 | var schema1 Schema = &OwnerSchema{rows: rows1, rowNum: -1} 168 | var schema2 Schema = &OwnerSchema{rows: rows2, rowNum: -1} 169 | 170 | doDiff(schema1, schema2) 171 | } 172 | -------------------------------------------------------------------------------- /pgdiff.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by the MIT 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | 13 | "os" 14 | "strings" 15 | 16 | flag "github.com/ogier/pflag" 17 | 18 | "github.com/joncrlsn/pgutil" 19 | _ "github.com/lib/pq" 20 | ) 21 | 22 | // Schema is a database definition (table, column, constraint, indes, role, etc) that can be 23 | // added, dropped, or changed to match another database. 24 | type Schema interface { 25 | Compare(schema interface{}) int 26 | Add() 27 | Drop() 28 | Change(schema interface{}) 29 | NextRow() bool 30 | } 31 | 32 | const ( 33 | version = "0.9.3" 34 | ) 35 | 36 | var ( 37 | args []string 38 | dbInfo1 pgutil.DbInfo 39 | dbInfo2 pgutil.DbInfo 40 | schemaType string 41 | ) 42 | 43 | /* 44 | * Initialize anything needed later 45 | */ 46 | func init() { 47 | } 48 | 49 | /* 50 | * Do the main logic 51 | */ 52 | func main() { 53 | 54 | var helpPtr = flag.BoolP("help", "?", false, "print help information") 55 | var versionPtr = flag.BoolP("version", "V", false, "print version information") 56 | 57 | dbInfo1, dbInfo2 = parseFlags() 58 | 59 | // Remaining args: 60 | args = flag.Args() 61 | 62 | if *helpPtr { 63 | usage() 64 | } 65 | 66 | if *versionPtr { 67 | fmt.Fprintf(os.Stderr, "%s - version %s\n", os.Args[0], version) 68 | fmt.Fprintln(os.Stderr, "Copyright (c) 2017 Jon Carlson. All rights reserved.") 69 | fmt.Fprintln(os.Stderr, "Use of this source code is governed by the MIT license") 70 | fmt.Fprintln(os.Stderr, "that can be found here: http://opensource.org/licenses/MIT") 71 | os.Exit(1) 72 | } 73 | 74 | if len(args) == 0 { 75 | fmt.Println("The required first argument is SchemaType: SCHEMA, ROLE, SEQUENCE, TABLE, VIEW, MATVIEW, COLUMN, INDEX, FOREIGN_KEY, OWNER, GRANT_RELATIONSHIP, GRANT_ATTRIBUTE") 76 | os.Exit(1) 77 | } 78 | 79 | // Verify schemas 80 | schemas := dbInfo1.DbSchema + dbInfo2.DbSchema 81 | if schemas != "**" && strings.Contains(schemas, "*") { 82 | fmt.Println("If one schema is an asterisk, both must be.") 83 | os.Exit(1) 84 | } 85 | 86 | schemaType = strings.ToUpper(args[0]) 87 | fmt.Println("-- schemaType:", schemaType) 88 | 89 | fmt.Println("-- db1:", dbInfo1) 90 | fmt.Println("-- db2:", dbInfo2) 91 | fmt.Println("-- Run the following SQL against db2:") 92 | 93 | conn1, err := dbInfo1.Open() 94 | check("opening database 1", err) 95 | 96 | conn2, err := dbInfo2.Open() 97 | check("opening database 2", err) 98 | 99 | // This section needs to be improved so that you do not need to choose the type 100 | // of alter statements to generate. Rather, all should be generated in the 101 | // proper order. 102 | if schemaType == "ALL" { 103 | if dbInfo1.DbSchema == "*" { 104 | compareSchematas(conn1, conn2) 105 | } 106 | compareSchematas(conn1, conn2) 107 | compareRoles(conn1, conn2) 108 | compareSequences(conn1, conn2) 109 | compareTables(conn1, conn2) 110 | compareColumns(conn1, conn2) 111 | compareIndexes(conn1, conn2) // includes PK and Unique constraints 112 | compareViews(conn1, conn2) 113 | compareMatViews(conn1, conn2) 114 | compareForeignKeys(conn1, conn2) 115 | compareFunctions(conn1, conn2) 116 | compareTriggers(conn1, conn2) 117 | compareOwners(conn1, conn2) 118 | compareGrantRelationships(conn1, conn2) 119 | compareGrantAttributes(conn1, conn2) 120 | } else if schemaType == "SCHEMA" { 121 | compareSchematas(conn1, conn2) 122 | } else if schemaType == "ROLE" { 123 | compareRoles(conn1, conn2) 124 | } else if schemaType == "SEQUENCE" { 125 | compareSequences(conn1, conn2) 126 | } else if schemaType == "TABLE" { 127 | compareTables(conn1, conn2) 128 | } else if schemaType == "COLUMN" { 129 | compareColumns(conn1, conn2) 130 | } else if schemaType == "TABLE_COLUMN" { 131 | compareTableColumns(conn1, conn2) 132 | } else if schemaType == "INDEX" { 133 | compareIndexes(conn1, conn2) 134 | } else if schemaType == "VIEW" { 135 | compareViews(conn1, conn2) 136 | } else if schemaType == "MATVIEW" { 137 | compareMatViews(conn1, conn2) 138 | } else if schemaType == "FOREIGN_KEY" { 139 | compareForeignKeys(conn1, conn2) 140 | } else if schemaType == "FUNCTION" { 141 | compareFunctions(conn1, conn2) 142 | } else if schemaType == "TRIGGER" { 143 | compareTriggers(conn1, conn2) 144 | } else if schemaType == "OWNER" { 145 | compareOwners(conn1, conn2) 146 | } else if schemaType == "GRANT_RELATIONSHIP" { 147 | compareGrantRelationships(conn1, conn2) 148 | } else if schemaType == "GRANT_ATTRIBUTE" { 149 | compareGrantAttributes(conn1, conn2) 150 | } else { 151 | fmt.Println("Not yet handled:", schemaType) 152 | } 153 | } 154 | 155 | /* 156 | * This is a generic diff function that compares tables, columns, indexes, roles, grants, etc. 157 | * Different behaviors are specified the Schema implementations 158 | */ 159 | func doDiff(db1 Schema, db2 Schema) { 160 | 161 | more1 := db1.NextRow() 162 | more2 := db2.NextRow() 163 | for more1 || more2 { 164 | compareVal := db1.Compare(db2) 165 | if compareVal == 0 { 166 | // table and column match, look for non-identifying changes 167 | db1.Change(db2) 168 | more1 = db1.NextRow() 169 | more2 = db2.NextRow() 170 | } else if compareVal < 0 { 171 | // db2 is missing a value that db1 has 172 | if more1 { 173 | db1.Add() 174 | more1 = db1.NextRow() 175 | } else { 176 | // db1 is at the end 177 | db2.Drop() 178 | more2 = db2.NextRow() 179 | } 180 | } else if compareVal > 0 { 181 | // db2 has an extra column that we don't want 182 | if more2 { 183 | db2.Drop() 184 | more2 = db2.NextRow() 185 | } else { 186 | // db2 is at the end 187 | db1.Add() 188 | more1 = db1.NextRow() 189 | } 190 | } 191 | } 192 | } 193 | 194 | func usage() { 195 | fmt.Fprintf(os.Stderr, "%s - version %s\n", os.Args[0], version) 196 | fmt.Fprintf(os.Stderr, "usage: %s [] \n", os.Args[0]) 197 | fmt.Fprintln(os.Stderr, ` 198 | Compares the schema between two PostgreSQL databases and generates alter statements 199 | that can be *manually* run against the second database. 200 | 201 | Options: 202 | -?, --help : print help information 203 | -V, --version : print version information 204 | -v, --verbose : print extra run information 205 | -U, --user1 : first postgres user 206 | -u, --user2 : second postgres user 207 | -H, --host1 : first database host. default is localhost 208 | -h, --host2 : second database host. default is localhost 209 | -P, --port1 : first port. default is 5432 210 | -p, --port2 : second port. default is 5432 211 | -D, --dbname1 : first database name 212 | -d, --dbname2 : second database name 213 | -S, --schema1 : first schema. default is all schemas 214 | -s, --schema2 : second schema. default is all schemas 215 | 216 | can be: ALL, SCHEMA, ROLE, SEQUENCE, TABLE, TABLE_COLUMN, VIEW, MATVIEW, COLUMN, INDEX, FOREIGN_KEY, OWNER, GRANT_RELATIONSHIP, GRANT_ATTRIBUTE, TRIGGER, FUNCTION`) 217 | 218 | os.Exit(2) 219 | } 220 | 221 | func check(msg string, err error) { 222 | if err != nil { 223 | log.Fatal("Error "+msg, err) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /pgdiff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # pgdiff.sh runs a compare on each schema type in the order that usually creates the fewest conflicts. 4 | # At each step you are allowed to review and change the generated SQL before optionally running it. 5 | # You are also allowed to rerun the diff on a type before continuing to the next type. This is 6 | # helpful when, for example dependent views are defined in the file before the view it depends on. 7 | # 8 | # If you convert this to a windows batch file (or, even better, a Go program), please share it. 9 | # 10 | # Example script usage: 11 | # USER1=db-user HOST1=db-server NAME1=db1 USER2=db-user HOST2=db-server NAME2=db2 pgdiff.sh 12 | # 13 | # Example without this script: 14 | # pgdiff -U postgres -W supersecret -H dbhost1 -P 5432 -D maindb -O 'sslmode=disable' \ 15 | # -u postgres -w supersecret -h dbhost2 -p 5432 -d stagingdb -o 'sslmode=disable' \ 16 | # COLUMN 17 | # 18 | 19 | [[ -z $USER1 ]] && USER1=c42 20 | [[ -z $HOST1 ]] && HOST1=localhost 21 | [[ -z $PORT1 ]] && PORT1=5432 22 | [[ -z $NAME1 ]] && NAME1='cp-520' 23 | [[ -z $OPT1 ]] && OPT1='sslmode=disable' 24 | 25 | [[ -z $USER2 ]] && USER2=c42 26 | [[ -z $HOST2 ]] && HOST2=localhost 27 | [[ -z $PORT2 ]] && PORT2=5432 28 | [[ -z $NAME2 ]] && NAME2=cp-pentest 29 | [[ -z $OPT2 ]] && OPT2='sslmode=disable' 30 | 31 | echo "This is the reference database:" 32 | echo " ${USER1}@${HOST1}:${PORT1}/$NAME1" 33 | read -sp "Enter DB password: " passw 34 | PASS1=$passw 35 | PASS2=$passw 36 | 37 | echo 38 | echo "This database may be changed (if you choose):" 39 | echo " ${USER2}@${HOST2}:${PORT2}/$NAME2" 40 | read -sp "Enter DB password (defaults to previous password): " passw 41 | [[ -n $passw ]] && PASS2=$passw 42 | echo 43 | 44 | let i=0 45 | function rundiff() { 46 | ((i++)) 47 | local TYPE=$1 48 | local sqlFile="${i}-${TYPE}.sql" 49 | local rerun=yes 50 | while [[ $rerun == yes ]]; do 51 | rerun=no 52 | echo "Generating diff for $TYPE... " 53 | ./pgdiff -U "$USER1" -W "$PASS1" -H "$HOST1" -P "$PORT1" -D "$NAME1" -O "$OPT1" \ 54 | -u "$USER2" -w "$PASS2" -h "$HOST2" -p "$PORT2" -d "$NAME2" -o "$OPT2" \ 55 | $TYPE > "$sqlFile" 56 | rc=$? && [[ $rc != 0 ]] && exit $rc 57 | if [[ $(cat "$sqlFile" | wc -l) -gt 4 ]]; then 58 | vi "$sqlFile" 59 | read -p "Do you wish to run this against ${NAME2}? [yN]: " yn 60 | if [[ $yn =~ ^y ]]; then 61 | PGPASSWORD="$PASS2" ./pgrun -U $USER2 -h $HOST2 -p $PORT2 -d $NAME2 -O "$OPT2" -f "$sqlFile" 62 | read -p "Rerun diff for $TYPE? [yN]: " yn 63 | [[ $yn =~ ^[yY] ]] && rerun=yes 64 | fi 65 | else 66 | read -p "No changes found for $TYPE (Press Enter) " x 67 | fi 68 | done 69 | echo 70 | } 71 | 72 | rundiff ROLE 73 | rundiff FUNCTION 74 | rundiff SCHEMA 75 | rundiff SEQUENCE 76 | rundiff TABLE 77 | rundiff COLUMN 78 | rundiff MATVIEW 79 | rundiff INDEX 80 | rundiff VIEW 81 | rundiff TRIGGER 82 | rundiff OWNER 83 | rundiff FOREIGN_KEY 84 | rundiff GRANT_RELATIONSHIP 85 | rundiff GRANT_ATTRIBUTE 86 | 87 | echo 88 | echo "Done!" 89 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2014 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by the MIT 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | "github.com/joncrlsn/misc" 13 | "github.com/joncrlsn/pgutil" 14 | "regexp" 15 | "sort" 16 | "strings" 17 | ) 18 | 19 | var curlyBracketRegex = regexp.MustCompile("[{}]") 20 | 21 | // RoleRows is a sortable slice of string maps 22 | type RoleRows []map[string]string 23 | 24 | func (slice RoleRows) Len() int { 25 | return len(slice) 26 | } 27 | 28 | func (slice RoleRows) Less(i, j int) bool { 29 | return slice[i]["rolname"] < slice[j]["rolname"] 30 | } 31 | 32 | func (slice RoleRows) Swap(i, j int) { 33 | slice[i], slice[j] = slice[j], slice[i] 34 | } 35 | 36 | // ================================== 37 | // RoleSchema definition 38 | // (implements Schema -- defined in pgdiff.go) 39 | // ================================== 40 | 41 | // RoleSchema holds a slice of rows from one of the databases as well as 42 | // a reference to the current row of data we're viewing. 43 | type RoleSchema struct { 44 | rows RoleRows 45 | rowNum int 46 | done bool 47 | } 48 | 49 | // get returns the value from the current row for the given key 50 | func (c *RoleSchema) get(key string) string { 51 | if c.rowNum >= len(c.rows) { 52 | return "" 53 | } 54 | return c.rows[c.rowNum][key] 55 | } 56 | 57 | // get returns the current row for the given key 58 | func (c *RoleSchema) getRow() map[string]string { 59 | if c.rowNum >= len(c.rows) { 60 | return make(map[string]string) 61 | } 62 | return c.rows[c.rowNum] 63 | } 64 | 65 | // NextRow increments the rowNum and tells you whether or not there are more 66 | func (c *RoleSchema) NextRow() bool { 67 | if c.rowNum >= len(c.rows)-1 { 68 | c.done = true 69 | } 70 | c.rowNum = c.rowNum + 1 71 | return !c.done 72 | } 73 | 74 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 75 | func (c *RoleSchema) Compare(obj interface{}) int { 76 | c2, ok := obj.(*RoleSchema) 77 | if !ok { 78 | fmt.Println("Error!!!, change needs a RoleSchema instance", c2) 79 | return +999 80 | } 81 | 82 | val := misc.CompareStrings(c.get("rolname"), c2.get("rolname")) 83 | return val 84 | } 85 | 86 | /* 87 | CREATE ROLE name [ [ WITH ] option [ ... ] ] 88 | 89 | where option can be: 90 | 91 | SUPERUSER | NOSUPERUSER 92 | | CREATEDB | NOCREATEDB 93 | | CREATEROLE | NOCREATEROLE 94 | | CREATEUSER | NOCREATEUSER 95 | | INHERIT | NOINHERIT 96 | | LOGIN | NOLOGIN 97 | | REPLICATION | NOREPLICATION 98 | | CONNECTION LIMIT connlimit 99 | | [ ENCRYPTED | UNENCRYPTED ] PASSWORD 'password' 100 | | VALID UNTIL 'timestamp' 101 | | IN ROLE role_name [, ...] 102 | | IN GROUP role_name [, ...] 103 | | ROLE role_name [, ...] 104 | | ADMIN role_name [, ...] 105 | | USER role_name [, ...] 106 | | SYSID uid 107 | */ 108 | 109 | // Add generates SQL to add the constraint/index 110 | func (c RoleSchema) Add() { 111 | 112 | // We don't care about efficiency here so we just concat strings 113 | options := " WITH PASSWORD 'changeme'" 114 | 115 | if c.get("rolcanlogin") == "true" { 116 | options += " LOGIN" 117 | } else { 118 | options += " NOLOGIN" 119 | } 120 | 121 | if c.get("rolsuper") == "true" { 122 | options += " SUPERUSER" 123 | } 124 | 125 | if c.get("rolcreatedb") == "true" { 126 | options += " CREATEDB" 127 | } 128 | 129 | if c.get("rolcreaterole") == "true" { 130 | options += " CREATEROLE" 131 | } 132 | 133 | if c.get("rolinherit") == "true" { 134 | options += " INHERIT" 135 | } else { 136 | options += " NOINHERIT" 137 | } 138 | 139 | if c.get("rolreplication") == "true" { 140 | options += " REPLICATION" 141 | } else { 142 | options += " NOREPLICATION" 143 | } 144 | 145 | if c.get("rolconnlimit") != "-1" && len(c.get("rolconnlimit")) > 0 { 146 | options += " CONNECTION LIMIT " + c.get("rolconnlimit") 147 | } 148 | if c.get("rolvaliduntil") != "null" { 149 | options += fmt.Sprintf(" VALID UNTIL '%s'", c.get("rolvaliduntil")) 150 | } 151 | 152 | fmt.Printf("CREATE ROLE %s%s;\n", c.get("rolname"), options) 153 | } 154 | 155 | // Drop generates SQL to drop the role 156 | func (c RoleSchema) Drop() { 157 | fmt.Printf("DROP ROLE %s;\n", c.get("rolname")) 158 | } 159 | 160 | // Change handles the case where the role name matches, but the details do not 161 | func (c RoleSchema) Change(obj interface{}) { 162 | c2, ok := obj.(*RoleSchema) 163 | if !ok { 164 | fmt.Println("Error!!!, Change needs a RoleSchema instance", c2) 165 | } 166 | 167 | options := "" 168 | if c.get("rolsuper") != c2.get("rolsuper") { 169 | if c.get("rolsuper") == "true" { 170 | options += " SUPERUSER" 171 | } else { 172 | options += " NOSUPERUSER" 173 | } 174 | } 175 | 176 | if c.get("rolcanlogin") != c2.get("rolcanlogin") { 177 | if c.get("rolcanlogin") == "true" { 178 | options += " LOGIN" 179 | } else { 180 | options += " NOLOGIN" 181 | } 182 | } 183 | 184 | if c.get("rolcreatedb") != c2.get("rolcreatedb") { 185 | if c.get("rolcreatedb") == "true" { 186 | options += " CREATEDB" 187 | } else { 188 | options += " NOCREATEDB" 189 | } 190 | } 191 | 192 | if c.get("rolcreaterole") != c2.get("rolcreaterole") { 193 | if c.get("rolcreaterole") == "true" { 194 | options += " CREATEROLE" 195 | } else { 196 | options += " NOCREATEROLE" 197 | } 198 | } 199 | 200 | if c.get("rolcreateuser") != c2.get("rolcreateuser") { 201 | if c.get("rolcreateuser") == "true" { 202 | options += " CREATEUSER" 203 | } else { 204 | options += " NOCREATEUSER" 205 | } 206 | } 207 | 208 | if c.get("rolinherit") != c2.get("rolinherit") { 209 | if c.get("rolinherit") == "true" { 210 | options += " INHERIT" 211 | } else { 212 | options += " NOINHERIT" 213 | } 214 | } 215 | 216 | if c.get("rolreplication") != c2.get("rolreplication") { 217 | if c.get("rolreplication") == "true" { 218 | options += " REPLICATION" 219 | } else { 220 | options += " NOREPLICATION" 221 | } 222 | } 223 | 224 | if c.get("rolconnlimit") != c2.get("rolconnlimit") { 225 | if len(c.get("rolconnlimit")) > 0 { 226 | options += " CONNECTION LIMIT " + c.get("rolconnlimit") 227 | } 228 | } 229 | 230 | if c.get("rolvaliduntil") != c2.get("rolvaliduntil") { 231 | if c.get("rolvaliduntil") != "null" { 232 | options += fmt.Sprintf(" VALID UNTIL '%s'", c.get("rolvaliduntil")) 233 | } 234 | } 235 | 236 | // Only alter if we have changes 237 | if len(options) > 0 { 238 | fmt.Printf("ALTER ROLE %s%s;\n", c.get("rolname"), options) 239 | } 240 | 241 | if c.get("memberof") != c2.get("memberof") { 242 | fmt.Println(c.get("memberof"), "!=", c2.get("memberof")) 243 | 244 | // Remove the curly brackets 245 | memberof1 := curlyBracketRegex.ReplaceAllString(c.get("memberof"), "") 246 | memberof2 := curlyBracketRegex.ReplaceAllString(c2.get("memberof"), "") 247 | 248 | // Split 249 | membersof1 := strings.Split(memberof1, ",") 250 | membersof2 := strings.Split(memberof2, ",") 251 | 252 | // TODO: Define INHERIT or not 253 | for _, mo1 := range membersof1 { 254 | if !misc.ContainsString(membersof2, mo1) { 255 | fmt.Printf("GRANT %s TO %s;\n", mo1, c.get("rolname")) 256 | } 257 | } 258 | 259 | for _, mo2 := range membersof2 { 260 | if !misc.ContainsString(membersof1, mo2) { 261 | fmt.Printf("REVOKE %s FROM %s;\n", mo2, c.get("rolname")) 262 | } 263 | } 264 | 265 | } 266 | } 267 | 268 | /* 269 | * Compare the roles between two databases or schemas 270 | */ 271 | func compareRoles(conn1 *sql.DB, conn2 *sql.DB) { 272 | sql := ` 273 | SELECT r.rolname 274 | , r.rolsuper 275 | , r.rolinherit 276 | , r.rolcreaterole 277 | , r.rolcreatedb 278 | , r.rolcanlogin 279 | , r.rolconnlimit 280 | , r.rolvaliduntil 281 | , r.rolreplication 282 | , ARRAY(SELECT b.rolname 283 | FROM pg_catalog.pg_auth_members m 284 | JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) 285 | WHERE m.member = r.oid) as memberof 286 | FROM pg_catalog.pg_roles AS r 287 | ORDER BY r.rolname; 288 | ` 289 | rowChan1, _ := pgutil.QueryStrings(conn1, sql) 290 | rowChan2, _ := pgutil.QueryStrings(conn2, sql) 291 | 292 | rows1 := make(RoleRows, 0) 293 | for row := range rowChan1 { 294 | rows1 = append(rows1, row) 295 | } 296 | sort.Sort(rows1) 297 | 298 | rows2 := make(RoleRows, 0) 299 | for row := range rowChan2 { 300 | rows2 = append(rows2, row) 301 | } 302 | sort.Sort(rows2) 303 | 304 | // We have to explicitly type this as Schema here for some unknown reason 305 | var schema1 Schema = &RoleSchema{rows: rows1, rowNum: -1} 306 | var schema2 Schema = &RoleSchema{rows: rows2, rowNum: -1} 307 | 308 | // Compare the roles 309 | doDiff(schema1, schema2) 310 | } 311 | -------------------------------------------------------------------------------- /schemata.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import "fmt" 10 | import "sort" 11 | import "database/sql" 12 | import "github.com/joncrlsn/pgutil" 13 | import "github.com/joncrlsn/misc" 14 | 15 | // ================================== 16 | // SchemataRows definition 17 | // ================================== 18 | 19 | // SchemataRows is a sortable slice of string maps 20 | type SchemataRows []map[string]string 21 | 22 | func (slice SchemataRows) Len() int { 23 | return len(slice) 24 | } 25 | 26 | func (slice SchemataRows) Less(i, j int) bool { 27 | return slice[i]["schema_name"] < slice[j]["schema_name"] 28 | } 29 | 30 | func (slice SchemataRows) Swap(i, j int) { 31 | slice[i], slice[j] = slice[j], slice[i] 32 | } 33 | 34 | // SchemataSchema holds a channel streaming schema meta information from one of the databases as well as 35 | // a reference to the current row of data we're viewing. 36 | // 37 | // SchemataSchema implements the Schema interface defined in pgdiff.go 38 | type SchemataSchema struct { 39 | rows SchemataRows 40 | rowNum int 41 | done bool 42 | } 43 | 44 | // get returns the value from the current row for the given key 45 | func (c *SchemataSchema) get(key string) string { 46 | if c.rowNum >= len(c.rows) { 47 | return "" 48 | } 49 | return c.rows[c.rowNum][key] 50 | } 51 | 52 | // NextRow increments the rowNum and tells you whether or not there are more 53 | func (c *SchemataSchema) NextRow() bool { 54 | if c.rowNum >= len(c.rows)-1 { 55 | c.done = true 56 | } 57 | c.rowNum = c.rowNum + 1 58 | return !c.done 59 | } 60 | 61 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 62 | func (c *SchemataSchema) Compare(obj interface{}) int { 63 | c2, ok := obj.(*SchemataSchema) 64 | if !ok { 65 | fmt.Println("Error!!!, Compare(obj) needs a SchemataSchema instance", c2) 66 | return +999 67 | } 68 | 69 | val := misc.CompareStrings(c.get("schema_name"), c2.get("schema_name")) 70 | //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("schema_name"), c2.get("schema_name")) 71 | return val 72 | } 73 | 74 | // Add returns SQL to add the schemata 75 | func (c SchemataSchema) Add() { 76 | // CREATE SCHEMA schema_name [ AUTHORIZATION user_name 77 | fmt.Printf("CREATE SCHEMA %s AUTHORIZATION %s;", c.get("schema_name"), c.get("schema_owner")) 78 | fmt.Println() 79 | } 80 | 81 | // Drop returns SQL to drop the schemata 82 | func (c SchemataSchema) Drop() { 83 | // DROP SCHEMA [ IF EXISTS ] name [, ...] [ CASCADE | RESTRICT ] 84 | fmt.Printf("DROP SCHEMA IF EXISTS %s;\n", c.get("schema_name")) 85 | } 86 | 87 | // Change handles the case where the schema name matches, but the details do not 88 | func (c SchemataSchema) Change(obj interface{}) { 89 | c2, ok := obj.(*SchemataSchema) 90 | if !ok { 91 | fmt.Println("Error!!!, Change needs a SchemataSchema instance", c2) 92 | } 93 | // There's nothing we need to do here 94 | } 95 | 96 | // compareSchematas outputs SQL to make the schema names match between DBs 97 | func compareSchematas(conn1 *sql.DB, conn2 *sql.DB) { 98 | 99 | // if we are comparing two schemas against each other, then 100 | // we won't compare to ensure they are created, although maybe we should. 101 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 102 | return 103 | } 104 | 105 | sql := ` 106 | SELECT schema_name 107 | , schema_owner 108 | , default_character_set_schema 109 | FROM information_schema.schemata 110 | WHERE schema_name NOT LIKE 'pg_%' 111 | AND schema_name <> 'information_schema' 112 | ORDER BY schema_name;` 113 | 114 | rowChan1, _ := pgutil.QueryStrings(conn1, sql) 115 | rowChan2, _ := pgutil.QueryStrings(conn2, sql) 116 | 117 | rows1 := make(SchemataRows, 0) 118 | for row := range rowChan1 { 119 | rows1 = append(rows1, row) 120 | } 121 | sort.Sort(rows1) 122 | 123 | rows2 := make(SchemataRows, 0) 124 | for row := range rowChan2 { 125 | rows2 = append(rows2, row) 126 | } 127 | sort.Sort(rows2) 128 | 129 | // We have to explicitly type this as Schema here 130 | var schema1 Schema = &SchemataSchema{rows: rows1, rowNum: -1} 131 | var schema2 Schema = &SchemataSchema{rows: rows2, rowNum: -1} 132 | 133 | // Compare the schematas 134 | doDiff(schema1, schema2) 135 | } 136 | -------------------------------------------------------------------------------- /sequence.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "text/template" 17 | ) 18 | 19 | var ( 20 | sequenceSqlTemplate = initSequenceSqlTemplate() 21 | ) 22 | 23 | // Initializes the Sql template 24 | func initSequenceSqlTemplate() *template.Template { 25 | sql := ` 26 | SELECT sequence_schema AS schema_name 27 | , {{if eq $.DbSchema "*" }}sequence_schema || '.' || {{end}}sequence_name AS compare_name 28 | , sequence_name 29 | , data_type 30 | , start_value 31 | , minimum_value 32 | , maximum_value 33 | , increment 34 | , cycle_option 35 | FROM information_schema.sequences 36 | WHERE true 37 | {{if eq $.DbSchema "*" }} 38 | AND sequence_schema NOT LIKE 'pg_%' 39 | AND sequence_schema <> 'information_schema' 40 | {{else}} 41 | AND sequence_schema = '{{$.DbSchema}}' 42 | {{end}} 43 | ` 44 | 45 | t := template.New("SequenceSqlTmpl") 46 | template.Must(t.Parse(sql)) 47 | return t 48 | } 49 | 50 | // ================================== 51 | // SequenceRows definition 52 | // ================================== 53 | 54 | // SequenceRows is a sortable slice of string maps 55 | type SequenceRows []map[string]string 56 | 57 | func (slice SequenceRows) Len() int { 58 | return len(slice) 59 | } 60 | 61 | func (slice SequenceRows) Less(i, j int) bool { 62 | return slice[i]["compare_name"] < slice[j]["compare_name"] 63 | } 64 | 65 | func (slice SequenceRows) Swap(i, j int) { 66 | slice[i], slice[j] = slice[j], slice[i] 67 | } 68 | 69 | // SequenceSchema holds a channel streaming sequence information from one of the databases as well as 70 | // a reference to the current row of data we're viewing. 71 | // 72 | // SequenceSchema implements the Schema interface defined in pgdiff.go 73 | type SequenceSchema struct { 74 | rows SequenceRows 75 | rowNum int 76 | done bool 77 | } 78 | 79 | // get returns the value from the current row for the given key 80 | func (c *SequenceSchema) get(key string) string { 81 | if c.rowNum >= len(c.rows) { 82 | return "" 83 | } 84 | return c.rows[c.rowNum][key] 85 | } 86 | 87 | // NextRow increments the rowNum and tells you whether or not there are more 88 | func (c *SequenceSchema) NextRow() bool { 89 | if c.rowNum >= len(c.rows)-1 { 90 | c.done = true 91 | } 92 | c.rowNum = c.rowNum + 1 93 | return !c.done 94 | } 95 | 96 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 97 | func (c *SequenceSchema) Compare(obj interface{}) int { 98 | c2, ok := obj.(*SequenceSchema) 99 | if !ok { 100 | fmt.Println("Error!!!, Compare(obj) needs a SequenceSchema instance", c2) 101 | return +999 102 | } 103 | 104 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 105 | return val 106 | } 107 | 108 | // Add returns SQL to add the sequence 109 | func (c SequenceSchema) Add() { 110 | schema := dbInfo2.DbSchema 111 | if schema == "*" { 112 | schema = c.get("schema_name") 113 | } 114 | fmt.Printf("CREATE SEQUENCE %s.%s INCREMENT %s MINVALUE %s MAXVALUE %s START %s;\n", schema, c.get("sequence_name"), c.get("increment"), c.get("minimum_value"), c.get("maximum_value"), c.get("start_value")) 115 | } 116 | 117 | // Drop returns SQL to drop the sequence 118 | func (c SequenceSchema) Drop() { 119 | fmt.Printf("DROP SEQUENCE %s.%s;\n", c.get("schema_name"), c.get("sequence_name")) 120 | } 121 | 122 | // Change doesn't do anything right now. 123 | func (c SequenceSchema) Change(obj interface{}) { 124 | c2, ok := obj.(*SequenceSchema) 125 | if !ok { 126 | fmt.Println("Error!!!, Change(obj) needs a SequenceSchema instance", c2) 127 | } 128 | // Don't know of anything helpful we should do here 129 | } 130 | 131 | // compareSequences outputs SQL to make the sequences match between DBs or schemas 132 | func compareSequences(conn1 *sql.DB, conn2 *sql.DB) { 133 | 134 | buf1 := new(bytes.Buffer) 135 | sequenceSqlTemplate.Execute(buf1, dbInfo1) 136 | 137 | buf2 := new(bytes.Buffer) 138 | sequenceSqlTemplate.Execute(buf2, dbInfo2) 139 | 140 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 141 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 142 | 143 | rows1 := make(SequenceRows, 0) 144 | for row := range rowChan1 { 145 | rows1 = append(rows1, row) 146 | } 147 | sort.Sort(rows1) 148 | 149 | rows2 := make(SequenceRows, 0) 150 | for row := range rowChan2 { 151 | rows2 = append(rows2, row) 152 | } 153 | sort.Sort(rows2) 154 | 155 | // We have to explicitly type this as Schema here for some unknown (to me) reason 156 | var schema1 Schema = &SequenceSchema{rows: rows1, rowNum: -1} 157 | var schema2 Schema = &SequenceSchema{rows: rows2, rowNum: -1} 158 | 159 | // Compare the sequences 160 | doDiff(schema1, schema2) 161 | } 162 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "text/template" 17 | ) 18 | 19 | var ( 20 | tableSqlTemplate = initTableSqlTemplate() 21 | ) 22 | 23 | // Initializes the Sql template 24 | func initTableSqlTemplate() *template.Template { 25 | 26 | sql := ` 27 | SELECT table_schema 28 | , {{if eq $.DbSchema "*" }}table_schema || '.' || {{end}}table_name AS compare_name 29 | , table_name 30 | , CASE table_type 31 | WHEN 'BASE TABLE' THEN 'TABLE' 32 | ELSE table_type END AS table_type 33 | , is_insertable_into 34 | FROM information_schema.tables 35 | WHERE table_type = 'BASE TABLE' 36 | {{if eq $.DbSchema "*" }} 37 | AND table_schema NOT LIKE 'pg_%' 38 | AND table_schema <> 'information_schema' 39 | {{else}} 40 | AND table_schema = '{{$.DbSchema}}' 41 | {{end}} 42 | ORDER BY compare_name; 43 | ` 44 | t := template.New("TableSqlTmpl") 45 | template.Must(t.Parse(sql)) 46 | return t 47 | } 48 | 49 | // ================================== 50 | // TableRows definition 51 | // ================================== 52 | 53 | // TableRows is a sortable slice of string maps 54 | type TableRows []map[string]string 55 | 56 | func (slice TableRows) Len() int { 57 | return len(slice) 58 | } 59 | 60 | func (slice TableRows) Less(i, j int) bool { 61 | return slice[i]["compare_name"] < slice[j]["compare_name"] 62 | } 63 | 64 | func (slice TableRows) Swap(i, j int) { 65 | slice[i], slice[j] = slice[j], slice[i] 66 | } 67 | 68 | // TableSchema holds a channel streaming table information from one of the databases as well as 69 | // a reference to the current row of data we're viewing. 70 | // 71 | // TableSchema implements the Schema interface defined in pgdiff.go 72 | type TableSchema struct { 73 | rows TableRows 74 | rowNum int 75 | done bool 76 | } 77 | 78 | // get returns the value from the current row for the given key 79 | func (c *TableSchema) get(key string) string { 80 | if c.rowNum >= len(c.rows) { 81 | return "" 82 | } 83 | return c.rows[c.rowNum][key] 84 | } 85 | 86 | // NextRow increments the rowNum and tells you whether or not there are more 87 | func (c *TableSchema) NextRow() bool { 88 | if c.rowNum >= len(c.rows)-1 { 89 | c.done = true 90 | } 91 | c.rowNum = c.rowNum + 1 92 | return !c.done 93 | } 94 | 95 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 96 | func (c *TableSchema) Compare(obj interface{}) int { 97 | c2, ok := obj.(*TableSchema) 98 | if !ok { 99 | fmt.Println("Error!!!, Compare(obj) needs a TableSchema instance", c2) 100 | return +999 101 | } 102 | 103 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 104 | //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("table_name"), c2.get("table_name")) 105 | return val 106 | } 107 | 108 | // Add returns SQL to add the table or view 109 | func (c TableSchema) Add() { 110 | schema := dbInfo2.DbSchema 111 | if schema == "*" { 112 | schema = c.get("table_schema") 113 | } 114 | fmt.Printf("CREATE %s %s.%s();", c.get("table_type"), schema, c.get("table_name")) 115 | fmt.Println() 116 | } 117 | 118 | // Drop returns SQL to drop the table or view 119 | func (c TableSchema) Drop() { 120 | fmt.Printf("DROP %s %s.%s;\n", c.get("table_type"), c.get("table_schema"), c.get("table_name")) 121 | } 122 | 123 | // Change handles the case where the table and column match, but the details do not 124 | func (c TableSchema) Change(obj interface{}) { 125 | c2, ok := obj.(*TableSchema) 126 | if !ok { 127 | fmt.Println("Error!!!, Change needs a TableSchema instance", c2) 128 | } 129 | // There's nothing we need to do here 130 | } 131 | 132 | // compareTables outputs SQL to make the table names match between DBs 133 | func compareTables(conn1 *sql.DB, conn2 *sql.DB) { 134 | 135 | buf1 := new(bytes.Buffer) 136 | tableSqlTemplate.Execute(buf1, dbInfo1) 137 | 138 | buf2 := new(bytes.Buffer) 139 | tableSqlTemplate.Execute(buf2, dbInfo2) 140 | 141 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 142 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 143 | 144 | rows1 := make(TableRows, 0) 145 | for row := range rowChan1 { 146 | rows1 = append(rows1, row) 147 | } 148 | sort.Sort(rows1) 149 | 150 | rows2 := make(TableRows, 0) 151 | for row := range rowChan2 { 152 | rows2 = append(rows2, row) 153 | } 154 | sort.Sort(rows2) 155 | 156 | // We have to explicitly type this as Schema here 157 | var schema1 Schema = &TableSchema{rows: rows1, rowNum: -1} 158 | var schema2 Schema = &TableSchema{rows: rows2, rowNum: -1} 159 | 160 | // Compare the tables 161 | doDiff(schema1, schema2) 162 | } 163 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | These are not automated tests (I'd rather have automated tests), but manual 2 | integration tests for verifying that individual schema types are working. 3 | 4 | These can be good templates for isolating bugs in the different data diffs. 5 | 6 | Connect to the database manually: 7 | sudo su - postgres -- -c "psql -d db1" 8 | 9 | -------------------------------------------------------------------------------- /test/example.dump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncrlsn/pgdiff/06bab900ae4806aebe5f6fd7a6885036ca9c836d/test/example.dump -------------------------------------------------------------------------------- /test/load-example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Load example dump found here: 4 | # http://postgresguide.com/setup/example.htmlhttp://postgresguide.com/setup/example.html 5 | 6 | #curl -L -O http://cl.ly/173L141n3402/download/example.dump 7 | sudo su - postgres -- -c " 8 | createdb pgguide 9 | pg_restore --no-owner --dbname pgguide example.dump 10 | psql --dbname pgguide 11 | " 12 | -------------------------------------------------------------------------------- /test/mypsql: -------------------------------------------------------------------------------- 1 | sudo su - postgres -c "psql -d db1" 2 | -------------------------------------------------------------------------------- /test/populate-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | db=$1 5 | 6 | PGPASSWORD=asdf psql -U u1 -h localhost -d $db </dev/null 7 | 8 | echo 9 | echo ============================================================== 10 | 11 | # 12 | # Compare the columns between two schemas in the same database 13 | # 14 | #psql -U u1 -h localhost -d db1 <<'EOS' 15 | ./populate-db.sh db1 " 16 | CREATE SCHEMA s1; 17 | CREATE TABLE s1.table9 ( 18 | id integer, 19 | name varchar(50) 20 | ); 21 | CREATE TABLE s1.table10 (id bigint); 22 | CREATE TABLE s1.table11 (); 23 | 24 | CREATE SCHEMA s2; 25 | CREATE TABLE s2.table9 ( -- Add name column 26 | id integer 27 | ); 28 | CREATE TABLE s2.table10 (id integer); -- change id to bigint 29 | CREATE TABLE s2.table11 (id integer); -- drop id column 30 | " 31 | 32 | echo 33 | echo "# Compare the columns between two schemas in the same database" 34 | echo "# Expect SQL:" 35 | echo "# Add s2.table9.name" 36 | echo "# Change s2.table10.id to bigint" 37 | echo "# Drop s2.table11.id" 38 | 39 | echo 40 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 41 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 42 | COLUMN | grep -v '^-- ' 43 | 44 | 45 | echo 46 | echo ============================================================== 47 | 48 | ./populate-db.sh db2 " 49 | CREATE SCHEMA s1; 50 | CREATE TABLE s1.table9 ( 51 | id integer, 52 | name varchar(40) 53 | ); 54 | CREATE TABLE s1.table10 (); 55 | CREATE TABLE s1.table11 (dropme integer); 56 | 57 | CREATE SCHEMA s2; 58 | CREATE TABLE s2.table9 ( -- Add name column 59 | id integer 60 | ); 61 | CREATE TABLE s2.table10 (id integer); -- change id to bigint 62 | CREATE TABLE s2.table11 (id integer); -- drop id column 63 | " 64 | 65 | echo 66 | echo "# Compare the columns in all schemas between two databases" 67 | echo "# Expect:" 68 | echo "# Change s1.table9.name to varchar(50) " 69 | echo "# Add s1.table10.id" 70 | echo "# Drop s1.table11.dropme" 71 | echo 72 | 73 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 74 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 75 | COLUMN | grep -v '^-- ' 76 | echo 77 | echo ============================================================== 78 | 79 | ./populate-db.sh db1 " 80 | CREATE SCHEMA s3; 81 | CREATE TABLE s3.table12 ( 82 | ids integer[], 83 | bigids bigint[], 84 | something text[][] -- dimensions don't seem to matter, so ignore them 85 | ); 86 | CREATE SCHEMA s4; 87 | CREATE TABLE s4.table12 ( -- add ids column 88 | bigids integer[], -- change bigids to int8[] 89 | something text[] -- no change 90 | ); 91 | " 92 | 93 | echo 94 | echo "# Compare array columns between two tables" 95 | echo "# Expect:" 96 | echo "# Add s4.table12.ids int4[]" 97 | echo "# Change s4.table12.bigids from to int8[]" 98 | echo 99 | 100 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s3" -O "sslmode=disable" \ 101 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s4" -o "sslmode=disable" \ 102 | COLUMN | grep -v '^-- ' 103 | echo 104 | -------------------------------------------------------------------------------- /test/test-foreignkey: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | 8 | echo 9 | echo ==================================================== 10 | echo 11 | 12 | # 13 | # Compare the foreign keys between two schemas in the same database 14 | # 15 | 16 | ./populate-db.sh db1 " 17 | CREATE SCHEMA s1; 18 | CREATE TABLE s1.table1 ( 19 | id integer PRIMARY KEY 20 | ); 21 | CREATE TABLE s1.table2 ( 22 | id integer PRIMARY KEY, 23 | table1_id integer REFERENCES s1.table1(id) 24 | ); 25 | CREATE TABLE s1.table3 ( 26 | id integer, 27 | table2_id integer 28 | ); 29 | 30 | CREATE SCHEMA s2; 31 | CREATE TABLE s2.table1 ( 32 | id integer PRIMARY KEY 33 | ); 34 | CREATE TABLE s2.table2 ( 35 | id integer PRIMARY KEY, 36 | table1_id integer 37 | ); 38 | CREATE TABLE s2.table3 ( 39 | id integer, 40 | table2_id integer REFERENCES s2.table2(id) -- This will be deleted 41 | ); 42 | " 43 | 44 | echo 45 | echo "# Compare the foreign keys between two schemas in the same database" 46 | echo "# Expect SQL:" 47 | echo "# Add foreign key on s2.table2.table1_id" 48 | echo "# Drop foreign key from s2.table3.table2_id" 49 | echo 50 | 51 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 52 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 53 | FOREIGN_KEY | grep -v '^-- ' 54 | 55 | echo 56 | echo ==================================================== 57 | echo 58 | 59 | 60 | # 61 | # Compare the foreign keys in all schemas between two databases 62 | # 63 | ./populate-db.sh db2 " 64 | CREATE SCHEMA s1; 65 | CREATE TABLE s1.table1 ( 66 | id integer PRIMARY KEY 67 | ); 68 | CREATE TABLE s1.table2 ( 69 | id integer PRIMARY KEY, 70 | table1_id integer -- a foreign key will be added 71 | ); 72 | CREATE TABLE s1.table3 ( 73 | id integer, 74 | table2_id integer 75 | ); 76 | 77 | CREATE SCHEMA s2; 78 | CREATE TABLE s2.table1 ( 79 | id integer PRIMARY KEY 80 | ); 81 | CREATE TABLE s2.table2 ( 82 | id integer PRIMARY KEY, 83 | table1_id integer REFERENCES s2.table1(id) -- This will be deleted 84 | 85 | ); 86 | CREATE TABLE s2.table3 ( 87 | id integer, 88 | table2_id integer REFERENCES s2.table2(id) 89 | ); 90 | " 91 | 92 | echo 93 | echo "# Compare the foreign keys in all schemas between two databases" 94 | echo "# Expect SQL:" 95 | echo "# Add foreign key on db2.s1.table2.table1_id" 96 | echo "# Drop foreign key on db2.s2.table2.table1_id" 97 | 98 | echo 99 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 100 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 101 | FOREIGN_KEY | grep -v '^-- ' 102 | echo 103 | -------------------------------------------------------------------------------- /test/test-function: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | echo 8 | echo ========================================================== 9 | echo 10 | 11 | # 12 | # Compare the functions between two schemas in the same database 13 | # 14 | 15 | ./populate-db.sh db1 "$(cat << 'EOF' 16 | CREATE SCHEMA s1; 17 | CREATE OR REPLACE FUNCTION s1.increment(i integer) RETURNS integer AS $$ 18 | BEGIN 19 | RETURN i + 1; 20 | END; 21 | $$ LANGUAGE plpgsql; 22 | CREATE FUNCTION s1.add(integer, integer) RETURNS integer 23 | AS 'select $1 + $2;' 24 | LANGUAGE SQL 25 | IMMUTABLE 26 | RETURNS NULL ON NULL INPUT; 27 | 28 | 29 | CREATE SCHEMA s2; 30 | CREATE OR REPLACE FUNCTION s2.add(bigint, bigint) RETURNS bigint 31 | AS 'select $1 + $2;' 32 | LANGUAGE SQL 33 | IMMUTABLE 34 | RETURNS NULL ON NULL INPUT; 35 | CREATE FUNCTION s2.minus(integer, integer) RETURNS integer 36 | AS 'select $1 - $2;' 37 | LANGUAGE SQL 38 | IMMUTABLE 39 | RETURNS NULL ON NULL INPUT; 40 | 41 | EOF 42 | )" 43 | 44 | echo 45 | echo "# Compare the functions between two schemas in the same database" 46 | echo "# Expect SQL (pseudocode):" 47 | echo "# Add function s2.increment" 48 | echo "# Replace function s2.add" 49 | echo "# Drop function s2.minus" 50 | echo 51 | 52 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 53 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 54 | FUNCTION #| grep -v '^-- ' 55 | echo 56 | echo ========================================================== 57 | echo 58 | 59 | 60 | # 61 | # Compare the functions in all schemas between two databases 62 | # 63 | ./populate-db.sh db2 "$(cat << 'EOF' 64 | CREATE SCHEMA s1; 65 | CREATE OR REPLACE FUNCTION s1.increment(i integer) RETURNS integer AS $$ 66 | BEGIN 67 | RETURN i + 1; 68 | END; 69 | $$ LANGUAGE plpgsql; 70 | CREATE FUNCTION s1.addition(integer, integer) RETURNS integer 71 | AS 'select $1 + $2;' 72 | LANGUAGE SQL 73 | IMMUTABLE 74 | RETURNS NULL ON NULL INPUT; 75 | 76 | 77 | CREATE SCHEMA s2; 78 | CREATE OR REPLACE FUNCTION s2.add(integer, integer) RETURNS integer 79 | AS 'select $1 + $2;' 80 | LANGUAGE SQL 81 | IMMUTABLE 82 | RETURNS NULL ON NULL INPUT; 83 | CREATE FUNCTION s2.minus(integer, integer) RETURNS integer 84 | AS 'select $1 - $2;' 85 | LANGUAGE SQL 86 | IMMUTABLE 87 | RETURNS NULL ON NULL INPUT; 88 | 89 | EOF 90 | )" 91 | 92 | 93 | echo 94 | echo "# Compare the functions in all schemas between two databases" 95 | echo "# Expect SQL (pseudocode):" 96 | echo "# Add function s1.add" 97 | echo "# Change/Replace function s2.add" 98 | echo "# Drop function s1.addition" 99 | echo 100 | 101 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 102 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 103 | FUNCTION #| grep -v '^-- ' 104 | echo 105 | echo 106 | -------------------------------------------------------------------------------- /test/test-grant-attribute: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | 8 | 9 | echo 10 | echo ========================================================== 11 | echo 12 | 13 | # 14 | # Compare the grants between two schemas in the same database 15 | # 16 | 17 | ./populate-db.sh db1 " 18 | -- Available Column Privileges: SELECT, INSERT, UPDATE, REFERENCES 19 | 20 | CREATE SCHEMA s1; 21 | CREATE SCHEMA s2; 22 | 23 | --------- 24 | 25 | CREATE TABLE s1.table1 (id integer, name varchar(30)); 26 | GRANT SELECT, UPDATE (name) ON s1.table1 TO u2; 27 | 28 | -- Drop REFERENCES, Add UPDATE 29 | CREATE TABLE s2.table1 (id integer, name varchar(30)); 30 | GRANT SELECT, REFERENCES (name) ON s2.table1 TO u2; 31 | 32 | --------- 33 | 34 | CREATE TABLE s1.table2 (id integer, name varchar(30)); 35 | -- u2 has no privileges 36 | 37 | -- Drop SELECT on s1.table2 38 | CREATE TABLE s2.table2 (id integer, name varchar(30)); 39 | GRANT SELECT (name) ON s2.table2 TO u2; 40 | 41 | --------- 42 | 43 | CREATE TABLE s1.table3 (id integer, name varchar(30)); 44 | GRANT SELECT (name) ON s1.table3 TO u2; 45 | 46 | -- Add SELECT on s1.table3 47 | CREATE TABLE s2.table3 (id integer, name varchar(30)); 48 | -- u2 has no privileges 49 | " 50 | 51 | echo 52 | echo "# Compare the grants between two schemas in the same database" 53 | echo "# Expect SQL (pseudocode):" 54 | echo "# Grant UPDATE (name) on s2.table1 for u2" 55 | echo "# Revoke REFERENCES (name) on s2.table1 for u2" 56 | echo "# Revoke SELECT (name) on s2.table2 for u2" 57 | echo "# Grant SELECT (name) on s2.table3 for u2" 58 | echo 59 | 60 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 61 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 62 | GRANT_ATTRIBUTE | grep -v '^-- ' 63 | 64 | echo 65 | echo ========================================================== 66 | echo 67 | 68 | 69 | source ./start-fresh.sh >/dev/null 70 | 71 | # 72 | # Compare the grants in all schemas between two databases 73 | # 74 | 75 | ./populate-db.sh db1 " 76 | CREATE SCHEMA s1; 77 | CREATE SCHEMA s2; 78 | --------- 79 | CREATE TABLE s1.table1 (id integer, name varchar(30)); 80 | GRANT SELECT, UPDATE (name) ON s1.table1 TO u2; 81 | 82 | CREATE TABLE s2.table1 (id integer, name varchar(30)); 83 | GRANT UPDATE (name) ON s2.table1 TO u2; 84 | 85 | CREATE TABLE s2.table3 (id integer, name varchar(30)); 86 | 87 | CREATE TABLE s2.table4 (id integer, name varchar(30)); 88 | GRANT SELECT, UPDATE (name) ON s2.table4 TO u2; 89 | " 90 | 91 | ./populate-db.sh db2 " 92 | CREATE SCHEMA s1; 93 | CREATE SCHEMA s2; 94 | --------- 95 | CREATE TABLE s1.table1 (id integer, name varchar(30)); 96 | GRANT SELECT, UPDATE (name) ON s1.table1 TO u2; 97 | GRANT SELECT (id) ON s1.table1 TO u2; 98 | 99 | CREATE TABLE s2.table1 (id integer, name varchar(30)); 100 | GRANT REFERENCES (name) ON s2.table1 TO u2; 101 | 102 | CREATE TABLE s2.table3 (id integer, name varchar(30)); 103 | GRANT UPDATE (name) ON s2.table3 TO u2; 104 | 105 | CREATE TABLE s2.table4 (id integer, name varchar(30)); 106 | " 107 | 108 | echo 109 | echo "# Compare the grants in all schemas between two databases" 110 | echo "# Expect SQL (pseudocode):" 111 | echo "# Revoke SELECT (id) on s1.table1 for u2" 112 | echo "# Grant UPDATE (name) on s2.table1 for u2" 113 | echo "# Revoke REFERENCES (name) on s2.table1 for u2" 114 | echo "# Revoke UPDATE (name) on s2.table3 for u2" 115 | echo "# Grant UPDATE (name) on s2.table4 for u2" 116 | echo 117 | 118 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 119 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 120 | GRANT_ATTRIBUTE | grep -v '^-- ' 121 | echo 122 | echo 123 | -------------------------------------------------------------------------------- /test/test-grant-relationship: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | 8 | 9 | echo 10 | echo ========================================================== 11 | echo 12 | 13 | # 14 | # Compare the grants between two schemas in the same database 15 | # 16 | 17 | ./populate-db.sh db1 " 18 | CREATE SCHEMA s1; 19 | CREATE TABLE s1.table1 (id integer); 20 | GRANT INSERT, UPDATE ON s1.table1 TO u2; 21 | CREATE TABLE s1.table2 (id integer); 22 | GRANT SELECT ON s1.table2 TO u2; 23 | CREATE TABLE s1.table3 (id integer); 24 | GRANT SELECT ON s1.table3 TO u2; 25 | 26 | CREATE SCHEMA s2; 27 | CREATE TABLE s2.table1 (id integer); 28 | GRANT SELECT ON s2.table1 TO u2; -- add INSERT, UPDATE 29 | CREATE TABLE s2.table2 (id integer); 30 | GRANT SELECT ON s2.table2 TO u2; -- no change 31 | CREATE TABLE s2.table3 (id integer); -- add SELECT 32 | GRANT SELECT ON s2.table3 TO u1; 33 | " 34 | 35 | echo 36 | echo "# Compare the grants between two schemas in the same database" 37 | echo "# Expect SQL (pseudocode):" 38 | echo "# Revoke SELECT on s2.table1 for u2" 39 | echo "# Grant INSERT, UPDATE on s2.table1 for u2" 40 | echo "# Grant SELECT on s2.table3 for u2" 41 | echo 42 | 43 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 44 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 45 | GRANT_RELATIONSHIP #| grep -v '^-- ' 46 | 47 | 48 | echo 49 | echo ========================================================== 50 | echo 51 | 52 | 53 | # 54 | # Compare the grants in all schemas between two databases 55 | # 56 | ./populate-db.sh db2 " 57 | CREATE SCHEMA s1; 58 | CREATE TABLE s1.table1 (id integer); 59 | GRANT SELECT ON s1.table1 TO u2; 60 | CREATE TABLE s1.table2 (id integer); 61 | GRANT SELECT ON s1.table2 TO u2; 62 | CREATE TABLE s1.table3 (id integer); 63 | GRANT SELECT ON s1.table3 TO u2; 64 | 65 | CREATE SCHEMA s2; 66 | CREATE TABLE s2.table1 (id integer); 67 | GRANT SELECT ON s2.table1 TO u2; 68 | CREATE TABLE s2.table2 (id integer); 69 | GRANT SELECT ON s2.table2 TO u2; 70 | CREATE TABLE s2.table3 (id integer); 71 | GRANT UPDATE ON s2.table3 TO u2; -- revoke 72 | " 73 | 74 | echo 75 | echo "# Compare the grants in all schemas between two databases" 76 | echo "# Expect SQL (pseudocode):" 77 | echo "# Revoke UPDATE on s2.table3 for u2" 78 | echo "# Grant INSERT,UPDATE on s1.table1 for u2" 79 | echo "# Revoke SELECT on s1.table1 for u2" 80 | echo 81 | 82 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 83 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 84 | GRANT_RELATIONSHIP #| grep -v '^-- ' 85 | echo 86 | echo 87 | -------------------------------------------------------------------------------- /test/test-identity-column: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | 8 | echo 9 | echo ============================================================== 10 | 11 | # 12 | # Compare the columns between two schemas in the same database 13 | # 14 | #psql -U u1 -h localhost -d db1 <<'EOS' 15 | ./populate-db.sh db1 " 16 | CREATE SCHEMA s1; 17 | CREATE TABLE s1.table13 ( 18 | id1 integer, 19 | id2 bigint generated by default as identity, 20 | id3 integer generated by default as identity 21 | ); 22 | 23 | CREATE SCHEMA s2; 24 | CREATE TABLE s2.table13 ( 25 | id1 integer generated by default as identity, -- drop identity 26 | id2 integer -- int to bigint, add identity 27 | -- add identity column 28 | ); 29 | " 30 | 31 | echo 32 | echo "# Compare differences in identity columns between two tables" 33 | echo "# Expect SQL:" 34 | echo "# Change s2.table13.id1 drop identity" 35 | echo "# Change s2.table13.id2 to bigint, add identity" 36 | echo "# Add s2.table13.id3" 37 | 38 | echo 39 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 40 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 41 | COLUMN | grep -v '^-- ' 42 | echo 43 | 44 | -------------------------------------------------------------------------------- /test/test-index: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | echo 8 | echo ========================================================== 9 | echo 10 | 11 | # 12 | # Compare the indexes between two schemas in the same database 13 | # 14 | 15 | ./populate-db.sh db1 " 16 | CREATE SCHEMA s1; 17 | CREATE TABLE s1.table1 ( 18 | id integer PRIMARY KEY, 19 | name varchar(32), 20 | url varchar(200) 21 | ); 22 | CREATE INDEX ON s1.table1(name); 23 | 24 | CREATE SCHEMA s2; 25 | CREATE TABLE s2.table1 ( 26 | id integer PRIMARY KEY, 27 | name varchar(32), 28 | url varchar(200) 29 | ); 30 | CREATE INDEX ON s2.table1(url); 31 | " 32 | 33 | echo 34 | echo "# Compare the indexes between two schemas in the same database" 35 | echo "# Expect SQL (pseudocode):" 36 | echo "# Add index on s2.table1.name" 37 | echo "# Drop index on s2.table1.url" 38 | echo 39 | 40 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 41 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 42 | INDEX | grep -v '^-- ' 43 | echo 44 | echo ========================================================== 45 | echo 46 | 47 | 48 | # 49 | # Compare the indexes in all schemas between two databases 50 | # 51 | ./populate-db.sh db2 " 52 | CREATE SCHEMA s1; 53 | CREATE TABLE s1.table1 ( 54 | id integer PRIMARY KEY, 55 | name varchar(32), 56 | url varchar(200) 57 | ); 58 | CREATE INDEX ON s1.table1(name); 59 | CREATE INDEX ON s1.table1(url); 60 | 61 | CREATE SCHEMA s2; 62 | CREATE TABLE s2.table1 ( 63 | id integer PRIMARY KEY, 64 | name varchar(32), 65 | url varchar(200) 66 | ); 67 | " 68 | 69 | echo 70 | echo "# Compare the indexes in all schemas between two databases" 71 | echo "# Expect SQL (pseudocode):" 72 | echo "# Drop index on db2 s1.table1.url" 73 | echo "# Add index on db2 s2.table1.url" 74 | echo 75 | 76 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 77 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 78 | INDEX | grep -v '^-- ' 79 | echo 80 | echo 81 | -------------------------------------------------------------------------------- /test/test-owner: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | # Rebuild the basic databases 7 | source ./start-fresh.sh >/dev/null 8 | 9 | 10 | echo 11 | echo ========================================================== 12 | echo 13 | 14 | # 15 | # Compare the table, etc owners between two schemas in the same database 16 | # 17 | 18 | ./populate-db.sh db1 " 19 | -- schema s1 20 | CREATE SCHEMA s1; 21 | CREATE TABLE s1.table1(); 22 | ALTER TABLE s1.table1 OWNER TO u2; 23 | CREATE TABLE s1.table2(); 24 | CREATE TABLE s1.table3(); 25 | CREATE TABLE s1.table4(); 26 | 27 | -- schema s2 28 | CREATE SCHEMA s2; 29 | CREATE TABLE s2.table1(); 30 | CREATE TABLE s2.table2(); 31 | ALTER TABLE s2.table2 OWNER TO u2; 32 | CREATE TABLE s2.table3(); 33 | CREATE TABLE s2.table5(); 34 | " 35 | 36 | echo 37 | echo "# Compare the table, etc. owners between two schemas in the same database" 38 | echo "# Expect SQL (pseudocode):" 39 | echo "# Change s2.table1 owner to u2" 40 | echo "# Change s2.table2 owner to u1" 41 | echo "# No changes to ownership of s2.table3" 42 | echo "# Messages about table4 and table5 not being in both schemas" 43 | echo 44 | 45 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 46 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 47 | OWNER #| grep -v '^-- ' 48 | 49 | 50 | echo 51 | echo ========================================================== 52 | echo 53 | 54 | 55 | # 56 | # Compare the table, etc. owners in all schemas between two databases 57 | # 58 | ./populate-db.sh db2 " 59 | -- schema s1 60 | CREATE SCHEMA s1; 61 | CREATE TABLE s1.table1(); 62 | ALTER TABLE s1.table1 OWNER TO u2; 63 | CREATE TABLE s1.table2(); 64 | CREATE TABLE s1.table3(); 65 | ALTER TABLE s1.table3 OWNER TO u2; 66 | 67 | -- schema s2 68 | CREATE SCHEMA s2; 69 | CREATE TABLE s2.table1(); 70 | CREATE TABLE s2.table2(); 71 | CREATE TABLE s2.table3(); 72 | " 73 | 74 | echo 75 | echo "# Compare the table, etc owners in all schemas between two databases" 76 | echo "# Expect SQL (pseudocode):" 77 | echo "# Change s1.table3 owner to u1 " 78 | echo "# Change s2.table2 owner to u2 " 79 | echo 80 | 81 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 82 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 83 | OWNER #| grep -v '^-- ' 84 | echo 85 | echo 86 | -------------------------------------------------------------------------------- /test/test-schemata: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | echo 8 | echo ========================================================== 9 | echo 10 | 11 | # 12 | # Compare the schemas (aka schematas) between two databases 13 | # 14 | ./populate-db.sh db1 " 15 | CREATE SCHEMA s1; -- matches db2 16 | CREATE SCHEMA s2; -- to be added to db2 17 | " 18 | ./populate-db.sh db2 " 19 | CREATE SCHEMA s1; -- matches db1 20 | CREATE SCHEMA s3; -- to be removed from this db 21 | " 22 | 23 | echo 24 | echo "# Compare the indexes in all schemas between two databases" 25 | echo "# Expect SQL (pseudocode):" 26 | echo "# Add schema on db2: s2 " 27 | echo "# Drop schema from db2: s3 " 28 | echo 29 | 30 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 31 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 32 | SCHEMA | grep -v '^-- ' 33 | echo 34 | echo 35 | -------------------------------------------------------------------------------- /test/test-sequence: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | echo 8 | echo ========================================================== 9 | echo 10 | 11 | # 12 | # Compare the sequences between two schemas in the same database 13 | # 14 | 15 | ./populate-db.sh db1 " 16 | CREATE SCHEMA s1; 17 | CREATE TABLE s1.table1 (id integer PRIMARY KEY); -- just for kicks 18 | CREATE SEQUENCE s1.sequence_1 19 | INCREMENT BY 2 20 | MINVALUE 1024 21 | MAXVALUE 99998 22 | START WITH 2048 23 | NO CYCLE 24 | OWNED BY s1.table1.id; 25 | CREATE SEQUENCE s1.sequence_2; 26 | 27 | CREATE SCHEMA s2; 28 | CREATE SEQUENCE s2.sequence_2; 29 | CREATE SEQUENCE s2.sequence_3; 30 | " 31 | 32 | echo 33 | echo "# Compare the sequences between two schemas in the same database" 34 | echo "# Expect SQL (pseudocode):" 35 | echo "# Add s2.sequence_1" 36 | echo "# Drop s2.sequence_3" 37 | echo 38 | 39 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 40 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 41 | SEQUENCE | grep -v '^-- ' 42 | 43 | 44 | echo 45 | echo ========================================================== 46 | echo 47 | 48 | # 49 | # Compare the sequences in all schemas between two databases 50 | # 51 | ./populate-db.sh db2 " 52 | CREATE SCHEMA s1; 53 | CREATE SEQUENCE s1.sequence_2; 54 | 55 | CREATE SCHEMA s2; 56 | CREATE SEQUENCE s2.sequence_2; 57 | CREATE SEQUENCE s2.sequence_3; 58 | CREATE SEQUENCE s2.sequence_4; 59 | " 60 | 61 | echo 62 | echo "# Compare the sequences in all schemas between two databases" 63 | echo "# Expect SQL (pseudocode):" 64 | echo "# Add sequence s1.sequence_1" 65 | echo "# Drop sequence s2.sequence_4" 66 | echo 67 | 68 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 69 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 70 | SEQUENCE | grep -v '^-- ' 71 | echo 72 | echo 73 | -------------------------------------------------------------------------------- /test/test-table: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | 8 | echo 9 | echo ============================================================== 10 | 11 | # 12 | # Compare the tables between two schemas in the same database 13 | # 14 | #psql -U u1 -h localhost -d db1 <<'EOS' 15 | ./populate-db.sh db1 " 16 | CREATE SCHEMA s1; 17 | CREATE TABLE s1.table9 (id integer); -- to be added to s2 18 | CREATE TABLE s1.table10 (id integer); 19 | 20 | CREATE SCHEMA s2; 21 | CREATE TABLE s2.table10 (id integer); 22 | CREATE TABLE s2.table11 (id integer); -- will be dropped from s2 23 | " 24 | 25 | echo 26 | echo "# Compare the tables between two schemas in the same database" 27 | echo "# Expect SQL:" 28 | echo "# Add table9 to schema s2" 29 | echo "# Drop table11 from schema s2" 30 | echo 31 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 32 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 33 | TABLE | grep -v '^-- ' 34 | 35 | echo 36 | echo ============================================================== 37 | 38 | ./populate-db.sh db2 " 39 | CREATE SCHEMA s1; 40 | CREATE TABLE s1.table9 (id integer); 41 | -- table10 will be added in db2 42 | 43 | CREATE SCHEMA s2; 44 | CREATE TABLE s2.table10 (id integer); 45 | CREATE TABLE s2.table11 (id integer); 46 | CREATE TABLE s2.table12 (id integer); -- will be dropped (not in db1) 47 | 48 | CREATE SCHEMA s3; 49 | " 50 | 51 | echo 52 | echo "# Compare the tables in all schemas between two databases" 53 | echo "# Expect:" 54 | echo "# Add s1.table10 to db2" 55 | echo "# Drop s2.table12 from db2" 56 | echo 57 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 58 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 59 | TABLE | grep -v '^-- ' 60 | echo 61 | -------------------------------------------------------------------------------- /test/test-table-column: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | 8 | echo 9 | echo ============================================================== 10 | 11 | # 12 | # Compare the table columns between two schemas in the same database 13 | # 14 | #psql -U u1 -h localhost -d db1 <<'EOS' 15 | ./populate-db.sh db1 " 16 | CREATE SCHEMA s1; 17 | CREATE TABLE s1.table9 ( 18 | id integer, 19 | name varchar(50) 20 | ); 21 | CREATE TABLE s1.table10 (id bigint); 22 | CREATE TABLE s1.table11 (); 23 | 24 | CREATE SCHEMA s2; 25 | CREATE TABLE s2.table9 ( -- Add name column 26 | id integer 27 | ); 28 | CREATE TABLE s2.table10 (id integer); -- change id to bigint 29 | CREATE TABLE s2.table11 (id integer); -- drop id column 30 | CREATE OR REPLACE VIEW s1.view1 AS 31 | SELECT * 32 | FROM s1.table10; 33 | " 34 | 35 | echo 36 | echo "# Compare the columns between two schemas in the same database" 37 | echo "# Expect SQL:" 38 | echo "# Add s2.table9.name" 39 | echo "# Change s2.table10.id to bigint" 40 | echo "# Drop s2.table11.id" 41 | 42 | echo 43 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 44 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 45 | TABLE_COLUMN | grep -v '^-- ' 46 | 47 | 48 | echo 49 | echo ============================================================== 50 | 51 | ./populate-db.sh db2 " 52 | CREATE SCHEMA s1; 53 | CREATE TABLE s1.table9 ( 54 | id integer, 55 | name varchar(40) 56 | ); 57 | CREATE TABLE s1.table10 (); 58 | CREATE TABLE s1.table11 (dropme integer); 59 | 60 | CREATE SCHEMA s2; 61 | CREATE TABLE s2.table9 ( -- Add name column 62 | id integer 63 | ); 64 | CREATE TABLE s2.table10 (id integer); -- change id to bigint 65 | CREATE TABLE s2.table11 (id integer); -- drop id column 66 | CREATE OR REPLACE VIEW s1.view1 AS 67 | SELECT * 68 | FROM s1.table10; 69 | " 70 | 71 | echo 72 | echo "# Compare the table columns in all schemas between two databases" 73 | echo "# Expect:" 74 | echo "# Change s1.table9.name to varchar(50) " 75 | echo "# Add s1.table10.id" 76 | echo "# Drop s1.table11.dropme" 77 | echo 78 | 79 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 80 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 81 | TABLE_COLUMN | grep -v '^-- ' 82 | echo 83 | -------------------------------------------------------------------------------- /test/test-trigger: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Useful for visually inspecting the output SQL to verify it is doing what it should 4 | # 5 | 6 | source ./start-fresh.sh >/dev/null 7 | echo 8 | echo ========================================================== 9 | echo 10 | 11 | # 12 | # Compare the triggers between two schemas in the same database 13 | # 14 | 15 | #CREATE [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } 16 | # ON table_name 17 | # [ FROM referenced_table_name ] 18 | # [ NOT DEFERRABLE | [ DEFERRABLE ] { INITIALLY IMMEDIATE | INITIALLY DEFERRED } ] 19 | # [ FOR [ EACH ] { ROW | STATEMENT } ] 20 | # [ WHEN ( condition ) ] 21 | # EXECUTE PROCEDURE function_name ( arguments ) 22 | 23 | ./populate-db.sh db1 "$(cat << 'EOF' 24 | -- Schema s1 25 | CREATE SCHEMA s1; 26 | CREATE TABLE s1.table1 (id integer); 27 | CREATE OR REPLACE FUNCTION s1.validate1() RETURNS TRIGGER AS $$ 28 | BEGIN 29 | SELECT 1; -- look like we are doing something ;^> 30 | END; 31 | $$ LANGUAGE plpgsql; 32 | CREATE TRIGGER trigger1 AFTER INSERT ON s1.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 33 | CREATE TRIGGER trigger2 AFTER INSERT ON s1.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 34 | 35 | 36 | -- Schema s2 37 | CREATE SCHEMA s2; 38 | CREATE TABLE s2.table1 (id integer); 39 | CREATE TRIGGER trigger2 BEFORE INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 40 | CREATE TRIGGER trigger3 AFTER INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 41 | 42 | EOF 43 | )" 44 | 45 | echo 46 | echo "# Compare the triggers between two schemas in the same database" 47 | echo "# Expect SQL (pseudocode):" 48 | echo "# Create trigger1 on s2.table1" 49 | echo "# Recreate trigger2 on s2.table1" 50 | echo "# Drop trigger3 on s2.table1" 51 | echo 52 | 53 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ 54 | -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ 55 | TRIGGER | grep -v '^-- ' 56 | 57 | echo 58 | echo ========================================================== 59 | echo 60 | 61 | # 62 | # Compare the triggers in all schemas between two databases 63 | # 64 | ./populate-db.sh db2 "$(cat << 'EOF' 65 | 66 | -- Schema s1 67 | CREATE SCHEMA s1; 68 | CREATE TABLE s1.table1 (id integer); 69 | CREATE OR REPLACE FUNCTION s1.validate1() RETURNS TRIGGER AS $$ 70 | BEGIN 71 | SELECT 1; -- look like we are doing something :^> 72 | END; 73 | $$ LANGUAGE plpgsql; 74 | CREATE TRIGGER trigger2 BEFORE INSERT ON s1.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 75 | 76 | 77 | -- Schema s2 78 | CREATE SCHEMA s2; 79 | CREATE TABLE s2.table1 (id integer); 80 | CREATE TRIGGER trigger2 BEFORE INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 81 | CREATE TRIGGER trigger3 AFTER INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 82 | CREATE TRIGGER trigger4 AFTER INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); 83 | 84 | EOF 85 | )" 86 | 87 | echo 88 | echo "# Compare the triggers in all schemas between two databases" 89 | echo "# Expect SQL (pseudocode):" 90 | echo "# Create trigger1 on s1.table1" 91 | echo "# Recreate trigger2 on s1.table1" 92 | echo "# Drop trigger4 on s2.table1" 93 | echo 94 | 95 | ../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ 96 | -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ 97 | TRIGGER | grep -v '^-- ' 98 | echo 99 | echo 100 | -------------------------------------------------------------------------------- /trigger.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "database/sql" 12 | "fmt" 13 | "github.com/joncrlsn/misc" 14 | "github.com/joncrlsn/pgutil" 15 | "sort" 16 | "strings" 17 | "text/template" 18 | ) 19 | 20 | var ( 21 | triggerSqlTemplate = initTriggerSqlTemplate() 22 | ) 23 | 24 | // Initializes the Sql template 25 | func initTriggerSqlTemplate() *template.Template { 26 | sql := ` 27 | SELECT n.nspname AS schema_name 28 | , {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}c.relname || '.' || t.tgname AS compare_name 29 | , c.relname AS table_name 30 | , t.tgname AS trigger_name 31 | , pg_catalog.pg_get_triggerdef(t.oid, true) AS trigger_def 32 | , t.tgenabled AS enabled 33 | FROM pg_catalog.pg_trigger t 34 | INNER JOIN pg_catalog.pg_class c ON (c.oid = t.tgrelid) 35 | INNER JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) 36 | WHERE not t.tgisinternal 37 | {{if eq $.DbSchema "*" }} 38 | AND n.nspname NOT LIKE 'pg_%' 39 | AND n.nspname <> 'information_schema' 40 | {{else}} 41 | AND n.nspname = '{{$.DbSchema}}' 42 | {{end}} 43 | ` 44 | t := template.New("TriggerSqlTmpl") 45 | template.Must(t.Parse(sql)) 46 | return t 47 | } 48 | 49 | // ================================== 50 | // TriggerRows definition 51 | // ================================== 52 | 53 | // TriggerRows is a sortable slice of string maps 54 | type TriggerRows []map[string]string 55 | 56 | func (slice TriggerRows) Len() int { 57 | return len(slice) 58 | } 59 | 60 | func (slice TriggerRows) Less(i, j int) bool { 61 | return slice[i]["compare_name"] < slice[j]["compare_name"] 62 | } 63 | 64 | func (slice TriggerRows) Swap(i, j int) { 65 | slice[i], slice[j] = slice[j], slice[i] 66 | } 67 | 68 | // TriggerSchema holds a channel streaming trigger information from one of the databases as well as 69 | // a reference to the current row of data we're viewing. 70 | // 71 | // TriggerSchema implements the Schema interface defined in pgdiff.go 72 | type TriggerSchema struct { 73 | rows TriggerRows 74 | rowNum int 75 | done bool 76 | } 77 | 78 | // get returns the value from the current row for the given key 79 | func (c *TriggerSchema) get(key string) string { 80 | if c.rowNum >= len(c.rows) { 81 | return "" 82 | } 83 | return c.rows[c.rowNum][key] 84 | } 85 | 86 | // NextRow increments the rowNum and tells you whether or not there are more 87 | func (c *TriggerSchema) NextRow() bool { 88 | if c.rowNum >= len(c.rows)-1 { 89 | c.done = true 90 | } 91 | c.rowNum = c.rowNum + 1 92 | return !c.done 93 | } 94 | 95 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 96 | func (c *TriggerSchema) Compare(obj interface{}) int { 97 | c2, ok := obj.(*TriggerSchema) 98 | if !ok { 99 | fmt.Println("Error!!!, Compare(obj) needs a TriggerSchema instance", c2) 100 | return +999 101 | } 102 | 103 | val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) 104 | return val 105 | } 106 | 107 | // Add returns SQL to create the trigger 108 | func (c TriggerSchema) Add() { 109 | // If we are comparing two different schemas against each other, we need to do some 110 | // modification of the first trigger definition so we create it in the right schema 111 | triggerDef := c.get("trigger_def") 112 | schemaName := c.get("schema_name") 113 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 114 | schemaName = dbInfo2.DbSchema 115 | triggerDef = strings.Replace( 116 | triggerDef, 117 | fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), 118 | fmt.Sprintf(" %s.%s ", schemaName, c.get("table_name")), 119 | -1) 120 | } 121 | 122 | fmt.Printf("%s;\n", triggerDef) 123 | } 124 | 125 | // Drop returns SQL to drop the trigger 126 | func (c TriggerSchema) Drop() { 127 | fmt.Printf("DROP TRIGGER %s ON %s.%s;\n", c.get("trigger_name"), c.get("schema_name"), c.get("table_name")) 128 | } 129 | 130 | // Change handles the case where the trigger names match, but the definition does not 131 | func (c TriggerSchema) Change(obj interface{}) { 132 | c2, ok := obj.(*TriggerSchema) 133 | if !ok { 134 | fmt.Println("Error!!!, Change needs a TriggerSchema instance", c2) 135 | } 136 | if c.get("trigger_def") != c2.get("trigger_def") { 137 | fmt.Println("-- This function looks different so we'll drop and recreate it:") 138 | 139 | // If we are comparing two different schemas against each other, we need to do some 140 | // modification of the first trigger definition so we create it in the right schema 141 | triggerDef := c.get("trigger_def") 142 | schemaName := c.get("schema_name") 143 | if dbInfo1.DbSchema != dbInfo2.DbSchema { 144 | schemaName = dbInfo2.DbSchema 145 | triggerDef = strings.Replace( 146 | triggerDef, 147 | fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), 148 | fmt.Sprintf(" %s.%s ", schemaName, c.get("table_name")), 149 | -1) 150 | } 151 | 152 | // The trigger_def column has everything needed to rebuild the function 153 | fmt.Printf("DROP TRIGGER %s ON %s.%s;\n", c.get("trigger_name"), schemaName, c.get("table_name")) 154 | fmt.Println("-- STATEMENT-BEGIN") 155 | fmt.Printf("%s;\n", triggerDef) 156 | fmt.Println("-- STATEMENT-END") 157 | } 158 | } 159 | 160 | // compareTriggers outputs SQL to make the triggers match between DBs 161 | func compareTriggers(conn1 *sql.DB, conn2 *sql.DB) { 162 | 163 | buf1 := new(bytes.Buffer) 164 | triggerSqlTemplate.Execute(buf1, dbInfo1) 165 | 166 | buf2 := new(bytes.Buffer) 167 | triggerSqlTemplate.Execute(buf2, dbInfo2) 168 | 169 | rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) 170 | rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) 171 | 172 | rows1 := make(TriggerRows, 0) 173 | for row := range rowChan1 { 174 | rows1 = append(rows1, row) 175 | } 176 | sort.Sort(rows1) 177 | 178 | rows2 := make(TriggerRows, 0) 179 | for row := range rowChan2 { 180 | rows2 = append(rows2, row) 181 | } 182 | sort.Sort(rows2) 183 | 184 | // We must explicitly type this as Schema here 185 | var schema1 Schema = &TriggerSchema{rows: rows1, rowNum: -1} 186 | var schema2 Schema = &TriggerSchema{rows: rows2, rowNum: -1} 187 | 188 | // Compare the triggers 189 | doDiff(schema1, schema2) 190 | } 191 | -------------------------------------------------------------------------------- /vendor/github.com/lib/pq/.gitignore: -------------------------------------------------------------------------------- 1 | .db 2 | *.test 3 | *~ 4 | *.swp 5 | .idea 6 | .vscode -------------------------------------------------------------------------------- /view.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2016 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "sort" 12 | "database/sql" 13 | "github.com/joncrlsn/pgutil" 14 | "github.com/joncrlsn/misc" 15 | ) 16 | 17 | // ================================== 18 | // ViewRows definition 19 | // ================================== 20 | 21 | // ViewRows is a sortable slice of string maps 22 | type ViewRows []map[string]string 23 | 24 | func (slice ViewRows) Len() int { 25 | return len(slice) 26 | } 27 | 28 | func (slice ViewRows) Less(i, j int) bool { 29 | return slice[i]["viewname"] < slice[j]["viewname"] 30 | } 31 | 32 | func (slice ViewRows) Swap(i, j int) { 33 | slice[i], slice[j] = slice[j], slice[i] 34 | } 35 | 36 | // ViewSchema holds a channel streaming view information from one of the databases as well as 37 | // a reference to the current row of data we're viewing. 38 | // 39 | // ViewSchema implements the Schema interface defined in pgdiff.go 40 | type ViewSchema struct { 41 | rows ViewRows 42 | rowNum int 43 | done bool 44 | } 45 | 46 | // get returns the value from the current row for the given key 47 | func (c *ViewSchema) get(key string) string { 48 | if c.rowNum >= len(c.rows) { 49 | return "" 50 | } 51 | return c.rows[c.rowNum][key] 52 | } 53 | 54 | // NextRow increments the rowNum and tells you whether or not there are more 55 | func (c *ViewSchema) NextRow() bool { 56 | if c.rowNum >= len(c.rows)-1 { 57 | c.done = true 58 | } 59 | c.rowNum = c.rowNum + 1 60 | return !c.done 61 | } 62 | 63 | // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row 64 | func (c *ViewSchema) Compare(obj interface{}) int { 65 | c2, ok := obj.(*ViewSchema) 66 | if !ok { 67 | fmt.Println("Error!!!, Compare(obj) needs a ViewSchema instance", c2) 68 | return +999 69 | } 70 | 71 | val := misc.CompareStrings(c.get("viewname"), c2.get("viewname")) 72 | //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("viewname"), c2.get("viewname")) 73 | return val 74 | } 75 | 76 | // Add returns SQL to create the view 77 | func (c ViewSchema) Add() { 78 | fmt.Printf("CREATE VIEW %s AS %s \n\n", c.get("viewname"), c.get("definition")) 79 | } 80 | 81 | // Drop returns SQL to drop the view 82 | func (c ViewSchema) Drop() { 83 | fmt.Printf("DROP VIEW %s;\n\n", c.get("viewname")) 84 | } 85 | 86 | // Change handles the case where the names match, but the definition does not 87 | func (c ViewSchema) Change(obj interface{}) { 88 | c2, ok := obj.(*ViewSchema) 89 | if !ok { 90 | fmt.Println("Error!!!, Change needs a ViewSchema instance", c2) 91 | } 92 | if c.get("definition") != c2.get("definition") { 93 | fmt.Printf("DROP VIEW %s;\n", c.get("viewname")) 94 | fmt.Printf("CREATE VIEW %s AS %s \n\n", c.get("viewname"), c.get("definition")) 95 | } 96 | } 97 | 98 | // compareViews outputs SQL to make the views match between DBs 99 | func compareViews(conn1 *sql.DB, conn2 *sql.DB) { 100 | sql := ` 101 | SELECT schemaname || '.' || viewname AS viewname 102 | , definition 103 | FROM pg_views 104 | WHERE schemaname NOT LIKE 'pg_%' AND schemaname!='infromation_schema' 105 | ORDER BY viewname; 106 | ` 107 | 108 | rowChan1, _ := pgutil.QueryStrings(conn1, sql) 109 | rowChan2, _ := pgutil.QueryStrings(conn2, sql) 110 | 111 | rows1 := make(ViewRows, 0) 112 | for row := range rowChan1 { 113 | rows1 = append(rows1, row) 114 | } 115 | sort.Sort(rows1) 116 | 117 | rows2 := make(ViewRows, 0) 118 | for row := range rowChan2 { 119 | rows2 = append(rows2, row) 120 | } 121 | sort.Sort(rows2) 122 | 123 | // We have to explicitly type this as Schema here 124 | var schema1 Schema = &ViewSchema{rows: rows1, rowNum: -1} 125 | var schema2 Schema = &ViewSchema{rows: rows2, rowNum: -1} 126 | 127 | // Compare the views 128 | doDiff(schema1, schema2) 129 | } 130 | --------------------------------------------------------------------------------