pg_examiner 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3febec877bf8581fc25e2d473161b5c49a7342a0
4
+ data.tar.gz: cc1fb74e5cec4fa2423f57fb9c39c75e1446f31a
5
+ SHA512:
6
+ metadata.gz: 57219283bfba79a7768afb659f7b0c245d3eecf7277454570d9e3a70c92c9e3f34626a07732566ab33eed49dd0b58650a66da335449a62eda31cea94e38a8e5e
7
+ data.tar.gz: bbe34e55e39676b49dcbdc68f7f434ebd70c4186f11f67ac909b89de05fef68386dc5a0987e20f0885153aad236394f729873cc1860314653c65066c10f9004d
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pg_examiner.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Chris Hanks
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # PGExaminer
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pg_examiner'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install pg_examiner
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/pg_examiner/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+
data/TODO.txt ADDED
@@ -0,0 +1,133 @@
1
+ (Notes on other system tables and columns that seem useful)
2
+
3
+
4
+
5
+
6
+
7
+ pg_aggregate (http://www.postgresql.org/docs/9.3/static/catalog-pg-aggregate.html)
8
+ aggfnoid
9
+ aggtransfn
10
+ aggfinalfn
11
+ aggsortopaggtranstype
12
+ agginitval
13
+
14
+ pg_attribute: (http://www.postgresql.org/docs/9.3/static/catalog-pg-attribute.html)
15
+ attstorage for toast storage configuration
16
+ attinhcount: to do with inheritance?
17
+ attcollation: defined collation for a column
18
+ attoptions:
19
+ In a dropped column's pg_attribute entry, atttypid is reset to zero, but attlen and the other fields copied from pg_type are still valid. This arrangement is needed to cope with the situation where the dropped column's data type was later dropped, and so there is no pg_type row anymore. attlen and the other fields can be used to interpret the contents of a row of the table.
20
+
21
+ pg_class: (http://www.postgresql.org/docs/9.3/static/catalog-pg-class.html)
22
+ relam: index type
23
+ relkind: table/index/view/sequence/matview/composite type/...
24
+
25
+ pg_constraint:
26
+ connamespace: schema oid?
27
+ contype: check/foreign key/primary key/exclusion/unique/trigger
28
+ condeferrable:
29
+ condeferred:
30
+ contypid: constraint on domain?
31
+ conindid:
32
+ confrelid: which table a foreign key is on
33
+ confmatchtype: MATCH TYPE
34
+ conkey
35
+ confkey
36
+ conexclop
37
+
38
+ pg_description (http://www.postgresql.org/docs/9.3/static/catalog-pg-description.html)
39
+ objoid
40
+ classoid
41
+ objsubid
42
+ description
43
+
44
+ pg_enum (http://www.postgresql.org/docs/9.3/static/catalog-pg-enum.html)
45
+ enumtypid
46
+ enumsortorder (don't care about exact values, just order)
47
+ enumlabel
48
+
49
+ pg_event_trigger (http://www.postgresql.org/docs/9.3/static/catalog-pg-event-trigger.html)
50
+ evtname
51
+ evtevent
52
+ evtfoid
53
+
54
+ pg_index (http://www.postgresql.org/docs/9.3/static/catalog-pg-index.html)
55
+ indisexclusion
56
+ indimmediate
57
+ indisclustered
58
+ indisvalid?
59
+ indisready?
60
+ indislive?
61
+ indnatts?
62
+
63
+ pg_inherits (http://www.postgresql.org/docs/9.3/static/catalog-pg-inherits.html)
64
+ inhrelid
65
+ inhparent
66
+ inhseqno
67
+
68
+ pg_language (http://www.postgresql.org/docs/9.3/static/catalog-pg-language.html)
69
+ lanname
70
+
71
+ pg_proc (http://www.postgresql.org/docs/9.3/static/catalog-pg-proc.html)
72
+ proname
73
+ pronamespace
74
+ procost
75
+ prorows?
76
+ provariadic
77
+ proisagg
78
+ proiswindow
79
+ proleakproof?
80
+ proisstrict
81
+ proretset
82
+ provolatile
83
+ pronargs
84
+ pronargdefaults
85
+ prorettype
86
+ proargtypes
87
+ proallargtypes
88
+ proargmodes
89
+ proargnames
90
+ nodeToString(proargdefaults)
91
+
92
+ pg_trigger (http://www.postgresql.org/docs/9.3/static/catalog-pg-trigger.html)
93
+ tgrelid
94
+ tgname
95
+ tgfoid
96
+ tgtype
97
+ tgisinternal
98
+ tgconstrrelid
99
+ togconstrindid
100
+ tgconstraint
101
+ tgdeferrable
102
+ tginitdeferred
103
+ tgnargs
104
+ tgattr
105
+ tgargs
106
+ tgqual (nodeToString())
107
+
108
+ pg_type (http://www.postgresql.org/docs/9.3/static/catalog-pg-type.html)
109
+ typname
110
+ typnamespace
111
+ typlen
112
+ typbyval
113
+ typtype
114
+ typdelim
115
+ typrelid
116
+ typelem
117
+ typarray
118
+ typalign
119
+ typstorage
120
+ typnotnull
121
+ typbasetype
122
+ typndims
123
+ typdefault
124
+
125
+ pg_views (http://www.postgresql.org/docs/9.3/static/view-pg-views.html)
126
+ schemaname
127
+ viewname
128
+ definition? or can get all this from pg_class?
129
+
130
+ pg_matviews (http://www.postgresql.org/docs/9.3/static/view-pg-matviews.html)
131
+ schemaname
132
+ matviewname
133
+ definition
@@ -0,0 +1,28 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Base
4
+ attr_reader :result, :row, :parent
5
+
6
+ def initialize(result, row, parent = nil)
7
+ @result, @row, @parent = result, row, parent
8
+ end
9
+
10
+ def oid
11
+ @row['oid']
12
+ end
13
+
14
+ def name
15
+ @row['name']
16
+ end
17
+
18
+ def ==(other)
19
+ columns = self.class::COMPARISON_COLUMNS
20
+ self.class == other.class && row.values_at(*columns) == other.row.values_at(*columns)
21
+ end
22
+
23
+ def inspect
24
+ "#<#{self.class} @row=#{@row.inspect}>"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Column < Base
4
+ COMPARISON_COLUMNS = %w(name attndims attnotnull atttypmod)
5
+
6
+ def type
7
+ @type ||= result.pg_type.find{|t| t['oid'] == row['atttypid']}['name']
8
+ end
9
+
10
+ def default
11
+ # Have to dance a bit so that the lack of a default becomes nil, but isn't recalculated each time.
12
+ if @default_calculated
13
+ @default
14
+ else
15
+ @default_calculated = true
16
+ @default = result.pg_attrdef.find{|d| d['adrelid'] == row['attrelid']}['default'] if row['atthasdef'] == 't'
17
+ end
18
+ end
19
+
20
+ def ==(other)
21
+ super &&
22
+ type == other.type &&
23
+ default == other.default
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Constraint < Base
4
+ COMPARISON_COLUMNS = %w(name definition)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Extension < Base
4
+ COMPARISON_COLUMNS = %w(name extversion)
5
+
6
+ def schema
7
+ @schema ||= result.schemas.find { |s| s.oid == row['extnamespace'] }
8
+ end
9
+
10
+ def ==(other)
11
+ super &&
12
+ (schema && schema.name) == (other.schema && other.schema.name)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Function < Base
4
+ COMPARISON_COLUMNS = %w(name proargmodes definition)
5
+
6
+ def argument_types
7
+ @argument_types ||= @row['proargtypes'].split.map do |oid|
8
+ result.pg_type.find{|t| t['oid'] == oid}['name']
9
+ end
10
+ end
11
+
12
+ def return_type
13
+ @return_type ||= result.pg_type.find{|t| t['oid'] == @row['prorettype']}['name']
14
+ end
15
+
16
+ def language
17
+ @language ||= result.pg_language.find{|l| l['oid'] == @row['prolang']}['name']
18
+ end
19
+
20
+ def ==(other)
21
+ super &&
22
+ argument_types == other.argument_types &&
23
+ return_type == other.return_type &&
24
+ language == other.language
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Index < Base
4
+ COMPARISON_COLUMNS = %w(name filter indisunique indisprimary)
5
+
6
+ def expression
7
+ @row['expression'] || @row['indkey'].split.map{|i| parent.columns.find{|c| c.row['attnum'] == i}}.map(&:name)
8
+ end
9
+
10
+ def ==(other)
11
+ super &&
12
+ expression == other.expression
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Language < Base
4
+ COMPARISON_COLUMNS = %w(name)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Schema < Base
4
+ COMPARISON_COLUMNS = %w(name)
5
+
6
+ def tables
7
+ @tables ||= result.pg_class.select do |c|
8
+ c['relnamespace'] == oid && c['relkind'] == 'r'
9
+ end.map{|row| Table.new(result, row, self)}.sort_by(&:name)
10
+ end
11
+
12
+ def functions
13
+ @functions ||= result.pg_proc.select do |c|
14
+ c['pronamespace'] == oid
15
+ end.map{|row| Function.new(result, row, self)}.sort_by(&:name)
16
+ end
17
+
18
+ def ==(other)
19
+ super &&
20
+ tables == other.tables &&
21
+ functions == other.functions
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Table < Base
4
+ COMPARISON_COLUMNS = %w(name relpersistence reloptions)
5
+
6
+ def columns
7
+ @columns ||= result.pg_attribute.select do |c|
8
+ c['attrelid'] == oid
9
+ end.sort_by{|c| c['attnum'].to_i}.map { |row| Column.new(result, row, self) }
10
+ end
11
+
12
+ def indexes
13
+ @indexes ||= result.pg_index.select do |c|
14
+ c['indrelid'] == oid
15
+ end.map{|row| Index.new(result, row, self)}.sort_by(&:name)
16
+ end
17
+
18
+ def constraints
19
+ @constraints ||= result.pg_constraint.select do |c|
20
+ c['conrelid'] == oid
21
+ end.map{|row| Constraint.new(result, row, self)}.sort_by(&:name)
22
+ end
23
+
24
+ def triggers
25
+ @triggers ||= result.pg_trigger.select do |t|
26
+ t['tgrelid'] == oid
27
+ end.map{|row| Trigger.new(result, row, self)}.sort_by(&:name)
28
+ end
29
+
30
+ def ==(other)
31
+ super &&
32
+ columns == other.columns &&
33
+ indexes == other.indexes &&
34
+ constraints == other.constraints &&
35
+ triggers == other.triggers
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ module PGExaminer
2
+ class Result
3
+ class Trigger < Base
4
+ COMPARISON_COLUMNS = %w(name tgtype)
5
+
6
+ def function
7
+ @function ||= result.pg_proc.find{|f| f['oid'] == @row['tgfoid']}['name']
8
+ end
9
+
10
+ def ==(other)
11
+ super &&
12
+ function == other.function
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,145 @@
1
+ require 'pg_examiner/result/base'
2
+ require 'pg_examiner/result/column'
3
+ require 'pg_examiner/result/constraint'
4
+ require 'pg_examiner/result/extension'
5
+ require 'pg_examiner/result/function'
6
+ require 'pg_examiner/result/index'
7
+ require 'pg_examiner/result/language'
8
+ require 'pg_examiner/result/schema'
9
+ require 'pg_examiner/result/table'
10
+ require 'pg_examiner/result/trigger'
11
+
12
+ module PGExaminer
13
+ class Result
14
+ attr_reader :pg_namespace,
15
+ :pg_class,
16
+ :pg_type,
17
+ :pg_index,
18
+ :pg_attrdef,
19
+ :pg_attribute,
20
+ :pg_extension,
21
+ :pg_constraint,
22
+ :pg_proc,
23
+ :pg_language,
24
+ :pg_trigger
25
+
26
+ def initialize(connection)
27
+ @conn = connection
28
+ load_schema
29
+ end
30
+
31
+ def schemas
32
+ @schemas ||= @pg_namespace.map{|row| Schema.new(self, row)}.sort_by(&:name)
33
+ end
34
+
35
+ def extensions
36
+ @extensions ||= @pg_extension.map{|row| Extension.new(self, row)}.sort_by(&:name)
37
+ end
38
+
39
+ def languages
40
+ @languages ||= @pg_language.map{|row| Language.new(self, row)}.sort_by(&:name)
41
+ end
42
+
43
+ def ==(other)
44
+ other.is_a?(Result) &&
45
+ schemas == other.schemas &&
46
+ extensions == other.extensions &&
47
+ languages == other.languages
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class} @schemas=#{@schemas.inspect}, @extensions=#{@extensions.inspect}>"
52
+ end
53
+
54
+ private
55
+
56
+ def execute(*args)
57
+ @conn.async_exec(*args).to_a
58
+ end
59
+
60
+ def load_schema
61
+ # Get all relevant schemas/namespaces, which includes public but not
62
+ # information_schema or system schemas, which are prefixed with pg_. It
63
+ # wouldn't be a good practice for anyone to name a custom schema
64
+ # starting with pg_ anyway.
65
+ @pg_namespace = execute <<-SQL
66
+ SELECT oid, nspname AS name
67
+ FROM pg_namespace
68
+ WHERE nspname != 'information_schema'
69
+ AND nspname NOT LIKE 'pg_%'
70
+ SQL
71
+
72
+ @pg_class = load_table @pg_namespace.map{|ns| ns['oid']}, <<-SQL
73
+ SELECT oid, relname AS name, relkind, relpersistence, reloptions, relnamespace
74
+ FROM pg_class
75
+ WHERE relnamespace IN (?)
76
+ SQL
77
+
78
+ @pg_attribute = load_table @pg_class.map{|ns| ns['oid']}, <<-SQL
79
+ SELECT atttypid, attname AS name, attndims, attnotnull, atttypmod, attrelid, atthasdef, attnum
80
+ FROM pg_attribute
81
+ WHERE attrelid IN (?)
82
+ AND attnum > 0 -- No system columns
83
+ AND NOT attisdropped -- Still active
84
+ SQL
85
+
86
+ @pg_type = execute <<-SQL
87
+ SELECT oid, typname AS name
88
+ FROM pg_type
89
+ SQL
90
+
91
+ @pg_index = load_table @pg_class.map{|ns| ns['oid']}, <<-SQL
92
+ SELECT c.relname AS name, i.indrelid, i.indkey, indisunique, indisprimary,
93
+ pg_get_expr(i.indpred, i.indexrelid) AS filter,
94
+ pg_get_expr(i.indexprs, i.indrelid) AS expression
95
+ FROM pg_index i
96
+ JOIN pg_class c ON c.oid = i.indexrelid
97
+ WHERE c.oid IN (?)
98
+ SQL
99
+
100
+ @pg_constraint = load_table @pg_class.map{|ns| ns['oid']}, <<-SQL
101
+ SELECT oid, conname AS name, conrelid,
102
+ pg_get_constraintdef(oid) AS definition
103
+ FROM pg_constraint c
104
+ WHERE conrelid IN (?)
105
+ SQL
106
+
107
+ @pg_trigger = load_table @pg_class.map{|ns| ns['oid']}, <<-SQL
108
+ SELECT oid, tgname AS name, tgrelid, tgtype, tgfoid
109
+ FROM pg_trigger
110
+ WHERE tgrelid IN (?)
111
+ AND tgconstrrelid = '0' -- Ignore foreign key triggers, which have unpredictable names.
112
+ SQL
113
+
114
+ @pg_attrdef = execute <<-SQL
115
+ SELECT oid, adrelid, pg_get_expr(adbin, adrelid) AS default
116
+ FROM pg_attrdef
117
+ SQL
118
+
119
+ @pg_proc = load_table @pg_namespace.map{|ns| ns['oid']}, <<-SQL
120
+ SELECT oid, proname AS name, pronamespace, proargtypes, prorettype, proargmodes, prolang, pg_get_functiondef(oid) AS definition
121
+ FROM pg_proc
122
+ WHERE pronamespace IN (?)
123
+ AND NOT proisagg -- prevent pg_get_functiondef() from throwing errors on aggregate functions.
124
+ SQL
125
+
126
+ @pg_extension = execute <<-SQL
127
+ SELECT extname AS name, extnamespace, extversion
128
+ FROM pg_extension
129
+ SQL
130
+
131
+ @pg_language = execute <<-SQL
132
+ SELECT oid, lanname AS name
133
+ FROM pg_language
134
+ SQL
135
+ end
136
+
137
+ def load_table(oids, sql)
138
+ if oids.any?
139
+ execute sql.gsub(/\?/, oids.map{|oid| "'#{oid}'"}.join(', '))
140
+ else
141
+ []
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,3 @@
1
+ module PGExaminer
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,10 @@
1
+ require 'pg_examiner/result'
2
+ require 'pg_examiner/version'
3
+
4
+ module PGExaminer
5
+ class << self
6
+ def examine(connection)
7
+ Result.new(connection)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pg_examiner/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'pg_examiner'
8
+ spec.version = PGExaminer::VERSION
9
+ spec.authors = ["Chris Hanks"]
10
+ spec.email = ["christopher.m.hanks@gmail.com"]
11
+ spec.summary = %q{Parse the schemas of Postgres databases in detail}
12
+ spec.description = %q{Examine and compare the tables, columns, constraints and other information that makes up the schema of a PG database}
13
+ spec.homepage = 'https://github.com/chanks/pg_examiner'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'pry'
24
+ end