diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d5fb46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +pgdiff +vendor/* \ No newline at end of file diff --git a/LICENSE b/LICENSE index d772589..9235b22 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Jon Carlson +Copyright (c) 2017 Jon Carlson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin-linux/README.md b/README-linux.md similarity index 69% rename from bin-linux/README.md rename to README-linux.md index 967bc0d..e6e3e41 100644 --- a/bin-linux/README.md +++ b/README-linux.md @@ -2,10 +2,10 @@ 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. -1. download pgdiff.tgz to your machine -1. untar pgdiff.tgz (a new directory will be created: called pgdiff) +1. download pgdiff-linux-\<version\>.tar.gz to your machine +1. untar it (a new directory will be created: called pgdiff) 1. cd into the new pgdiff directory -1. optionally 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) +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) 1. run pgdiff.sh ## tar contents diff --git a/README-macos-arm64.md b/README-macos-arm64.md new file mode 100644 index 0000000..110f729 --- /dev/null +++ b/README-macos-arm64.md @@ -0,0 +1,16 @@ +## OSX / Mac / ARM64 pgdiff instructions + +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. + +1. download pgdiff-arm64-\<version\>.tar.gz to your machine +1. untar it (a new directory will be created: called pgdiff) +1. cd into the new pgdiff directory +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) +1. run pgdiff.sh + +## tar contents +* pgdiff - an OSX executable +* pgrun - an OSX executable for running SQL +* pgdiff.sh - a bash shell script to coordinate your interaction with pgdiff and pgrun + +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. diff --git a/bin-osx/README.md b/README-osx.md similarity index 53% rename from bin-osx/README.md rename to README-osx.md index fa51472..9611c66 100644 --- a/bin-osx/README.md +++ b/README-osx.md @@ -2,10 +2,10 @@ 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. -1. download pgdiff.tgz to your machine -1. untar pgdiff.tgz (a new directory will be created: called pgdiff) +1. download pgdiff-mac-\<version\>.tar.gz to your machine +1. untar it (a new directory will be created: called pgdiff) 1. cd into the new pgdiff directory -1. optionally 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) +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) 1. run pgdiff.sh ## tar contents @@ -13,4 +13,4 @@ These instructions will guide you through the process of generating SQL, reviewi * pgrun - an OSX executable for running SQL * pgdiff.sh - a bash shell script to coordinate your interaction with pgdiff and pgrun -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). +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. diff --git a/README-win.md b/README-win.md new file mode 100644 index 0000000..ab9f95c --- /dev/null +++ b/README-win.md @@ -0,0 +1,9 @@ +### getting started on windows + +1. download pgdiff.exe from the release page on github +1. either install cygwin so you can run pgdiff.sh or... +1. manually run pgdiff.exe for each schema type listed in the usage section above +1. review the SQL output and, if you want to make them match, run it against the second db + +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. + diff --git a/README.md b/README.md index ac1319f..4ca2dc2 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ pgdiff is transparent in what it does, so it never modifies a database directly. pgdiff is written to be easy to expand and improve the accuracy of the diff. -### download -[osx](https://github.com/joncrlsn/pgdiff/raw/master/bin-osx/pgdiff.tgz "OSX version") [linux](https://github.com/joncrlsn/pgdiff/raw/master/bin-linux/pgdiff.tgz "Linux version") [windows](https://github.com/joncrlsn/pgdiff/raw/master/bin-win/pgdiff.exe "Windows version") +### download 1.0 beta 1 +[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") ### usage @@ -16,34 +16,34 @@ pgdiff is written to be easy to expand and improve the accuracy of the diff. (where options and <schemaType> are listed below) -I have found that there is 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. This is the order that has worked for me, however "your mileage may vary". +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. 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. Schema type ordering: -1. ROLE -1. FUNCTION -1. SEQUENCE 1. SCHEMA +1. ROLE 1. SEQUENCE 1. TABLE 1. COLUMN 1. INDEX 1. VIEW -1. OWNER 1. FOREIGN\_KEY +1. FUNCTION +1. TRIGGER +1. OWNER 1. GRANT\_RELATIONSHIP 1. GRANT\_ATTRIBUTE -1. TRIGGER +1. ALL (all above in one run) ### example 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. ``` -pgdiff -U dbuser -H localhost -D refDB -O "sslmode=disable" \ - -u dbuser -h localhost -d compDB -o "sslmode=disable" \ +pgdiff -U dbuser -H localhost -D refDB -O "sslmode=disable" -S public \ + -u dbuser -h localhost -d compDB -o "sslmode=disable" -s public \ TABLE ``` @@ -58,12 +58,14 @@ options | explanation -u, --user2 | second postgres user -W, --password1 | first db password -w, --password2 | second db password - -H, --host1 | first db host. default is localhost + -H, --host1 | first db host. default is localhost -h, --host2 | second db host. default is localhost - -P, --port1 | first db port number. default is 5432 + -P, --port1 | first db port number. default is 5432 -p, --port2 | second db port number. default is 5432 -D, --dbname1 | first db name -d, --dbname2 | second db name + -S, --schema1 | first schema name. default is * (all non-system schemas) + -s, --schema2 | second schema name. default is * (all non-system schemas) -O, --option1 | first db options. example: sslmode=disable -o, --option2 | second db options. example: sslmode=disable @@ -77,25 +79,31 @@ linux and osx binaries are packaged with an extra, optional bash script and pgru 1. cd to the new pgdiff directory 1. edit the db connection defaults in pgdiff.sh 1. ...or manually run pgdiff for each schema type listed in the usage section above -1. review the SQL output for each schema type and, if you want to make them match, run it against db2 (Function SQL requires the use of pgrun instead of psql) +1. review the SQL output for each schema type and, if you want to make them match, run it against the second db ### getting started on windows 1. download pgdiff.exe from the bin-win directory on github -1. edit the db connection defaults in pgdiff.sh or... -1. manually run pgdiff for each schema type listed in the usage section above -1. review the SQL output and, if you want to make them match, run it against db2 +1. either install cygwin so you can run pgdiff.sh or... +1. manually run pgdiff.exe for each schema type listed in the usage section above +1. review the SQL output and, if you want to make them match, run it against the second db + +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. ### version history -1. 0.9.0 - Implemented ROLE, SEQUENCE, TABLE, COLUMN, INDEX, FOREIGN\_KEY, OWNER, GRANT\_RELATIONSHIP, GRANT\_ATTRIBUTE -1. 0.9.1 - Added VIEW, FUNCTION, and TRIGGER (Thank you, Shawn Carroll AKA SparkeyG) -1. 0.9.2 - Fixed bug when using the non-default port +* 0.9.0 - Implemented ROLE, SEQUENCE, TABLE, COLUMN, INDEX, FOREIGN\_KEY, OWNER, GRANT\_RELATIONSHIP, GRANT\_ATTRIBUTE +* 0.9.1 - Added VIEW, FUNCTION, and TRIGGER (Thank you, Shawn Carroll AKA SparkeyG) +* 0.9.2 - Fixed bug when using the non-default port +* 0.9.3 - Fixed VARCHAR bug when no max length specified +* 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) +### getting help +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. ### todo -1. fix SQL for adding an array column -1. create windows version of pgdiff.sh (or even better: re-write it all in Go) -1. allow editing of individual SQL lines after failure (this would probably be done in the script pgdiff.sh) -1. store failed SQL statements in an error file for later fixing and rerunning? +* fix SQL for adding an array column +* create windows version of pgdiff.sh (or even better: re-write it all in Go) +* allow editing of individual SQL lines after failure (this would probably be done in the script pgdiff.sh) +* store failed SQL statements in an error file for later fixing and rerunning? diff --git a/bin-linux/pgdiff.tgz.REMOVED.git-id b/bin-linux/pgdiff.tgz.REMOVED.git-id deleted file mode 100644 index 7a217a3..0000000 --- a/bin-linux/pgdiff.tgz.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -ad224e038d165562a2deafc0216717379f8beadb \ No newline at end of file diff --git a/bin-osx/pgdiff.tgz.REMOVED.git-id b/bin-osx/pgdiff.tgz.REMOVED.git-id deleted file mode 100644 index cef9b8c..0000000 --- a/bin-osx/pgdiff.tgz.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -85ad23c83c6c3cdb2cf777c8eaaec0a6d70cfb37 \ No newline at end of file diff --git a/bin-win/pgdiff.exe.REMOVED.git-id b/bin-win/pgdiff.exe.REMOVED.git-id deleted file mode 100644 index 71d7f5b..0000000 --- a/bin-win/pgdiff.exe.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -32fa2c8940cbed63e546ff852722035c5f317be2 \ No newline at end of file diff --git a/bin-win/pgrun.exe.REMOVED.git-id b/bin-win/pgrun.exe.REMOVED.git-id deleted file mode 100644 index 1cc8f19..0000000 --- a/bin-win/pgrun.exe.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -1675e9f7afb3e2aa2a11db7aa34ccc08f7637a94 \ No newline at end of file diff --git a/build.sh b/build.sh index 19e3919..305cbd7 100755 --- a/build.sh +++ b/build.sh @@ -1,16 +1,36 @@ -#!/bin/bash -x +#!/bin/bash # -# For OSX and Linux, this script: +# Builds executable and downloadable bundle for 3 platforms +# +# For OSX and Linux: # * builds pgdiff # * downloads pgrun -# * combines them and pgdiff.sh into a tgz file +# * combines them, a README, and pgdiff.sh into a tgz file +# +# For Windows: +# * builds pgdiff.exe +# * downloads pgrun.exe +# * combines them, a README, and pgdiff.sh into a zip file # SCRIPT_DIR="$(dirname `ls -l $0 | awk '{ print $NF }'`)" [[ -z $APPNAME ]] && APPNAME=pgdiff +[[ -z $VERSION ]] && read -p "Enter version number: " VERSION + +LINUX_README=README-linux.md +LINUX_FILE="${APPNAME}-linux-${VERSION}.tar.gz" + +OSX_README=README-osx.md +OSX_FILE="${APPNAME}-osx-${VERSION}.tar.gz" -if [[ -d bin-linux ]]; then +ARM64_README=README-macos-arm64.md +ARM64_FILE="${APPNAME}-macos-arm64-${VERSION}.tar.gz" + +WIN_README=README-win.md +WIN_FILE="${APPNAME}-win-${VERSION}.zip" + +if [[ -f $LINUX_README ]]; then echo " ==== Building Linux ====" tempdir="$(mktemp -d)" workdir="$tempdir/$APPNAME" @@ -22,18 +42,20 @@ if [[ -d bin-linux ]]; then wget -O "$workdir/pgrun" "https://github.com/joncrlsn/pgrun/raw/master/bin-linux/pgrun" # Copy the bash runtime script to the temp directory cp pgdiff.sh "$workdir/" + cp "${SCRIPT_DIR}/${LINUX_README}" "$workdir/README.md" cd "$tempdir" # Make everything executable chmod -v ugo+x $APPNAME/* - COPYFILE_DISABLE=true tar -cvzf "${APPNAME}.tgz" $APPNAME + tarName="${tempdir}/${LINUX_FILE}" + COPYFILE_DISABLE=true tar -cvzf "$tarName" $APPNAME cd - - mv "${tempdir}/${APPNAME}.tgz" "${SCRIPT_DIR}/bin-linux/" + mv "$tarName" "${SCRIPT_DIR}/" echo "Built linux." else - echo "Skipping linux. No bin-linux directory." + echo "Skipping linux. No $LINUX_README file." fi -if [[ -d bin-osx ]]; then +if [[ -f $OSX_README ]]; then echo " ==== Building OSX ====" tempdir="$(mktemp -d)" workdir="$tempdir/$APPNAME" @@ -45,21 +67,63 @@ if [[ -d bin-osx ]]; then wget -O "$workdir/pgrun" "https://github.com/joncrlsn/pgrun/raw/master/bin-osx/pgrun" # Copy the bash runtime script to the temp directory cp pgdiff.sh "$workdir/" + cp "${SCRIPT_DIR}/${OSX_README}" "$workdir/README.md" cd "$tempdir" # Make everything executable chmod -v ugo+x $APPNAME/* - COPYFILE_DISABLE=true tar -cvzf "${APPNAME}.tgz" $APPNAME + tarName="${tempdir}/${OSX_FILE}" + COPYFILE_DISABLE=true tar -cvzf "$tarName" $APPNAME cd - - mv "${tempdir}/${APPNAME}.tgz" "${SCRIPT_DIR}/bin-osx/" + mv "$tarName" "${SCRIPT_DIR}/" echo "Built osx." else - echo "Skipping osx. No bin-osx directory." + echo "Skipping osx. No $OSX_README file." fi -if [[ -d bin-win ]]; then +if [[ -f $ARM64_README ]]; then + echo " ==== Building macOS-arm64 ====" + tempdir="$(mktemp -d)" + workdir="$tempdir/$APPNAME" + echo $workdir + mkdir -p $workdir + # Build the executable + GOOS=darwin GOARCH=arm64 go build -o "$workdir/$APPNAME" + # Download pgrun to the work directory + 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" + # Copy the bash runtime script to the temp directory + cp pgdiff.sh "$workdir/" + cp "${SCRIPT_DIR}/${ARM64_README}" "$workdir/README.md" + cd "$tempdir" + # Make everything executable + chmod -v ugo+x $APPNAME/* + tarName="${tempdir}/${ARM64_FILE}" + COPYFILE_DISABLE=true tar -cvzf "$tarName" $APPNAME + cd - + mv "$tarName" "${SCRIPT_DIR}/" + echo "Built macOS-arm64." +else + echo "Skipping macOS-arm64. No $ARM64_README file." +fi + +if [[ -f $WIN_README ]]; then echo " ==== Building Windows ====" - GOOS=windows GOARCH=386 go build -o bin-win/${APPNAME}.exe + tempdir="$(mktemp -d)" + workdir="$tempdir/$APPNAME" + echo $workdir + mkdir -p $workdir + GOOS=windows GOARCH=386 go build -o "${workdir}/${APPNAME}.exe" + # Download pgrun to the work directory + # Copy the bash runtime script to the temp directory + cp "${SCRIPT_DIR}/${WIN_README}" "$workdir/README.md" + cd "$tempdir" + # Make everything executable + chmod -v ugo+x $APPNAME/* + wget -O "${workdir}/pgrun.exe" "https://github.com/joncrlsn/pgrun/raw/master/bin-win/pgrun.exe" + zipName="${tempdir}/${WIN_FILE}" + zip -r "$zipName" $APPNAME + cd - + mv "$zipName" "${SCRIPT_DIR}/" echo "Built win." else - echo "Skipping win. No bin-win directory." + echo "Skipping win. No $WIN_README file." fi diff --git a/column.go b/column.go index 3bb15a9..76c12d3 100644 --- a/column.go +++ b/column.go @@ -6,13 +6,87 @@ package main -import "sort" -import "fmt" -import "strconv" -import "strings" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "strconv" + "strings" + "text/template" +) + +var ( + columnSqlTemplate = initColumnSqlTemplate() +) + +// Initializes the Sql template +func initColumnSqlTemplate() *template.Template { + sql := ` +SELECT table_schema + , {{if eq $.DbSchema "*" }}table_schema || '.' || {{end}}table_name || '.' ||lpad(cast (ordinal_position as varchar), 5, '0')|| column_name AS compare_name + , table_name + , column_name + , data_type + , is_nullable + , column_default + , character_maximum_length + , is_identity + , identity_generation + , substring(udt_name from 2) AS array_type +FROM information_schema.columns +WHERE is_updatable = 'YES' +{{if eq $.DbSchema "*" }} +AND table_schema NOT LIKE 'pg_%' +AND table_schema <> 'information_schema' +{{else}} +AND table_schema = '{{$.DbSchema}}' +{{end}} +ORDER BY compare_name ASC; +` + t := template.New("ColumnSqlTmpl") + template.Must(t.Parse(sql)) + return t +} + +var ( + tableColumnSqlTemplate = initTableColumnSqlTemplate() +) + +// Initializes the Sql template +func initTableColumnSqlTemplate() *template.Template { + sql := ` +SELECT a.table_schema + , {{if eq $.DbSchema "*" }}a.table_schema || '.' || {{end}}a.table_name || '.' || column_name AS compare_name + , a.table_name + , column_name + , data_type + , is_nullable + , column_default + , character_maximum_length +FROM information_schema.columns a +INNER JOIN information_schema.tables b + ON a.table_schema = b.table_schema AND + a.table_name = b.table_name AND + b.table_type = 'BASE TABLE' +WHERE is_updatable = 'YES' +{{if eq $.DbSchema "*" }} +AND a.table_schema NOT LIKE 'pg_%' +AND a.table_schema <> 'information_schema' +{{else}} +AND a.table_schema = '{{$.DbSchema}}' +{{end}} +{{ if $.TableType }} +AND b.table_type = '{{ $.TableType }}' +{{ end }} +ORDER BY compare_name ASC; +` + t := template.New("ColumnSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // Column Rows definition @@ -26,10 +100,7 @@ func (slice ColumnRows) Len() int { } func (slice ColumnRows) Less(i, j int) bool { - if slice[i]["table_name"] != slice[j]["table_name"] { - return slice[i]["table_name"] < slice[j]["table_name"] - } - return slice[i]["column_name"] < slice[j]["column_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice ColumnRows) Swap(i, j int) { @@ -73,31 +144,41 @@ func (c *ColumnSchema) Compare(obj interface{}) int { fmt.Println("Error!!!, Compare needs a ColumnSchema instance", c2) } - val := misc.CompareStrings(c.get("table_name"), c2.get("table_name")) - if val != 0 { - // Table name differed so return that value - return val - } - - // Table name was the same so compare column name - val = misc.CompareStrings(c.get("column_name"), c2.get("column_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) return val } // Add prints SQL to add the column func (c *ColumnSchema) Add() { + + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("table_schema") + } + + // Knowing the version of db2 would eliminate the need for this warning + if c.get("is_identity") == "YES" { + fmt.Println("-- WARNING: identity columns are not supported in PostgreSQL versions < 10.") + fmt.Println("-- Attempting to create identity columns in earlier versions will probably result in errors.") + } + if c.get("data_type") == "character varying" { maxLength, valid := getMaxLength(c.get("character_maximum_length")) if !valid { - fmt.Printf("ALTER TABLE %s ADD COLUMN %s character varying", c.get("table_name"), c.get("column_name")) + fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s character varying", schema, c.get("table_name"), c.get("column_name")) } else { - fmt.Printf("ALTER TABLE %s ADD COLUMN %s character varying(%s)", c.get("table_name"), c.get("column_name"), maxLength) + fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s character varying(%s)", schema, c.get("table_name"), c.get("column_name"), maxLength) } } else { - if c.get("data_type") == "ARRAY" { - fmt.Println("-- Note that adding of array data types are not yet generated properly.") + dataType := c.get("data_type") + //if c.get("data_type") == "ARRAY" { + //fmt.Println("-- Note that adding of array data types are not yet generated properly.") + //} + if dataType == "ARRAY" { + dataType = c.get("array_type")+"[]" } - fmt.Printf("ALTER TABLE %s ADD COLUMN %s %s", c.get("table_name"), c.get("column_name"), c.get("data_type")) + //fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s %s", schema, c.get("table_name"), c.get("column_name"), c.get("data_type")) + fmt.Printf("ALTER TABLE %s.%s ADD COLUMN %s %s", schema, c.get("table_name"), c.get("column_name"), dataType) } if c.get("is_nullable") == "NO" { @@ -106,13 +187,18 @@ func (c *ColumnSchema) Add() { if c.get("column_default") != "null" { fmt.Printf(" DEFAULT %s", c.get("column_default")) } + // NOTE: there are more identity column sequence options according to the PostgreSQL + // CREATE TABLE docs, but these do not appear to be available as of version 10.1 + if c.get("is_identity") == "YES" { + fmt.Printf(" GENERATED %s AS IDENTITY", c.get("identity_generation")) + } fmt.Printf(";\n") } // Drop prints SQL to drop the column func (c *ColumnSchema) Drop() { // if dropping column - fmt.Printf("ALTER TABLE %s DROP COLUMN IF EXISTS %s;\n", c.get("table_name"), c.get("column_name")) + fmt.Printf("ALTER TABLE %s.%s DROP COLUMN IF EXISTS %s;\n", c.get("table_schema"), c.get("table_name"), c.get("column_name")) } // Change handles the case where the table and column match, but the details do not @@ -122,10 +208,20 @@ func (c *ColumnSchema) Change(obj interface{}) { fmt.Println("Error!!!, ColumnSchema.Change(obj) needs a ColumnSchema instance", c2) } - // Detect column type change (mostly varchar length, or number size increase) + // Adjust data type for array columns + dataType1 := c.get("data_type") + if dataType1 == "ARRAY" { + dataType1 = c.get("array_type")+"[]" + } + dataType2 := c2.get("data_type") + if dataType2 == "ARRAY" { + dataType2 = c2.get("array_type")+"[]" + } + + // Detect column type change (mostly varchar length, or number size increase) // (integer to/from bigint is OK) - if c.get("data_type") == c2.get("data_type") { - if c.get("data_type") == "character varying" { + if dataType1 == dataType2 { + if dataType1 == "character varying" { max1, max1Valid := getMaxLength(c.get("character_maximum_length")) max2, max2Valid := getMaxLength(c2.get("character_maximum_length")) if !max1Valid && !max2Valid { @@ -141,68 +237,84 @@ func (c *ColumnSchema) Change(obj interface{}) { if max1Int < max2Int { fmt.Println("-- WARNING: The next statement will shorten a character varying column, which may result in data loss.") } - fmt.Printf("-- max1Valid: %v max2Valid: %v ", max1Valid, max2Valid) - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s TYPE character varying(%s);\n", c.get("table_name"), c.get("column_name"), max1) + fmt.Printf("-- max1Valid: %v max2Valid: %v \n", max1Valid, max2Valid) + 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) } } } // Code and test a column change from integer to bigint - if c.get("data_type") != c2.get("data_type") { - fmt.Printf("-- WARNING: This type change may not work well: (%s to %s).\n", c2.get("data_type"), c.get("data_type")) - if strings.HasPrefix(c.get("data_type"), "character") { + if dataType1 != dataType2 { + fmt.Printf("-- WARNING: This type change may not work well: (%s to %s).\n", dataType2, dataType1) + if strings.HasPrefix(dataType1, "character") { max1, max1Valid := getMaxLength(c.get("character_maximum_length")) if !max1Valid { fmt.Println("-- WARNING: varchar column has no maximum length. Setting to 1024") } - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s TYPE %s(%s);\n", c.get("table_name"), c.get("column_name"), c.get("data_type"), max1) + 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) } else { - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s TYPE %s;\n", c.get("table_name"), c.get("column_name"), c.get("data_type")) + 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) } } // Detect column default change (or added, dropped) if c.get("column_default") == "null" { - if c.get("column_default") != "null" { - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT;\n", c.get("table_name"), c.get("column_name")) + if c2.get("column_default") != "null" { + fmt.Printf("ALTER TABLE %s.%s ALTER COLUMN %s DROP DEFAULT;\n", c2.get("table_schema"), c.get("table_name"), c.get("column_name")) } } else if c.get("column_default") != c2.get("column_default") { - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s;\n", c.get("table_name"), c.get("column_name"), c.get("column_default")) + 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")) + } + + // Detect identity column change + // Save result to variable instead of printing because order for adding/removing + // is_nullable affects identity columns + var identitySql string + if c.get("is_identity") != c2.get("is_identity") { + // Knowing the version of db2 would eliminate the need for this warning + fmt.Println("-- WARNING: identity columns are not supported in PostgreSQL versions < 10.") + fmt.Println("-- Attempting to create identity columns in earlier versions will probably result in errors.") + if c.get("is_identity") == "YES" { + 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")) + } else { + 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")) + } } // Detect not-null and nullable change if c.get("is_nullable") != c2.get("is_nullable") { if c.get("is_nullable") == "YES" { - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL;\n", c.get("table_name"), c.get("column_name")) + if identitySql != "" { + fmt.Printf(identitySql) + } + 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")) } else { - fmt.Printf("ALTER TABLE %s ALTER COLUMN %s SET NOT NULL;\n", c.get("table_name"), c.get("column_name")) + 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")) + if identitySql != "" { + fmt.Printf(identitySql) + } + } + } else { + if identitySql != "" { + fmt.Printf(identitySql) } } } // ================================== -// Functions +// Standalone Functions // ================================== -/* - * Compare the columns in the two databases - */ -func compareColumns(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` -SELECT table_schema || '.' || table_name AS table_name - , column_name - , data_type - , is_nullable - , column_default - , character_maximum_length -FROM information_schema.columns -WHERE table_schema NOT LIKE 'pg_%' - AND table_schema <> 'information_schema' - AND is_updatable = 'YES' -ORDER BY table_name, column_name;` +// compare outputs SQL to make the columns match between two databases or schemas +func compare(conn1 *sql.DB, conn2 *sql.DB, tpl *template.Template) { + buf1 := new(bytes.Buffer) + tpl.Execute(buf1, dbInfo1) - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf2 := new(bytes.Buffer) + tpl.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) //rows1 := make([]map[string]string, 500) rows1 := make(ColumnRows, 0) @@ -224,6 +336,21 @@ ORDER BY table_name, column_name;` // Compare the columns doDiff(schema1, schema2) + +} + +// compareColumns outputs SQL to make the columns match between two databases or schemas +func compareColumns(conn1 *sql.DB, conn2 *sql.DB) { + + compare(conn1, conn2, columnSqlTemplate) + +} + +// compareColumns outputs SQL to make the tables columns (without views columns) match between two databases or schemas +func compareTableColumns(conn1 *sql.DB, conn2 *sql.DB) { + + compare(conn1, conn2, tableColumnSqlTemplate) + } // getMaxLength returns the maximum length and whether or not it is valid @@ -235,3 +362,4 @@ func getMaxLength(maxLength string) (string, bool) { } return maxLength, true } + diff --git a/flags.go b/flags.go index 50c92a3..c5c067e 100644 --- a/flags.go +++ b/flags.go @@ -18,6 +18,7 @@ func parseFlags() (pgutil.DbInfo, pgutil.DbInfo) { var dbHost1 = flag.StringP("host1", "H", "localhost", "db host") var dbPort1 = flag.IntP("port1", "P", 5432, "db port") var dbName1 = flag.StringP("dbname1", "D", "", "db name") + var dbSchema1 = flag.StringP("schema1", "S", "*", "schema name or * for all schemas") var dbOptions1 = flag.StringP("options1", "O", "", "db options (eg. sslmode=disable)") var dbUser2 = flag.StringP("user2", "u", "", "db user") @@ -25,13 +26,14 @@ func parseFlags() (pgutil.DbInfo, pgutil.DbInfo) { var dbHost2 = flag.StringP("host2", "h", "localhost", "db host") var dbPort2 = flag.IntP("port2", "p", 5432, "db port") var dbName2 = flag.StringP("dbname2", "d", "", "db name") + var dbSchema2 = flag.StringP("schema2", "s", "*", "schema name or * for all schemas") var dbOptions2 = flag.StringP("options2", "o", "", "db options (eg. sslmode=disable)") flag.Parse() - dbInfo1 := pgutil.DbInfo{DbName: *dbName1, DbHost: *dbHost1, DbPort: int32(*dbPort1), DbUser: *dbUser1, DbPass: *dbPass1, DbOptions: *dbOptions1} + dbInfo1 := pgutil.DbInfo{DbName: *dbName1, DbHost: *dbHost1, DbPort: int32(*dbPort1), DbUser: *dbUser1, DbPass: *dbPass1, DbSchema: *dbSchema1, DbOptions: *dbOptions1} - dbInfo2 := pgutil.DbInfo{DbName: *dbName2, DbHost: *dbHost2, DbPort: int32(*dbPort2), DbUser: *dbUser2, DbPass: *dbPass2, DbOptions: *dbOptions2} + dbInfo2 := pgutil.DbInfo{DbName: *dbName2, DbHost: *dbHost2, DbPort: int32(*dbPort2), DbUser: *dbUser2, DbPass: *dbPass2, DbSchema: *dbSchema2, DbOptions: *dbOptions2} return dbInfo1, dbInfo2 } diff --git a/foreignkey.go b/foreignkey.go index 6f9298e..bccd770 100644 --- a/foreignkey.go +++ b/foreignkey.go @@ -1,16 +1,52 @@ // -// Copyright (c) 2014 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "sort" -import "fmt" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "text/template" +) + +var ( + foreignKeySqlTemplate = initForeignKeySqlTemplate() +) + +// Initializes the Sql template +func initForeignKeySqlTemplate() *template.Template { + sql := ` +SELECT {{if eq $.DbSchema "*" }}ns.nspname || '.' || {{end}}cl.relname || '.' || c.conname AS compare_name + , ns.nspname AS schema_name + , cl.relname AS table_name + , c.conname AS fk_name + , pg_catalog.pg_get_constraintdef(c.oid, true) as constraint_def +FROM pg_catalog.pg_constraint c +INNER JOIN pg_class AS cl ON (c.conrelid = cl.oid) +INNER JOIN pg_namespace AS ns ON (ns.oid = c.connamespace) +WHERE c.contype = 'f' +{{if eq $.DbSchema "*"}} +AND ns.nspname NOT LIKE 'pg_%' +AND ns.nspname <> 'information_schema' +{{else}} +AND ns.nspname = '{{$.DbSchema}}' +{{end}} +` + t := template.New("ForeignKeySqlTmpl") + template.Must(t.Parse(sql)) + return t +} + +// ================================== +// ForeignKeyRows definition +// ================================== // ForeignKeyRows is a sortable string map type ForeignKeyRows []map[string]string @@ -20,13 +56,10 @@ func (slice ForeignKeyRows) Len() int { } func (slice ForeignKeyRows) Less(i, j int) bool { - if slice[i]["table_name"] != slice[j]["table_name"] { - return slice[i]["table_name"] < slice[j]["table_name"] - } - if slice[i]["constraint_def"] != slice[j]["constraint_def"] { - return slice[i]["constraint_def"] < slice[j]["constraint_def"] + if slice[i]["compare_name"] != slice[j]["compare_name"] { + return slice[i]["compare_name"] < slice[j]["compare_name"] } - return slice[i]["table_name"] < slice[j]["table_name"] + return slice[i]["constraint_def"] < slice[j]["constraint_def"] } func (slice ForeignKeyRows) Swap(i, j int) { @@ -80,7 +113,7 @@ func (c *ForeignKeySchema) Compare(obj interface{}) int { } //fmt.Printf("Comparing %s with %s", c.get("table_name"), c2.get("table_name")) - val := misc.CompareStrings(c.get("table_name"), c2.get("table_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) if val != 0 { return val } @@ -91,37 +124,40 @@ func (c *ForeignKeySchema) Compare(obj interface{}) int { // Add returns SQL to add the foreign key func (c *ForeignKeySchema) Add() { - fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s %s;\n", c.get("table_name"), c.get("fk_name"), c.get("constraint_def")) + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("schema_name") + } + fmt.Printf("ALTER TABLE %s.%s ADD CONSTRAINT %s %s;\n", schema, c.get("table_name"), c.get("fk_name"), c.get("constraint_def")) } // Drop returns SQL to drop the foreign key func (c ForeignKeySchema) Drop() { - fmt.Printf("ALTER TABLE %s DROP CONSTRAINT %s; -- %s\n", c.get("table_name"), c.get("fk_name"), c.get("constraint_def")) + 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")) } // Change handles the case where the table and foreign key name, but the details do not func (c *ForeignKeySchema) Change(obj interface{}) { c2, ok := obj.(*ForeignKeySchema) if !ok { - fmt.Println("Error!!!, Change(obj) needs a ForeignKeySchema instance", c2) + fmt.Println("Error!!!, ForeignKeySchema.Change(obj) needs a ForeignKeySchema instance", c2) } // There is no "changing" a foreign key. It either gets created or dropped (or left as-is). } /* - * Compare the foreign keys in the two databases. We do not recreate foreign keys if just the name is different. + * Compare the foreign keys in the two databases. */ func compareForeignKeys(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` -SELECT c.conname AS fk_name - , cl.relname AS table_name - , pg_catalog.pg_get_constraintdef(c.oid, true) as constraint_def -FROM pg_catalog.pg_constraint c -INNER JOIN pg_class AS cl ON (c.conrelid = cl.oid) -WHERE c.contype = 'f'; -` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + + buf1 := new(bytes.Buffer) + foreignKeySqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + foreignKeySqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(ForeignKeyRows, 0) for row := range rowChan1 { @@ -139,6 +175,6 @@ WHERE c.contype = 'f'; var schema1 Schema = &ForeignKeySchema{rows: rows1, rowNum: -1} var schema2 Schema = &ForeignKeySchema{rows: rows2, rowNum: -1} - // Compare the columns + // Compare the foreign keys doDiff(schema1, schema2) } diff --git a/function.go b/function.go index 6e7aa00..ded686a 100644 --- a/function.go +++ b/function.go @@ -1,16 +1,51 @@ // -// Copyright (c) 2016 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "fmt" -import "sort" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "strings" + "text/template" +) + +var ( + functionSqlTemplate = initFunctionSqlTemplate() +) + +// Initializes the Sql template +func initFunctionSqlTemplate() *template.Template { + sql := ` + SELECT n.nspname AS schema_name + , {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}p.proname AS compare_name + , p.proname AS function_name + , p.oid::regprocedure AS fancy + , t.typname AS return_type + , pg_get_functiondef(p.oid) AS definition + FROM pg_proc AS p + JOIN pg_type t ON (p.prorettype = t.oid) + JOIN pg_namespace n ON (n.oid = p.pronamespace) + JOIN pg_language l ON (p.prolang = l.oid AND l.lanname IN ('c','plpgsql', 'sql')) + WHERE true + {{if eq $.DbSchema "*" }} + AND n.nspname NOT LIKE 'pg_%' + AND n.nspname <> 'information_schema' + {{else}} + AND n.nspname = '{{$.DbSchema}}' + {{end}}; + ` + t := template.New("FunctionSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // FunctionRows definition @@ -24,7 +59,7 @@ func (slice FunctionRows) Len() int { } func (slice FunctionRows) Less(i, j int) bool { - return slice[i]["function_name"] < slice[j]["function_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice FunctionRows) Swap(i, j int) { @@ -66,24 +101,35 @@ func (c *FunctionSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("function_name"), c2.get("function_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("function_name"), c2.get("function_name")) return val } // Add returns SQL to create the function func (c FunctionSchema) Add() { + // If we are comparing two different schemas against each other, we need to do some + // modification of the first function definition so we create it in the right schema + functionDef := c.get("definition") + if dbInfo1.DbSchema != dbInfo2.DbSchema { + functionDef = strings.Replace( + functionDef, + fmt.Sprintf("FUNCTION %s.%s(", c.get("schema_name"), c.get("function_name")), + fmt.Sprintf("FUNCTION %s.%s(", dbInfo2.DbSchema, c.get("function_name")), + -1) + } + fmt.Println("-- STATEMENT-BEGIN") - fmt.Println(c.get("definition")) + fmt.Println(functionDef, ";") fmt.Println("-- STATEMENT-END") } // Drop returns SQL to drop the function func (c FunctionSchema) Drop() { fmt.Println("-- Note that CASCADE in the statement below will also drop any triggers depending on this function.") - fmt.Println("-- Also, if there are two functions with this name, you will need to add arguments to identify the correct one to drop.") + fmt.Println("-- Also, if there are two functions with this name, you will want to add arguments to identify the correct one to drop.") fmt.Println("-- (See http://www.postgresql.org/docs/9.4/interactive/sql-dropfunction.html) ") - fmt.Printf("DROP FUNCTION %s CASCADE;\n", c.get("function_name")) + fmt.Printf("DROP FUNCTION %s.%s CASCADE;\n", c.get("schema_name"), c.get("function_name")) } // Change handles the case where the function names match, but the definition does not @@ -94,28 +140,40 @@ func (c FunctionSchema) Change(obj interface{}) { } if c.get("definition") != c2.get("definition") { fmt.Println("-- This function is different so we'll recreate it:") + + // If we are comparing two different schemas against each other, we need to do some + // modification of the first function definition so we create it in the right schema + functionDef := c.get("definition") + if dbInfo1.DbSchema != dbInfo2.DbSchema { + functionDef = strings.Replace( + functionDef, + fmt.Sprintf("FUNCTION %s.%s(", c.get("schema_name"), c.get("function_name")), + fmt.Sprintf("FUNCTION %s.%s(", dbInfo2.DbSchema, c.get("function_name")), + -1) + } + // The definition column has everything needed to rebuild the function fmt.Println("-- STATEMENT-BEGIN") - fmt.Println(c.get("definition")) + fmt.Printf("%s;\n", functionDef) fmt.Println("-- STATEMENT-END") } } +// ================================== +// Functions +// ================================== + // compareFunctions outputs SQL to make the functions match between DBs func compareFunctions(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` - SELECT n.nspname || '.' || p.oid::regprocedure AS function_name - , t.typname AS return_type - , pg_get_functiondef(p.oid) AS definition - FROM pg_proc AS p - JOIN pg_type t ON (p.prorettype = t.oid) - JOIN pg_namespace n ON (n.oid = p.pronamespace) - JOIN pg_language l ON (p.prolang = l.oid AND l.lanname IN ('c','plpgsql', 'sql')) - WHERE n.nspname NOT LIKE 'pg_%'; - ` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf1 := new(bytes.Buffer) + functionSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + functionSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(FunctionRows, 0) for row := range rowChan1 { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b715a54 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module pgdiff + +go 1.13 + +require ( + github.com/joncrlsn/fileutil v0.0.0-20150212043926-71757336e569 // indirect + github.com/joncrlsn/misc v0.0.0-20160408024000-193a3fcec166 + github.com/joncrlsn/pgutil v0.0.0-20171213024902-4c8aab9306b4 + github.com/kr/pretty v0.2.1 // indirect + github.com/lib/pq v1.10.9 + github.com/ogier/pflag v0.0.1 + github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b // indirect + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fbedfd7 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/joncrlsn/fileutil v0.0.0-20150212043926-71757336e569 h1:bGOVwE4GrUyU3Pz22yNL7mML4tc/8b8zSjUZzofLpFA= +github.com/joncrlsn/fileutil v0.0.0-20150212043926-71757336e569/go.mod h1:YFE9T2vDUoqBSIywxQRZi1FWDcLsBgo+KDbLSw7HDNM= +github.com/joncrlsn/misc v0.0.0-20160408024000-193a3fcec166 h1:urNZ026xorI3t6Nzivkd8KSNACPjHxeeuxG1FGQXBD8= +github.com/joncrlsn/misc v0.0.0-20160408024000-193a3fcec166/go.mod h1:ZnHyWkKQ3JfdVYRo3PrjvB4RMdLs7SaQqTyUmk/rjIg= +github.com/joncrlsn/pgutil v0.0.0-20171213024902-4c8aab9306b4 h1:wTFs1uYdQfopjUVlbpJj0k2pHqKGa4M6D6gxGSH54Z8= +github.com/joncrlsn/pgutil v0.0.0-20171213024902-4c8aab9306b4/go.mod h1:iKyJCP0yj+Z+jEAobsEBh+t6WrUFxMQiClq3r7yI4z8= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= +github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750= +github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= +github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b h1:GlTM/aMVIwU3luIuSN2SIVRuTqGPt1P97YxAi514ulw= +github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b/go.mod h1:CC7OXV9IjEZRA+znA6/Kz5vbSwh69QioernOHeDCatU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/grant-attribute.go b/grant-attribute.go index f941ef9..aa7d0ff 100644 --- a/grant-attribute.go +++ b/grant-attribute.go @@ -1,17 +1,61 @@ // -// Copyright (c) 2014 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "sort" -import "fmt" -import "strings" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "strings" + "text/template" +) + +var ( + grantAttributeSqlTemplate = initGrantAttributeSqlTemplate() +) + +// Initializes the Sql template +func initGrantAttributeSqlTemplate() *template.Template { + sql := ` +-- Attribute/Column ACL only +SELECT + n.nspname AS schema_name + , {{ if eq $.DbSchema "*" }}n.nspname::text || '.' || {{ end }}c.relkind::text || '.' || c.relname::text || '.' || a.attname AS compare_name + , CASE c.relkind + WHEN 'r' THEN 'TABLE' + WHEN 'v' THEN 'VIEW' + WHEN 'f' THEN 'FOREIGN TABLE' + END as type + , c.relname AS relationship_name + , a.attname AS attribute_name + , a.attacl AS attribute_acl +FROM pg_catalog.pg_class c +LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) +INNER JOIN (SELECT attname, unnest(attacl) AS attacl, attrelid + FROM pg_catalog.pg_attribute + WHERE NOT attisdropped AND attacl IS NOT NULL) + AS a ON (a.attrelid = c.oid) +WHERE c.relkind IN ('r', 'v', 'f') +--AND pg_catalog.pg_table_is_visible(c.oid) +{{ if eq $.DbSchema "*" }} +AND n.nspname NOT LIKE 'pg_%' +AND n.nspname <> 'information_schema' +{{ else }} +AND n.nspname = '{{ $.DbSchema }}' +{{ end }}; +` + + t := template.New("GrantAttributeSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // GrantAttributeRows definition @@ -25,14 +69,8 @@ func (slice GrantAttributeRows) Len() int { } func (slice GrantAttributeRows) Less(i, j int) bool { - if slice[i]["schema"] != slice[j]["schema"] { - return slice[i]["schema"] < slice[j]["schema"] - } - if slice[i]["relationship_name"] != slice[j]["relationship_name"] { - return slice[i]["relationship_name"] < slice[j]["relationship_name"] - } - if slice[i]["attribute_name"] != slice[j]["attribute_name"] { - return slice[i]["attribute_name"] < slice[j]["attribute_name"] + if slice[i]["compare_name"] != slice[j]["compare_name"] { + return slice[i]["compare_name"] < slice[j]["compare_name"] } // Only compare the role part of the ACL @@ -89,7 +127,8 @@ func (c *GrantAttributeSchema) NextRow() bool { return !c.done } -// Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row +// Compare tells you, in one pass, whether or not the first row matches, is less than, +// or greater than the second row. func (c *GrantAttributeSchema) Compare(obj interface{}) int { c2, ok := obj.(*GrantAttributeSchema) if !ok { @@ -97,17 +136,7 @@ func (c *GrantAttributeSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("schema"), c2.get("schema")) - if val != 0 { - return val - } - - val = misc.CompareStrings(c.get("relationship_name"), c2.get("relationship_name")) - if val != 0 { - return val - } - - val = misc.CompareStrings(c.get("attribute_name"), c2.get("attribute_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) if val != 0 { return val } @@ -118,19 +147,24 @@ func (c *GrantAttributeSchema) Compare(obj interface{}) int { return val } -// Add prints SQL to add the column +// Add prints SQL to add the grant func (c *GrantAttributeSchema) Add() { + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("schema_name") + } + role, grants := parseGrants(c.get("attribute_acl")) - fmt.Printf("GRANT %s (%s) ON %s TO %s; -- Add\n", strings.Join(grants, ", "), c.get("attribute_name"), c.get("relationship_name"), role) + 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) } -// Drop prints SQL to drop the column +// Drop prints SQL to drop the grant func (c *GrantAttributeSchema) Drop() { role, grants := parseGrants(c.get("attribute_acl")) - fmt.Printf("REVOKE %s (%s) ON %s FROM %s; -- Drop\n", strings.Join(grants, ", "), c.get("attribute_name"), c.get("relationship_name"), role) + 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) } -// Change handles the case where the relationship and column match, but the details do not +// Change handles the case where the relationship and column match, but the grant does not func (c *GrantAttributeSchema) Change(obj interface{}) { c2, ok := obj.(*GrantAttributeSchema) if !ok { @@ -149,7 +183,8 @@ func (c *GrantAttributeSchema) Change(obj interface{}) { } } if len(grantList) > 0 { - fmt.Printf("GRANT %s (%s) ON %s TO %s; -- Change\n", strings.Join(grantList, ", "), c.get("attribute_name"), c.get("relationship_name"), role) + fmt.Printf("GRANT %s (%s) ON %s.%s TO %s; -- Change\n", strings.Join(grantList, ", "), + c.get("attribute_name"), c2.get("schema_name"), c.get("relationship_name"), role) } // Find grants in the second db that are not in the first @@ -161,59 +196,46 @@ func (c *GrantAttributeSchema) Change(obj interface{}) { } } if len(revokeList) > 0 { - fmt.Printf("REVOKE %s (%s) ON %s FROM %s; -- Change\n", strings.Join(grantList, ", "), c.get("attribute_name"), c.get("relationship_name"), role) + 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) } - // 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")) - // 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")) + //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")) + //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")) } // ================================== // Functions // ================================== -/* - * Compare the columns in the two databases - */ +// compareGrantAttributes outputs SQL to make the granted permissions match between DBs or schemas func compareGrantAttributes(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` --- Attribute/Column ACL only -SELECT - n.nspname AS schema - , CASE c.relkind - WHEN 'r' THEN 'TABLE' - WHEN 'v' THEN 'VIEW' - WHEN 'f' THEN 'FOREIGN TABLE' - END as type - , c.relname AS relationship_name - , a.attname AS attribute_name - , a.attacl AS attribute_acl -FROM pg_catalog.pg_class c -LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) -INNER JOIN (SELECT attname, unnest(attacl) AS attacl, attrelid - FROM pg_catalog.pg_attribute - WHERE NOT attisdropped AND attacl IS NOT NULL) - AS a ON (a.attrelid = c.oid) -WHERE c.relkind IN ('r', 'v', 'f') -AND n.nspname !~ '^pg_' -AND pg_catalog.pg_table_is_visible(c.oid) -ORDER BY n.nspname, c.relname, a.attname; -` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf1 := new(bytes.Buffer) + grantAttributeSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + grantAttributeSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(GrantAttributeRows, 0) for row := range rowChan1 { rows1 = append(rows1, row) } sort.Sort(rows1) + //for _, row := range rows1 { + //fmt.Printf("--1b compare:%s, col:%s, colAcl:%s\n", row["compare_name"], row["attribute_name"], row["attribute_acl"]) + //} rows2 := make(GrantAttributeRows, 0) for row := range rowChan2 { rows2 = append(rows2, row) } sort.Sort(rows2) + //for _, row := range rows2 { + //fmt.Printf("--2b compare:%s, col:%s, colAcl:%s\n", row["compare_name"], row["attribute_name"], row["attribute_acl"]) + //} // We have to explicitly type this as Schema here for some unknown reason var schema1 Schema = &GrantAttributeSchema{rows: rows1, rowNum: -1} diff --git a/grant-relationship.go b/grant-relationship.go index 8f0ac42..5a2bc64 100644 --- a/grant-relationship.go +++ b/grant-relationship.go @@ -1,17 +1,55 @@ // -// Copyright (c) 2014 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "sort" -import "fmt" -import "strings" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "strings" + "text/template" +) + +var ( + grantRelationshipSqlTemplate = initGrantRelationshipSqlTemplate() +) + +// Initializes the Sql template +func initGrantRelationshipSqlTemplate() *template.Template { + sql := ` +SELECT n.nspname AS schema_name + , {{ if eq $.DbSchema "*" }}n.nspname::text || '.' || {{ end }}c.relkind::text || '.' || c.relname::text AS compare_name + , CASE c.relkind + WHEN 'r' THEN 'TABLE' + WHEN 'v' THEN 'VIEW' + WHEN 'S' THEN 'SEQUENCE' + WHEN 'f' THEN 'FOREIGN TABLE' + END as type + , c.relname AS relationship_name + , unnest(c.relacl) AS relationship_acl +FROM pg_catalog.pg_class c +LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) +WHERE c.relkind IN ('r', 'v', 'S', 'f') +--AND pg_catalog.pg_table_is_visible(c.oid) +{{ if eq $.DbSchema "*" }} +AND n.nspname NOT LIKE 'pg_%' +AND n.nspname <> 'information_schema' +{{ else }} +AND n.nspname = '{{ $.DbSchema }}' +{{ end }}; +` + + t := template.New("GrantRelationshipSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // GrantRelationshipRows definition @@ -25,11 +63,8 @@ func (slice GrantRelationshipRows) Len() int { } func (slice GrantRelationshipRows) Less(i, j int) bool { - if slice[i]["schema"] != slice[j]["schema"] { - return slice[i]["schema"] < slice[j]["schema"] - } - if slice[i]["relationship_name"] != slice[j]["relationship_name"] { - return slice[i]["relationship_name"] < slice[j]["relationship_name"] + if slice[i]["compare_name"] != slice[j]["compare_name"] { + return slice[i]["compare_name"] < slice[j]["compare_name"] } // Only compare the role part of the ACL @@ -94,12 +129,7 @@ func (c *GrantRelationshipSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("schema"), c2.get("schema")) - if val != 0 { - return val - } - - val = misc.CompareStrings(c.get("relationship_name"), c2.get("relationship_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) if val != 0 { return val } @@ -108,26 +138,30 @@ func (c *GrantRelationshipSchema) Compare(obj interface{}) int { relRole2, _ := parseAcl(c2.get("relationship_acl")) val = misc.CompareStrings(relRole1, relRole2) return val - } -// Add prints SQL to add the column +// Add prints SQL to add the grant func (c *GrantRelationshipSchema) Add() { + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("schema_name") + } + role, grants := parseGrants(c.get("relationship_acl")) - fmt.Printf("GRANT %s ON %s TO %s; -- Add\n", strings.Join(grants, ", "), c.get("relationship_name"), role) + fmt.Printf("GRANT %s ON %s.%s TO %s; -- Add\n", strings.Join(grants, ", "), schema, c.get("relationship_name"), role) } -// Drop prints SQL to drop the column +// Drop prints SQL to drop the grant func (c *GrantRelationshipSchema) Drop() { role, grants := parseGrants(c.get("relationship_acl")) - fmt.Printf("REVOKE %s ON %s FROM %s; -- Drop\n", strings.Join(grants, ", "), c.get("relationship_name"), role) + fmt.Printf("REVOKE %s ON %s.%s FROM %s; -- Drop\n", strings.Join(grants, ", "), c.get("schema_name"), c.get("relationship_name"), role) } -// Change handles the case where the relationship and column match, but the details do not +// Change handles the case where the relationship and column match, but the grant does not func (c *GrantRelationshipSchema) Change(obj interface{}) { c2, ok := obj.(*GrantRelationshipSchema) if !ok { - fmt.Println("-- Error!!!, change needs a GrantRelationshipSchema instance", c2) + fmt.Println("-- Error!!!, Change needs a GrantRelationshipSchema instance", c2) } role, grants1 := parseGrants(c.get("relationship_acl")) @@ -142,7 +176,7 @@ func (c *GrantRelationshipSchema) Change(obj interface{}) { } } if len(grantList) > 0 { - fmt.Printf("GRANT %s ON %s TO %s; -- Change\n", strings.Join(grantList, ", "), c.get("relationship_name"), role) + fmt.Printf("GRANT %s ON %s.%s TO %s; -- Change\n", strings.Join(grantList, ", "), c2.get("schema_name"), c.get("relationship_name"), role) } // Find grants in the second db that are not in the first @@ -154,7 +188,7 @@ func (c *GrantRelationshipSchema) Change(obj interface{}) { } } if len(revokeList) > 0 { - fmt.Printf("REVOKE %s ON %s FROM %s; -- Change\n", strings.Join(revokeList, ", "), c.get("relationship_name"), role) + fmt.Printf("REVOKE %s ON %s.%s FROM %s; -- Change\n", strings.Join(revokeList, ", "), c2.get("schema_name"), c.get("relationship_name"), role) } // 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")) @@ -165,31 +199,17 @@ func (c *GrantRelationshipSchema) Change(obj interface{}) { // Functions // ================================== -/* - * Compare the columns in the two databases - */ +// compareGrantRelationships outputs SQL to make the granted permissions match between DBs or schemas func compareGrantRelationships(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` -SELECT - n.nspname AS schema - , CASE c.relkind - WHEN 'r' THEN 'TABLE' - WHEN 'v' THEN 'VIEW' - WHEN 'S' THEN 'SEQUENCE' - WHEN 'f' THEN 'FOREIGN TABLE' - END as type - , c.relname AS relationship_name - , unnest(c.relacl) AS relationship_acl -FROM pg_catalog.pg_class c -LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) -WHERE c.relkind IN ('r', 'v', 'S', 'f') - AND n.nspname NOT LIKE 'pg_%' - AND pg_catalog.pg_table_is_visible(c.oid) -ORDER BY n.nspname, c.relname; -` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf1 := new(bytes.Buffer) + grantRelationshipSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + grantRelationshipSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(GrantRelationshipRows, 0) for row := range rowChan1 { @@ -203,7 +223,7 @@ ORDER BY n.nspname, c.relname; } sort.Sort(rows2) - // We have to explicitly type this as Schema here for some unknown reason + // We have to explicitly type this as Schema here for some unknown (to me) reason var schema1 Schema = &GrantRelationshipSchema{rows: rows1, rowNum: -1} var schema2 Schema = &GrantRelationshipSchema{rows: rows2, rowNum: -1} diff --git a/grant.go b/grant.go index 4843f8f..6d7e5d6 100644 --- a/grant.go +++ b/grant.go @@ -8,12 +8,14 @@ package main -import "sort" -import "fmt" -import "strings" -import "regexp" +import ( + "sort" + "fmt" + "strings" + "regexp" +) -var aclRegex = regexp.MustCompile(`([a-zA-Z0-9]+)*=([rwadDxtXUCcT]+)/([a-zA-Z0-9]+)$`) +var aclRegex = regexp.MustCompile(`([a-zA-Z0-9_]+)*=([rwadDxtXUCcT]+)/([a-zA-Z0-9_]+)$`) var permMap = map[string]string{ "a": "INSERT", diff --git a/grant_test.go b/grant_test.go index 6a5a651..552b57f 100644 --- a/grant_test.go +++ b/grant_test.go @@ -6,63 +6,13 @@ import ( ) func Test_parseAcls(t *testing.T) { - doParseAcls(t, "c42ro=rwa/c42", "c42ro", 3) + doParseAcls(t, "user1=rwa/c42", "user1", 3) doParseAcls(t, "=arwdDxt/c42", "public", 7) // first of two lines - doParseAcls(t, "c42=rwad/postgres", "c42", 4) // second of two lines + doParseAcls(t, "u3=rwad/postgres", "u3", 4) // second of two lines doParseAcls(t, "user2=arwxt/postgres", "user2", 5) doParseAcls(t, "", "", 0) } -/* - schema | type | relationship_name | relationship_acl | column_name | column_acl ---------+----------+-----------------------------------------------+----------------------+------------------------------+------------- - public | TABLE | t_brand | c42ro=r/c42 | | - public | TABLE | t_brand | office42=arwdDxt/c42 | | - public | TABLE | t_brand | c42=arwdDxt/c42 | | - public | TABLE | t_computer | c42=arwdDxt/c42 | active | c42ro=r/c42 - public | TABLE | t_computer | office42=arwdDxt/c42 | active | c42ro=r/c42 - public | TABLE | t_computer | c42=arwdDxt/c42 | address | c42ro=r/c42 - public | TABLE | t_computer | office42=arwdDxt/c42 | address | c42ro=r/c42 -*/ - -// Note that these must be sorted for this to work -var relationship1 = []map[string]string{ - {"schema": "public", "type": "TABLE", "relationship_name": "table1", "relationship_acl": "c42=rwa/postgres"}, - {"schema": "public", "type": "TABLE", "relationship_name": "table1", "relationship_acl": "o42=xdwra/postgres"}, - {"schema": "public", "type": "TABLE", "relationship_name": "table2", "relationship_acl": "c42=rwa/postgres"}, -} - -// Note that these must be sorted for this to work -var relationship2 = []map[string]string{ - {"schema": "public", "relationship_name": "table1", "type": "TABLE", "relationship_acl": "c42=r/postgres"}, - {"schema": "public", "relationship_name": "table2", "type": "TABLE", "relationship_acl": "c42=rwad/postgres"}, -} - -// Note that these must be sorted for this to work -var attribute1 = []map[string]string{ - {"schema": "public", "type": "TABLE", "relationship_name": "table1", "attribute_name": "column1", "attribute_acl": "c42ro=r/postgres"}, - {"schema": "public", "type": "TABLE", "relationship_name": "table1", "attribute_name": "column1", "attribute_acl": "o42ro=rwa/postgres"}, - {"schema": "public", "type": "TABLE", "relationship_name": "table2", "attribute_name": "column2", "attribute_acl": "c42ro=r/postgres"}, -} - -// Note that these must be sorted for this to work -var attribute2 = []map[string]string{ - {"schema": "public", "type": "TABLE", "relationship_name": "table1", "attribute_name": "column1", "attribute_acl": "c42ro=r/postgres"}, - {"schema": "public", "type": "TABLE", "relationship_name": "table1", "attribute_name": "column1", "attribute_acl": "o42ro=r/postgres"}, -} - -func Test_diffGrants(t *testing.T) { - fmt.Println("-- ==========\n-- Grants - Relationships \n-- ==========") - var relSchema1 Schema = &GrantRelationshipSchema{rows: relationship1, rowNum: -1} - var relSchema2 Schema = &GrantRelationshipSchema{rows: relationship2, rowNum: -1} - doDiff(relSchema1, relSchema2) - - fmt.Println("-- ==========\n-- Grants - Attributes \n-- ==========") - var attSchema1 Schema = &GrantAttributeSchema{rows: attribute1, rowNum: -1} - var attSchema2 Schema = &GrantAttributeSchema{rows: attribute2, rowNum: -1} - doDiff(attSchema1, attSchema2) -} - func doParseAcls(t *testing.T, acl string, expectedRole string, expectedPermCount int) { fmt.Println("Testing", acl) role, perms := parseAcl(acl) @@ -70,6 +20,6 @@ func doParseAcls(t *testing.T, acl string, expectedRole string, expectedPermCoun t.Error("Wrong role parsed: " + role + " instead of " + expectedRole) } if len(perms) != expectedPermCount { - t.Error("Incorrect number of permissions parsed: %d instead of %d", len(perms), expectedPermCount) + t.Errorf("Incorrect number of permissions parsed: %d instead of %d", len(perms), expectedPermCount) } } diff --git a/index.go b/index.go index 29dddcc..8ac2438 100644 --- a/index.go +++ b/index.go @@ -1,17 +1,56 @@ // -// Copyright (c) 2014 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "sort" -import "fmt" -import "strings" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "strings" + "text/template" +) + +var ( + indexSqlTemplate = initIndexSqlTemplate() +) + +// Initializes the Sql template +func initIndexSqlTemplate() *template.Template { + sql := ` +SELECT {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}c.relname || '.' || c2.relname AS compare_name + , n.nspname AS schema_name + , c.relname AS table_name + , c2.relname AS index_name + , i.indisprimary AS pk + , i.indisunique AS uq + , pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS index_def + , pg_catalog.pg_get_constraintdef(con.oid, true) AS constraint_def + , con.contype AS typ +FROM pg_catalog.pg_index AS i +INNER JOIN pg_catalog.pg_class AS c ON (c.oid = i.indrelid) +INNER JOIN pg_catalog.pg_class AS c2 ON (c2.oid = i.indexrelid) +LEFT OUTER JOIN pg_catalog.pg_constraint con + ON (con.conrelid = i.indrelid AND con.conindid = i.indexrelid AND con.contype IN ('p','u','x')) +INNER JOIN pg_catalog.pg_namespace AS n ON (c2.relnamespace = n.oid) +WHERE true +{{if eq $.DbSchema "*"}} +AND n.nspname NOT LIKE 'pg_%' +AND n.nspname <> 'information_schema' +{{else}} +AND n.nspname = '{{$.DbSchema}}' +{{end}} +` + t := template.New("IndexSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // IndexRows definition @@ -26,10 +65,7 @@ func (slice IndexRows) Len() int { func (slice IndexRows) Less(i, j int) bool { //fmt.Printf("--Less %s:%s with %s:%s", slice[i]["table_name"], slice[i]["column_name"], slice[j]["table_name"], slice[j]["column_name"]) - if slice[i]["table_name"] == slice[j]["table_name"] { - return slice[i]["index_name"] < slice[j]["index_name"] - } - return slice[i]["table_name"] < slice[j]["table_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice IndexRows) Swap(i, j int) { @@ -84,64 +120,68 @@ func (c *IndexSchema) Compare(obj interface{}) int { } if len(c.get("table_name")) == 0 || len(c.get("index_name")) == 0 { - fmt.Printf("--Comparing (table_name or index_name is empty): %v\n-- %v\n", c.getRow(), c2.getRow()) + fmt.Printf("--Comparing (table_name and/or index_name is empty): %v\n", c.getRow()) + fmt.Printf("-- %v\n", c2.getRow()) } - val := misc.CompareStrings(c.get("table_name"), c2.get("table_name")) - if val != 0 { - // Table name differed so return that value - return val - } - - // Table name was the same so compare index name - val = misc.CompareStrings(c.get("index_name"), c2.get("index_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) return val } -// Add prints SQL to add the column +// Add prints SQL to add the index func (c *IndexSchema) Add() { - //fmt.Println("--\n--Add\n--") + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("schema_name") + } // Assertion if c.get("index_def") == "null" || len(c.get("index_def")) == 0 { - fmt.Printf("-- Add Unexpected situation in index.go: there is no index_def for %s %s\n", c.get("table_name"), c.get("index_name")) + 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")) return } - // Create the index first - fmt.Printf("%s;\n", c.get("index_def")) + // If we are comparing two different schemas against each other, we need to do some + // modification of the first index_def so we create the index in the write schema + indexDef := c.get("index_def") + if dbInfo1.DbSchema != dbInfo2.DbSchema { + indexDef = strings.Replace( + indexDef, + fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), + fmt.Sprintf(" %s.%s ", dbInfo2.DbSchema, c.get("table_name")), + -1) + } + + fmt.Printf("%v;\n", indexDef) if c.get("constraint_def") != "null" { // Create the constraint using the index we just created if c.get("pk") == "true" { // Add primary key using the index - fmt.Printf("ALTER TABLE ONLY %s ADD CONSTRAINT %s PRIMARY KEY USING INDEX %s;\n", c.get("table_name"), c.get("index_name"), c.get("index_name")) + 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")) } else if c.get("uq") == "true" { // Add unique constraint using the index - fmt.Printf("ALTER TABLE ONLY %s ADD CONSTRAINT %s UNIQUE USING INDEX %s;\n", c.get("table_name"), c.get("index_name"), c.get("index_name")) + 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")) } } } -// Drop prints SQL to drop the column +// Drop prints SQL to drop the index func (c *IndexSchema) Drop() { - //fmt.Println("--\n--Drop\n--") if c.get("constraint_def") != "null" { 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.") - //fmt.Printf("ALTER TABLE ONLY %s DROP CONSTRAINT IF EXISTS %s CASCADE; -- %s\n", c.get("table_name"), c.get("index_name"), c.get("constraint_def")) - fmt.Printf("ALTER TABLE ONLY %s DROP CONSTRAINT IF EXISTS %s CASCADE;\n", c.get("table_name"), c.get("index_name")) + 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")) } - // The second line has no index_def - //fmt.Printf("DROP INDEX IF EXISTS %s; -- %s \n", c.get("index_name"), c.get("index_def")) - fmt.Printf("DROP INDEX IF EXISTS %s;\n", c.get("index_name")) + fmt.Printf("DROP INDEX %s.%s;\n", c.get("schema_name"), c.get("index_name")) } // Change handles the case where the table and column match, but the details do not func (c *IndexSchema) Change(obj interface{}) { c2, ok := obj.(*IndexSchema) if !ok { - fmt.Println("-- Error!!!, change needs an IndexSchema instance", c2) + fmt.Println("-- Error!!!, Change needs an IndexSchema instance", c2) } + // Table and constraint name matches... We need to make sure the details match // NOTE that there should always be an index_def for both c and c2 (but we're checking below anyway) @@ -156,11 +196,11 @@ func (c *IndexSchema) Change(obj interface{}) { if c.get("constraint_def") != c2.get("constraint_def") { // c1.constraint and c2.constraint are just different - fmt.Printf("-- CHANGE: Different defs:\n-- %s\n-- %s\n", c.get("constraint_def"), c2.get("constraint_def")) + fmt.Printf("-- CHANGE: Different defs on %s:\n-- %s\n-- %s\n", c.get("table_name"), c.get("constraint_def"), c2.get("constraint_def")) if c.get("constraint_def") == "null" { // c1.constraint does not exist, c2.constraint does, so // Drop constraint - fmt.Printf("DROP INDEX IF EXISTS %s; -- %s \n", c2.get("index_name"), c2.get("index_def")) + fmt.Printf("DROP INDEX %s; -- %s \n", c2.get("index_name"), c2.get("index_def")) } else if c2.get("constraint_def") == "null" { // c1.constraint exists, c2.constraint does not, so // Add constraint @@ -169,16 +209,16 @@ func (c *IndexSchema) Change(obj interface{}) { // Add constraint using the index if c.get("pk") == "true" { // Add primary key using the index - fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s PRIMARY KEY USING INDEX %s;\n", c.get("table_name"), c.get("index_name"), c.get("index_name")) + 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")) } else if c.get("uq") == "true" { // Add unique constraint using the index - fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s UNIQUE USING INDEX %s;\n", c.get("table_name"), c.get("index_name"), c.get("index_name")) + 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")) } else { } } else { // Drop the c2 index, create a copy of the c1 index - fmt.Printf("DROP INDEX IF EXISTS %s; -- %s \n", c2.get("index_name"), c2.get("index_def")) + fmt.Printf("DROP INDEX %s; -- %s \n", c2.get("index_name"), c2.get("index_def")) } // WIP //fmt.Printf("ALTER TABLE %s ADD CONSTRAINT %s %s;\n", c.get("table_name"), c.get("index_name"), c.get("constraint_def")) @@ -186,13 +226,33 @@ func (c *IndexSchema) Change(obj interface{}) { } else if c.get("index_def") != c2.get("index_def") { // The constraints match } - } else if c.get("index_def") != c2.get("index_def") { + + return + } + + // At this point, we know that the constraint_def matches. Compare the index_def + + indexDef1 := c.get("index_def") + indexDef2 := c2.get("index_def") + + // If we are comparing two different schemas against each other, we need to do + // some modification of the first index_def so it looks more like the second + if dbInfo1.DbSchema != dbInfo2.DbSchema { + indexDef1 = strings.Replace( + indexDef1, + fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), + fmt.Sprintf(" %s.%s ", c2.get("schema_name"), c2.get("table_name")), + -1, + ) + } + + if indexDef1 != indexDef2 { + // Notice that, if we are here, then the two constraint_defs match (both may be empty) + // The indexes do not match, but the constraints do if !strings.HasPrefix(c.get("index_def"), c2.get("index_def")) && !strings.HasPrefix(c2.get("index_def"), c.get("index_def")) { - fmt.Println("--\n--Change index defs different\n--") - // Remember, if we are here, then the two constraint_defs match (both may be empty) - // The indexes do not match, but the constraints do - fmt.Printf("--CHANGE: Different index defs:\n-- %s\n-- %s\n", c.get("index_def"), c2.get("index_def")) + fmt.Println("--\n--CHANGE: index defs are different for identical constraint defs:") + fmt.Printf("-- %s\n-- %s\n", c.get("index_def"), c2.get("index_def")) // Drop the index (and maybe the constraint) so we can recreate the index c.Drop() @@ -204,35 +264,17 @@ func (c *IndexSchema) Change(obj interface{}) { } -// ================================== -// Functions -// ================================== - -/* - * Compare the columns in the two databases - */ +// compareIndexes outputs Sql to make the indexes match between to DBs or schemas func compareIndexes(conn1 *sql.DB, conn2 *sql.DB) { - // This SQL was generated with psql -E -c "\d t_org" - // The "magic" is in pg_get_indexdef and pg_get_constraint - sql := ` -SELECT n.nspname || '.' || c.relname AS table_name - , c2.relname AS index_name - , i.indisprimary AS pk - , i.indisunique AS uq - , pg_catalog.pg_get_indexdef(i.indexrelid, 0, true) AS index_def - , pg_catalog.pg_get_constraintdef(con.oid, true) AS constraint_def - , con.contype AS typ -FROM pg_catalog.pg_index AS i -INNER JOIN pg_catalog.pg_class AS c ON (c.oid = i.indrelid) -INNER JOIN pg_catalog.pg_class AS c2 ON (c2.oid = i.indexrelid) -LEFT JOIN pg_catalog.pg_constraint con - ON (con.conrelid = i.indrelid AND con.conindid = i.indexrelid AND con.contype IN ('p','u','x')) -INNER JOIN pg_catalog.pg_namespace AS n ON (c2.relnamespace = n.oid) -WHERE c.relname NOT LIKE 'pg_%' -AND n.nspname NOT LIKE 'pg_%'; -` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + + buf1 := new(bytes.Buffer) + indexSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + indexSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(IndexRows, 0) for row := range rowChan1 { @@ -250,6 +292,6 @@ AND n.nspname NOT LIKE 'pg_%'; var schema1 Schema = &IndexSchema{rows: rows1, rowNum: -1} var schema2 Schema = &IndexSchema{rows: rows2, rowNum: -1} - // Compare the columns + // Compare the indexes doDiff(schema1, schema2) } diff --git a/mat_view.go b/mat_view.go new file mode 100644 index 0000000..38b09e6 --- /dev/null +++ b/mat_view.go @@ -0,0 +1,139 @@ +// +// Copyright (c) 2016 Jon Carlson. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. +// + +package main + +import ( + "database/sql" + "fmt" + "sort" + + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" +) + +// ================================== +// MatViewRows definition +// ================================== + +// MatViewRows is a sortable slice of string maps +type MatViewRows []map[string]string + +func (slice MatViewRows) Len() int { + return len(slice) +} + +func (slice MatViewRows) Less(i, j int) bool { + return slice[i]["matviewname"] < slice[j]["matviewname"] +} + +func (slice MatViewRows) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} + +// MatViewSchema holds a channel streaming matview information from one of the databases as well as +// a reference to the current row of data we're matviewing. +// +// MatViewSchema implements the Schema interface defined in pgdiff.go +type MatViewSchema struct { + rows MatViewRows + rowNum int + done bool +} + +// get returns the value from the current row for the given key +func (c *MatViewSchema) get(key string) string { + if c.rowNum >= len(c.rows) { + return "" + } + return c.rows[c.rowNum][key] +} + +// NextRow increments the rowNum and tells you whether or not there are more +func (c *MatViewSchema) NextRow() bool { + if c.rowNum >= len(c.rows)-1 { + c.done = true + } + c.rowNum = c.rowNum + 1 + return !c.done +} + +// Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row +func (c *MatViewSchema) Compare(obj interface{}) int { + c2, ok := obj.(*MatViewSchema) + if !ok { + fmt.Println("Error!!!, Compare(obj) needs a MatViewSchema instance", c2) + return +999 + } + + val := misc.CompareStrings(c.get("matviewname"), c2.get("matviewname")) + //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("matviewname"), c2.get("matviewname")) + return val +} + +// Add returns SQL to create the matview +func (c MatViewSchema) Add() { + fmt.Printf("CREATE MATERIALIZED VIEW %s AS %s \n\n%s \n\n", c.get("matviewname"), c.get("definition"), c.get("indexdef")) +} + +// Drop returns SQL to drop the matview +func (c MatViewSchema) Drop() { + fmt.Printf("DROP MATERIALIZED VIEW %s;\n\n", c.get("matviewname")) +} + +// Change handles the case where the names match, but the definition does not +func (c MatViewSchema) Change(obj interface{}) { + c2, ok := obj.(*MatViewSchema) + if !ok { + fmt.Println("Error!!!, Change needs a MatViewSchema instance", c2) + } + if c.get("definition") != c2.get("definition") { + fmt.Printf("DROP MATERIALIZED VIEW %s;\n\n", c.get("matviewname")) + fmt.Printf("CREATE MATERIALIZED VIEW %s AS %s \n\n%s \n\n", c.get("matviewname"), c.get("definition"), c.get("indexdef")) + } +} + +// compareMatViews outputs SQL to make the matviews match between DBs +func compareMatViews(conn1 *sql.DB, conn2 *sql.DB) { + sql := ` + WITH matviews as ( SELECT schemaname || '.' || matviewname AS matviewname, + definition + FROM pg_catalog.pg_matviews + WHERE schemaname NOT LIKE 'pg_%' + ) + SELECT + matviewname, + definition, + COALESCE(string_agg(indexdef, ';' || E'\n\n') || ';', '') as indexdef + FROM matviews + LEFT JOIN pg_catalog.pg_indexes on matviewname = schemaname || '.' || tablename + group by matviewname, definition + ORDER BY + matviewname; + ` + + rowChan1, _ := pgutil.QueryStrings(conn1, sql) + rowChan2, _ := pgutil.QueryStrings(conn2, sql) + + rows1 := make(MatViewRows, 0) + for row := range rowChan1 { + rows1 = append(rows1, row) + } + sort.Sort(rows1) + + rows2 := make(MatViewRows, 0) + for row := range rowChan2 { + rows2 = append(rows2, row) + } + sort.Sort(rows2) + + // We have to explicitly type this as Schema here + var schema1 Schema = &MatViewSchema{rows: rows1, rowNum: -1} + var schema2 Schema = &MatViewSchema{rows: rows2, rowNum: -1} + + // Compare the matviews + doDiff(schema1, schema2) +} diff --git a/notes.md b/notes.md deleted file mode 100644 index e3a112a..0000000 --- a/notes.md +++ /dev/null @@ -1,112 +0,0 @@ - ==== Constraints - -SELECT c.conname AS constraint_name, - CASE c.contype - WHEN 'c' THEN 'CHECK' - WHEN 'f' THEN 'FOREIGN KEY' - WHEN 'p' THEN 'PRIMARY KEY' - WHEN 'u' THEN 'UNIQUE' - END AS "constraint_type", - CASE WHEN c.condeferrable = 'f' THEN 0 ELSE 1 END AS is_deferrable, - CASE WHEN c.condeferred = 'f' THEN 0 ELSE 1 END AS is_deferred, - t.relname AS table_name, - array_to_string(c.conkey, ' ') AS constraint_key, - CASE confupdtype - WHEN 'a' THEN 'NO ACTION' - WHEN 'r' THEN 'RESTRICT' - WHEN 'c' THEN 'CASCADE' - WHEN 'n' THEN 'SET NULL' - WHEN 'd' THEN 'SET DEFAULT' - END AS on_update, - CASE confdeltype - WHEN 'a' THEN 'NO ACTION' - WHEN 'r' THEN 'RESTRICT' - WHEN 'c' THEN 'CASCADE' - WHEN 'n' THEN 'SET NULL' - WHEN 'd' THEN 'SET DEFAULT' - END AS on_delete, - CASE confmatchtype - WHEN 'u' THEN 'UNSPECIFIED' - WHEN 'f' THEN 'FULL' - WHEN 'p' THEN 'PARTIAL' - END AS match_type, - t2.relname AS references_table, - array_to_string(c.confkey, ' ') AS fk_constraint_key - FROM pg_constraint c -LEFT JOIN pg_class t ON c.conrelid = t.oid -LEFT JOIN pg_class t2 ON c.confrelid = t2.oid; - - - SELECT tc.constraint_name - , tc.constraint_type - , tc.table_name - , kcu.column_name - , tc.is_deferrable - , tc.initially_deferred - , rc.match_option AS match_type - , rc.update_rule AS on_update - , rc.delete_rule AS on_delete - , ccu.table_name AS references_table - , ccu.column_name AS references_field - FROM information_schema.table_constraints tc -LEFT JOIN information_schema.key_column_usage kcu - ON tc.constraint_catalog = kcu.constraint_catalog - AND tc.constraint_schema = kcu.constraint_schema - AND tc.constraint_name = kcu.constraint_name -LEFT JOIN information_schema.referential_constraints rc - ON tc.constraint_catalog = rc.constraint_catalog - AND tc.constraint_schema = rc.constraint_schema - AND tc.constraint_name = rc.constraint_name -LEFT JOIN information_schema.constraint_column_usage ccu - ON rc.unique_constraint_catalog = ccu.constraint_catalog - AND rc.unique_constraint_schema = ccu.constraint_schema - AND rc.unique_constraint_name = ccu.constraint_name -WHERE tc.constraint_schema = 'public' -AND tc.constraint_type = 'UNIQUE'; - - - ==== Trigger and Function - -SELECT * - FROM information_schema.triggers - WHERE trigger_schema NOT IN - ('pg_catalog', 'information_schema'); - - -CREATE TABLE emp ( - empname text, - salary integer, - last_date timestamp, - last_user text -); - -CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$ - BEGIN - -- Check that empname and salary are given - IF NEW.empname IS NULL THEN - RAISE EXCEPTION 'empname cannot be null'; - END IF; - IF NEW.salary IS NULL THEN - RAISE EXCEPTION '% cannot have null salary', NEW.empname; - END IF; - - -- Who works for us when she must pay for it? - IF NEW.salary < 0 THEN - RAISE EXCEPTION '% cannot have a negative salary', NEW.empname; - END IF; - - -- Remember who changed the payroll when - NEW.last_date := current_timestamp; - NEW.last_user := current_user; - RETURN NEW; - END; -$emp_stamp$ LANGUAGE plpgsql; - -CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp - FOR EACH ROW EXECUTE PROCEDURE emp_stamp(); - -CREATE TRIGGER check_update - BEFORE UPDATE ON emp - FOR EACH ROW - WHEN (OLD.empname IS DISTINCT FROM NEW.empname) - EXECUTE PROCEDURE emp_stamp(); diff --git a/owner.go b/owner.go index 4811a16..3751d67 100644 --- a/owner.go +++ b/owner.go @@ -1,16 +1,52 @@ // -// Copyright (c) 2014 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "fmt" -import "database/sql" -import "sort" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "text/template" +) + +var ( + ownerSqlTemplate = initOwnerSqlTemplate() +) + +// Initializes the Sql template +func initOwnerSqlTemplate() *template.Template { + sql := ` +SELECT n.nspname AS schema_name + , {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}c.relname || '.' || c.relname AS compare_name + , c.relname AS relationship_name + , a.rolname AS owner + , CASE WHEN c.relkind = 'r' THEN 'TABLE' + WHEN c.relkind = 'S' THEN 'SEQUENCE' + WHEN c.relkind = 'v' THEN 'VIEW' + ELSE c.relkind::varchar END AS type +FROM pg_class AS c +INNER JOIN pg_roles AS a ON (a.oid = c.relowner) +INNER JOIN pg_namespace AS n ON (n.oid = c.relnamespace) +WHERE c.relkind IN ('r', 'S', 'v') +{{if eq $.DbSchema "*" }} +AND n.nspname NOT LIKE 'pg_%' +AND n.nspname <> 'information_schema' +{{else}} +AND n.nspname = '{{$.DbSchema}}' +{{end}} +;` + + t := template.New("OwnerSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // OwnerRows definition @@ -24,7 +60,7 @@ func (slice OwnerRows) Len() int { } func (slice OwnerRows) Less(i, j int) bool { - return slice[i]["relationship_name"] < slice[j]["relationship_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice OwnerRows) Swap(i, j int) { @@ -77,21 +113,21 @@ func (c *OwnerSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("relationship_name"), c2.get("relationship_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) return val } // Add generates SQL to add the table/view owner func (c OwnerSchema) Add() { - fmt.Printf("-- Notice, db2 has no %s named %s. You probably need to run pgdiff with the %s option first.\n", c.get("type"), c.get("relationship_name"), c.get("type")) + 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")) } -// Drop generates SQL to drop the role +// Drop generates SQL to drop the owner func (c OwnerSchema) Drop() { - fmt.Printf("-- Notice, db2 has a %s that db1 does not: %s. Cannot compare owners.\n", c.get("type"), c.get("relationship_name")) + 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")) } -// Change handles the case where the role name matches, but the details do not +// Change handles the case where the relationship name matches, but the owner does not func (c OwnerSchema) Change(obj interface{}) { c2, ok := obj.(*OwnerSchema) if !ok { @@ -99,30 +135,21 @@ func (c OwnerSchema) Change(obj interface{}) { } if c.get("owner") != c2.get("owner") { - fmt.Printf("ALTER %s %s OWNER TO %s; \n", c.get("type"), c.get("relationship_name"), c.get("owner")) + fmt.Printf("ALTER %s %s.%s OWNER TO %s; \n", c.get("type"), c2.get("schema_name"), c.get("relationship_name"), c.get("owner")) } } -/* - * Compare the ownership of tables, sequences, and views in the two databases - */ +// compareOwners compares the ownership of tables, sequences, and views between two databases or schemas func compareOwners(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` -SELECT n.nspname AS schema - , c.relname AS relationship_name - , a.rolname AS owner - , CASE WHEN c.relkind = 'r' THEN 'TABLE' - WHEN c.relkind = 'S' THEN 'SEQUENCE' - WHEN c.relkind = 'v' THEN 'VIEW' - ELSE c.relkind::varchar END AS type -FROM pg_class AS c -INNER JOIN pg_authid AS a ON (a.oid = c.relowner) -INNER JOIN pg_namespace AS n ON (n.oid = c.relnamespace) -WHERE n.nspname = 'public' -AND c.relkind IN ('r', 'S', 'v'); -` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + + buf1 := new(bytes.Buffer) + ownerSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + ownerSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(OwnerRows, 0) for row := range rowChan1 { diff --git a/pgdiff.go b/pgdiff.go index a1d821b..98ac5fd 100644 --- a/pgdiff.go +++ b/pgdiff.go @@ -6,13 +6,18 @@ package main -import "fmt" -import "log" -import flag "github.com/ogier/pflag" -import "os" -import "strings" -import _ "github.com/lib/pq" -import "github.com/joncrlsn/pgutil" +import ( + "fmt" + "log" + + "os" + "strings" + + flag "github.com/ogier/pflag" + + "github.com/joncrlsn/pgutil" + _ "github.com/lib/pq" +) // Schema is a database definition (table, column, constraint, indes, role, etc) that can be // added, dropped, or changed to match another database. @@ -25,7 +30,7 @@ type Schema interface { } const ( - version = "0.9.2" + version = "0.9.3" ) var ( @@ -60,16 +65,24 @@ func main() { if *versionPtr { fmt.Fprintf(os.Stderr, "%s - version %s\n", os.Args[0], version) - fmt.Fprintln(os.Stderr, "Copyright (c) 2016 Jon Carlson. All rights reserved.") + fmt.Fprintln(os.Stderr, "Copyright (c) 2017 Jon Carlson. All rights reserved.") fmt.Fprintln(os.Stderr, "Use of this source code is governed by the MIT license") fmt.Fprintln(os.Stderr, "that can be found here: http://opensource.org/licenses/MIT") os.Exit(1) } if len(args) == 0 { - fmt.Println("The required first argument is SchemaType: ROLE, SEQUENCE, TABLE, VIEW, COLUMN, INDEX, FOREIGN_KEY, OWNER, GRANT_RELATIONSHIP, GRANT_ATTRIBUTE") + fmt.Println("The required first argument is SchemaType: SCHEMA, ROLE, SEQUENCE, TABLE, VIEW, MATVIEW, COLUMN, INDEX, FOREIGN_KEY, OWNER, GRANT_RELATIONSHIP, GRANT_ATTRIBUTE") os.Exit(1) } + + // Verify schemas + schemas := dbInfo1.DbSchema + dbInfo2.DbSchema + if schemas != "**" && strings.Contains(schemas, "*") { + fmt.Println("If one schema is an asterisk, both must be.") + os.Exit(1) + } + schemaType = strings.ToUpper(args[0]) fmt.Println("-- schemaType:", schemaType) @@ -87,19 +100,23 @@ func main() { // of alter statements to generate. Rather, all should be generated in the // proper order. if schemaType == "ALL" { - compareRoles(conn1, conn2) - compareFunctions(conn1, conn2) + if dbInfo1.DbSchema == "*" { + compareSchematas(conn1, conn2) + } compareSchematas(conn1, conn2) + compareRoles(conn1, conn2) compareSequences(conn1, conn2) compareTables(conn1, conn2) compareColumns(conn1, conn2) compareIndexes(conn1, conn2) // includes PK and Unique constraints compareViews(conn1, conn2) - compareOwners(conn1, conn2) + compareMatViews(conn1, conn2) compareForeignKeys(conn1, conn2) + compareFunctions(conn1, conn2) + compareTriggers(conn1, conn2) + compareOwners(conn1, conn2) compareGrantRelationships(conn1, conn2) compareGrantAttributes(conn1, conn2) - compareTriggers(conn1, conn2) } else if schemaType == "SCHEMA" { compareSchematas(conn1, conn2) } else if schemaType == "ROLE" { @@ -110,22 +127,26 @@ func main() { compareTables(conn1, conn2) } else if schemaType == "COLUMN" { compareColumns(conn1, conn2) - } else if schemaType == "FUNCTION" { - compareFunctions(conn1, conn2) - } else if schemaType == "VIEW" { - compareViews(conn1, conn2) + } else if schemaType == "TABLE_COLUMN" { + compareTableColumns(conn1, conn2) } else if schemaType == "INDEX" { compareIndexes(conn1, conn2) + } else if schemaType == "VIEW" { + compareViews(conn1, conn2) + } else if schemaType == "MATVIEW" { + compareMatViews(conn1, conn2) } else if schemaType == "FOREIGN_KEY" { compareForeignKeys(conn1, conn2) + } else if schemaType == "FUNCTION" { + compareFunctions(conn1, conn2) + } else if schemaType == "TRIGGER" { + compareTriggers(conn1, conn2) } else if schemaType == "OWNER" { compareOwners(conn1, conn2) } else if schemaType == "GRANT_RELATIONSHIP" { compareGrantRelationships(conn1, conn2) } else if schemaType == "GRANT_ATTRIBUTE" { compareGrantAttributes(conn1, conn2) - } else if schemaType == "TRIGGER" { - compareTriggers(conn1, conn2) } else { fmt.Println("Not yet handled:", schemaType) } @@ -183,15 +204,16 @@ Options: -v, --verbose : print extra run information -U, --user1 : first postgres user -u, --user2 : second postgres user - -H, --host1 : first database host. default is localhost + -H, --host1 : first database host. default is localhost -h, --host2 : second database host. default is localhost - -P, --port1 : first port. default is 5432 + -P, --port1 : first port. default is 5432 -p, --port2 : second port. default is 5432 -D, --dbname1 : first database name -d, --dbname2 : second database name + -S, --schema1 : first schema. default is all schemas + -s, --schema2 : second schema. default is all schemas -<schemaTpe> can be: ROLE, SEQUENCE, TABLE, VIEW, COLUMN, INDEX, FOREIGN_KEY, OWNER, GRANT_RELATIONSHIP, GRANT_ATTRIBUTE -`) +<schemaTpe> can be: ALL, SCHEMA, ROLE, SEQUENCE, TABLE, TABLE_COLUMN, VIEW, MATVIEW, COLUMN, INDEX, FOREIGN_KEY, OWNER, GRANT_RELATIONSHIP, GRANT_ATTRIBUTE, TRIGGER, FUNCTION`) os.Exit(2) } diff --git a/pgdiff.sh b/pgdiff.sh index a75aadf..9a16343 100755 --- a/pgdiff.sh +++ b/pgdiff.sh @@ -75,14 +75,14 @@ rundiff SCHEMA rundiff SEQUENCE rundiff TABLE rundiff COLUMN +rundiff MATVIEW rundiff INDEX rundiff VIEW +rundiff TRIGGER rundiff OWNER rundiff FOREIGN_KEY rundiff GRANT_RELATIONSHIP rundiff GRANT_ATTRIBUTE -rundiff TRIGGER echo echo "Done!" - diff --git a/role.go b/role.go index 3273d1c..b0bb5eb 100644 --- a/role.go +++ b/role.go @@ -108,6 +108,7 @@ where option can be: // Add generates SQL to add the constraint/index func (c RoleSchema) Add() { + // We don't care about efficiency here so we just concat strings options := " WITH PASSWORD 'changeme'" @@ -160,7 +161,7 @@ func (c RoleSchema) Drop() { func (c RoleSchema) Change(obj interface{}) { c2, ok := obj.(*RoleSchema) if !ok { - fmt.Println("Error!!!, change needs a RoleSchema instance", c2) + fmt.Println("Error!!!, Change needs a RoleSchema instance", c2) } options := "" @@ -251,13 +252,13 @@ func (c RoleSchema) Change(obj interface{}) { // TODO: Define INHERIT or not for _, mo1 := range membersof1 { if !misc.ContainsString(membersof2, mo1) { - fmt.Printf("GRANT %s TO %s;\n", mo1, c.get("rolename")) + fmt.Printf("GRANT %s TO %s;\n", mo1, c.get("rolname")) } } for _, mo2 := range membersof2 { if !misc.ContainsString(membersof1, mo2) { - fmt.Printf("REVOKE %s FROM %s;\n", mo2, c.get("rolename")) + fmt.Printf("REVOKE %s FROM %s;\n", mo2, c.get("rolname")) } } @@ -265,7 +266,7 @@ func (c RoleSchema) Change(obj interface{}) { } /* - * Compare the roles in the two databases + * Compare the roles between two databases or schemas */ func compareRoles(conn1 *sql.DB, conn2 *sql.DB) { sql := ` @@ -304,5 +305,6 @@ ORDER BY r.rolname; var schema1 Schema = &RoleSchema{rows: rows1, rowNum: -1} var schema2 Schema = &RoleSchema{rows: rows2, rowNum: -1} + // Compare the roles doDiff(schema1, schema2) } diff --git a/role_test.go b/role_test.go deleted file mode 100644 index a1eab97..0000000 --- a/role_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright (c) 2014 Jon Carlson. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. -// -// grant_test.go -package main - -import ( - "fmt" - "testing" -) - -/* -SELECT r.rolname - , r.rolsuper - , r.rolinherit - , r.rolcreaterole - , r.rolcreatedb - , r.rolcanlogin - , r.rolconnlimit - , r.rolvaliduntil - , r.rolreplication -FROM pg_catalog.pg_roles AS r -ORDER BY r.rolname; -*/ - -// Note that these must be sorted by rolname for this to work -var testRoles1a = []map[string]string{ - {"rolname": "addme2", "rolsuper": "false", "rolinherit": "false", "rolcreaterole": "true", "rolcreatedb": "true", "rolcanlogin": "true", "rolconnlimit": "100", "rolvaliduntil": "null"}, - {"rolname": "changeme", "rolsuper": "false", "rolinherit": "false", "rolcreaterole": "true", "rolcreatedb": "true", "rolcanlogin": "true", "rolconnlimit": "100", "rolvaliduntil": "null"}, - {"rolname": "matchme", "rolsuper": "false", "rolinherit": "false", "rolcreaterole": "true", "rolcreatedb": "true", "rolcanlogin": "true", "rolconnlimit": "100", "rolvaliduntil": "null"}, - {"rolname": "x-addme1", "rolsuper": "true", "rolinherit": "false", "rolcreaterole": "true", "rolcreatedb": "true", "rolcanlogin": "true", "rolconnlimit": "-1", "rolvaliduntil": "null"}, -} - -// Note that these must be sorted by rolname for this to work -var testRoles1b = []map[string]string{ - {"rolname": "changeme", "rolsuper": "false", "rolinherit": "false", "rolcreaterole": "false", "rolcreatedb": "false", "rolcanlogin": "true", "rolconnlimit": "10", "rolvaliduntil": "null"}, - {"rolname": "deleteme"}, - {"rolname": "matchme", "rolsuper": "false", "rolinherit": "false", "rolcreaterole": "true", "rolcreatedb": "true", "rolcanlogin": "true", "rolconnlimit": "100", "rolvaliduntil": "null"}, -} - -func Test_diffRoles(t *testing.T) { - fmt.Println("-- ==========\n-- Roles\n-- ==========") - var schema1 Schema = &RoleSchema{rows: testRoles1a, rowNum: -1} - var schema2 Schema = &RoleSchema{rows: testRoles1b, rowNum: -1} - doDiff(schema1, schema2) -} diff --git a/schemata.go b/schemata.go index cc4c684..5c28708 100644 --- a/schemata.go +++ b/schemata.go @@ -58,8 +58,6 @@ func (c *SchemataSchema) NextRow() bool { return !c.done } - - // Compare tells you, in one pass, whether or not the first row matches, is less than, or greater than the second row func (c *SchemataSchema) Compare(obj interface{}) int { c2, ok := obj.(*SchemataSchema) @@ -97,6 +95,13 @@ func (c SchemataSchema) Change(obj interface{}) { // compareSchematas outputs SQL to make the schema names match between DBs func compareSchematas(conn1 *sql.DB, conn2 *sql.DB) { + + // if we are comparing two schemas against each other, then + // we won't compare to ensure they are created, although maybe we should. + if dbInfo1.DbSchema != dbInfo2.DbSchema { + return + } + sql := ` SELECT schema_name , schema_owner @@ -125,6 +130,6 @@ ORDER BY schema_name;` var schema1 Schema = &SchemataSchema{rows: rows1, rowNum: -1} var schema2 Schema = &SchemataSchema{rows: rows2, rowNum: -1} - // Compare the tables + // Compare the schematas doDiff(schema1, schema2) } diff --git a/schemata_test.go b/schemata_test.go deleted file mode 100644 index a2be4d0..0000000 --- a/schemata_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright (c) 2017 Jon Carlson. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. -// -// schemata_test.go -package main - -import ( - "fmt" - "testing" -) - -/* -SELECT schema_name - , schema_owner - , default_character_set_schema -FROM information_schema.schemata -WHERE table_schema NOT LIKE 'pg_%' -WHERE table_schema <> 'information_schema' -ORDER BY schema_name; -*/ - -// Note that these must be sorted by table name for this to work -var testSchematas1= []map[string]string{ - {"schema_name": "schema_add", "schema_owner": "noop"}, - {"schema_name": "schema_same", "schema_owner": "noop"}, -} - -// Note that these must be sorted by schema_name for this to work -var testSchematas2= []map[string]string{ - {"schema_name": "schema_delete", "schema_owner": "noop"}, - {"schema_name": "schema_same", "schema_owner": "noop"}, -} - -func Test_diffSchematas(t *testing.T) { - fmt.Println("-- ==========\n-- Schematas\n-- ==========") - var schema1 Schema = &SchemataSchema{rows: testSchematas1, rowNum: -1} - var schema2 Schema = &SchemataSchema{rows: testSchematas2, rowNum: -1} - doDiff(schema1, schema2) -} diff --git a/sequence.go b/sequence.go index 57db965..566ae3e 100644 --- a/sequence.go +++ b/sequence.go @@ -6,11 +6,46 @@ package main -import "sort" -import "fmt" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "text/template" +) + +var ( + sequenceSqlTemplate = initSequenceSqlTemplate() +) + +// Initializes the Sql template +func initSequenceSqlTemplate() *template.Template { + sql := ` +SELECT sequence_schema AS schema_name + , {{if eq $.DbSchema "*" }}sequence_schema || '.' || {{end}}sequence_name AS compare_name + , sequence_name + , data_type + , start_value + , minimum_value + , maximum_value + , increment + , cycle_option +FROM information_schema.sequences +WHERE true +{{if eq $.DbSchema "*" }} +AND sequence_schema NOT LIKE 'pg_%' +AND sequence_schema <> 'information_schema' +{{else}} +AND sequence_schema = '{{$.DbSchema}}' +{{end}} +` + + t := template.New("SequenceSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // SequenceRows definition @@ -24,14 +59,14 @@ func (slice SequenceRows) Len() int { } func (slice SequenceRows) Less(i, j int) bool { - return slice[i]["sequence_name"] < slice[j]["sequence_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice SequenceRows) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } -// SequenceSchema holds a channel streaming table information from one of the databases as well as +// SequenceSchema holds a channel streaming sequence information from one of the databases as well as // a reference to the current row of data we're viewing. // // SequenceSchema implements the Schema interface defined in pgdiff.go @@ -66,22 +101,25 @@ func (c *SequenceSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("sequence_name"), c2.get("sequence_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) return val } -// Add returns SQL to add the table +// Add returns SQL to add the sequence func (c SequenceSchema) Add() { - fmt.Printf("CREATE SEQUENCE %s INCREMENT %s MINVALUE %s MAXVALUE %s START %s;\n", c.get("sequence_name"), c.get("increment"), c.get("minimum_value"), c.get("maximum_value"), c.get("start_value")) - + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("schema_name") + } + 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")) } -// Drop returns SQL to drop the table +// Drop returns SQL to drop the sequence func (c SequenceSchema) Drop() { - fmt.Printf("DROP SEQUENCE IF EXISTS %s;\n", c.get("sequence_name")) + fmt.Printf("DROP SEQUENCE %s.%s;\n", c.get("schema_name"), c.get("sequence_name")) } -// Change handles the case where the table and column match, but the details do not +// Change doesn't do anything right now. func (c SequenceSchema) Change(obj interface{}) { c2, ok := obj.(*SequenceSchema) if !ok { @@ -90,22 +128,17 @@ func (c SequenceSchema) Change(obj interface{}) { // Don't know of anything helpful we should do here } -// compareSequences outputs SQL to make the sequences match between DBs +// compareSequences outputs SQL to make the sequences match between DBs or schemas func compareSequences(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` -SELECT sequence_schema || '.' || sequence_name AS sequence_name - , data_type - , start_value - , minimum_value - , maximum_value - , increment - , cycle_option -FROM information_schema.sequences -WHERE sequence_schema NOT LIKE 'pg_%'; -` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf1 := new(bytes.Buffer) + sequenceSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + sequenceSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(SequenceRows, 0) for row := range rowChan1 { @@ -119,10 +152,10 @@ WHERE sequence_schema NOT LIKE 'pg_%'; } sort.Sort(rows2) - // We have to explicitly type this as Schema here for some unknown reason + // We have to explicitly type this as Schema here for some unknown (to me) reason var schema1 Schema = &SequenceSchema{rows: rows1, rowNum: -1} var schema2 Schema = &SequenceSchema{rows: rows2, rowNum: -1} - // Compare the tables + // Compare the sequences doDiff(schema1, schema2) } diff --git a/table.go b/table.go index ccd130f..a7f2cc8 100644 --- a/table.go +++ b/table.go @@ -6,11 +6,45 @@ package main -import "fmt" -import "sort" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "text/template" +) + +var ( + tableSqlTemplate = initTableSqlTemplate() +) + +// Initializes the Sql template +func initTableSqlTemplate() *template.Template { + + sql := ` +SELECT table_schema + , {{if eq $.DbSchema "*" }}table_schema || '.' || {{end}}table_name AS compare_name + , table_name + , CASE table_type + WHEN 'BASE TABLE' THEN 'TABLE' + ELSE table_type END AS table_type + , is_insertable_into +FROM information_schema.tables +WHERE table_type = 'BASE TABLE' +{{if eq $.DbSchema "*" }} +AND table_schema NOT LIKE 'pg_%' +AND table_schema <> 'information_schema' +{{else}} +AND table_schema = '{{$.DbSchema}}' +{{end}} +ORDER BY compare_name; +` + t := template.New("TableSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // TableRows definition @@ -24,7 +58,7 @@ func (slice TableRows) Len() int { } func (slice TableRows) Less(i, j int) bool { - return slice[i]["table_name"] < slice[j]["table_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice TableRows) Swap(i, j int) { @@ -66,20 +100,24 @@ func (c *TableSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("table_name"), c2.get("table_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) //fmt.Printf("-- Compared %v: %s with %s \n", val, c.get("table_name"), c2.get("table_name")) return val } // Add returns SQL to add the table or view func (c TableSchema) Add() { - fmt.Printf("CREATE %s %s();", c.get("table_type"), c.get("table_name")) + schema := dbInfo2.DbSchema + if schema == "*" { + schema = c.get("table_schema") + } + fmt.Printf("CREATE %s %s.%s();", c.get("table_type"), schema, c.get("table_name")) fmt.Println() } // Drop returns SQL to drop the table or view func (c TableSchema) Drop() { - fmt.Printf("DROP %s IF EXISTS %s;\n", c.get("table_type"), c.get("table_name")) + fmt.Printf("DROP %s %s.%s;\n", c.get("table_type"), c.get("table_schema"), c.get("table_name")) } // Change handles the case where the table and column match, but the details do not @@ -93,18 +131,15 @@ func (c TableSchema) Change(obj interface{}) { // compareTables outputs SQL to make the table names match between DBs func compareTables(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` -SELECT table_schema || '.' || table_name AS table_name - , CASE table_type WHEN 'BASE TABLE' THEN 'TABLE' ELSE table_type END AS table_type - , is_insertable_into -FROM information_schema.tables -WHERE table_schema NOT LIKE 'pg_%' - AND table_schema <> 'information_schema' - AND table_type = 'BASE TABLE' -ORDER BY table_name;` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf1 := new(bytes.Buffer) + tableSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + tableSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(TableRows, 0) for row := range rowChan1 { diff --git a/table_test.go b/table_test.go deleted file mode 100644 index 86a305a..0000000 --- a/table_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright (c) 2017 Jon Carlson. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. -// -// table_test.go -package main - -import ( - "fmt" - "testing" -) - -/* -SELECT table_schema || '.' || table_name AS table_name - , CASE table_type WHEN 'BASE TABLE' THEN 'TABLE' ELSE table_type END AS table_type - , is_insertable_into -FROM information_schema.tables -WHERE table_schema NOT LIKE 'pg_%' -WHERE table_schema <> 'information_schema' -AND table_type = 'BASE TABLE' -ORDER BY table_name; -*/ - -// Note that these must be sorted by table name for this to work -var testTables1a = []map[string]string{ - {"table_name": "schema1.add", "table_type": "TABLE"}, - {"table_name": "schema1.same", "table_type": "TABLE"}, - {"table_name": "schema2.add", "table_type": "TABLE"}, - {"table_name": "schema2.same", "table_type": "TABLE"}, -} - -// Note that these must be sorted by table_name for this to work -var testTables1b= []map[string]string{ - {"table_name": "schema1.delete", "table_type": "TABLE"}, - {"table_name": "schema1.same", "table_type": "TABLE"}, - {"table_name": "schema2.same", "table_type": "TABLE"}, -} - -func Test_diffTables(t *testing.T) { - fmt.Println("-- ==========\n-- Tables\n-- ==========") - var schema1 Schema = &TableSchema{rows: testTables1a, rowNum: -1} - var schema2 Schema = &TableSchema{rows: testTables1b, rowNum: -1} - doDiff(schema1, schema2) -} diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..59dc1ea --- /dev/null +++ b/test/README.md @@ -0,0 +1,8 @@ +These are not automated tests (I'd rather have automated tests), but manual +integration tests for verifying that individual schema types are working. + +These can be good templates for isolating bugs in the different data diffs. + +Connect to the database manually: + sudo su - postgres -- -c "psql -d db1" + diff --git a/test/example.dump b/test/example.dump new file mode 100644 index 0000000..fc33498 Binary files /dev/null and b/test/example.dump differ diff --git a/test/load-example.sh b/test/load-example.sh new file mode 100644 index 0000000..68da63a --- /dev/null +++ b/test/load-example.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# +# Load example dump found here: +# http://postgresguide.com/setup/example.htmlhttp://postgresguide.com/setup/example.html + +#curl -L -O http://cl.ly/173L141n3402/download/example.dump +sudo su - postgres -- -c " + createdb pgguide + pg_restore --no-owner --dbname pgguide example.dump + psql --dbname pgguide +" diff --git a/test/mypsql b/test/mypsql new file mode 100755 index 0000000..75601c6 --- /dev/null +++ b/test/mypsql @@ -0,0 +1 @@ +sudo su - postgres -c "psql -d db1" diff --git a/test/populate-db.sh b/test/populate-db.sh new file mode 100755 index 0000000..1ebab6e --- /dev/null +++ b/test/populate-db.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# + +db=$1 + +PGPASSWORD=asdf psql -U u1 -h localhost -d $db <<EOS + $2 +EOS + diff --git a/test/start-fresh.sh b/test/start-fresh.sh new file mode 100755 index 0000000..f8207af --- /dev/null +++ b/test/start-fresh.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Drop and recreate 2 testing users (u1, u2) +# Drop and recreate 2 known databases (db1, db2) used for testing +# + +sql=" + DROP DATABASE IF EXISTS db1; + DROP DATABASE IF EXISTS db2; + + DROP USER IF EXISTS u1; + CREATE USER u1 WITH SUPERUSER PASSWORD 'asdf'; + + CREATE DATABASE db1 WITH OWNER = u1 TEMPLATE = template0; + CREATE DATABASE db2 WITH OWNER = u1 TEMPLATE = template0; + + DROP USER IF EXISTS u2; + CREATE USER u2 PASSWORD 'asdf'; +" + +sudo su - postgres -- -c "psql <<< \"$sql\"" diff --git a/test/test-column b/test/test-column new file mode 100755 index 0000000..a26bb13 --- /dev/null +++ b/test/test-column @@ -0,0 +1,103 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + +echo +echo ============================================================== + +# +# Compare the columns between two schemas in the same database +# +#psql -U u1 -h localhost -d db1 <<'EOS' +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table9 ( + id integer, + name varchar(50) + ); + CREATE TABLE s1.table10 (id bigint); + CREATE TABLE s1.table11 (); + + CREATE SCHEMA s2; + CREATE TABLE s2.table9 ( -- Add name column + id integer + ); + CREATE TABLE s2.table10 (id integer); -- change id to bigint + CREATE TABLE s2.table11 (id integer); -- drop id column +" + +echo +echo "# Compare the columns between two schemas in the same database" +echo "# Expect SQL:" +echo "# Add s2.table9.name" +echo "# Change s2.table10.id to bigint" +echo "# Drop s2.table11.id" + +echo +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + COLUMN | grep -v '^-- ' + + +echo +echo ============================================================== + +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE TABLE s1.table9 ( + id integer, + name varchar(40) + ); + CREATE TABLE s1.table10 (); + CREATE TABLE s1.table11 (dropme integer); + + CREATE SCHEMA s2; + CREATE TABLE s2.table9 ( -- Add name column + id integer + ); + CREATE TABLE s2.table10 (id integer); -- change id to bigint + CREATE TABLE s2.table11 (id integer); -- drop id column +" + +echo +echo "# Compare the columns in all schemas between two databases" +echo "# Expect:" +echo "# Change s1.table9.name to varchar(50) " +echo "# Add s1.table10.id" +echo "# Drop s1.table11.dropme" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + COLUMN | grep -v '^-- ' +echo +echo ============================================================== + +./populate-db.sh db1 " + CREATE SCHEMA s3; + CREATE TABLE s3.table12 ( + ids integer[], + bigids bigint[], + something text[][] -- dimensions don't seem to matter, so ignore them + ); + CREATE SCHEMA s4; + CREATE TABLE s4.table12 ( -- add ids column + bigids integer[], -- change bigids to int8[] + something text[] -- no change + ); +" + +echo +echo "# Compare array columns between two tables" +echo "# Expect:" +echo "# Add s4.table12.ids int4[]" +echo "# Change s4.table12.bigids from to int8[]" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s3" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s4" -o "sslmode=disable" \ + COLUMN | grep -v '^-- ' +echo diff --git a/test/test-foreignkey b/test/test-foreignkey new file mode 100755 index 0000000..d25b6a0 --- /dev/null +++ b/test/test-foreignkey @@ -0,0 +1,102 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + +echo +echo ==================================================== +echo + +# +# Compare the foreign keys between two schemas in the same database +# + +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 ( + id integer PRIMARY KEY + ); + CREATE TABLE s1.table2 ( + id integer PRIMARY KEY, + table1_id integer REFERENCES s1.table1(id) + ); + CREATE TABLE s1.table3 ( + id integer, + table2_id integer + ); + + CREATE SCHEMA s2; + CREATE TABLE s2.table1 ( + id integer PRIMARY KEY + ); + CREATE TABLE s2.table2 ( + id integer PRIMARY KEY, + table1_id integer + ); + CREATE TABLE s2.table3 ( + id integer, + table2_id integer REFERENCES s2.table2(id) -- This will be deleted + ); +" + +echo +echo "# Compare the foreign keys between two schemas in the same database" +echo "# Expect SQL:" +echo "# Add foreign key on s2.table2.table1_id" +echo "# Drop foreign key from s2.table3.table2_id" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + FOREIGN_KEY | grep -v '^-- ' + +echo +echo ==================================================== +echo + + +# +# Compare the foreign keys in all schemas between two databases +# +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 ( + id integer PRIMARY KEY + ); + CREATE TABLE s1.table2 ( + id integer PRIMARY KEY, + table1_id integer -- a foreign key will be added + ); + CREATE TABLE s1.table3 ( + id integer, + table2_id integer + ); + + CREATE SCHEMA s2; + CREATE TABLE s2.table1 ( + id integer PRIMARY KEY + ); + CREATE TABLE s2.table2 ( + id integer PRIMARY KEY, + table1_id integer REFERENCES s2.table1(id) -- This will be deleted + + ); + CREATE TABLE s2.table3 ( + id integer, + table2_id integer REFERENCES s2.table2(id) + ); +" + +echo +echo "# Compare the foreign keys in all schemas between two databases" +echo "# Expect SQL:" +echo "# Add foreign key on db2.s1.table2.table1_id" +echo "# Drop foreign key on db2.s2.table2.table1_id" + +echo +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + FOREIGN_KEY | grep -v '^-- ' +echo diff --git a/test/test-function b/test/test-function new file mode 100755 index 0000000..27f8cba --- /dev/null +++ b/test/test-function @@ -0,0 +1,105 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null +echo +echo ========================================================== +echo + +# +# Compare the functions between two schemas in the same database +# + +./populate-db.sh db1 "$(cat << 'EOF' +CREATE SCHEMA s1; +CREATE OR REPLACE FUNCTION s1.increment(i integer) RETURNS integer AS $$ + BEGIN + RETURN i + 1; + END; +$$ LANGUAGE plpgsql; +CREATE FUNCTION s1.add(integer, integer) RETURNS integer + AS 'select $1 + $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; + + +CREATE SCHEMA s2; +CREATE OR REPLACE FUNCTION s2.add(bigint, bigint) RETURNS bigint + AS 'select $1 + $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; +CREATE FUNCTION s2.minus(integer, integer) RETURNS integer + AS 'select $1 - $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; + +EOF +)" + +echo +echo "# Compare the functions between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Add function s2.increment" +echo "# Replace function s2.add" +echo "# Drop function s2.minus" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + FUNCTION #| grep -v '^-- ' +echo +echo ========================================================== +echo + + +# +# Compare the functions in all schemas between two databases +# +./populate-db.sh db2 "$(cat << 'EOF' +CREATE SCHEMA s1; +CREATE OR REPLACE FUNCTION s1.increment(i integer) RETURNS integer AS $$ + BEGIN + RETURN i + 1; + END; +$$ LANGUAGE plpgsql; +CREATE FUNCTION s1.addition(integer, integer) RETURNS integer + AS 'select $1 + $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; + + +CREATE SCHEMA s2; +CREATE OR REPLACE FUNCTION s2.add(integer, integer) RETURNS integer + AS 'select $1 + $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; +CREATE FUNCTION s2.minus(integer, integer) RETURNS integer + AS 'select $1 - $2;' + LANGUAGE SQL + IMMUTABLE + RETURNS NULL ON NULL INPUT; + +EOF +)" + + +echo +echo "# Compare the functions in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Add function s1.add" +echo "# Change/Replace function s2.add" +echo "# Drop function s1.addition" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + FUNCTION #| grep -v '^-- ' +echo +echo diff --git a/test/test-grant-attribute b/test/test-grant-attribute new file mode 100755 index 0000000..bb63bbc --- /dev/null +++ b/test/test-grant-attribute @@ -0,0 +1,122 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + + +echo +echo ========================================================== +echo + +# +# Compare the grants between two schemas in the same database +# + +./populate-db.sh db1 " + -- Available Column Privileges: SELECT, INSERT, UPDATE, REFERENCES + + CREATE SCHEMA s1; + CREATE SCHEMA s2; + + --------- + + CREATE TABLE s1.table1 (id integer, name varchar(30)); + GRANT SELECT, UPDATE (name) ON s1.table1 TO u2; + + -- Drop REFERENCES, Add UPDATE + CREATE TABLE s2.table1 (id integer, name varchar(30)); + GRANT SELECT, REFERENCES (name) ON s2.table1 TO u2; + + --------- + + CREATE TABLE s1.table2 (id integer, name varchar(30)); + -- u2 has no privileges + + -- Drop SELECT on s1.table2 + CREATE TABLE s2.table2 (id integer, name varchar(30)); + GRANT SELECT (name) ON s2.table2 TO u2; + + --------- + + CREATE TABLE s1.table3 (id integer, name varchar(30)); + GRANT SELECT (name) ON s1.table3 TO u2; + + -- Add SELECT on s1.table3 + CREATE TABLE s2.table3 (id integer, name varchar(30)); + -- u2 has no privileges +" + +echo +echo "# Compare the grants between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Grant UPDATE (name) on s2.table1 for u2" +echo "# Revoke REFERENCES (name) on s2.table1 for u2" +echo "# Revoke SELECT (name) on s2.table2 for u2" +echo "# Grant SELECT (name) on s2.table3 for u2" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + GRANT_ATTRIBUTE | grep -v '^-- ' + +echo +echo ========================================================== +echo + + +source ./start-fresh.sh >/dev/null + +# +# Compare the grants in all schemas between two databases +# + +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE SCHEMA s2; + --------- + CREATE TABLE s1.table1 (id integer, name varchar(30)); + GRANT SELECT, UPDATE (name) ON s1.table1 TO u2; + + CREATE TABLE s2.table1 (id integer, name varchar(30)); + GRANT UPDATE (name) ON s2.table1 TO u2; + + CREATE TABLE s2.table3 (id integer, name varchar(30)); + + CREATE TABLE s2.table4 (id integer, name varchar(30)); + GRANT SELECT, UPDATE (name) ON s2.table4 TO u2; +" + +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE SCHEMA s2; + --------- + CREATE TABLE s1.table1 (id integer, name varchar(30)); + GRANT SELECT, UPDATE (name) ON s1.table1 TO u2; + GRANT SELECT (id) ON s1.table1 TO u2; + + CREATE TABLE s2.table1 (id integer, name varchar(30)); + GRANT REFERENCES (name) ON s2.table1 TO u2; + + CREATE TABLE s2.table3 (id integer, name varchar(30)); + GRANT UPDATE (name) ON s2.table3 TO u2; + + CREATE TABLE s2.table4 (id integer, name varchar(30)); +" + +echo +echo "# Compare the grants in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Revoke SELECT (id) on s1.table1 for u2" +echo "# Grant UPDATE (name) on s2.table1 for u2" +echo "# Revoke REFERENCES (name) on s2.table1 for u2" +echo "# Revoke UPDATE (name) on s2.table3 for u2" +echo "# Grant UPDATE (name) on s2.table4 for u2" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + GRANT_ATTRIBUTE | grep -v '^-- ' +echo +echo diff --git a/test/test-grant-relationship b/test/test-grant-relationship new file mode 100755 index 0000000..1f6583d --- /dev/null +++ b/test/test-grant-relationship @@ -0,0 +1,86 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + + +echo +echo ========================================================== +echo + +# +# Compare the grants between two schemas in the same database +# + +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 (id integer); + GRANT INSERT, UPDATE ON s1.table1 TO u2; + CREATE TABLE s1.table2 (id integer); + GRANT SELECT ON s1.table2 TO u2; + CREATE TABLE s1.table3 (id integer); + GRANT SELECT ON s1.table3 TO u2; + + CREATE SCHEMA s2; + CREATE TABLE s2.table1 (id integer); + GRANT SELECT ON s2.table1 TO u2; -- add INSERT, UPDATE + CREATE TABLE s2.table2 (id integer); + GRANT SELECT ON s2.table2 TO u2; -- no change + CREATE TABLE s2.table3 (id integer); -- add SELECT + GRANT SELECT ON s2.table3 TO u1; +" + +echo +echo "# Compare the grants between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Revoke SELECT on s2.table1 for u2" +echo "# Grant INSERT, UPDATE on s2.table1 for u2" +echo "# Grant SELECT on s2.table3 for u2" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + GRANT_RELATIONSHIP #| grep -v '^-- ' + + +echo +echo ========================================================== +echo + + +# +# Compare the grants in all schemas between two databases +# +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 (id integer); + GRANT SELECT ON s1.table1 TO u2; + CREATE TABLE s1.table2 (id integer); + GRANT SELECT ON s1.table2 TO u2; + CREATE TABLE s1.table3 (id integer); + GRANT SELECT ON s1.table3 TO u2; + + CREATE SCHEMA s2; + CREATE TABLE s2.table1 (id integer); + GRANT SELECT ON s2.table1 TO u2; + CREATE TABLE s2.table2 (id integer); + GRANT SELECT ON s2.table2 TO u2; + CREATE TABLE s2.table3 (id integer); + GRANT UPDATE ON s2.table3 TO u2; -- revoke +" + +echo +echo "# Compare the grants in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Revoke UPDATE on s2.table3 for u2" +echo "# Grant INSERT,UPDATE on s1.table1 for u2" +echo "# Revoke SELECT on s1.table1 for u2" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + GRANT_RELATIONSHIP #| grep -v '^-- ' +echo +echo diff --git a/test/test-identity-column b/test/test-identity-column new file mode 100755 index 0000000..ded8bb1 --- /dev/null +++ b/test/test-identity-column @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + +echo +echo ============================================================== + +# +# Compare the columns between two schemas in the same database +# +#psql -U u1 -h localhost -d db1 <<'EOS' +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table13 ( + id1 integer, + id2 bigint generated by default as identity, + id3 integer generated by default as identity + ); + + CREATE SCHEMA s2; + CREATE TABLE s2.table13 ( + id1 integer generated by default as identity, -- drop identity + id2 integer -- int to bigint, add identity + -- add identity column + ); +" + +echo +echo "# Compare differences in identity columns between two tables" +echo "# Expect SQL:" +echo "# Change s2.table13.id1 drop identity" +echo "# Change s2.table13.id2 to bigint, add identity" +echo "# Add s2.table13.id3" + +echo +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + COLUMN | grep -v '^-- ' +echo + diff --git a/test/test-index b/test/test-index new file mode 100755 index 0000000..463f80b --- /dev/null +++ b/test/test-index @@ -0,0 +1,80 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null +echo +echo ========================================================== +echo + +# +# Compare the indexes between two schemas in the same database +# + +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 ( + id integer PRIMARY KEY, + name varchar(32), + url varchar(200) + ); + CREATE INDEX ON s1.table1(name); + + CREATE SCHEMA s2; + CREATE TABLE s2.table1 ( + id integer PRIMARY KEY, + name varchar(32), + url varchar(200) + ); + CREATE INDEX ON s2.table1(url); +" + +echo +echo "# Compare the indexes between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Add index on s2.table1.name" +echo "# Drop index on s2.table1.url" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + INDEX | grep -v '^-- ' +echo +echo ========================================================== +echo + + +# +# Compare the indexes in all schemas between two databases +# +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 ( + id integer PRIMARY KEY, + name varchar(32), + url varchar(200) + ); + CREATE INDEX ON s1.table1(name); + CREATE INDEX ON s1.table1(url); + + CREATE SCHEMA s2; + CREATE TABLE s2.table1 ( + id integer PRIMARY KEY, + name varchar(32), + url varchar(200) + ); +" + +echo +echo "# Compare the indexes in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Drop index on db2 s1.table1.url" +echo "# Add index on db2 s2.table1.url" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + INDEX | grep -v '^-- ' +echo +echo diff --git a/test/test-owner b/test/test-owner new file mode 100755 index 0000000..6368b45 --- /dev/null +++ b/test/test-owner @@ -0,0 +1,85 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +# Rebuild the basic databases +source ./start-fresh.sh >/dev/null + + +echo +echo ========================================================== +echo + +# +# Compare the table, etc owners between two schemas in the same database +# + +./populate-db.sh db1 " + -- schema s1 + CREATE SCHEMA s1; + CREATE TABLE s1.table1(); + ALTER TABLE s1.table1 OWNER TO u2; + CREATE TABLE s1.table2(); + CREATE TABLE s1.table3(); + CREATE TABLE s1.table4(); + + -- schema s2 + CREATE SCHEMA s2; + CREATE TABLE s2.table1(); + CREATE TABLE s2.table2(); + ALTER TABLE s2.table2 OWNER TO u2; + CREATE TABLE s2.table3(); + CREATE TABLE s2.table5(); +" + +echo +echo "# Compare the table, etc. owners between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Change s2.table1 owner to u2" +echo "# Change s2.table2 owner to u1" +echo "# No changes to ownership of s2.table3" +echo "# Messages about table4 and table5 not being in both schemas" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + OWNER #| grep -v '^-- ' + + +echo +echo ========================================================== +echo + + +# +# Compare the table, etc. owners in all schemas between two databases +# +./populate-db.sh db2 " + -- schema s1 + CREATE SCHEMA s1; + CREATE TABLE s1.table1(); + ALTER TABLE s1.table1 OWNER TO u2; + CREATE TABLE s1.table2(); + CREATE TABLE s1.table3(); + ALTER TABLE s1.table3 OWNER TO u2; + + -- schema s2 + CREATE SCHEMA s2; + CREATE TABLE s2.table1(); + CREATE TABLE s2.table2(); + CREATE TABLE s2.table3(); +" + +echo +echo "# Compare the table, etc owners in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Change s1.table3 owner to u1 " +echo "# Change s2.table2 owner to u2 " +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + OWNER #| grep -v '^-- ' +echo +echo diff --git a/test/test-schemata b/test/test-schemata new file mode 100755 index 0000000..0ab2080 --- /dev/null +++ b/test/test-schemata @@ -0,0 +1,34 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null +echo +echo ========================================================== +echo + +# +# Compare the schemas (aka schematas) between two databases +# +./populate-db.sh db1 " + CREATE SCHEMA s1; -- matches db2 + CREATE SCHEMA s2; -- to be added to db2 +" +./populate-db.sh db2 " + CREATE SCHEMA s1; -- matches db1 + CREATE SCHEMA s3; -- to be removed from this db +" + +echo +echo "# Compare the indexes in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Add schema on db2: s2 " +echo "# Drop schema from db2: s3 " +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + SCHEMA | grep -v '^-- ' +echo +echo diff --git a/test/test-sequence b/test/test-sequence new file mode 100755 index 0000000..c13d26f --- /dev/null +++ b/test/test-sequence @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null +echo +echo ========================================================== +echo + +# +# Compare the sequences between two schemas in the same database +# + +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table1 (id integer PRIMARY KEY); -- just for kicks + CREATE SEQUENCE s1.sequence_1 + INCREMENT BY 2 + MINVALUE 1024 + MAXVALUE 99998 + START WITH 2048 + NO CYCLE + OWNED BY s1.table1.id; + CREATE SEQUENCE s1.sequence_2; + + CREATE SCHEMA s2; + CREATE SEQUENCE s2.sequence_2; + CREATE SEQUENCE s2.sequence_3; +" + +echo +echo "# Compare the sequences between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Add s2.sequence_1" +echo "# Drop s2.sequence_3" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + SEQUENCE | grep -v '^-- ' + + +echo +echo ========================================================== +echo + +# +# Compare the sequences in all schemas between two databases +# +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE SEQUENCE s1.sequence_2; + + CREATE SCHEMA s2; + CREATE SEQUENCE s2.sequence_2; + CREATE SEQUENCE s2.sequence_3; + CREATE SEQUENCE s2.sequence_4; +" + +echo +echo "# Compare the sequences in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Add sequence s1.sequence_1" +echo "# Drop sequence s2.sequence_4" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + SEQUENCE | grep -v '^-- ' +echo +echo diff --git a/test/test-table b/test/test-table new file mode 100755 index 0000000..7530cf3 --- /dev/null +++ b/test/test-table @@ -0,0 +1,60 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + +echo +echo ============================================================== + +# +# Compare the tables between two schemas in the same database +# +#psql -U u1 -h localhost -d db1 <<'EOS' +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table9 (id integer); -- to be added to s2 + CREATE TABLE s1.table10 (id integer); + + CREATE SCHEMA s2; + CREATE TABLE s2.table10 (id integer); + CREATE TABLE s2.table11 (id integer); -- will be dropped from s2 +" + +echo +echo "# Compare the tables between two schemas in the same database" +echo "# Expect SQL:" +echo "# Add table9 to schema s2" +echo "# Drop table11 from schema s2" +echo +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + TABLE | grep -v '^-- ' + +echo +echo ============================================================== + +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE TABLE s1.table9 (id integer); + -- table10 will be added in db2 + + CREATE SCHEMA s2; + CREATE TABLE s2.table10 (id integer); + CREATE TABLE s2.table11 (id integer); + CREATE TABLE s2.table12 (id integer); -- will be dropped (not in db1) + + CREATE SCHEMA s3; +" + +echo +echo "# Compare the tables in all schemas between two databases" +echo "# Expect:" +echo "# Add s1.table10 to db2" +echo "# Drop s2.table12 from db2" +echo +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + TABLE | grep -v '^-- ' +echo diff --git a/test/test-table-column b/test/test-table-column new file mode 100755 index 0000000..baae26a --- /dev/null +++ b/test/test-table-column @@ -0,0 +1,82 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null + +echo +echo ============================================================== + +# +# Compare the table columns between two schemas in the same database +# +#psql -U u1 -h localhost -d db1 <<'EOS' +./populate-db.sh db1 " + CREATE SCHEMA s1; + CREATE TABLE s1.table9 ( + id integer, + name varchar(50) + ); + CREATE TABLE s1.table10 (id bigint); + CREATE TABLE s1.table11 (); + + CREATE SCHEMA s2; + CREATE TABLE s2.table9 ( -- Add name column + id integer + ); + CREATE TABLE s2.table10 (id integer); -- change id to bigint + CREATE TABLE s2.table11 (id integer); -- drop id column + CREATE OR REPLACE VIEW s1.view1 AS + SELECT * + FROM s1.table10; +" + +echo +echo "# Compare the columns between two schemas in the same database" +echo "# Expect SQL:" +echo "# Add s2.table9.name" +echo "# Change s2.table10.id to bigint" +echo "# Drop s2.table11.id" + +echo +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + TABLE_COLUMN | grep -v '^-- ' + + +echo +echo ============================================================== + +./populate-db.sh db2 " + CREATE SCHEMA s1; + CREATE TABLE s1.table9 ( + id integer, + name varchar(40) + ); + CREATE TABLE s1.table10 (); + CREATE TABLE s1.table11 (dropme integer); + + CREATE SCHEMA s2; + CREATE TABLE s2.table9 ( -- Add name column + id integer + ); + CREATE TABLE s2.table10 (id integer); -- change id to bigint + CREATE TABLE s2.table11 (id integer); -- drop id column + CREATE OR REPLACE VIEW s1.view1 AS + SELECT * + FROM s1.table10; +" + +echo +echo "# Compare the table columns in all schemas between two databases" +echo "# Expect:" +echo "# Change s1.table9.name to varchar(50) " +echo "# Add s1.table10.id" +echo "# Drop s1.table11.dropme" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + TABLE_COLUMN | grep -v '^-- ' +echo diff --git a/test/test-trigger b/test/test-trigger new file mode 100755 index 0000000..8ee17c2 --- /dev/null +++ b/test/test-trigger @@ -0,0 +1,99 @@ +#!/bin/bash +# +# Useful for visually inspecting the output SQL to verify it is doing what it should +# + +source ./start-fresh.sh >/dev/null +echo +echo ========================================================== +echo + +# +# Compare the triggers between two schemas in the same database +# + +#CREATE [ CONSTRAINT ] TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } +# ON table_name +# [ FROM referenced_table_name ] +# [ NOT DEFERRABLE | [ DEFERRABLE ] { INITIALLY IMMEDIATE | INITIALLY DEFERRED } ] +# [ FOR [ EACH ] { ROW | STATEMENT } ] +# [ WHEN ( condition ) ] +# EXECUTE PROCEDURE function_name ( arguments ) + +./populate-db.sh db1 "$(cat << 'EOF' +-- Schema s1 +CREATE SCHEMA s1; +CREATE TABLE s1.table1 (id integer); +CREATE OR REPLACE FUNCTION s1.validate1() RETURNS TRIGGER AS $$ + BEGIN + SELECT 1; -- look like we are doing something ;^> + END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER trigger1 AFTER INSERT ON s1.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); +CREATE TRIGGER trigger2 AFTER INSERT ON s1.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); + + +-- Schema s2 +CREATE SCHEMA s2; +CREATE TABLE s2.table1 (id integer); +CREATE TRIGGER trigger2 BEFORE INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); +CREATE TRIGGER trigger3 AFTER INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); + +EOF +)" + +echo +echo "# Compare the triggers between two schemas in the same database" +echo "# Expect SQL (pseudocode):" +echo "# Create trigger1 on s2.table1" +echo "# Recreate trigger2 on s2.table1" +echo "# Drop trigger3 on s2.table1" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "s1" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db1" -s "s2" -o "sslmode=disable" \ + TRIGGER | grep -v '^-- ' + +echo +echo ========================================================== +echo + +# +# Compare the triggers in all schemas between two databases +# +./populate-db.sh db2 "$(cat << 'EOF' + +-- Schema s1 +CREATE SCHEMA s1; +CREATE TABLE s1.table1 (id integer); +CREATE OR REPLACE FUNCTION s1.validate1() RETURNS TRIGGER AS $$ + BEGIN + SELECT 1; -- look like we are doing something :^> + END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER trigger2 BEFORE INSERT ON s1.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); + + +-- Schema s2 +CREATE SCHEMA s2; +CREATE TABLE s2.table1 (id integer); +CREATE TRIGGER trigger2 BEFORE INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); +CREATE TRIGGER trigger3 AFTER INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); +CREATE TRIGGER trigger4 AFTER INSERT ON s2.table1 FOR EACH ROW EXECUTE PROCEDURE s1.validate1(); + +EOF +)" + +echo +echo "# Compare the triggers in all schemas between two databases" +echo "# Expect SQL (pseudocode):" +echo "# Create trigger1 on s1.table1" +echo "# Recreate trigger2 on s1.table1" +echo "# Drop trigger4 on s2.table1" +echo + +../pgdiff -U "u1" -W "asdf" -H "localhost" -D "db1" -S "*" -O "sslmode=disable" \ + -u "u1" -w "asdf" -h "localhost" -d "db2" -s "*" -o "sslmode=disable" \ + TRIGGER | grep -v '^-- ' +echo +echo diff --git a/trigger.go b/trigger.go index aeeee6c..e362096 100644 --- a/trigger.go +++ b/trigger.go @@ -1,16 +1,50 @@ // -// Copyright (c) 2016 Jon Carlson. All rights reserved. +// Copyright (c) 2017 Jon Carlson. All rights reserved. // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. // package main -import "fmt" -import "sort" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "bytes" + "database/sql" + "fmt" + "github.com/joncrlsn/misc" + "github.com/joncrlsn/pgutil" + "sort" + "strings" + "text/template" +) + +var ( + triggerSqlTemplate = initTriggerSqlTemplate() +) + +// Initializes the Sql template +func initTriggerSqlTemplate() *template.Template { + sql := ` + SELECT n.nspname AS schema_name + , {{if eq $.DbSchema "*" }}n.nspname || '.' || {{end}}c.relname || '.' || t.tgname AS compare_name + , c.relname AS table_name + , t.tgname AS trigger_name + , pg_catalog.pg_get_triggerdef(t.oid, true) AS trigger_def + , t.tgenabled AS enabled + FROM pg_catalog.pg_trigger t + INNER JOIN pg_catalog.pg_class c ON (c.oid = t.tgrelid) + INNER JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace) + WHERE not t.tgisinternal + {{if eq $.DbSchema "*" }} + AND n.nspname NOT LIKE 'pg_%' + AND n.nspname <> 'information_schema' + {{else}} + AND n.nspname = '{{$.DbSchema}}' + {{end}} + ` + t := template.New("TriggerSqlTmpl") + template.Must(t.Parse(sql)) + return t +} // ================================== // TriggerRows definition @@ -24,10 +58,7 @@ func (slice TriggerRows) Len() int { } func (slice TriggerRows) Less(i, j int) bool { - if slice[i]["table_name"] != slice[j]["table_name"] { - return slice[i]["table_name"] < slice[j]["table_name"] - } - return slice[i]["trigger_name"] < slice[j]["trigger_name"] + return slice[i]["compare_name"] < slice[j]["compare_name"] } func (slice TriggerRows) Swap(i, j int) { @@ -69,22 +100,31 @@ func (c *TriggerSchema) Compare(obj interface{}) int { return +999 } - val := misc.CompareStrings(c.get("table_name"), c2.get("table_name")) - if val != 0 { - return val - } - val = misc.CompareStrings(c.get("trigger_name"), c2.get("trigger_name")) + val := misc.CompareStrings(c.get("compare_name"), c2.get("compare_name")) return val } // Add returns SQL to create the trigger func (c TriggerSchema) Add() { - fmt.Printf("%s;\n", c.get("definition")) + // If we are comparing two different schemas against each other, we need to do some + // modification of the first trigger definition so we create it in the right schema + triggerDef := c.get("trigger_def") + schemaName := c.get("schema_name") + if dbInfo1.DbSchema != dbInfo2.DbSchema { + schemaName = dbInfo2.DbSchema + triggerDef = strings.Replace( + triggerDef, + fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), + fmt.Sprintf(" %s.%s ", schemaName, c.get("table_name")), + -1) + } + + fmt.Printf("%s;\n", triggerDef) } // Drop returns SQL to drop the trigger func (c TriggerSchema) Drop() { - fmt.Printf("DROP TRIGGER %s ON %s;\n", c.get("trigger_name"), c.get("table_name")) + fmt.Printf("DROP TRIGGER %s ON %s.%s;\n", c.get("trigger_name"), c.get("schema_name"), c.get("table_name")) } // Change handles the case where the trigger names match, but the definition does not @@ -93,35 +133,41 @@ func (c TriggerSchema) Change(obj interface{}) { if !ok { fmt.Println("Error!!!, Change needs a TriggerSchema instance", c2) } - if c.get("definition") != c2.get("definition") { - fmt.Println("-- This function looks different so we'll recreate it:") - // The definition column has everything needed to rebuild the function + if c.get("trigger_def") != c2.get("trigger_def") { + fmt.Println("-- This function looks different so we'll drop and recreate it:") + + // If we are comparing two different schemas against each other, we need to do some + // modification of the first trigger definition so we create it in the right schema + triggerDef := c.get("trigger_def") + schemaName := c.get("schema_name") + if dbInfo1.DbSchema != dbInfo2.DbSchema { + schemaName = dbInfo2.DbSchema + triggerDef = strings.Replace( + triggerDef, + fmt.Sprintf(" %s.%s ", c.get("schema_name"), c.get("table_name")), + fmt.Sprintf(" %s.%s ", schemaName, c.get("table_name")), + -1) + } + + // The trigger_def column has everything needed to rebuild the function + fmt.Printf("DROP TRIGGER %s ON %s.%s;\n", c.get("trigger_name"), schemaName, c.get("table_name")) fmt.Println("-- STATEMENT-BEGIN") - fmt.Println(c.get("definition")) + fmt.Printf("%s;\n", triggerDef) fmt.Println("-- STATEMENT-END") } } // compareTriggers outputs SQL to make the triggers match between DBs func compareTriggers(conn1 *sql.DB, conn2 *sql.DB) { - sql := ` - SELECT tbl.nspname || '.' || tbl.relname AS table_name - , t.tgname AS trigger_name - , pg_catalog.pg_get_triggerdef(t.oid, true) AS definition - , t.tgenabled AS enabled - FROM pg_catalog.pg_trigger t - INNER JOIN ( - SELECT c.oid, n.nspname, c.relname - FROM pg_catalog.pg_class c - JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace AND n.nspname NOT LIKE 'pg_%') - WHERE pg_catalog.pg_table_is_visible(c.oid)) AS tbl - ON (tbl.oid = t.tgrelid) - AND NOT t.tgisinternal - ORDER BY 1; - ` - rowChan1, _ := pgutil.QueryStrings(conn1, sql) - rowChan2, _ := pgutil.QueryStrings(conn2, sql) + buf1 := new(bytes.Buffer) + triggerSqlTemplate.Execute(buf1, dbInfo1) + + buf2 := new(bytes.Buffer) + triggerSqlTemplate.Execute(buf2, dbInfo2) + + rowChan1, _ := pgutil.QueryStrings(conn1, buf1.String()) + rowChan2, _ := pgutil.QueryStrings(conn2, buf2.String()) rows1 := make(TriggerRows, 0) for row := range rowChan1 { diff --git a/vendor/github.com/lib/pq/.gitignore b/vendor/github.com/lib/pq/.gitignore new file mode 100644 index 0000000..3243952 --- /dev/null +++ b/vendor/github.com/lib/pq/.gitignore @@ -0,0 +1,6 @@ +.db +*.test +*~ +*.swp +.idea +.vscode \ No newline at end of file diff --git a/view.go b/view.go index 4ea7a07..5954388 100644 --- a/view.go +++ b/view.go @@ -6,11 +6,13 @@ package main -import "fmt" -import "sort" -import "database/sql" -import "github.com/joncrlsn/pgutil" -import "github.com/joncrlsn/misc" +import ( + "fmt" + "sort" + "database/sql" + "github.com/joncrlsn/pgutil" + "github.com/joncrlsn/misc" +) // ================================== // ViewRows definition @@ -78,7 +80,7 @@ func (c ViewSchema) Add() { // Drop returns SQL to drop the view func (c ViewSchema) Drop() { - fmt.Printf("DROP VIEW IF EXISTS %s;\n\n", c.get("viewname")) + fmt.Printf("DROP VIEW %s;\n\n", c.get("viewname")) } // Change handles the case where the names match, but the definition does not @@ -99,7 +101,7 @@ func compareViews(conn1 *sql.DB, conn2 *sql.DB) { SELECT schemaname || '.' || viewname AS viewname , definition FROM pg_views - WHERE schemaname NOT LIKE 'pg_%' + WHERE schemaname NOT LIKE 'pg_%' AND schemaname!='infromation_schema' ORDER BY viewname; `