pg_examiner 0.1.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 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