meta_db 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1 @@
1
+ ruby-2.6.6
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.1
7
+ before_install: gem install bundler -v 1.16.5
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in meta_db.gemspec
6
+ gemspec
@@ -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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
+
@@ -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
+
@@ -0,0 +1,3 @@
1
+ module MetaDb
2
+ VERSION = "0.1.0"
3
+ end
@@ -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: []