pgdiff 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|