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.
@@ -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