pg_meta 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,572 @@
1
+ require 'yaml'
2
+
3
+ module PgMeta
4
+ class Node
5
+ # Database object. Equal to self for Database objects
6
+ attr_reader :root
7
+
8
+ # Parent object. nil for Database objects
9
+ attr_reader :parent
10
+
11
+ # Unique name within parent context. This is usually what we understand as
12
+ # the name of an object but functions have the full signature as "name"
13
+ attr_reader :name
14
+
15
+ def uid()
16
+ @uid ||= [parent.uid, name].compact.join(".")
17
+ end
18
+
19
+ def guid()
20
+ @guid ||= [parent.guid, name].compact.join(".")
21
+ end
22
+
23
+ def initialize(parent, name)
24
+ @root = parent&.root || self
25
+ @parent = parent
26
+ @name = name
27
+ parent && parent.root.send(:add_node, self) # Add object to global database lookup
28
+ end
29
+
30
+ def to_h() raise StandardError, "Undefined method" end
31
+ def to_yaml() to_h.to_yaml end
32
+
33
+ def inspect() "#<#{self.class}:#{guid}>" end
34
+
35
+ protected
36
+ def attrs_to_h(*attrs)
37
+ h = {}
38
+ Array(attrs).flatten.each { |attr|
39
+ value = self.send(attr)
40
+ h[attr] =
41
+ case value
42
+ when Array
43
+ if value.first.is_a?(Symbol)
44
+ value
45
+ else
46
+ value.map(&:name)
47
+ end
48
+ when Hash
49
+ value.values.map { |element| element.to_h }
50
+ when Node
51
+ value.uid
52
+ else
53
+ value
54
+ end
55
+ }
56
+ h
57
+ end
58
+
59
+ private
60
+ def add_node(node) raise StandardError, "Undefined method" end
61
+ end
62
+
63
+ class Database < Node
64
+ # A database has no UID
65
+ def uid() nil end
66
+
67
+ # GUID is the name of the database
68
+ def guid() name end
69
+
70
+ # Owner of the database. Defined by #load_conn
71
+ attr_reader :owner
72
+
73
+ # Hash of schemas
74
+ attr_reader :schemas
75
+
76
+ # Hash of hidden schemas
77
+ attr_reader :hidden_schemas
78
+
79
+ # Redefine Node#parent
80
+ def parent() nil end
81
+
82
+ def initialize(name, owner = nil)
83
+ super(nil, name)
84
+ @owner = owner
85
+ @schemas = {}
86
+ @hidden_schemas = {}
87
+ @nodes = {}
88
+ end
89
+
90
+ # Lookup database object by UID
91
+ def [](uid) @nodes[uid] end
92
+
93
+ # Return true if the given UID exists
94
+ def exist?(uid) @nodes.key?(uid) end
95
+
96
+ # Make Database pretend to be an instance of the PgMeta module
97
+ def is_a?(klass) klass == PgMeta or super end
98
+
99
+ def to_h() attrs_to_h(:name, :owner, :schemas) end
100
+ def to_marshal() Marshal.dump(self) end
101
+
102
+ private
103
+ def add_node(node)
104
+ @nodes[node.uid] = node
105
+ end
106
+ end
107
+
108
+ class Schema < Node
109
+ # Database of the schema
110
+ alias_method :database, :parent
111
+
112
+ # Owner of the schema
113
+ attr_reader :owner
114
+
115
+ # Hash of tables _and_ views. TODO: Rename #relations
116
+ attr_reader :tables
117
+
118
+ # Hash of views
119
+ def views() @views ||= @tables.select { |_, table| table.view? } end
120
+
121
+ # Hash of functions
122
+ attr_reader :functions
123
+
124
+ # Hash of procedures
125
+ attr_reader :procedures
126
+
127
+ def initialize(database, name, owner, hidden: false)
128
+ super(database, name)
129
+ @owner = owner
130
+ @tables = {}
131
+ @functions = {}
132
+ @procedures = {}
133
+ @hidden = hidden
134
+ database.schemas[name] = self
135
+ end
136
+
137
+ # True if schema is hidden. This can be set dynamically
138
+ def hidden?() @hidden end
139
+
140
+ # Set hidden property
141
+ def hidden=(hide)
142
+ if @hidden
143
+ database.schemas[name] = database.hidden_schemas.delete(name) if !hide
144
+ else
145
+ database.hidden_schemas[name] = database.schemas.delete(name) if hide
146
+ end
147
+ @hidden = hide
148
+ end
149
+
150
+ def to_h() attrs_to_h(:name, :owner, :tables, :views, :functions, :procedures) end
151
+ end
152
+
153
+ class Table < Node
154
+ # Schema of the table
155
+ alias_method :schema, :parent
156
+
157
+ # True iff table is a real table and not a view
158
+ def table?() true end
159
+
160
+ # True iff table is a view
161
+ def view?() !table? end
162
+
163
+ # True iff table is a materialized view
164
+ def materialized?() false end
165
+
166
+ # True if the table/view is insertable
167
+ def insertable?() @is_insertable end
168
+
169
+ # True if the table/view is typed
170
+ def typed?() @is_typed end
171
+
172
+ # Hash of columns
173
+ attr_reader :columns
174
+
175
+ # The primary key column. nil if the table has multiple primary key columns
176
+ def primary_key_column
177
+ return @primary_key_column if @primary_key_column != :undefined
178
+ if primary_key_columns.size == 1
179
+ @primary_key_column = primary_key_columns.first
180
+ else
181
+ @primary_key_column = nil
182
+ end
183
+ end
184
+
185
+ # List of primary key columns
186
+ #
187
+ # Note: Assigned by PrimaryKeyConstraint#initialize
188
+ attr_reader :primary_key_columns
189
+
190
+ # Hash of all constraints
191
+ attr_reader :constraints
192
+
193
+ # List of primary key constraints (there is only one element)
194
+ attr_reader :primary_key_constraints
195
+
196
+ # Hash of unique constraints. Maps from constraint name to UniqueConstraint
197
+ # object. Note that because constraint names are unpredictable, you'll most
198
+ # often use +unqiue_constraints.values?
199
+ attr_reader :unique_constraints
200
+
201
+ # Hash of check constraints. Maps from constraint name to CheckConstraint
202
+ # object object
203
+ attr_reader :check_constraints
204
+
205
+ # Hash of referential constraints. Maps from constraint name to
206
+ # ReferentialConstraint object
207
+ attr_reader :referential_constraints
208
+
209
+ # Hash of triggers. Maps from trigger name to Trigger object
210
+ attr_reader :triggers
211
+
212
+ # List of tables that directly or indirectly depends on this table. Note
213
+ # that the tables are sorted by name to make testing in rspec easier
214
+ def depending_tables() @depending_tables ||= @depending_tables_hash.keys.sort_by(&:uid) end
215
+
216
+ # List of views that directly or indirectly depends on this table. This is
217
+ # the opposite of View#defining_tables. Note that the tables are sorted by
218
+ # name to make testing in rspec easier
219
+ def depending_views() @depending_views ||= @depending_views_hash.keys.sort_by(&:uid) end
220
+
221
+ def initialize(schema, name, is_insertable, is_typed)
222
+ super(schema, name)
223
+ @is_insertable = is_insertable
224
+ @is_typed = is_typed
225
+ @columns = {}
226
+ @primary_key_column = :undefined
227
+ @primary_key_columns = []
228
+ @constraints = {}
229
+ @primary_key_constraints = []
230
+ @unique_constraints = {}
231
+ @check_constraints = {}
232
+ @referential_constraints = {}
233
+ @triggers = {}
234
+ @depending_tables_hash = {}
235
+ @depending_views_hash = {}
236
+ schema.tables[name] = self
237
+ end
238
+
239
+ def to_h
240
+ attrs_to_h(
241
+ :name, :table?, :view?, :materialized?, :insertable?, :typed?,
242
+ :columns, :primary_key_columns, :constraints,
243
+ :referential_constraints, :depending_tables, :depending_views, :triggers)
244
+ end
245
+
246
+ protected
247
+ # Only non-empty for view objects
248
+ def defining_tables() [] end
249
+
250
+ private
251
+ # Accessed by MetaDb::load_conn
252
+ def add_depending_table(table)
253
+ @depending_tables = nil
254
+ @depending_tables_hash[table] = true
255
+ end
256
+
257
+ # Accessed by MetaDb::load_conn
258
+ def add_depending_view(view)
259
+ @depending_views = nil
260
+ @depending_views_hash[view] = true
261
+ end
262
+ end
263
+
264
+ class View < Table
265
+ # List of views and tables used directly in the definition of this view
266
+ attr_reader :defining_relations
267
+
268
+ # List of tables used directly or indirectly in the definition of this view
269
+ attr_reader :defining_tables
270
+
271
+ def table?() false end
272
+
273
+ def initialize(schema, name, is_insertable, is_typed)
274
+ super
275
+ @defining_relations = []
276
+ @defining_tables = []
277
+ end
278
+
279
+ def to_h()
280
+ h = super
281
+ h[:defining_relations] = defining_relations.map(&:uid)
282
+ h[:defining_tables] = defining_tables.map(&:uid)
283
+ h
284
+ end
285
+
286
+ private
287
+ # Used by MetaDb::load_conn
288
+ def add_defining_relation(relation)
289
+ @defining_relations << relation
290
+ @defining_tables << relation if relation.table?
291
+ end
292
+ end
293
+
294
+ class MaterializedView < View
295
+ def materialized?() true end
296
+ end
297
+
298
+ class Column < Node
299
+ # Table of the column
300
+ alias_method :table, :parent
301
+
302
+ # Ordinal number of the column
303
+ attr_reader :ordinal
304
+
305
+ # Type of the column
306
+ attr_reader :type
307
+
308
+ # Element type if type is an array and nil otherwise
309
+ attr_reader :element_type
310
+
311
+ # Number of dimensions if an array, 0 if not
312
+ attr_reader :dimensions
313
+
314
+ # Default value
315
+ attr_reader :default
316
+
317
+ # True if column is an identity column
318
+ def identity?() @is_identity end
319
+
320
+ # True if column is auto generated
321
+ def generated?() @is_generated end
322
+
323
+ # True if column is nullable
324
+ def nullable?() @is_nullable end
325
+
326
+ # True if column is updatable
327
+ def updatable?() @is_updatable end
328
+
329
+ # True if column is an array
330
+ def array?() !@element_type.nil? end
331
+
332
+ # True if column is unique (not that this information is not stored in the
333
+ # Column but in a table constraint)
334
+ def unique?()
335
+ table.unique_constraints.values.any? { |constraint| constraint.columns == [self] }
336
+ end
337
+
338
+ # True if column is the single primary key of the table and false otherwise. Always nil for tables
339
+ # with multiple primary keys
340
+ def primary_key?()
341
+ return nil if table.primary_key_columns.size != 1
342
+ table.table? && self == table.primary_key_column || false
343
+ end
344
+
345
+ # True is column is (part of) the primary key of the table
346
+ def primary_key_column?() table.table? && table.primary_key_columns.include?(self) end
347
+
348
+ # True if column is referencing other records. This includes columns in
349
+ # multi-column references
350
+ def reference?()
351
+ @reference ||= !references.empty?
352
+ end
353
+
354
+ # True if column is a single-column reference to another record and is of
355
+ # type varchar or text
356
+ def kind?()
357
+ @kind ||=
358
+ references.size == 1 &&
359
+ references.first.referencing_columns.size == 1 &&
360
+ !references.first.referenced_columns.first.primary_key?
361
+ end
362
+
363
+ # List of referential constraints that involve this column
364
+ def references
365
+ @references ||= begin
366
+ table.referential_constraints.values.select { |constraint|
367
+ constraint.referencing_columns.include?(self)
368
+ }
369
+ end
370
+ end
371
+
372
+ def initialize(
373
+ table, name, ordinal, type, element_type, dimensions, default,
374
+ is_identity, is_generated, is_nullable, is_updatable)
375
+ super(table, name)
376
+ @type, @element_type, @dimensions, @ordinal, @default, @is_identity,
377
+ @is_generated, @is_nullable, @is_updatable =
378
+ type, element_type, dimensions, ordinal, default, is_identity,
379
+ is_generated, is_nullable, is_updatable
380
+ table.columns[name] = self
381
+ end
382
+
383
+ def to_h()
384
+ attrs_to_h(
385
+ :name, :ordinal, :type, :element_type, :dimensions, :default, :identity?, :generated?,
386
+ :nullable?, :updatable?, :primary_key?, :reference?, :kind?)
387
+ end
388
+
389
+ # Compare columns by table and ordinal. FIXME: Compare by schema too (and what's with the 'super'?)
390
+ def <=>(other)
391
+ if other.is_a?(Column) && table == other.table
392
+ ordinal <=> other.ordinal
393
+ else
394
+ super
395
+ end
396
+ end
397
+ end
398
+
399
+ class Constraint < Node
400
+ # Table of the constraint
401
+ alias_method :table, :parent
402
+
403
+ # Constraint column. Raise an error if the constraint is multi-column
404
+ def column
405
+ columns.size == 1 or raise "Multicolumn constraint"
406
+ columns.first
407
+ end
408
+
409
+ # List of columns in the constraint. Empty for CheckConstraint objects
410
+ # except not null constraints
411
+ attr_reader :columns
412
+
413
+ # Constraint kind. Either :primary_key, :unique, :check, or :referential
414
+ def kind() CONSTRAINT_KINDS[self.class] end
415
+
416
+ def initialize(table, name, columns)
417
+ super(table, name)
418
+ @table = table
419
+ @columns = columns
420
+ table.constraints[name] = self
421
+ end
422
+
423
+ def to_h() attrs_to_h(:name, :kind, :columns) end
424
+ end
425
+
426
+ class PrimaryKeyConstraint < Constraint
427
+ def initialize(table, name, columns)
428
+ super
429
+ columns.each { |c| c.table.primary_key_columns << c }
430
+ table.primary_key_constraints << self
431
+ end
432
+ end
433
+
434
+ class UniqueConstraint < Constraint
435
+ def initialize(table, name, columns)
436
+ super
437
+ table.unique_constraints[name] = self
438
+ end
439
+ end
440
+
441
+ # Note that #columns is always empty for check constraints
442
+ class CheckConstraint < Constraint
443
+ # SQL check expression
444
+ attr_reader :expression
445
+
446
+ # Half-baked SQL-to-ruby expression transpiler
447
+ def ruby_expression # Very simple
448
+ @ruby ||= sql.sub(/\((.*)\)/, "\\1").gsub(/\((\w+) IS NOT NULL\)/, "!\\1.nil?").gsub(/ OR /, " || ")
449
+ end
450
+
451
+ # True if this is a not-null check constraint. In that case, #columns will
452
+ # contain the column which is otherwise empty for CheckConstraint objects.
453
+ # Note that the column is not registered directly in Postgres meta tables
454
+ # and have to be parsed from the expression
455
+ def not_null?() !columns.empty? end
456
+
457
+ def initialize(table, name, expression)
458
+ super(table, name, [])
459
+ @expression = expression
460
+ table.check_constraints[name] = self
461
+ if @expression =~ /^\(\((\S+) IS NOT NULL\)\)$/
462
+ columns = [table.columns[$1]]
463
+ else
464
+ columns = []
465
+ end
466
+ super(table, name, columns)
467
+ end
468
+
469
+ def to_h() attrs_to_h(:name, :kind, :expression) end
470
+ end
471
+
472
+ class ReferentialConstraint < Constraint
473
+ # The referencing table
474
+ alias_method :referencing_table, :table
475
+
476
+ # The referencing columns. Can't be empty
477
+ alias_method :referencing_columns, :columns
478
+
479
+ # The referenced constraint
480
+ attr_reader :referenced_constraint
481
+
482
+ # The referenced table
483
+ def referenced_table() referenced_constraint.table end
484
+
485
+ # The referenced columns
486
+ def referenced_columns() referenced_constraint.columns end
487
+
488
+ def initialize(referencing_table, name, referencing_columns, referenced_constraint)
489
+ super(referencing_table, name, referencing_columns)
490
+ @referenced_constraint = referenced_constraint
491
+ table.referential_constraints[name] = self
492
+ end
493
+
494
+ def to_h() attrs_to_h(:name, :kind, :referencing_columns, :referenced_constraint) end
495
+ end
496
+
497
+ class Function < Node
498
+ # Schema of the function
499
+ alias_method :schema, :parent
500
+
501
+ # Owner of the function
502
+ attr_reader :owner
503
+
504
+ # Security (:definer or :invoker)
505
+ attr_reader :security
506
+
507
+ # True if security is 'definer'
508
+ def suid?() security == 'definer' end
509
+
510
+ # True if this is a function
511
+ def function?() true end
512
+
513
+ # True if this is a procedure
514
+ def procedure?() !function? end
515
+
516
+ def initialize(schema, name, owner, security)
517
+ super(schema, name)
518
+ @owner = owner
519
+ @security = security.to_sym
520
+ if function?
521
+ schema.functions[name] = self
522
+ else
523
+ schema.procedures[name] = self
524
+ end
525
+ end
526
+
527
+ def to_h() attrs_to_h(:name, :owner, :security, :function?) end
528
+ end
529
+
530
+ class Procedure < Function
531
+ def function?() false end
532
+ end
533
+
534
+ class Trigger < Node
535
+ # Table of trigger
536
+ alias_method :table, :parent
537
+
538
+ # Trigger function
539
+ attr_reader :function
540
+
541
+ # Trigger level (:stmt or :row)
542
+ attr_reader :level
543
+
544
+ # When trigger is fired (:before, :after, or :instead)
545
+ attr_reader :timing
546
+
547
+ # Array of events (:insert, :update, :delete, or :truncate) causing the trigger to fire
548
+ attr_reader :events
549
+
550
+ # Note that trigger names have a '()' suffixed. This avoid namespace
551
+ # collisions with field names
552
+ def initialize(table, name, function, level, timing, events)
553
+ super(table, name)
554
+ @name = "#{name}()"
555
+ @function = function
556
+ @level = level.to_sym
557
+ @timing = timing.to_sym
558
+ @events = events
559
+ table.triggers[@name] = self
560
+ end
561
+
562
+ def to_h() attrs_to_h(:name, :function, :level, :timing, :events) end
563
+ end
564
+
565
+ CONSTRAINT_KINDS = {
566
+ PrimaryKeyConstraint => :primary_key,
567
+ UniqueConstraint => :unique,
568
+ CheckConstraint => :check,
569
+ ReferentialConstraint => :referential
570
+ }
571
+ end
572
+
@@ -0,0 +1,3 @@
1
+ module PgMeta
2
+ VERSION = "0.1.0"
3
+ end
data/lib/pg_meta.rb ADDED
@@ -0,0 +1,43 @@
1
+
2
+ require "pg"
3
+ require "pg_conn"
4
+
5
+ require "ext/hash.rb"
6
+
7
+ require "pg_meta/version.rb"
8
+ require "pg_meta/meta.rb"
9
+ require "pg_meta/load_conn.rb"
10
+ require "pg_meta/load_yaml.rb"
11
+ require "pg_meta/dump.rb"
12
+
13
+ module PgMeta
14
+ class Error < StandardError; end
15
+
16
+ # :call-seq:
17
+ # initialize(pg_conn_connection)
18
+ # initialize(*pg_conn_initializers)
19
+ #
20
+ # Initialize a Database object
21
+ #
22
+ def self.new(*args)
23
+ Database.load_conn(PgConn.ensure(*args))
24
+ end
25
+
26
+ # Load data from a YAML object
27
+ def self.load_yaml(yaml)
28
+ Database.load_yaml(yaml)
29
+ end
30
+
31
+ # Load data from a YAML file
32
+ def self.load_file(file)
33
+ load_yaml(YAML.load(IO.read file))
34
+ end
35
+
36
+ def self.load_marshal(file)
37
+ Marshal.load(IO.read file)
38
+ end
39
+
40
+ # Make the PgMeta module pretend to have PgMeta::Database object instances
41
+ def self.===(element) element.is_a?(PgMeta::Database) or super end
42
+ end
43
+
data/pg_meta.gemspec ADDED
@@ -0,0 +1,50 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "pg_meta/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pg_meta"
8
+ spec.version = PgMeta::VERSION
9
+ spec.authors = ["Claus Rasmussen"]
10
+ spec.email = ["claus.l.rasmussen@gmail.com"]
11
+
12
+ spec.summary = %q{pg_meta gem}
13
+ spec.description = %q{pg_meta gem}
14
+ spec.homepage = "http://www.nowhere.com/"
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
+ #
20
+ # spec.metadata["homepage_uri"] = spec.homepage
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against " \
23
+ # "public gem pushes."
24
+ # end
25
+
26
+ # Specify which files should be added to the gem when it is released.
27
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
29
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency "rake"
37
+ spec.add_development_dependency "rspec"
38
+
39
+ # spec.add_dependency GEM [, VERSION]
40
+ spec.add_dependency "indented_io"
41
+ spec.add_dependency "pg"
42
+ spec.add_dependency "pg_conn", "0.2.1"
43
+ spec.add_dependency "shellopts", "2.0.6"
44
+
45
+ # spec.add_development_dependency GEM [, VERSION]
46
+
47
+ # Also un-comment in spec/spec_helper to use simplecov
48
+ # spec.add_development_dependency "simplecov"
49
+
50
+ end