meta_db 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 +18 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/README.md +37 -0
- data/Rakefile +6 -0
- data/TODO +12 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/meta_db.rb +182 -0
- data/lib/meta_db/connection.rb +97 -0
- data/lib/meta_db/db_object.rb +352 -0
- data/lib/meta_db/dump.rb +31 -0
- data/lib/meta_db/version.rb +3 -0
- data/meta_db.gemspec +40 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1e0499ce2a211cd8e953ffd5ce447913f59d7f40d4db6c56b3515d1f0f810d74
|
4
|
+
data.tar.gz: cca551c4c6e6537d267ab140f789977cca5ca2a8ccac12d976527780071e3e02
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2d8670680900ef650d36a453904f13e635c996e1634ccbf0d9cf197355652eb84f74cb07405f0a62e632a5ee70f5425381886edfddc1eb8d32c42a5065804ee7
|
7
|
+
data.tar.gz: 452f2f681469529b9deb7f7e70ca91cc492c9d7a87d2c0bf22d6c7fd36f7e2e74a1635f6b7a15b80312d793851142fadc5ce9289c62b362e12812b539b07b7db
|
data/.gitignore
ADDED
@@ -0,0 +1,18 @@
|
|
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 Gemfile.lock. See https://stackoverflow.com/questions/4151495/should-gemfile-lock-be-included-in-gitignore
|
14
|
+
/Gemfile.lock
|
15
|
+
|
16
|
+
# Ignore generated yaml and marshall files
|
17
|
+
/*.yaml
|
18
|
+
/*.marshal
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.6.6
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# MetaDb
|
2
|
+
|
3
|
+
Models the meta data (schema) of a database. The model implements parts of the
|
4
|
+
information schema and extends it with postgresql specific features and
|
5
|
+
features for practical use.
|
6
|
+
|
7
|
+
This is work in progress. Use at your own peril
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'meta_db'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install meta_db
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
TODO: Write usage instructions here
|
28
|
+
|
29
|
+
## Development
|
30
|
+
|
31
|
+
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.
|
32
|
+
|
33
|
+
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).
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/clrgit/meta_db.
|
data/Rakefile
ADDED
data/TODO
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
o Tests
|
2
|
+
o Handle trigger constraints
|
3
|
+
o Handle exclusion constraints
|
4
|
+
o Handle array types better
|
5
|
+
|
6
|
+
+ Use information_schema
|
7
|
+
+ Implement Schema
|
8
|
+
+ Default values
|
9
|
+
+ Include schemas in [] and lookup on MetaDb::Db
|
10
|
+
|
11
|
+
- Rename to PgMeta
|
12
|
+
- Implement RDBMS
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "meta_db"
|
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/lib/meta_db.rb
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
require "meta_db/version"
|
2
|
+
|
3
|
+
require "indented_io"
|
4
|
+
|
5
|
+
require 'pg'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require 'meta_db/connection.rb'
|
9
|
+
require 'meta_db/db_object.rb'
|
10
|
+
|
11
|
+
module MetaDb
|
12
|
+
# TODO
|
13
|
+
def self.load_yaml(file)
|
14
|
+
YAML.load_file(file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.save_yaml(db, file, pretty: true)
|
18
|
+
File.open(file, 'w') do |f|
|
19
|
+
arg = pretty ? { :indentation => 3 } : {}
|
20
|
+
f.write(YAML.dump(db, arg))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_marshal(file)
|
25
|
+
File.open(file) { |f| Marshal.load(f) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.save_marshal(db, file)
|
29
|
+
File.open(file, 'w') { |f| Marshal.dump(db, f) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# The options argument is a hash as PG::Connection options. :host, :port,
|
33
|
+
# :dbname, :user, and :password are some of the most used
|
34
|
+
def self.load_pg_conn(options)
|
35
|
+
PostgresConnection.open(options) { |conn| load_conn(conn) }
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
# Connect to the given database and return a MetaDb::Db object. If host is
|
40
|
+
# nil, a socket is used to connect to the database
|
41
|
+
#
|
42
|
+
# The connection object should support #name and #exec
|
43
|
+
def self.load_conn(conn)
|
44
|
+
# TODO: Start transaction
|
45
|
+
|
46
|
+
# Build database
|
47
|
+
r = conn.select %(
|
48
|
+
select usename::varchar
|
49
|
+
from pg_database d
|
50
|
+
join pg_user u on u.usesysid = d.datdba
|
51
|
+
where d.datname = '#{conn.database}'
|
52
|
+
)
|
53
|
+
r.size == 1 or raise "Internal error"
|
54
|
+
db = MetaDb::Database.new(conn.database, r.each_array.first)
|
55
|
+
|
56
|
+
# Build schemas
|
57
|
+
conn.select(%(
|
58
|
+
select schema_name::varchar as name,
|
59
|
+
schema_owner::varchar as owner
|
60
|
+
from information_schema.schemata
|
61
|
+
where schema_name !~ '^pg_' AND schema_name <> 'information_schema'
|
62
|
+
)).each_hash { |row|
|
63
|
+
row[:database] = db
|
64
|
+
MetaDb::Schema.init(row)
|
65
|
+
}
|
66
|
+
|
67
|
+
# Build tables
|
68
|
+
conn.select(%(
|
69
|
+
select table_schema::varchar as schema,
|
70
|
+
table_name::varchar as name,
|
71
|
+
table_type::varchar as type,
|
72
|
+
is_insertable_into = 'YES' as "insertable?",
|
73
|
+
is_typed = 'YES' as "typed?"
|
74
|
+
from information_schema.tables
|
75
|
+
where table_schema !~ '^pg_' AND table_schema <> 'information_schema'
|
76
|
+
)).each_hash { |row|
|
77
|
+
row[:schema] = db.dot row[:schema]
|
78
|
+
klass = (row[:type] == 'VIEW' ? MetaDb::View : MetaDb::Table)
|
79
|
+
klass.init(row)
|
80
|
+
}
|
81
|
+
|
82
|
+
# Build columns
|
83
|
+
conn.select(%(
|
84
|
+
select table_schema::varchar || '.' || table_name as table,
|
85
|
+
ordinal_position as ordinal,
|
86
|
+
column_name::varchar as name,
|
87
|
+
data_type::varchar as type,
|
88
|
+
column_default as default,
|
89
|
+
is_identity = 'YES' as "identity?",
|
90
|
+
is_generated = 'YES' as "generated?",
|
91
|
+
is_nullable = 'YES' as "nullable?",
|
92
|
+
is_updatable = 'YES' as "updatable?"
|
93
|
+
from information_schema.columns
|
94
|
+
where table_schema !~ '^pg_' AND table_schema <> 'information_schema'
|
95
|
+
)).each_hash { |row|
|
96
|
+
row[:table] = db.dot row[:table]
|
97
|
+
MetaDb::Column.init(row)
|
98
|
+
}
|
99
|
+
|
100
|
+
# Build simple constraints
|
101
|
+
conn.select(%(
|
102
|
+
select c.constraint_type::varchar,
|
103
|
+
c.table_schema || '.' || c.table_name as table,
|
104
|
+
c.constraint_name::varchar as name,
|
105
|
+
cc.check_clause as expression,
|
106
|
+
(
|
107
|
+
select array_agg(column_name::varchar)
|
108
|
+
from information_schema.constraint_column_usage ccu
|
109
|
+
where ccu.table_schema = c.table_schema
|
110
|
+
and ccu.table_name = c.table_name
|
111
|
+
and ccu.constraint_schema = c.constraint_schema
|
112
|
+
and ccu.constraint_name = c.constraint_name
|
113
|
+
) as columns
|
114
|
+
from information_schema.table_constraints c
|
115
|
+
left join information_schema.check_constraints cc
|
116
|
+
on cc.constraint_schema = c.table_schema and
|
117
|
+
cc.constraint_name = c.constraint_name
|
118
|
+
where c.table_schema !~ '^pg_' AND c.table_schema <> 'information_schema'
|
119
|
+
and c.constraint_type in ('PRIMARY KEY', 'UNIQUE', 'CHECK')
|
120
|
+
)).each_hash { |row|
|
121
|
+
row[:table] = db.dot row[:table]
|
122
|
+
row[:columns] = lookup_columns(row[:table], row[:columns] || [])
|
123
|
+
case row[:constraint_type]
|
124
|
+
when "PRIMARY KEY"; MetaDb::PrimaryKeyConstraint.init(row)
|
125
|
+
when "UNIQUE"; MetaDb::UniqueConstraint.init(row)
|
126
|
+
when "CHECK"; MetaDb::CheckConstraint.init(row)
|
127
|
+
else
|
128
|
+
raise "Oops"
|
129
|
+
end
|
130
|
+
}
|
131
|
+
|
132
|
+
# Build referential constraints
|
133
|
+
#
|
134
|
+
# Referential constraints has to be initialized after unique constraints
|
135
|
+
#
|
136
|
+
# The GROUP BY is necessary because we re-assign constraints from schema to
|
137
|
+
# table. This requires joining key_column_usage again to get the name of
|
138
|
+
# the referenced table and that yields a row for each column in the unique
|
139
|
+
# key (TODO: Can this be omitted?)
|
140
|
+
#
|
141
|
+
conn.select(%(
|
142
|
+
select rc.constraint_schema::varchar as schema,
|
143
|
+
rc.constraint_name::varchar as name,
|
144
|
+
cu_refing.table_schema || '.' || cu_refing.table_name as "referencing_table",
|
145
|
+
(
|
146
|
+
select array_agg(column_name::varchar order by ordinal_position)
|
147
|
+
from information_schema.key_column_usage kcu
|
148
|
+
where kcu.constraint_name = rc.constraint_name
|
149
|
+
) as "referencing_columns",
|
150
|
+
cu_refed.table_schema || '.' || cu_refed.table_name || '.' || cu_refed.constraint_name
|
151
|
+
as "referenced_constraint"
|
152
|
+
from information_schema.referential_constraints rc
|
153
|
+
join information_schema.key_column_usage cu_refing on
|
154
|
+
cu_refing.constraint_schema = rc.constraint_schema
|
155
|
+
and cu_refing.constraint_name = rc.constraint_name
|
156
|
+
join information_schema.key_column_usage cu_refed on
|
157
|
+
cu_refed.constraint_schema = rc.unique_constraint_schema
|
158
|
+
and cu_refed.constraint_name = rc.unique_constraint_name
|
159
|
+
where cu_refing.table_schema !~ '^pg_' AND cu_refing.table_schema <> 'information_schema'
|
160
|
+
group by
|
161
|
+
rc.constraint_schema,
|
162
|
+
rc.constraint_name,
|
163
|
+
cu_refing.table_schema,
|
164
|
+
cu_refing.table_name,
|
165
|
+
cu_refed.table_schema,
|
166
|
+
cu_refed.table_name,
|
167
|
+
cu_refed.constraint_name
|
168
|
+
)).each_hash { |row|
|
169
|
+
row[:schema] = db.dot row[:schema]
|
170
|
+
row[:referencing_table] = db.dot row[:referencing_table]
|
171
|
+
row[:referencing_columns] = lookup_columns(row[:referencing_table], row[:referencing_columns])
|
172
|
+
row[:referenced_constraint] = db.dot row[:referenced_constraint]
|
173
|
+
MetaDb::ReferentialConstraint.init(row)
|
174
|
+
}
|
175
|
+
db
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.lookup_columns(table, column_names)
|
179
|
+
column_names.map { |n| table.children[n] }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
module MetaDb
|
3
|
+
class Connection
|
4
|
+
# In derived classes, no initialization may take place after the call to
|
5
|
+
# super because otherwise #initialize would call the user-supplied block
|
6
|
+
# with a partial initialized object
|
7
|
+
def initialize(options)
|
8
|
+
@conn = self.class.connect(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.open(options, &block)
|
12
|
+
if block_given?
|
13
|
+
begin
|
14
|
+
conn = self.new(options)
|
15
|
+
return yield(conn)
|
16
|
+
ensure
|
17
|
+
conn&.close
|
18
|
+
end
|
19
|
+
else
|
20
|
+
self.new(options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def host() raise end
|
25
|
+
def port() raise end
|
26
|
+
def database() raise end
|
27
|
+
def user() raise end
|
28
|
+
def password() raise end
|
29
|
+
|
30
|
+
# Escapes s as a SQL string value
|
31
|
+
def escape(s) raise end
|
32
|
+
|
33
|
+
def execute(sql) raise end
|
34
|
+
def select(sql, &block) raise end
|
35
|
+
|
36
|
+
def close() raise end
|
37
|
+
def closed?() raise end
|
38
|
+
|
39
|
+
private
|
40
|
+
def self.connect(options) raise end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Result
|
44
|
+
def initialize(result) @result = result end
|
45
|
+
def size() raise end
|
46
|
+
def each_hash() raise end
|
47
|
+
def each_array() raise end
|
48
|
+
def to_a() each_array end
|
49
|
+
end
|
50
|
+
|
51
|
+
class PostgresResult < Result
|
52
|
+
def size()
|
53
|
+
@result.ntuples
|
54
|
+
end
|
55
|
+
|
56
|
+
def each_hash(&block)
|
57
|
+
if block_given?
|
58
|
+
each_hash.each(&block)
|
59
|
+
else
|
60
|
+
@result.map { |row| row.map { |field, value| [field.to_sym, value] }.to_h }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def each_array(&block)
|
65
|
+
if block_given?
|
66
|
+
each_array.each(&block)
|
67
|
+
else
|
68
|
+
@result.each_row
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_a() @result.values end
|
73
|
+
end
|
74
|
+
|
75
|
+
class PostgresConnection < Connection
|
76
|
+
def host() @conn.host end
|
77
|
+
def port() @conn.port end
|
78
|
+
def database() @conn.db end
|
79
|
+
def user() @conn.user end
|
80
|
+
def password() @conn.pass end
|
81
|
+
|
82
|
+
def escape(s) @conn.escape_string(s) end
|
83
|
+
|
84
|
+
def execute(sql) @conn.exec(sql) end
|
85
|
+
def select(sql) PostgresResult.new(@conn.exec(sql)) end
|
86
|
+
|
87
|
+
def close() @conn.finish end
|
88
|
+
def closed?() @conn.finished? end
|
89
|
+
|
90
|
+
private
|
91
|
+
def self.connect(options)
|
92
|
+
conn = PG::Connection.new(options)
|
93
|
+
conn.type_map_for_results = PG::BasicTypeMapForResults.new conn
|
94
|
+
conn
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,352 @@
|
|
1
|
+
|
2
|
+
require 'meta_db/dump.rb'
|
3
|
+
|
4
|
+
module MetaDb
|
5
|
+
class DbObject
|
6
|
+
# Used to multi-assign values and for dump
|
7
|
+
#
|
8
|
+
# TODO: rename db_attr or 'column', and create read/writer methods
|
9
|
+
def self.attrs(*attrs)
|
10
|
+
attrs = Array(attrs)
|
11
|
+
if attrs.empty?
|
12
|
+
@attrs
|
13
|
+
else
|
14
|
+
@attrs = attrs
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.init(row)
|
19
|
+
h = []
|
20
|
+
self.attrs.each { |a| h << row[a] if row.key?(a) }
|
21
|
+
self.new(*h)
|
22
|
+
end
|
23
|
+
|
24
|
+
attrs :parent, :name, :children
|
25
|
+
|
26
|
+
# Name of object. Unique within contianing object. Not nil
|
27
|
+
attr_reader :name
|
28
|
+
|
29
|
+
# Parent DbObject. Non-nil except for the top-level MetaDb::Database object
|
30
|
+
attr_reader :parent
|
31
|
+
|
32
|
+
# Hash from name to contained objects
|
33
|
+
attr_reader :children
|
34
|
+
|
35
|
+
def initialize(parent, name)
|
36
|
+
@name, @parent = name, parent
|
37
|
+
@children = {}
|
38
|
+
parent&.send(:attach, self)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Unique dot-separated list of names leading from the top-level MetaDb::Database object to
|
42
|
+
# self. Omits the first path-element if strip is true
|
43
|
+
def path(strip: false)
|
44
|
+
r = []
|
45
|
+
node = self
|
46
|
+
while node
|
47
|
+
r << node.name
|
48
|
+
node = node.parent
|
49
|
+
end
|
50
|
+
r.pop if strip
|
51
|
+
r.reverse.compact.join(".")
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return child object with the given name
|
55
|
+
def [](name) @children[name] end
|
56
|
+
|
57
|
+
# Recursively lookup object by dot-separated list of names. If strip is true, the first element
|
58
|
+
# will be stripped from the path
|
59
|
+
def dot(path, strip: false)
|
60
|
+
elems = path.split(".")
|
61
|
+
elems.shift if strip
|
62
|
+
elems.inject(self) { |a,e| a[e] } or raise "Can't lookup '#{path}' in #{self.path}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# Compare two objects by identity
|
66
|
+
def <=>(r) path <=> r.path end
|
67
|
+
|
68
|
+
# Mostly for debug
|
69
|
+
def inspect
|
70
|
+
string = StringIO.new
|
71
|
+
stdout = $stdout
|
72
|
+
begin
|
73
|
+
$stdout = string
|
74
|
+
dump
|
75
|
+
ensure
|
76
|
+
$stdout = stdout
|
77
|
+
end
|
78
|
+
string.string
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
def constrain_children(klass)
|
83
|
+
children.values.select { |v| v.is_a?(klass) }
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def attach(child)
|
88
|
+
!@children.key?(child.name) or raise "Duplicate child key: #{child.name.inspect}"
|
89
|
+
child.instance_variable_set(:@parent, self)
|
90
|
+
@children[child.name] = child
|
91
|
+
end
|
92
|
+
|
93
|
+
def detach(child)
|
94
|
+
@children.key?(child.name) or raise "Non-existing child key: #{child.name.inspect}"
|
95
|
+
child.instance_variable_set(:@parent, nil)
|
96
|
+
@children.delete(child.name)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class Database < DbObject
|
101
|
+
attrs :name, :owner, :schemas
|
102
|
+
|
103
|
+
# List of schemas
|
104
|
+
def schemas()
|
105
|
+
@schemas ||= children.values
|
106
|
+
end
|
107
|
+
|
108
|
+
# Owner of the database
|
109
|
+
attr_reader :owner
|
110
|
+
|
111
|
+
def initialize(name, owner)
|
112
|
+
super(nil, name)
|
113
|
+
@owner = owner
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Schema < DbObject
|
118
|
+
attrs :database, :name, :owner, :tables, :views, :functions, :procedures
|
119
|
+
|
120
|
+
# Database of the schema
|
121
|
+
alias_method :database, :parent
|
122
|
+
|
123
|
+
# Owner of the schema
|
124
|
+
attr_reader :owner
|
125
|
+
|
126
|
+
# List of tables (and views)
|
127
|
+
def tables() @tables ||= constrain_children(MetaDb::Table) end
|
128
|
+
|
129
|
+
# List of views
|
130
|
+
def views() @views ||= constrain_children(MetaDb::View) end
|
131
|
+
|
132
|
+
# List of functions
|
133
|
+
def functions() @functions ||= constrain_children(MetaDb::Function) end
|
134
|
+
|
135
|
+
# List of procedures
|
136
|
+
def procedures() @procedures ||= constrain_children(MetaDb::Procedure) end
|
137
|
+
|
138
|
+
def initialize(database, name, owner)
|
139
|
+
super(database, name)
|
140
|
+
@owner = owner
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class Table < DbObject
|
145
|
+
attrs \
|
146
|
+
:schema, :name, :type, :table?, :view?, :insertable?, :typed?, :columns,
|
147
|
+
:primary_key_columns, :constraints, :referential_constraints, :triggers
|
148
|
+
|
149
|
+
# Schema of the table. Redefines #parent
|
150
|
+
alias_method :schema, :parent
|
151
|
+
|
152
|
+
# Type of table. Either 'BASE TABLE' or 'VIEW'
|
153
|
+
attr_reader :type
|
154
|
+
|
155
|
+
# True iff table is a real table and not a view
|
156
|
+
def table?() true end
|
157
|
+
|
158
|
+
# True iff table is a view
|
159
|
+
def view?() !table? end
|
160
|
+
|
161
|
+
# True if the table/view is insertable
|
162
|
+
def insertable?() @is_insertable end
|
163
|
+
|
164
|
+
# True if the table/view is typed
|
165
|
+
def typed?() @is_typed end
|
166
|
+
|
167
|
+
# List of columns. Columns are sorted by ordinal
|
168
|
+
def columns() @columns ||= constrain_children(MetaDb::Column).sort end
|
169
|
+
|
170
|
+
# The primary key column. nil if the table has multiple primary key columns
|
171
|
+
def primary_key_column
|
172
|
+
return @primary_key_column if @primary_key_column != :undefined
|
173
|
+
if primary_key_columns.size == 1
|
174
|
+
@primary_key_column = primary_key_columns.first
|
175
|
+
else
|
176
|
+
@primary_key_column = nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# List of primary key columns
|
181
|
+
#
|
182
|
+
# Note: Assigned by PrimaryKeyConstraint#initialize
|
183
|
+
attr_reader :primary_key_columns
|
184
|
+
|
185
|
+
# List of constraints
|
186
|
+
def constraints()
|
187
|
+
@constraints ||= constrain_children(MetaDb::Constraint)
|
188
|
+
end
|
189
|
+
|
190
|
+
# List of referential constraints
|
191
|
+
def referential_constraints()
|
192
|
+
@referential_constraints ||= constrain_children(MetaDb::ReferentialConstraint)
|
193
|
+
end
|
194
|
+
|
195
|
+
# List of triggers
|
196
|
+
def triggers() @triggers ||= constrain_children(MetaDb::Trigger) end
|
197
|
+
|
198
|
+
def initialize(schema, name, type, is_insertable, is_typed)
|
199
|
+
super(schema, name)
|
200
|
+
@type, @is_insertable, @is_typed = type, is_insertable, is_typed
|
201
|
+
@primary_key_column = :undefined
|
202
|
+
@primary_key_columns = []
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
class View < Table
|
208
|
+
def table?() false end
|
209
|
+
end
|
210
|
+
|
211
|
+
class Column < DbObject
|
212
|
+
attrs \
|
213
|
+
:table, :ordinal, :name, :type, :default, :identity?, :generated?,
|
214
|
+
:nullable?, :updatable?, :primary_key?
|
215
|
+
|
216
|
+
# Table of the column
|
217
|
+
alias_method :table, :parent
|
218
|
+
|
219
|
+
# Ordinal number of the column
|
220
|
+
attr_reader :ordinal
|
221
|
+
|
222
|
+
# Type of the column
|
223
|
+
attr_reader :type
|
224
|
+
|
225
|
+
# Default value
|
226
|
+
attr_reader :default
|
227
|
+
|
228
|
+
# True if column is an identity column
|
229
|
+
def identity?() @is_identity end
|
230
|
+
|
231
|
+
# True if column is auto generated
|
232
|
+
def generated?() @is_generated end
|
233
|
+
|
234
|
+
# True if column is nullable
|
235
|
+
def nullable?() @is_nullable end
|
236
|
+
|
237
|
+
# True if column is updatable
|
238
|
+
def updatable?() @is_updatable end
|
239
|
+
|
240
|
+
# True if column is the single primary key of the table. Always false for tables
|
241
|
+
# with multiple primary keys
|
242
|
+
def primary_key?() table.table? && self == table.primary_key_column end
|
243
|
+
|
244
|
+
# True is column is (part of) the primary key of the table
|
245
|
+
def primary_key_column?() table.table? && table.primary_key_columns.include?(self) end
|
246
|
+
|
247
|
+
def initialize(table, ordinal, name, type, default, is_identity, is_generated, is_nullable, is_updatable)
|
248
|
+
super(table, name)
|
249
|
+
@type, @ordinal, @default, @is_identity, @is_generated, @is_nullable, @is_updatable =
|
250
|
+
type, ordinal, default, is_identity, is_generated, is_nullable, is_updatable
|
251
|
+
end
|
252
|
+
|
253
|
+
# Compare columns by table and ordinal
|
254
|
+
def <=>(other)
|
255
|
+
if other.is_a?(Column) && table == other.table
|
256
|
+
ordinal <=> other.ordinal
|
257
|
+
else
|
258
|
+
super
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
class Constraint < DbObject
|
264
|
+
attrs :table, :name, :columns
|
265
|
+
|
266
|
+
# Table of the constraint
|
267
|
+
alias_method :table, :parent
|
268
|
+
|
269
|
+
# List of columns in the constraint. Empty for CheckConstraint objects
|
270
|
+
attr_reader :columns
|
271
|
+
|
272
|
+
# Constraint kind. Either :primary_key, :foreign_key, :unique, or :check
|
273
|
+
def kind() CONSTRAINT_KINDS[self.class] end
|
274
|
+
|
275
|
+
def initialize(table, name, columns)
|
276
|
+
super(table, name)
|
277
|
+
@columns = columns
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class PrimaryKeyConstraint < Constraint
|
282
|
+
attrs :table, :name, :columns
|
283
|
+
|
284
|
+
def initialize(table, name, columns)
|
285
|
+
super
|
286
|
+
columns.each { |c| c.table.primary_key_columns << c }
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
class UniqueConstraint < Constraint
|
291
|
+
attrs :table, :name, :columns
|
292
|
+
end
|
293
|
+
|
294
|
+
# Note that #columns is always empty for check constraints
|
295
|
+
class CheckConstraint < Constraint
|
296
|
+
attrs :table, :name, :expression
|
297
|
+
|
298
|
+
# SQL check expression
|
299
|
+
attr_reader :expression
|
300
|
+
|
301
|
+
# Half-baked SQL-to-ruby expression transpiler
|
302
|
+
def ruby_expression # Very simple
|
303
|
+
@ruby ||= sql.sub(/\((.*)\)/, "\\1").gsub(/\((\w+) IS NOT NULL\)/, "!\\1.nil?").gsub(/ OR /, " || ")
|
304
|
+
end
|
305
|
+
|
306
|
+
def initialize(table, name, expression)
|
307
|
+
super(table, name, [])
|
308
|
+
@expression = expression
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
class ReferentialConstraint < Constraint
|
313
|
+
attrs :referencing_table, :name, :referencing_columns, :referenced_constraint
|
314
|
+
|
315
|
+
# The referencing tabla
|
316
|
+
alias_method :referencing_table, :table
|
317
|
+
|
318
|
+
# The referencing columns. Can't be empty
|
319
|
+
alias_method :referencing_columns, :columns
|
320
|
+
|
321
|
+
# The referenced constraint
|
322
|
+
attr_reader :referenced_constraint
|
323
|
+
|
324
|
+
# The referenced table
|
325
|
+
def referenced_table() referenced_constraint.table end
|
326
|
+
|
327
|
+
# The referenced columns
|
328
|
+
def referenced_columns() referenced_constraints.columns end
|
329
|
+
|
330
|
+
def initialize(referencing_table, name, referencing_columns, referenced_constraint)
|
331
|
+
super(referencing_table, name, referencing_columns)
|
332
|
+
@referenced_constraint = referenced_constraint
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
class Function < DbObject
|
337
|
+
end
|
338
|
+
|
339
|
+
class Procedure < DbObject
|
340
|
+
end
|
341
|
+
|
342
|
+
class Trigger < DbObject
|
343
|
+
end
|
344
|
+
|
345
|
+
CONSTRAINT_KINDS = {
|
346
|
+
PrimaryKeyConstraint => :primary_key,
|
347
|
+
ReferentialConstraint => :foreign_key,
|
348
|
+
UniqueConstraint => :unique,
|
349
|
+
CheckConstraint => :check
|
350
|
+
}
|
351
|
+
end
|
352
|
+
|
data/lib/meta_db/dump.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
require 'indented_io'
|
3
|
+
|
4
|
+
module MetaDb
|
5
|
+
class DbObject
|
6
|
+
def dump
|
7
|
+
puts self.class.to_s
|
8
|
+
dump_attrs
|
9
|
+
end
|
10
|
+
|
11
|
+
def dump_attrs(*attrs)
|
12
|
+
attrs = Array(attrs)
|
13
|
+
attrs = self.class.attrs if attrs.empty?
|
14
|
+
indent {
|
15
|
+
for attr in Array(attrs)
|
16
|
+
value = self.send(attr)
|
17
|
+
case value
|
18
|
+
when Array
|
19
|
+
puts "#{attr}:"
|
20
|
+
indent { value.each { |v| v.dump } }
|
21
|
+
when DbObject
|
22
|
+
puts "#{attr}: #{value.name} (#{value.class})"
|
23
|
+
else
|
24
|
+
puts "#{attr}: #{self.send(attr).inspect}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
data/meta_db.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "meta_db/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "meta_db"
|
8
|
+
spec.version = MetaDb::VERSION
|
9
|
+
spec.authors = ["Claus Rasmussen"]
|
10
|
+
spec.email = ["claus.l.rasmussen@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Meta model of database}
|
13
|
+
spec.description = %q{Reads in the information schema of a database and models it as ruby objects}
|
14
|
+
spec.homepage = "http://www.nowhere.com/meta_db"
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
17
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
20
|
+
else
|
21
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
22
|
+
"public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
26
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
27
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
28
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
35
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
37
|
+
|
38
|
+
spec.add_dependency "indented_io"
|
39
|
+
spec.add_dependency "pg"
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: meta_db
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Claus Rasmussen
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: indented_io
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Reads in the information schema of a database and models it as ruby objects
|
84
|
+
email:
|
85
|
+
- claus.l.rasmussen@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".rspec"
|
92
|
+
- ".ruby-version"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- TODO
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- lib/meta_db.rb
|
101
|
+
- lib/meta_db/connection.rb
|
102
|
+
- lib/meta_db/db_object.rb
|
103
|
+
- lib/meta_db/dump.rb
|
104
|
+
- lib/meta_db/version.rb
|
105
|
+
- meta_db.gemspec
|
106
|
+
homepage: http://www.nowhere.com/meta_db
|
107
|
+
licenses: []
|
108
|
+
metadata:
|
109
|
+
allowed_push_host: https://rubygems.org
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubygems_version: 3.0.8
|
126
|
+
signing_key:
|
127
|
+
specification_version: 4
|
128
|
+
summary: Meta model of database
|
129
|
+
test_files: []
|