pg_meta 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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/TODO +3 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/pg_meta +28 -0
- data/lib/ext/hash.rb +9 -0
- data/lib/pg_meta/dump.rb +37 -0
- data/lib/pg_meta/load_conn.rb +356 -0
- data/lib/pg_meta/load_yaml.rb +76 -0
- data/lib/pg_meta/meta.rb +572 -0
- data/lib/pg_meta/version.rb +3 -0
- data/lib/pg_meta.rb +43 -0
- data/pg_meta.gemspec +50 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9a7711ea44e1ae9ff9fd3ed45016e7ba2a00a1554af08e5b311e036cb29c6f01
|
4
|
+
data.tar.gz: 5045d4b33fe8a0b2aeacac9958d7a2f80e8ed2ae3b27c1b69a457acc4d343875
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2e342aef3595dd33c6728ad702efa4a7e4e80640cecccf6c345ad5839f5d3c913b30215b8be7f858fa90faa84eef5c497083978d423c271e008dfb13737c8ba7
|
7
|
+
data.tar.gz: 38b665922c5005182d764db3126948defc7e87aeab3355782f695f3d89a3976e648aa405e191b871863da1ca42377be3b6ff1fcaeb9ea4657c5c272ddc40c3f4
|
data/.gitignore
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/_yardoc/
|
4
|
+
/coverage/
|
5
|
+
/doc/
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/tmp/
|
9
|
+
|
10
|
+
# rspec failure tracking
|
11
|
+
.rspec_status
|
12
|
+
|
13
|
+
# Ignore auto-generated main file
|
14
|
+
/main
|
15
|
+
|
16
|
+
# Ignore Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
|
17
|
+
/Gemfile.lock
|
18
|
+
|
19
|
+
# Put your personal ignore files in /home/clr/.config/git/ignore
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.7.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# PgMeta
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pg_meta`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'pg_meta'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install pg_meta
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pg_meta.
|
data/Rakefile
ADDED
data/TODO
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pg_meta"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/pg_meta
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'shellopts'
|
4
|
+
require 'pg_meta'
|
5
|
+
|
6
|
+
include ShellOpts
|
7
|
+
|
8
|
+
SPEC = %(
|
9
|
+
-f,format=FORMAT:yaml,marshal,dump
|
10
|
+
Control format of output. FORMAT can be one of 'yaml', 'marshal' or the
|
11
|
+
human-readable 'dump' format (the default)
|
12
|
+
|
13
|
+
-- DATABASE
|
14
|
+
)
|
15
|
+
|
16
|
+
opts, args = ShellOpts.process(SPEC, ARGV, version: PgMeta::VERSION)
|
17
|
+
|
18
|
+
format = opts.format
|
19
|
+
database = args.expect(1)
|
20
|
+
conn = PgConn.new(database)
|
21
|
+
meta = PgMeta.new(conn)
|
22
|
+
|
23
|
+
case format
|
24
|
+
when "yaml"; puts meta.to_yaml
|
25
|
+
when "marshal"; puts meta.to_marshal
|
26
|
+
else meta.dump
|
27
|
+
end
|
28
|
+
|
data/lib/ext/hash.rb
ADDED
data/lib/pg_meta/dump.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
require 'indented_io'
|
3
|
+
|
4
|
+
module PgMeta
|
5
|
+
class Node
|
6
|
+
def dump
|
7
|
+
dump_value(to_h)
|
8
|
+
end
|
9
|
+
|
10
|
+
def dump_value(v)
|
11
|
+
case v
|
12
|
+
when Hash
|
13
|
+
puts "{"
|
14
|
+
indent {
|
15
|
+
v.each { |k,v|
|
16
|
+
print "#{k}: "
|
17
|
+
dump_value(v)
|
18
|
+
}
|
19
|
+
}
|
20
|
+
puts "}"
|
21
|
+
when []
|
22
|
+
puts "[]"
|
23
|
+
when Array
|
24
|
+
puts "["
|
25
|
+
indent {
|
26
|
+
v.each { |v|
|
27
|
+
dump_value(v)
|
28
|
+
}
|
29
|
+
}
|
30
|
+
puts "]"
|
31
|
+
else
|
32
|
+
puts (v.nil? ? "nil" : v)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,356 @@
|
|
1
|
+
|
2
|
+
module PgMeta
|
3
|
+
class Database
|
4
|
+
using Ext::Hash
|
5
|
+
|
6
|
+
# Load the database from the given PgConn::Connection object. This method is a
|
7
|
+
# transaction wrapper around #do_load_conn
|
8
|
+
def self.load_conn(pg_conn)
|
9
|
+
pg_conn.pg_connection.transaction { |conn|
|
10
|
+
conn.exec "set transaction isolation level serializable read only deferrable"
|
11
|
+
do_load_from_connection(conn)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
# Load the given database into self. Note that +conn+ is a PG::Connection object
|
17
|
+
def self.do_load_from_connection(conn)
|
18
|
+
# Get database owner
|
19
|
+
r = conn.exec %(
|
20
|
+
select rolname::varchar
|
21
|
+
from pg_database d
|
22
|
+
join pg_roles r on r.oid = d.datdba
|
23
|
+
where d.datname = '#{conn.db}'
|
24
|
+
)
|
25
|
+
owner = r.values.first.first
|
26
|
+
|
27
|
+
# Create database object
|
28
|
+
db = Database.new(conn.db, owner)
|
29
|
+
|
30
|
+
# Build schemas
|
31
|
+
conn.exec(%(
|
32
|
+
select schema_name::text as name,
|
33
|
+
schema_owner::text as owner
|
34
|
+
from information_schema.schemata
|
35
|
+
where schema_name !~ '^pg_' and schema_name <> 'information_schema'
|
36
|
+
)).each { |row|
|
37
|
+
Schema.new(db, *row.project(:name, :owner))
|
38
|
+
}
|
39
|
+
|
40
|
+
# Build tables and views
|
41
|
+
conn.exec(%(
|
42
|
+
select n.nspname::text as "schema",
|
43
|
+
c.relname::text as "name",
|
44
|
+
c.relkind,
|
45
|
+
coalesce(i.is_insertable_into, 'NO') = 'YES' as "insertable?",
|
46
|
+
coalesce(i.is_typed, 'NO') = 'YES' as "typed?"
|
47
|
+
from pg_class c
|
48
|
+
join pg_namespace n on n.oid = c.relnamespace
|
49
|
+
left join information_schema.tables i on
|
50
|
+
i.table_schema = n.nspname
|
51
|
+
and i.table_name = c.relname
|
52
|
+
where n.nspname !~ '^pg_' and n.nspname <> 'information_schema'
|
53
|
+
and c.relkind in ('r', 'v', 'm')
|
54
|
+
)).each { |row|
|
55
|
+
schema = db.schemas[row[:schema]]
|
56
|
+
klass =
|
57
|
+
case row[:relkind]
|
58
|
+
when 'r'; Table
|
59
|
+
when 'v'; View
|
60
|
+
when 'm'; MaterializedView
|
61
|
+
end
|
62
|
+
klass.new(schema, *row.project(:name, :insertable?, :typed?))
|
63
|
+
}
|
64
|
+
|
65
|
+
# Find transitive closure of dependencies between tables
|
66
|
+
conn.exec(%(
|
67
|
+
with recursive chain as (
|
68
|
+
select distinct
|
69
|
+
c.conrelid as this,
|
70
|
+
c.confrelid as that
|
71
|
+
from pg_depend d
|
72
|
+
join pg_constraint c on c.oid = d.objid
|
73
|
+
join pg_class t1 on t1.oid = c.conrelid
|
74
|
+
join pg_namespace n1 on n1.oid = t1.relnamespace
|
75
|
+
join pg_class t2 on t2.oid = c.confrelid
|
76
|
+
join pg_namespace n2 on n2.oid = t2.relnamespace
|
77
|
+
where n1.nspname = 'auth'
|
78
|
+
and deptype = 'n'
|
79
|
+
union
|
80
|
+
select ch.this as this,
|
81
|
+
c.confrelid as that
|
82
|
+
from pg_depend d
|
83
|
+
join pg_constraint c on c.oid = d.objid
|
84
|
+
join chain ch on ch.that = c.conrelid
|
85
|
+
)
|
86
|
+
select n1.nspname as this_schema,
|
87
|
+
t1.relname as this_table,
|
88
|
+
n2.nspname as that_schema,
|
89
|
+
t2.relname as that_table
|
90
|
+
from chain c
|
91
|
+
join pg_class t1 on t1.oid = c.this
|
92
|
+
join pg_namespace n1 on n1.oid = t1.relnamespace
|
93
|
+
join pg_class t2 on t2.oid = c.that
|
94
|
+
join pg_namespace n2 on n2.oid = t2.relnamespace
|
95
|
+
)).each { |row|
|
96
|
+
this_table = db.schemas[row[:this_schema]].tables[row[:this_table]]
|
97
|
+
that_table = db.schemas[row[:that_schema]].tables[row[:that_table]]
|
98
|
+
that_table.send(:add_depending_table, this_table)
|
99
|
+
}
|
100
|
+
|
101
|
+
# Build defining-relation dependencies
|
102
|
+
conn.exec(%(
|
103
|
+
select distinct
|
104
|
+
n2.nspname::text as "dep_schema",
|
105
|
+
c2.relname::text as "dep_view",
|
106
|
+
n.nspname::text as "def_schema",
|
107
|
+
c.relname::text as "def_relation"
|
108
|
+
from pg_class c
|
109
|
+
join pg_namespace n on n.oid = c.relnamespace
|
110
|
+
join pg_depend d on d.refobjid = c.oid
|
111
|
+
join pg_rewrite r on r.oid = d.objid
|
112
|
+
join pg_class c2 on c2.oid = r.ev_class
|
113
|
+
join pg_namespace n2 on n2.oid = c2.relnamespace
|
114
|
+
where n.nspname !~ '^pg_' and n.nspname <> 'information_schema'
|
115
|
+
and c.relkind in ('r', 'v', 'm')
|
116
|
+
and d.classid = 'pg_rewrite'::regclass
|
117
|
+
and d.refclassid = 'pg_class'::regclass
|
118
|
+
and d.deptype = 'n'
|
119
|
+
and c2.relkind in ('r̈́', 'v', 'm')
|
120
|
+
and c.oid <> c2.oid
|
121
|
+
)).each { |row|
|
122
|
+
depending_view = db.schemas[row[:dep_schema]].tables[row[:dep_view]]
|
123
|
+
defining_relation = db.schemas[row[:def_schema]].tables[row[:def_relation]]
|
124
|
+
|
125
|
+
defining_relation.send(:add_depending_view, depending_view)
|
126
|
+
depending_view.send(:add_defining_relation, defining_relation)
|
127
|
+
}
|
128
|
+
|
129
|
+
# Build columns
|
130
|
+
conn.exec(%(
|
131
|
+
select n.nspname::text as "schema",
|
132
|
+
c.relname::text as "table",
|
133
|
+
a.attname::text as "name",
|
134
|
+
a.attnum as "ordinal",
|
135
|
+
t.typname::text as "type",
|
136
|
+
t2.typname::text as "element_type",
|
137
|
+
a.attndims::integer as "dimensions",
|
138
|
+
i.column_default::text as "default",
|
139
|
+
coalesce(i.is_identity, 'NO') = 'YES' as "identity?",
|
140
|
+
coalesce(i.is_generated, 'NO') = 'ALWAYS' as "generated?",
|
141
|
+
coalesce(i.is_nullable, 'NO') = 'YES' as "nullable?",
|
142
|
+
coalesce(i.is_updatable, 'NO') = 'YES' as "updatable?"
|
143
|
+
from pg_namespace n
|
144
|
+
join pg_class c on c.relnamespace = n.oid
|
145
|
+
join pg_attribute a on a.attrelid = c.oid
|
146
|
+
join pg_type t on t.oid = a.atttypid
|
147
|
+
left join pg_type t2 on t2.oid = t.typelem
|
148
|
+
left join information_schema.columns i on
|
149
|
+
i.table_schema = n.nspname
|
150
|
+
and i.table_name = c.relname
|
151
|
+
and i.column_name = a.attname
|
152
|
+
where n.nspname !~ '^pg_' and n.nspname <> 'information_schema'
|
153
|
+
and c.relkind in ('r', 'v', 'm')
|
154
|
+
and a.attnum > 0
|
155
|
+
)).each { |row|
|
156
|
+
table = db.schemas[row[:schema]].tables[row[:table]]
|
157
|
+
Column.new(table, *row.project(
|
158
|
+
:name, :ordinal, :type, :element_type, :dimensions, :default, :identity?, :generated?,
|
159
|
+
:nullable?, :updatable?))
|
160
|
+
}
|
161
|
+
|
162
|
+
# Build simple constraints
|
163
|
+
conn.exec(%(
|
164
|
+
select c.table_schema::text as schema,
|
165
|
+
c.table_name::text as table,
|
166
|
+
c.constraint_type::text,
|
167
|
+
c.constraint_name::text as name,
|
168
|
+
cc.check_clause as expression,
|
169
|
+
(
|
170
|
+
select array_agg(column_name::text)
|
171
|
+
from information_schema.constraint_column_usage ccu
|
172
|
+
where ccu.table_schema = c.table_schema
|
173
|
+
and ccu.table_name = c.table_name
|
174
|
+
and ccu.constraint_schema = c.constraint_schema
|
175
|
+
and ccu.constraint_name = c.constraint_name
|
176
|
+
) as columns
|
177
|
+
from information_schema.table_constraints c
|
178
|
+
left join information_schema.check_constraints cc
|
179
|
+
on cc.constraint_schema = c.table_schema and
|
180
|
+
cc.constraint_name = c.constraint_name
|
181
|
+
where c.table_schema !~ '^pg_' and c.table_schema <> 'information_schema'
|
182
|
+
and c.constraint_type in ('PRIMARY KEY', 'UNIQUE', 'CHECK')
|
183
|
+
)).each { |row|
|
184
|
+
table = db.schemas[row[:schema]].tables[row[:table]]
|
185
|
+
name = row[:name]
|
186
|
+
columns = lookup_columns(table, row[:columns] || [])
|
187
|
+
expression = row[:expression]
|
188
|
+
case row[:constraint_type]
|
189
|
+
when "PRIMARY KEY"; PrimaryKeyConstraint.new(table, name, columns)
|
190
|
+
when "UNIQUE"; UniqueConstraint.new(table, name, columns)
|
191
|
+
when "CHECK"
|
192
|
+
constraint = CheckConstraint.new(table, name, expression)
|
193
|
+
# patch-up column if constraint is a not null constraint
|
194
|
+
if constraint.not_null?
|
195
|
+
constraint.column.instance_variable_set(:@is_nullable, false)
|
196
|
+
end
|
197
|
+
else
|
198
|
+
raise ArgumentError
|
199
|
+
end
|
200
|
+
}
|
201
|
+
|
202
|
+
# Build implicit unique constraints from indexes. Only indexes not used
|
203
|
+
# in constraints are considered
|
204
|
+
conn.exec(%(
|
205
|
+
select n.nspname as "schema",
|
206
|
+
c.relname as "table",
|
207
|
+
array_agg(a.attname::text) as "fields",
|
208
|
+
i.relname as "index",
|
209
|
+
x.indisunique as "unique"
|
210
|
+
from pg_class c
|
211
|
+
join pg_namespace n on n.oid = c.relnamespace
|
212
|
+
join pg_index x on x.indrelid = c.oid
|
213
|
+
join pg_class i on i.oid = x.indexrelid
|
214
|
+
join ( select attrelid, attname from pg_attribute order by attnum ) a on a.attrelid = i.oid
|
215
|
+
where n.nspname !~ '^pg_' and n.nspname <> 'information_schema'
|
216
|
+
and i.oid not in (select conindid from pg_constraint)
|
217
|
+
and x.indisunique
|
218
|
+
group by
|
219
|
+
"schema",
|
220
|
+
"table",
|
221
|
+
"index",
|
222
|
+
"unique"
|
223
|
+
order by
|
224
|
+
"schema",
|
225
|
+
"table",
|
226
|
+
"index"
|
227
|
+
)).each { |row|
|
228
|
+
table = db.schemas[row[:schema]].tables[row[:table]]
|
229
|
+
name = "#{row[:schema]}.#{row[:table]}.#{row[:index]}_constr"
|
230
|
+
columns = lookup_columns(table, row[:fields] || [])
|
231
|
+
UniqueConstraint.new(table, name, columns)
|
232
|
+
}
|
233
|
+
|
234
|
+
# Build referential constraints
|
235
|
+
#
|
236
|
+
# Referential constraints has to be initialized after unique constraints
|
237
|
+
#
|
238
|
+
# The GROUP BY is necessary because we re-assign constraints from schema to
|
239
|
+
# table. This requires joining key_column_usage again to get the name of
|
240
|
+
# the referenced table and that yields a row for each column in the unique
|
241
|
+
# key (TODO: Can this be omitted?)
|
242
|
+
#
|
243
|
+
conn.exec(%(
|
244
|
+
select rc.constraint_schema::text as schema,
|
245
|
+
rc.constraint_name::text as name,
|
246
|
+
cu_refing.table_schema::text as "referencing_schema",
|
247
|
+
cu_refing.table_name::text as "referencing_table",
|
248
|
+
(
|
249
|
+
select array_agg(column_name::text order by ordinal_position)
|
250
|
+
from information_schema.key_column_usage kcu
|
251
|
+
where kcu.constraint_schema = rc.constraint_schema
|
252
|
+
and kcu.constraint_name = rc.constraint_name
|
253
|
+
) as "referencing_columns",
|
254
|
+
cu_refed.table_schema::text as referenced_schema,
|
255
|
+
cu_refed.table_name::text as referenced_table,
|
256
|
+
cu_refed.constraint_name::text as referenced_constraint
|
257
|
+
from information_schema.referential_constraints rc
|
258
|
+
join information_schema.key_column_usage cu_refing
|
259
|
+
on cu_refing.constraint_schema = rc.constraint_schema
|
260
|
+
and cu_refing.constraint_name = rc.constraint_name
|
261
|
+
join information_schema.key_column_usage cu_refed
|
262
|
+
on cu_refed.constraint_schema = rc.unique_constraint_schema
|
263
|
+
and cu_refed.constraint_name = rc.unique_constraint_name
|
264
|
+
where cu_refing.table_schema !~ '^pg_' and cu_refing.table_schema <> 'information_schema'
|
265
|
+
group by
|
266
|
+
rc.constraint_schema,
|
267
|
+
rc.constraint_name,
|
268
|
+
cu_refing.table_schema,
|
269
|
+
cu_refing.table_name,
|
270
|
+
cu_refed.table_schema,
|
271
|
+
cu_refed.table_name,
|
272
|
+
cu_refed.constraint_name
|
273
|
+
)).each { |row|
|
274
|
+
schema = db.schemas[row[:schema]]
|
275
|
+
name = row[:name]
|
276
|
+
referencing_table = schema.tables[row[:referencing_table]]
|
277
|
+
referencing_columns = lookup_columns(referencing_table, row[:referencing_columns])
|
278
|
+
referenced_constraint =
|
279
|
+
db.schemas[row[:referenced_schema]] \
|
280
|
+
.tables[row[:referenced_table]] \
|
281
|
+
.constraints[row[:referenced_constraint]]
|
282
|
+
ref = ReferentialConstraint.new(referencing_table, name, referencing_columns, referenced_constraint)
|
283
|
+
ref.referenced_table.send(:add_depending_table, referencing_table)
|
284
|
+
}
|
285
|
+
|
286
|
+
# Build functions and procedures
|
287
|
+
conn.exec(%(
|
288
|
+
select s.nspname::text as "schema",
|
289
|
+
pg_get_userbyid(p.proowner)::text as "owner",
|
290
|
+
format('%I(%s)', p.proname, oidvectortypes(p.proargtypes))
|
291
|
+
|| ': '
|
292
|
+
|| format_type(p.prorettype, null) as "name",
|
293
|
+
case format_type(p.prorettype, null)
|
294
|
+
when 'void' then 'procedure'
|
295
|
+
else 'function'
|
296
|
+
end as "kind",
|
297
|
+
case
|
298
|
+
when prosecdef then 'definer'
|
299
|
+
else 'invoker'
|
300
|
+
end as "security"
|
301
|
+
from pg_proc p
|
302
|
+
join pg_namespace s on (p.pronamespace = s.oid)
|
303
|
+
where s.nspname !~ '^pg_' and s.nspname <> 'information_schema'
|
304
|
+
)).each { |row|
|
305
|
+
schema = db.schemas[row[:schema]]
|
306
|
+
klass = (row[:kind] == 'function' ? Function : Procedure)
|
307
|
+
klass.new(schema, *row.project(:name, :owner, :security))
|
308
|
+
}
|
309
|
+
|
310
|
+
# Build user-defined triggers
|
311
|
+
conn.exec(%(
|
312
|
+
select n.nspname::text as "schema",
|
313
|
+
c.relname::text as "table",
|
314
|
+
t.tgname::text as "name",
|
315
|
+
fn.nspname::text as "function_schema",
|
316
|
+
format('%I(%s)', p.proname::text, oidvectortypes(p.proargtypes))
|
317
|
+
|| ': '
|
318
|
+
|| format_type(p.prorettype, null) as "function_name",
|
319
|
+
case when (tgtype::int::bit(7) & b'0000001')::int = 0 then 'stmt' else 'row' end as "level",
|
320
|
+
coalesce(
|
321
|
+
case when (tgtype::int::bit(7) & b'0000010')::int = 0 then null else 'before' end,
|
322
|
+
case when (tgtype::int::bit(7) & b'0000010')::int = 0 then 'after' else null end,
|
323
|
+
case when (tgtype::int::bit(7) & b'1000000')::int = 0 then null else 'instead' end,
|
324
|
+
''
|
325
|
+
)::text as "timing",
|
326
|
+
(case when (tgtype::int::bit(7) & b'0000100')::int = 0 then '' else ' insert' end) ||
|
327
|
+
(case when (tgtype::int::bit(7) & b'0001000')::int = 0 then '' else ' delete' end) ||
|
328
|
+
(case when (tgtype::int::bit(7) & b'0010000')::int = 0 then '' else ' update' end) ||
|
329
|
+
(case when (tgtype::int::bit(7) & b'0100000')::int = 0 then '' else ' truncate' end)
|
330
|
+
as "events"
|
331
|
+
from pg_trigger t
|
332
|
+
join pg_proc p on t.tgfoid = p.oid
|
333
|
+
join pg_class c on c.oid = t.tgrelid
|
334
|
+
join pg_namespace n on n.oid = c.relnamespace
|
335
|
+
join pg_namespace fn on fn.oid = p.pronamespace
|
336
|
+
where not t.tgisinternal
|
337
|
+
)).each { |row|
|
338
|
+
schema = db.schemas[row[:schema]]
|
339
|
+
table = schema.tables[row[:table]]
|
340
|
+
name, level, timing = *row.project(:name, :level, :timing)
|
341
|
+
function_schema = db.schemas[row[:function_schema]]
|
342
|
+
function = function_schema.functions[row[:function_name]]
|
343
|
+
events = row[:events].split.map(&:to_sym)
|
344
|
+
Trigger.new(table, name, function, level, timing, events)
|
345
|
+
}
|
346
|
+
|
347
|
+
# Return database object
|
348
|
+
db
|
349
|
+
end
|
350
|
+
|
351
|
+
def self.lookup_columns(table, column_names)
|
352
|
+
column_names.map { |n| table.columns[n] }
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
|
2
|
+
module PgMeta
|
3
|
+
class Database
|
4
|
+
using Ext::Hash
|
5
|
+
|
6
|
+
# Create a Database object from the given YAML representation and return
|
7
|
+
# it. The YAML representation can be generated by Database#to_h
|
8
|
+
def self.load_yaml(database_yaml)
|
9
|
+
database = Database.new *database_yaml.project(:name, :owner)
|
10
|
+
|
11
|
+
# Accumulators for second pass
|
12
|
+
views = []
|
13
|
+
referential_constraints = []
|
14
|
+
|
15
|
+
# First pass: Build everything except referential constraints
|
16
|
+
for schema_yaml in database_yaml[:schemas]
|
17
|
+
schema = Schema.new database, *schema_yaml.project(:name, :owner)
|
18
|
+
for table_yaml in schema_yaml[:tables]
|
19
|
+
klass = table_yaml[:table?] ? Table : (table_yaml[:materialized?] ? MaterializedView : View)
|
20
|
+
table = klass.new schema, *table_yaml.project(:name, :insertable?, :typed?)
|
21
|
+
views << [table, table_yaml[:defining_relations]] if klass <= View || klass <= MaterializedView
|
22
|
+
for column_yaml in table_yaml[:columns]
|
23
|
+
column = Column.new(table,
|
24
|
+
*column_yaml.project(
|
25
|
+
:name, :ordinal, :type, :element_type, :dimensions, :default, :identity?, :generated?,
|
26
|
+
:nullable?, :updatable?))
|
27
|
+
end
|
28
|
+
for constraint_yaml in table_yaml[:constraints]
|
29
|
+
name = constraint_yaml[:name]
|
30
|
+
case kind = constraint_yaml[:kind]
|
31
|
+
when :primary_key
|
32
|
+
columns = constraint_yaml[:columns]&.map { |name| table.columns[name] }
|
33
|
+
PrimaryKeyConstraint.new(table, name, columns)
|
34
|
+
when :unique
|
35
|
+
columns = constraint_yaml[:columns]&.map { |name| table.columns[name] }
|
36
|
+
UniqueConstraint.new(table, name, columns)
|
37
|
+
when :check
|
38
|
+
CheckConstraint.new(table, name, constraint_yaml[:expression])
|
39
|
+
when :referential
|
40
|
+
# Postpone to second pass
|
41
|
+
columns = constraint_yaml[:referencing_columns]&.map { |name| table.columns[name] }
|
42
|
+
referential_constraints << [table, name, columns, constraint_yaml[:referenced_constraint]]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
for trigger_yaml in table_yaml[:triggers]
|
46
|
+
Trigger.new(table, *trigger_yaml.project(:name, :function, :level, :timing, :events))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
for function_yaml in schema_yaml[:functions]
|
50
|
+
klass = function_yaml[:function?] ? Function : Procedure
|
51
|
+
klass.new(schema, *function_yaml.project(:name, :owner, :security))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Second pass: Add referential constraints
|
56
|
+
referential_constraints.each { |table, name, columns, referenced_constraint_uid|
|
57
|
+
referenced_constraint = database[referenced_constraint_uid] or
|
58
|
+
raise Error, "Can't find UID '#{referenced_constraint_uid}'"
|
59
|
+
ReferentialConstraint.new(table, name, columns, referenced_constraint)
|
60
|
+
referenced_constraint.table.send(:add_depending_table, table)
|
61
|
+
}
|
62
|
+
|
63
|
+
# Second pass: Build defining tables
|
64
|
+
views.each { |view, defining_relation_uids|
|
65
|
+
defining_relation_uids.each { |uid|
|
66
|
+
relation = database[uid]
|
67
|
+
relation.send(:add_depending_view, view)
|
68
|
+
view.send(:add_defining_relation, relation)
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
database
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|