pgdiff 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.rdoc +25 -0
- data/README.rdoc +54 -0
- data/Rakefile +15 -0
- data/bin/pgdiff +31 -0
- data/lib/attribute.rb +23 -0
- data/lib/database.rb +112 -0
- data/lib/diff.rb +345 -0
- data/lib/function.rb +56 -0
- data/lib/pgdiff.rb +9 -0
- data/lib/rule.rb +15 -0
- data/lib/sequence.rb +11 -0
- data/lib/table.rb +90 -0
- data/lib/trigger.rb +15 -0
- data/lib/view.rb +18 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0ed8304d8136891ddc01a5de766d293ef7b95534
|
4
|
+
data.tar.gz: 955748783f3ee852a7fe09cff074c4f2e42808c6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7907cd30ec595f41d7a8ab10faa38cf0b54cde413d9ed7e34ef6a3ec1d73037334cd82acf160c25387e0f3e117feb98e68590f293261a06c3d84dcc00a5ad8e7
|
7
|
+
data.tar.gz: aae251d848b7d31af3082850ca4e9e482577cb081dcf246bfa458aa5bae0cbb10f37cd273207ed2034066b095994bd69f20b983dd610f3fdcf868cc4bb60c6a2
|
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
= Changelog
|
2
|
+
|
3
|
+
=== 1.0.0 (January 31, 2015)
|
4
|
+
* Improve constraint checking
|
5
|
+
* Change output ordering
|
6
|
+
* Wrap up as Ruby Gem
|
7
|
+
|
8
|
+
=== 0.6.0 (July 20, 2011)
|
9
|
+
* Fix constraint checking for postgresql 9.0
|
10
|
+
|
11
|
+
=== 0.5.0 (August 12, 2010)
|
12
|
+
* Fix script due to changes in pg gem where returned rows are hash tables and not arrays.
|
13
|
+
* Fix looking up index names
|
14
|
+
|
15
|
+
=== 0.4.0 (November 21, 2009)
|
16
|
+
* Fix to work with new pg gem
|
17
|
+
|
18
|
+
=== 0.3.0 (March 30, 2009)
|
19
|
+
* Added indexes were not reported correctly.
|
20
|
+
|
21
|
+
=== 0.2.0 (April 12, 2007)
|
22
|
+
* Added ability to check differences in table field orders
|
23
|
+
|
24
|
+
=== 0.1.0 (December 7, 2005)
|
25
|
+
* Original version posted to http://www.dzone.com/snippets/pgdiff-compare-two-postgresql
|
data/README.rdoc
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
= pgdiff
|
2
|
+
|
3
|
+
== Overview
|
4
|
+
|
5
|
+
This ruby gem provides a pgdiff script that compares two PostgreSQL databases and generates SQL statements to make
|
6
|
+
their structures the same. The original version was posted at http://www.dzone.com/snippets/pgdiff-compare-two-postgresql.
|
7
|
+
|
8
|
+
The script detects differences in:
|
9
|
+
|
10
|
+
* Domains
|
11
|
+
* Schemas
|
12
|
+
* Tables
|
13
|
+
* Table field order
|
14
|
+
* Sequences
|
15
|
+
* Views
|
16
|
+
* Constraints
|
17
|
+
* Indices
|
18
|
+
* Functions
|
19
|
+
* Triggers
|
20
|
+
* Rules
|
21
|
+
|
22
|
+
Two objects with the same name are considered equal if they have the same definitions.
|
23
|
+
|
24
|
+
pgdiff does not currently compare ownership, user rights, object dependencies, table inheritance, type casts, aggregates
|
25
|
+
or operators. Patches are welcome to add this functionality.
|
26
|
+
|
27
|
+
== Installation
|
28
|
+
|
29
|
+
Install pgdiff using Ruby Gems:
|
30
|
+
|
31
|
+
gem install "pgdiff"
|
32
|
+
|
33
|
+
== Usage
|
34
|
+
|
35
|
+
To use pgdiff open a command prompt and runn the following command:
|
36
|
+
|
37
|
+
pg_diff "source_connection_string" "destination_connection_string"
|
38
|
+
|
39
|
+
The format of the two connection strings is documented in the Ruby pg gem. For more information
|
40
|
+
see http://www.rubydoc.info/gems/pg/PG/Connection:initialize
|
41
|
+
|
42
|
+
== Output
|
43
|
+
|
44
|
+
pgdiff will output the necesssary sql statements to update the source database to have the same structure
|
45
|
+
as the destination database. PLEASE VERIFY the accuracy of the generated sql statements before running them,
|
46
|
+
it is always possible the script has bugs.
|
47
|
+
|
48
|
+
== Support
|
49
|
+
|
50
|
+
If you have any questions or want to contribute to pgdiff please visit https://github.com/cfis/pgdiff.git
|
51
|
+
|
52
|
+
== License
|
53
|
+
|
54
|
+
pgdiff is provided under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rubygems/package_task'
|
5
|
+
|
6
|
+
GEM_NAME = 'pgdiff'
|
7
|
+
|
8
|
+
# Read the spec file
|
9
|
+
spec = Gem::Specification.load("#{GEM_NAME}.gemspec")
|
10
|
+
|
11
|
+
# Setup generic gem
|
12
|
+
Gem::PackageTask.new(spec) do |pkg|
|
13
|
+
pkg.package_dir = 'pkg'
|
14
|
+
pkg.need_tar = false
|
15
|
+
end
|
data/bin/pgdiff
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
|
3
|
+
# This is a simple approach to track database schema changes in PostgreSQL.
|
4
|
+
# In some way it is similar to diff program, finding out structure changes
|
5
|
+
# and results in SQL script to upgrade to new schema.
|
6
|
+
#
|
7
|
+
# Differences are tracked on schemas, domains, sequences, views, tables, indices, constraints, rules, functions, triggers.
|
8
|
+
# Two objects with the same name are considered equal if they have the same definitions.
|
9
|
+
#
|
10
|
+
# Missing features: tracking of ownership, user rights, object dependencies, table inheritance, type casts, aggregates, operators.
|
11
|
+
#
|
12
|
+
# Usage:
|
13
|
+
# ./pg_diff source_connection_string destination_connection_string
|
14
|
+
#
|
15
|
+
# The format of the connection strings is documented in the Ruby pg gem
|
16
|
+
# at http://www.rubydoc.info/gems/pg/PG/Connection:initialize.
|
17
|
+
|
18
|
+
require 'pg'
|
19
|
+
require 'pgdiff'
|
20
|
+
|
21
|
+
if ARGV.length != 2
|
22
|
+
raise(ArgumentError, "You must specify two arguments - a source and target db connection string. For " +
|
23
|
+
"more information about how to format connection strings see http://www.rubydoc.info/gems/pg/PG/Connection:initialize")
|
24
|
+
end
|
25
|
+
|
26
|
+
source_db = ARGV[0]
|
27
|
+
target_db = ARGV[1]
|
28
|
+
|
29
|
+
diff = PgDiff::Diff.new(ARGV[0], ARGV[1])
|
30
|
+
diff.run_compare
|
31
|
+
puts diff.output
|
data/lib/attribute.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Attribute
|
3
|
+
attr_accessor :name, :type_def, :notnull, :default
|
4
|
+
|
5
|
+
def initialize(name, typedef, notnull, default)
|
6
|
+
@name = name
|
7
|
+
@type_def = typedef
|
8
|
+
@notnull = notnull
|
9
|
+
@default = default
|
10
|
+
end
|
11
|
+
|
12
|
+
def definition
|
13
|
+
out = [' ', @name, @type_def]
|
14
|
+
out << 'NOT NULL' if @notnull
|
15
|
+
out << 'DEFAULT ' + @default if @default
|
16
|
+
out.join(" ")
|
17
|
+
end
|
18
|
+
|
19
|
+
def == (other)
|
20
|
+
definition == other.definition
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/database.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Database
|
3
|
+
attr_accessor :tables, :views, :sequences, :schemas, :domains, :rules, :functions, :triggers
|
4
|
+
|
5
|
+
def initialize(conn)
|
6
|
+
cls_query = <<-EOT
|
7
|
+
SELECT n.nspname, c.relname, c.relkind
|
8
|
+
FROM pg_catalog.pg_class c
|
9
|
+
LEFT JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner
|
10
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
11
|
+
WHERE c.relkind IN ('r','S','v')
|
12
|
+
AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema')
|
13
|
+
ORDER BY 1,2;
|
14
|
+
EOT
|
15
|
+
@views = {}
|
16
|
+
@tables = {}
|
17
|
+
@sequences = {}
|
18
|
+
@schemas = {}
|
19
|
+
@domains = {}
|
20
|
+
@functions = {}
|
21
|
+
@rules = {}
|
22
|
+
@triggers = {}
|
23
|
+
|
24
|
+
conn.query(cls_query).each do |tuple|
|
25
|
+
schema = tuple['nspname']
|
26
|
+
relname = tuple['relname']
|
27
|
+
relkind = tuple['relkind']
|
28
|
+
case relkind
|
29
|
+
when 'r'
|
30
|
+
@tables["#{schema}.#{relname}"] = Table.new(conn, schema, relname)
|
31
|
+
when 'v'
|
32
|
+
@views["#{schema}.#{relname}"] = View.new(conn, schema, relname)
|
33
|
+
when 'S'
|
34
|
+
@sequences["#{schema}.#{relname}"] = Sequence.new(conn, schema, relname)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
domain_qry = <<-EOT
|
39
|
+
SELECT n.nspname, t.typname, pg_catalog.format_type(t.typbasetype, t.typtypmod) || ' ' ||
|
40
|
+
CASE WHEN t.typnotnull AND t.typdefault IS NOT NULL THEN 'not null default '||t.typdefault
|
41
|
+
WHEN t.typnotnull AND t.typdefault IS NULL THEN 'not null'
|
42
|
+
WHEN NOT t.typnotnull AND t.typdefault IS NOT NULL THEN 'default '|| t.typdefault
|
43
|
+
ELSE ''
|
44
|
+
END AS def
|
45
|
+
FROM pg_catalog.pg_type t
|
46
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
47
|
+
WHERE t.typtype = 'd'
|
48
|
+
ORDER BY 1, 2
|
49
|
+
EOT
|
50
|
+
conn.query(domain_qry).each do |tuple|
|
51
|
+
schema = tuple['nspname']
|
52
|
+
typename = tuple['typname']
|
53
|
+
value = tuple['def']
|
54
|
+
@domains["#{schema}.#{typename}"] = value
|
55
|
+
end
|
56
|
+
|
57
|
+
schema_qry = <<-EOT
|
58
|
+
select nspname from pg_namespace
|
59
|
+
EOT
|
60
|
+
conn.query(schema_qry).each do |tuple|
|
61
|
+
schema = tuple['nspname']
|
62
|
+
@schemas[schema] = schema
|
63
|
+
end
|
64
|
+
|
65
|
+
func_query = <<-EOT
|
66
|
+
SELECT proname AS function_name
|
67
|
+
, nspname AS namespace
|
68
|
+
, lanname AS language_name
|
69
|
+
, pg_catalog.obj_description(pg_proc.oid, 'pg_proc') AS comment
|
70
|
+
, proargtypes AS function_args
|
71
|
+
, proargnames AS function_arg_names
|
72
|
+
, prosrc AS source_code
|
73
|
+
, proretset AS returns_set
|
74
|
+
, prorettype AS return_type,
|
75
|
+
provolatile, proisstrict, prosecdef
|
76
|
+
FROM pg_catalog.pg_proc
|
77
|
+
JOIN pg_catalog.pg_language ON (pg_language.oid = prolang)
|
78
|
+
JOIN pg_catalog.pg_namespace ON (pronamespace = pg_namespace.oid)
|
79
|
+
JOIN pg_catalog.pg_type ON (prorettype = pg_type.oid)
|
80
|
+
WHERE pg_namespace.nspname !~ 'pg_catalog|information_schema'
|
81
|
+
AND proname != 'plpgsql_call_handler'
|
82
|
+
AND proname != 'plpgsql_validator'
|
83
|
+
EOT
|
84
|
+
|
85
|
+
conn.exec(func_query).each_with_index do |tuple, i|
|
86
|
+
func = Function.new(conn, tuple)
|
87
|
+
@functions[func.signature] = func
|
88
|
+
end
|
89
|
+
|
90
|
+
rule_query = <<-EOT
|
91
|
+
select schemaname || '.' || tablename || '.' || rulename as rule_name,
|
92
|
+
schemaname || '.' || tablename as tab_name,
|
93
|
+
rulename, definition
|
94
|
+
from pg_rules
|
95
|
+
where schemaname !~ 'pg_catalog|information_schema'
|
96
|
+
EOT
|
97
|
+
conn.exec(rule_query).each do |tuple|
|
98
|
+
@rules[tuple['rule_name']] = Rule.new(tuple['tab_name'], tuple['rulename'], tuple['definition'])
|
99
|
+
end
|
100
|
+
|
101
|
+
trigger_query = <<-EOT
|
102
|
+
select nspname || '.' || relname as tgtable, tgname, pg_get_triggerdef(t.oid) as tg_def
|
103
|
+
from pg_trigger t join pg_class c ON (tgrelid = c.oid ) JOIN pg_namespace n ON (c.relnamespace = n.oid)
|
104
|
+
where not tgisinternal
|
105
|
+
and nspname !~ 'pg_catalog|information_schema'
|
106
|
+
EOT
|
107
|
+
conn.exec(trigger_query).each do |tuple|
|
108
|
+
@triggers[tuple['tgtable'] + "." + tuple['tgname']] = Trigger.new(tuple['tgtable'], tuple['tgname'], tuple['tg_def'])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/diff.rb
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Diff
|
3
|
+
def initialize(old_db_spec, new_db_spec)
|
4
|
+
@old_conn = PGconn.new(old_db_spec)
|
5
|
+
@new_conn = PGconn.new(new_db_spec)
|
6
|
+
@sections = [
|
7
|
+
:domains_drop,
|
8
|
+
:domains_create,
|
9
|
+
:schemas_drop,
|
10
|
+
:schemas_create,
|
11
|
+
:tables_drop,
|
12
|
+
:tables_change,
|
13
|
+
:tables_create,
|
14
|
+
:sequences_drop,
|
15
|
+
:sequences_create,
|
16
|
+
:views_drop,
|
17
|
+
:views_create,
|
18
|
+
:constraints_drop,
|
19
|
+
:constraints_change,
|
20
|
+
:constraints_create,
|
21
|
+
:indices_drop,
|
22
|
+
:indices_create,
|
23
|
+
:functions_drop,
|
24
|
+
:functions_create ,
|
25
|
+
:triggers_drop,
|
26
|
+
:triggers_create ,
|
27
|
+
:rules_drop,
|
28
|
+
:rules_create
|
29
|
+
]
|
30
|
+
@script = {}
|
31
|
+
@sections.each {|s| @script[s] = []}
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_compare
|
35
|
+
@old_database = Database.new(@old_conn)
|
36
|
+
@new_database = Database.new(@new_conn)
|
37
|
+
compare_schemas
|
38
|
+
compare_domains
|
39
|
+
compare_sequences
|
40
|
+
compare_triggers_drop
|
41
|
+
compare_rules_drop
|
42
|
+
compare_views_drop
|
43
|
+
compare_table_attrs
|
44
|
+
compare_views_create
|
45
|
+
compare_functions
|
46
|
+
compare_rules_create
|
47
|
+
compare_triggers_create
|
48
|
+
compare_table_constraints
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_script(section, statement)
|
52
|
+
@script[section] << statement
|
53
|
+
end
|
54
|
+
|
55
|
+
def compare_schemas
|
56
|
+
@old_database.schemas.keys.each do |name|
|
57
|
+
add_script(:schemas_drop , "DROP SCHEMA #{name};") unless @new_database.schemas.has_key?(name)
|
58
|
+
end
|
59
|
+
@new_database.schemas.keys.each do |name|
|
60
|
+
add_script(:schemas_create , "CREATE SCHEMA #{name};") unless @old_database.schemas.has_key?(name)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def compare_domains
|
65
|
+
@old_database.domains.keys.each do |name|
|
66
|
+
add_script(:domains_drop , "DROP DOMAIN #{name} CASCADE;") unless @new_database.domains.has_key?(name)
|
67
|
+
end
|
68
|
+
@new_database.domains.each do |name, df|
|
69
|
+
add_script(:domains_create , "CREATE DOMAIN #{name} AS #{df};") unless @old_database.domains.has_key?(name)
|
70
|
+
old_domain = @old_database.domains[name]
|
71
|
+
if old_domain && old_domain != df
|
72
|
+
add_script(:domains_drop, "DROP DOMAIN #{name} CASCADE;")
|
73
|
+
add_script(:domains_create, "-- [changed domain] :")
|
74
|
+
add_script(:domains_create, "-- OLD: #{old_domain}")
|
75
|
+
add_script(:domains_create, "CREATE DOMAIN #{name} AS #{df};")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def compare_sequences
|
81
|
+
@old_database.sequences.keys.each do |name|
|
82
|
+
add_script(:sequences_drop , "DROP SEQUENCE #{name} CASCADE;") unless @new_database.sequences.has_key?(name)
|
83
|
+
end
|
84
|
+
@new_database.sequences.keys.each do |name|
|
85
|
+
add_script(:sequences_create , "CREATE SEQUENCE #{name};") unless @old_database.sequences.has_key?(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def compare_functions
|
90
|
+
@old_database.functions.keys.each do |name|
|
91
|
+
add_script(:functions_drop , "DROP FUNCTION #{name} CASCADE;") unless @new_database.functions.has_key?(name)
|
92
|
+
end
|
93
|
+
@new_database.functions.each do |name, func|
|
94
|
+
add_script(:functions_create , func.definition) unless @old_database.functions.has_key?(name)
|
95
|
+
old_function = @old_database.functions[name]
|
96
|
+
if old_function && old_function.definition != func.definition
|
97
|
+
add_script(:functions_create , '-- [changed function] :')
|
98
|
+
add_script(:functions_create , '-- OLD :')
|
99
|
+
add_script(:functions_create , old_function.definition.gsub(/^/, "--> ") )
|
100
|
+
add_script(:functions_create , func.definition)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def compare_rules_drop
|
106
|
+
@old_database.rules.each do |name, rule|
|
107
|
+
add_script(:rules_drop , "DROP RULE #{rule.name} ON #{rule.table_name} CASCADE;") unless @new_database.rules.has_key?(name)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def compare_rules_create
|
112
|
+
@new_database.rules.each do |name, rule|
|
113
|
+
add_script(:rules_create , rule.definition) unless @old_database.rules.has_key?(name)
|
114
|
+
old_rule = @old_database.rules[name]
|
115
|
+
if old_rule && old_rule != rule
|
116
|
+
add_script(:rules_drop , "DROP RULE #{rule.name} ON #{rule.table_name} CASCADE;")
|
117
|
+
add_script(:rules_create , "-- [changed rule] :")
|
118
|
+
add_script(:rules_create , "-- OLD: #{old_rule.definition}")
|
119
|
+
add_script(:rules_create , rule.definition )
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def compare_triggers_drop
|
125
|
+
@old_database.triggers.each do |name, trigger|
|
126
|
+
add_script(:triggers_drop , "DROP trigger #{trigger.name} ON #{trigger.table_name} CASCADE;") unless @new_database.triggers.has_key?(name)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def compare_triggers_create
|
131
|
+
@new_database.triggers.each do |name, trigger|
|
132
|
+
add_script(:triggers_create , trigger.definition) unless @old_database.triggers.has_key?(name)
|
133
|
+
old_trigger = @old_database.triggers[name]
|
134
|
+
if old_trigger && old_trigger != trigger
|
135
|
+
add_script(:triggers_drop , "DROP trigger #{trigger.name} ON #{trigger.table_name} CASCADE;")
|
136
|
+
add_script(:triggers_create , "-- [changed trigger] :")
|
137
|
+
add_script(:triggers_create , "-- OLD #{old_trigger.definition}")
|
138
|
+
add_script(:triggers_create , trigger.definition)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def compare_views_drop
|
144
|
+
@old_database.views.keys.each do |name|
|
145
|
+
add_script(:views_drop , "DROP VIEW #{name};") unless @new_database.views.has_key?(name)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def compare_views_create
|
150
|
+
@new_database.views.each do |name, df|
|
151
|
+
add_script(:views_create , df.definition) unless @old_database.views.has_key?(name)
|
152
|
+
old_view = @old_database.views[name]
|
153
|
+
if old_view && df.definition != old_view.definition
|
154
|
+
add_script(:views_drop , "DROP VIEW #{name};")
|
155
|
+
add_script(:views_create , "-- [changed view] :")
|
156
|
+
add_script(:views_create , "-- #{old_view.definition.gsub(/\n/, ' ')}")
|
157
|
+
add_script(:views_create , df.definition)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def compare_table_attrs
|
163
|
+
@old_database.tables.each do |name, table|
|
164
|
+
add_script(:tables_drop, "DROP TABLE #{name} CASCADE;") unless @new_database.tables.has_key?(name)
|
165
|
+
end
|
166
|
+
@to_compare = []
|
167
|
+
@new_database.tables.each do |name, table|
|
168
|
+
unless @old_database.tables.has_key?(name)
|
169
|
+
add_script(:tables_create , table.table_creation)
|
170
|
+
add_script(:indices_create , table.index_creation) unless table.indexes.empty?
|
171
|
+
@to_compare << name
|
172
|
+
else
|
173
|
+
diff_attributes(@old_database.tables[name], table)
|
174
|
+
diff_indexes(@old_database.tables[name], table)
|
175
|
+
@to_compare << name
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def compare_table_constraints
|
181
|
+
@c_check = []
|
182
|
+
@c_primary = []
|
183
|
+
@c_unique = []
|
184
|
+
@c_foreign = []
|
185
|
+
@to_compare.each do |name|
|
186
|
+
if @old_database.tables[name]
|
187
|
+
diff_constraints(@old_database.tables[name], @new_database.tables[name])
|
188
|
+
else
|
189
|
+
@new_database.tables[name].constraints.each do |cname, cdef|
|
190
|
+
add_cnstr(name, cname, cdef)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
@script[:constraints_create] += @c_check
|
195
|
+
@script[:constraints_create] += @c_primary
|
196
|
+
@script[:constraints_create] += @c_unique
|
197
|
+
@script[:constraints_create] += @c_foreign
|
198
|
+
end
|
199
|
+
|
200
|
+
def output
|
201
|
+
out = []
|
202
|
+
@sections.each do |sect|
|
203
|
+
if @script[sect].empty?
|
204
|
+
out << "-- [SKIP SECTION : #{sect.to_s.upcase}] : no changes\n"
|
205
|
+
else
|
206
|
+
out << "-- [START SECTION : #{sect.to_s.upcase}]"
|
207
|
+
out += @script[sect]
|
208
|
+
out << "-- [END SECTION : #{sect.to_s.upcase}]\n"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
out.join("\n")
|
212
|
+
end
|
213
|
+
|
214
|
+
def diff_attributes(old_table, new_table)
|
215
|
+
dropped = []
|
216
|
+
added = []
|
217
|
+
changed = []
|
218
|
+
|
219
|
+
order = []
|
220
|
+
old_table.attributes.keys.each do |attname|
|
221
|
+
if new_table.has_attribute?(attname)
|
222
|
+
changed << attname if old_table.attributes[attname] != new_table.attributes[attname]
|
223
|
+
else
|
224
|
+
dropped << attname
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
old_table.attributes.keys.each do |attname|
|
229
|
+
if new_table.has_attribute?(attname)
|
230
|
+
old_index = old_table.attribute_index(attname)
|
231
|
+
new_index = new_table.attribute_index(attname)
|
232
|
+
order << attname if old_index != new_index
|
233
|
+
end
|
234
|
+
end
|
235
|
+
new_table.attributes.keys.each do |attname|
|
236
|
+
added << attname unless old_table.has_attribute?(attname)
|
237
|
+
end
|
238
|
+
add_script(:tables_change , "-- [#{old_table.name}] dropped attributes") unless dropped.empty?
|
239
|
+
dropped.each do |attname|
|
240
|
+
add_script(:tables_change , "ALTER TABLE #{old_table.name} DROP COLUMN #{attname} CASCADE;")
|
241
|
+
end
|
242
|
+
add_script(:tables_change , "-- [#{old_table.name}] added attributes") unless added.empty?
|
243
|
+
added.each do |attname|
|
244
|
+
add_script(:tables_change , "ALTER TABLE #{old_table.name} ADD COLUMN #{new_table.attributes[attname].definition};")
|
245
|
+
end
|
246
|
+
add_script(:tables_change , "-- [#{old_table.name}] changed attributes") unless changed.empty?
|
247
|
+
changed.each do |attname|
|
248
|
+
old_att = old_table.attributes[attname]
|
249
|
+
new_att = new_table.attributes[attname]
|
250
|
+
add_script(:tables_change , "-- attribute: #{attname}")
|
251
|
+
add_script(:tables_change , "-- OLD : #{old_att.definition}")
|
252
|
+
add_script(:tables_change , "-- NEW : #{new_att.definition}")
|
253
|
+
if old_att.type_def != new_att.type_def
|
254
|
+
add_script(:tables_change , "ALTER TABLE #{old_table.name} ALTER COLUMN #{attname} TYPE #{new_att.type_def};")
|
255
|
+
end
|
256
|
+
if old_att.default != new_att.default
|
257
|
+
if new_att.default.nil?
|
258
|
+
add_script(:tables_change , "ALTER TABLE #{old_table.name} ALTER COLUMN #{attname} DROP DEFAULT;")
|
259
|
+
else
|
260
|
+
add_script(:tables_change , "ALTER TABLE #{old_table.name} ALTER COLUMN #{attname} SET DEFAULT #{new_att.default};")
|
261
|
+
end
|
262
|
+
end
|
263
|
+
if old_att.notnull != new_att.notnull
|
264
|
+
add_script(:tables_change , "ALTER TABLE #{old_table.name} ALTER COLUMN #{attname} #{new_att.notnull ? 'SET' : 'DROP'} NOT NULL;")
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
add_script(:tables_change , "-- [#{old_table.name}] attribute order changed") unless order.empty?
|
269
|
+
order.each do |attname|
|
270
|
+
add_script(:tables_change , " #{attname}. Old index: #{old_table.attribute_index(attname)}, New index: #{new_table.attribute_index(attname)}")
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def diff_constraints(old_table, new_table)
|
275
|
+
dropped = []
|
276
|
+
added = []
|
277
|
+
changed = []
|
278
|
+
|
279
|
+
old_table.constraints.keys.each do |conname|
|
280
|
+
if new_table.has_constraint?(conname)
|
281
|
+
if old_table.constraints[conname] != new_table.constraints[conname]
|
282
|
+
changed << conname
|
283
|
+
end
|
284
|
+
else
|
285
|
+
dropped << conname
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
new_table.constraints.keys.each do |conname|
|
290
|
+
added << conname unless old_table.has_constraint?(conname)
|
291
|
+
end
|
292
|
+
|
293
|
+
dropped.each do |name|
|
294
|
+
add_script(:constraints_drop , "ALTER TABLE #{old_table.name} DROP CONSTRAINT #{name};")
|
295
|
+
end
|
296
|
+
|
297
|
+
added.each do |name|
|
298
|
+
add_cnstr(old_table.name, name, new_table.constraints[name])
|
299
|
+
end
|
300
|
+
|
301
|
+
changed.each do |name|
|
302
|
+
add_script(:constraints_change,
|
303
|
+
"-- Previous: #{old_table.constraints[name]}\n" +
|
304
|
+
"ALTER TABLE #{old_table.name} DROP CONSTRAINT #{name};\n" +
|
305
|
+
"ALTER TABLE #{new_table.name} ADD CONSTRAINT #{name} #{new_table.constraints[name]};\n")
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def add_cnstr(tablename, cnstrname, cnstrdef)
|
310
|
+
c_string = "ALTER TABLE #{tablename} ADD CONSTRAINT #{cnstrname} #{cnstrdef} ;"
|
311
|
+
case cnstrdef
|
312
|
+
when /^CHECK / then @c_check << c_string
|
313
|
+
when /^PRIMARY / then @c_primary << c_string
|
314
|
+
when /^FOREIGN / then @c_foreign << c_string
|
315
|
+
when /^UNIQUE / then @c_unique << c_string
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def diff_indexes(old_table, new_table)
|
320
|
+
dropped = []
|
321
|
+
added = []
|
322
|
+
|
323
|
+
old_table.indexes.keys.each do |name|
|
324
|
+
if new_table.has_index?(name)
|
325
|
+
if old_table.indexes[name] != new_table.indexes[name]
|
326
|
+
dropped << name
|
327
|
+
added << name
|
328
|
+
end
|
329
|
+
else
|
330
|
+
dropped << name
|
331
|
+
end
|
332
|
+
end
|
333
|
+
new_table.indexes.keys.each do |name|
|
334
|
+
added << name unless old_table.has_index?(name)
|
335
|
+
end
|
336
|
+
|
337
|
+
dropped.each do |name|
|
338
|
+
add_script(:indices_drop , "DROP INDEX #{name};")
|
339
|
+
end
|
340
|
+
added.each do |name|
|
341
|
+
add_script(:indices_create , (new_table.indexes[name] + ";")) if new_table.indexes[name]
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
data/lib/function.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Function
|
3
|
+
def initialize(conn, tuple)
|
4
|
+
@name = tuple['namespace'] + "." + tuple['function_name']
|
5
|
+
@language = tuple['language_name']
|
6
|
+
@src = tuple['source_code']
|
7
|
+
@returns_set = tuple['returns_set']
|
8
|
+
@return_type = format_type(conn, tuple['return_type'])
|
9
|
+
@tipes = tuple['function_args'].split(" ")
|
10
|
+
if tuple['function_arg_names'] && tuple['function_arg_names'] =~ /^\{(.*)\}$/
|
11
|
+
@arnames = $1.split(',')
|
12
|
+
elsif tuple['function_arg_names'].is_a? Array # my version of ruby-postgres
|
13
|
+
@arnames = tuple['function_arg_names']
|
14
|
+
else
|
15
|
+
@arnames = [""] * @tipes.length
|
16
|
+
end
|
17
|
+
alist = []
|
18
|
+
@tipes.each_with_index do |typ,idx|
|
19
|
+
alist << (@arnames[idx] + " " + format_type(conn, typ))
|
20
|
+
end
|
21
|
+
@arglist = alist.join(" , ")
|
22
|
+
@strict = tuple['proisstrict'] ? ' STRICT' : ''
|
23
|
+
@secdef = tuple['prosecdef'] ? ' SECURITY DEFINER' : ''
|
24
|
+
@volatile = case tuple['provolatile']
|
25
|
+
when 'i' then ' IMMUTABLE'
|
26
|
+
when 's' then ' STABLE'
|
27
|
+
else ''
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def signature
|
32
|
+
"#{@name}(#{@arglist})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def definition
|
36
|
+
<<-EOT
|
37
|
+
CREATE OR REPLACE FUNCTION #{@name} (#{@arglist}) RETURNS #{@returns_set ? 'SETOF' : ''} #{@return_type} AS $_$#{@src}$_$ LANGUAGE '#{@language}' #{@volatile}#{@strict}#{@secdef};
|
38
|
+
EOT
|
39
|
+
end
|
40
|
+
|
41
|
+
def == (other)
|
42
|
+
definition == other.definition
|
43
|
+
end
|
44
|
+
|
45
|
+
def format_type(conn, oid)
|
46
|
+
t_query = <<-EOT
|
47
|
+
SELECT pg_catalog.format_type(pg_type.oid, typtypmod) AS type_name
|
48
|
+
FROM pg_catalog.pg_type
|
49
|
+
JOIN pg_catalog.pg_namespace ON (pg_namespace.oid = typnamespace)
|
50
|
+
WHERE pg_type.oid =
|
51
|
+
EOT
|
52
|
+
tuple = conn.query(t_query + oid.to_s).first
|
53
|
+
tuple['type_name']
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/pgdiff.rb
ADDED
data/lib/rule.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Rule
|
3
|
+
attr_reader :table_name, :name, :definition
|
4
|
+
|
5
|
+
def initialize(table_name, name, df)
|
6
|
+
@table_name = table_name
|
7
|
+
@name = name
|
8
|
+
@definition = df
|
9
|
+
end
|
10
|
+
|
11
|
+
def == (other)
|
12
|
+
other.definition == definition
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/sequence.rb
ADDED
data/lib/table.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Table
|
3
|
+
attr_accessor :table_name, :schema, :attributes, :constraints, :indexes
|
4
|
+
|
5
|
+
def initialize(conn, schema, table_name)
|
6
|
+
@schema = schema
|
7
|
+
@table_name = table_name
|
8
|
+
@attributes = {}
|
9
|
+
@constraints = {}
|
10
|
+
@indexes = {}
|
11
|
+
@atlist = []
|
12
|
+
|
13
|
+
att_query = <<-EOT
|
14
|
+
select attname, format_type(atttypid, atttypmod) as a_type, attnotnull, pg_get_expr(adbin, attrelid) as a_default
|
15
|
+
from pg_attribute left join pg_attrdef on (adrelid = attrelid and adnum = attnum)
|
16
|
+
where attrelid = '#{schema}.#{table_name}'::regclass and not attisdropped and attnum > 0
|
17
|
+
order by attnum
|
18
|
+
EOT
|
19
|
+
conn.query(att_query).each do |tuple|
|
20
|
+
attname = tuple['attname']
|
21
|
+
typedef = tuple['a_type']
|
22
|
+
notnull = tuple['attnotnull']
|
23
|
+
default = tuple['a_default']
|
24
|
+
@attributes[attname] = Attribute.new(attname, typedef, notnull, default)
|
25
|
+
@atlist << attname
|
26
|
+
end
|
27
|
+
|
28
|
+
ind_query = <<-EOT
|
29
|
+
select indexrelid::regclass as indname, pg_get_indexdef(indexrelid) as def
|
30
|
+
from pg_index where indrelid = '#{schema}.#{table_name}'::regclass and not indisprimary
|
31
|
+
EOT
|
32
|
+
conn.query(ind_query).each do |tuple|
|
33
|
+
name = tuple['indname']
|
34
|
+
value = tuple['def']
|
35
|
+
@indexes[name] = value
|
36
|
+
end
|
37
|
+
|
38
|
+
cons_query = <<-EOT
|
39
|
+
select conname, pg_get_constraintdef(oid) from pg_constraint where conrelid = '#{schema}.#{table_name}'::regclass
|
40
|
+
EOT
|
41
|
+
conn.query(cons_query).each do |tuple|
|
42
|
+
name = tuple['conname']
|
43
|
+
value = tuple['pg_get_constraintdef']
|
44
|
+
@constraints[name] = value
|
45
|
+
end
|
46
|
+
@constraints.keys.each do |cname|
|
47
|
+
@indexes.delete("#{schema}.#{cname}") if has_index?(cname)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def has_attribute?(name)
|
52
|
+
@attributes.has_key?(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def attribute_index(name)
|
56
|
+
@atlist.index(name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_index?(name)
|
60
|
+
@indexes.has_key?(name) || @indexes.has_key?("#{schema}.#{name}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_constraint?(name)
|
64
|
+
@constraints.has_key?(name)
|
65
|
+
end
|
66
|
+
|
67
|
+
def table_creation
|
68
|
+
out = ["CREATE TABLE #{name} ("]
|
69
|
+
stmt = []
|
70
|
+
@atlist.each do |attname|
|
71
|
+
stmt << @attributes[attname].definition
|
72
|
+
end
|
73
|
+
out << stmt.join(",\n")
|
74
|
+
out << ");"
|
75
|
+
out.join("\n")
|
76
|
+
end
|
77
|
+
|
78
|
+
def name
|
79
|
+
"#{schema}.#{table_name}"
|
80
|
+
end
|
81
|
+
|
82
|
+
def index_creation
|
83
|
+
out = []
|
84
|
+
@indexes.values.each do |c|
|
85
|
+
out << (c+";")
|
86
|
+
end
|
87
|
+
out.join("\n")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/trigger.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class Trigger
|
3
|
+
attr_reader :table_name, :name, :definition
|
4
|
+
|
5
|
+
def initialize(table_name, name, df)
|
6
|
+
@table_name = table_name
|
7
|
+
@name = name
|
8
|
+
@definition = df + ";"
|
9
|
+
end
|
10
|
+
|
11
|
+
def == (other)
|
12
|
+
other.definition == definition
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/view.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module PgDiff
|
2
|
+
class View
|
3
|
+
attr_reader :def, :name
|
4
|
+
|
5
|
+
def initialize(conn, sch, relname)
|
6
|
+
@name = "#{sch}.#{relname}"
|
7
|
+
view_qery = <<-EOT
|
8
|
+
SELECT pg_catalog.pg_get_viewdef('#{@name}'::regclass, true)
|
9
|
+
EOT
|
10
|
+
tuple = conn.query(view_qery).first
|
11
|
+
@def = tuple['pg_get_viewdef']
|
12
|
+
end
|
13
|
+
|
14
|
+
def definition
|
15
|
+
"CREATE VIEW #{@name} AS #{@def}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pgdiff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Charlie Savage
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-01-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pg
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.17.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.17.0
|
27
|
+
description: |
|
28
|
+
Compares two PostgreSQL databases and generates the SQL statements needed to make their structure the same.
|
29
|
+
email:
|
30
|
+
executables:
|
31
|
+
- pgdiff
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- CHANGELOG.rdoc
|
36
|
+
- README.rdoc
|
37
|
+
- Rakefile
|
38
|
+
- bin/pgdiff
|
39
|
+
- lib/attribute.rb
|
40
|
+
- lib/database.rb
|
41
|
+
- lib/diff.rb
|
42
|
+
- lib/function.rb
|
43
|
+
- lib/pgdiff.rb
|
44
|
+
- lib/rule.rb
|
45
|
+
- lib/sequence.rb
|
46
|
+
- lib/table.rb
|
47
|
+
- lib/trigger.rb
|
48
|
+
- lib/view.rb
|
49
|
+
homepage: https://github.com/cfis/pgdiff.git
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
metadata: {}
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.9.3
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 2.4.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: Provides a ruby script that compares two PostgreSQL databases and generates
|
73
|
+
the SQL statements needed to make their structure the same. The original version
|
74
|
+
was posted at http://www.dzone.com/snippets/pgdiff-compare-two-postgresql.
|
75
|
+
test_files: []
|