pg_graph 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/README.md +36 -0
- data/Rakefile +6 -0
- data/TODO +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/diagram.drawio +1 -0
- data/exe/pg_graph +152 -0
- data/lib/data/association.rb +98 -0
- data/lib/data/data.rb +551 -0
- data/lib/data/dimension.rb +51 -0
- data/lib/data/read.rb +44 -0
- data/lib/data/render.rb +237 -0
- data/lib/data/value.rb +96 -0
- data/lib/ext/meta.rb +56 -0
- data/lib/ext/module.rb +18 -0
- data/lib/pg_graph/inflector.rb +105 -0
- data/lib/pg_graph/reflector.rb +187 -0
- data/lib/pg_graph/timer.rb +119 -0
- data/lib/pg_graph/version.rb +3 -0
- data/lib/pg_graph.rb +124 -0
- data/lib/type/dump_type.rb +69 -0
- data/lib/type/read.rb +269 -0
- data/lib/type/type.rb +617 -0
- data/pg_graph.gemspec +40 -0
- data/snippets/1-1.sql +19 -0
- data/snippets/N-M.sql +24 -0
- data/snippets/dag.sql +19 -0
- data/snippets/db.sql +52 -0
- data/snippets/kind.sql +19 -0
- data/snippets/recur.sql +14 -0
- metadata +205 -0
data/lib/data/data.rb
ADDED
@@ -0,0 +1,551 @@
|
|
1
|
+
require 'constrain'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
include Constrain
|
5
|
+
|
6
|
+
module PgGraph::Data
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class Node
|
10
|
+
# Type of the node (a PgGraph::Type::Node object)
|
11
|
+
attr_reader :type
|
12
|
+
|
13
|
+
# Type of the value of a node
|
14
|
+
def value_type() type end
|
15
|
+
|
16
|
+
# Dimension of the node
|
17
|
+
attr_reader :dimension
|
18
|
+
|
19
|
+
def initialize(type, dimension: nil)
|
20
|
+
constrain type, PgGraph::Type::Node
|
21
|
+
Dimension.validate(dimension) if dimension
|
22
|
+
@type = type
|
23
|
+
@dimension = dimension
|
24
|
+
end
|
25
|
+
|
26
|
+
# Node converted to plain ruby objects structured as given by #dimension.
|
27
|
+
# This is the same as #value except for M:M associations
|
28
|
+
def data() @impl end
|
29
|
+
|
30
|
+
# Node converted to plain ruby objects structured as given by the dimension
|
31
|
+
# kind but with flattened M:M associations so duplicates can happen
|
32
|
+
def value() data end
|
33
|
+
|
34
|
+
# Usually the node itself but associations redefines this to be the
|
35
|
+
# associated object. It is used in client code to reference the value
|
36
|
+
# object of an associated field, ie: +node.object.value()+ will give you the
|
37
|
+
# value of a field even if the field is an association
|
38
|
+
def object() self end
|
39
|
+
|
40
|
+
def inspect(payload = inspect_inner)
|
41
|
+
"#<#{self.class}:#{uid}#{payload ? " #{payload}" : ""}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h() raise NotThis end
|
45
|
+
def to_yaml() raise NotThis end
|
46
|
+
|
47
|
+
protected
|
48
|
+
def inspect_inner() nil end
|
49
|
+
end
|
50
|
+
|
51
|
+
class DatabaseObject < Node
|
52
|
+
# The database (Data::Database) of this objectD. It is defined "globally"
|
53
|
+
# because #dot requires quick access to the containing database
|
54
|
+
def database() raise NotThis end
|
55
|
+
|
56
|
+
# Unique id within a database. Note that #uid has to be defined before
|
57
|
+
# #initialize is called as it uses the UID as a key in the database-wide
|
58
|
+
# lookup table
|
59
|
+
def uid() type.uid end
|
60
|
+
|
61
|
+
# Global unique id including database
|
62
|
+
def guid() type.guid end
|
63
|
+
|
64
|
+
# Name of object. Defaults to the name of the type
|
65
|
+
def name() type.name end
|
66
|
+
|
67
|
+
def initialize(*args, **opts)
|
68
|
+
super
|
69
|
+
database.send(:add_object, self)
|
70
|
+
end
|
71
|
+
|
72
|
+
def dot(path) database.dot["#{uid}.#{path}"] end
|
73
|
+
# def ==(other) raise PgGraph::NotThis end
|
74
|
+
def <=>(other) uid <=> other.uid end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Database < DatabaseObject
|
78
|
+
# Redefine DatabaseObject#database to return self
|
79
|
+
def database() self end
|
80
|
+
|
81
|
+
# List of Schema objects
|
82
|
+
def schemas() @impl.values end
|
83
|
+
|
84
|
+
# +data_source+ can be a PgConn object, a Hash, or nil
|
85
|
+
def initialize(type, data_source = nil)
|
86
|
+
constrain type, PgGraph::Type::Database
|
87
|
+
constrain data_source, PgConn, Hash, NilClass
|
88
|
+
@queued_objects = [] # has to go before super
|
89
|
+
@objects = nil # Object cache. Maps from UID to object. Has to go before super
|
90
|
+
super(type)
|
91
|
+
initialize_impl
|
92
|
+
read(data_source) if data_source
|
93
|
+
end
|
94
|
+
|
95
|
+
def dup() Database.new(type, to_h) end
|
96
|
+
|
97
|
+
# Make Database pretend it is a PgGraph::Data object
|
98
|
+
def is_a?(klass) klass == PgGraph::Type or super end
|
99
|
+
|
100
|
+
# Get schema by name
|
101
|
+
forward_to :@impl, :[]
|
102
|
+
|
103
|
+
# Iterate schemas
|
104
|
+
def each(&block) schemas.each { |schema| yield schema } end
|
105
|
+
|
106
|
+
# Return database as a hash from schema name to schema hash
|
107
|
+
def data() @impl.map { |k,v| [k, v.data] }.to_h end
|
108
|
+
|
109
|
+
# Return object by the given UID
|
110
|
+
def dot(uid) @objects ||= map_objects[uid] end
|
111
|
+
|
112
|
+
# TODO: Maybe use this instead of #dot
|
113
|
+
def lookup(path, id = nil)
|
114
|
+
object = (@objects ||= map_objects)[path]
|
115
|
+
id.nil? ? object : object[id]
|
116
|
+
end
|
117
|
+
|
118
|
+
def ==(other) to_h == other.to_h end
|
119
|
+
|
120
|
+
alias_method :to_h, :data
|
121
|
+
|
122
|
+
# The #to_yaml format is like #to_h but with records formatted as an array
|
123
|
+
# instead of as a map from ID to record
|
124
|
+
def to_yaml() @impl.map { |k,v| [k, v.to_yaml] }.to_h end
|
125
|
+
|
126
|
+
# Note that #to_sql in derived classes should be deleted FIXME
|
127
|
+
def to_sql(format: :sql, ids: {}, delete: :all, files: [])
|
128
|
+
render = SqlRender.new(self, format, ids: ids, delete: delete, files: files)
|
129
|
+
render.to_s
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_exec_sql(ids: {}, delete: :all)
|
133
|
+
to_sql(format: :exec, ids: ids, delete: delete)
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_psql_sql(files = [], ids: {}, delete: :all)
|
137
|
+
to_sql(format: :psql, ids: ids, delete: delete, files: files)
|
138
|
+
end
|
139
|
+
|
140
|
+
# :call-seq:
|
141
|
+
# read(connection)
|
142
|
+
# read(hash)
|
143
|
+
# read(yaml)
|
144
|
+
#
|
145
|
+
# Read data from a source. Returns self
|
146
|
+
#
|
147
|
+
def read(arg)
|
148
|
+
constrain arg, PgConn, Hash
|
149
|
+
case arg
|
150
|
+
when PgConn; read_connection(arg)
|
151
|
+
when Hash; read_hash(arg)
|
152
|
+
end
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
# Write data to database. +ids+ is a hash from table UID to id. If a record
|
157
|
+
# has an ID equal to or less than the corresponding ID in +ids+, the SQL
|
158
|
+
# will be rendered as an 'update' statement instead of an 'insert'
|
159
|
+
# statement. Returns self
|
160
|
+
def write(connection, ids: {}, delete: :all)
|
161
|
+
constrain connection, PgConn
|
162
|
+
constrain ids, String => Integer
|
163
|
+
connection.exec(to_exec_sql(ids: ids, delete: :all))
|
164
|
+
self
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
# Sets up a database with empty tables
|
169
|
+
def initialize_impl
|
170
|
+
@impl = {} # Hash from schema name to Schema object
|
171
|
+
type.schemas.each { |schema| @impl[schema.name] = Schema.new(self, schema) }
|
172
|
+
schemas.each { |schema| schema.tables.each { |table| table.initialize_associations } }
|
173
|
+
end
|
174
|
+
|
175
|
+
def add_object(object)
|
176
|
+
@queued_objects << object
|
177
|
+
end
|
178
|
+
|
179
|
+
def map_objects()
|
180
|
+
r = @queued_objects.map { |object| [object.uid, object] }.to_h
|
181
|
+
@queued_objects = []
|
182
|
+
r
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
class Schema < DatabaseObject
|
187
|
+
attr_reader :database
|
188
|
+
|
189
|
+
# List of Table objects
|
190
|
+
def tables() @impl.values end
|
191
|
+
|
192
|
+
def initialize(database, type)
|
193
|
+
constrain database, Database
|
194
|
+
constrain type, PgGraph::Type::Schema
|
195
|
+
@database = database
|
196
|
+
super(type)
|
197
|
+
@impl = {} # A hash from table name to Table object
|
198
|
+
type.tables.each { |table| @impl[table.name] = Table.new(self, table) }
|
199
|
+
end
|
200
|
+
|
201
|
+
# Get table by name
|
202
|
+
forward_to :@impl, :[]
|
203
|
+
|
204
|
+
# Iterate tables
|
205
|
+
def each(&block) tables.each { |table| yield table } end
|
206
|
+
|
207
|
+
# Return schema as a hash from table name to table hash
|
208
|
+
def data() @impl.map { |k,v| [k, v.data] }.to_h end
|
209
|
+
|
210
|
+
alias_method :to_h, :data
|
211
|
+
# def to_sql()
|
212
|
+
# tables.select { |table| !table.empty? }.map(&:to_sql).join("\n")
|
213
|
+
# end
|
214
|
+
def to_yaml() @impl.map { |k,v| [k, v.to_yaml] }.to_h end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Note that Query objects have the same name as the referenced table but
|
218
|
+
# different UID. Queries are not included in the schemas list of tables
|
219
|
+
class Table < DatabaseObject
|
220
|
+
# Redefine DatabaseObject#database
|
221
|
+
def database() schema.database end
|
222
|
+
|
223
|
+
# Schema of table
|
224
|
+
attr_reader :schema
|
225
|
+
|
226
|
+
# Cached association objects. Map from record or table field name to
|
227
|
+
# Association object. Associations are cached in Table because they have
|
228
|
+
# non-trivial constructors. Initialized by #initialize_associations
|
229
|
+
attr_reader :associations
|
230
|
+
|
231
|
+
# List of Record objects (including duplicates)
|
232
|
+
def records() @impl.values end
|
233
|
+
|
234
|
+
def initialize(schema, type, dimension: 2)
|
235
|
+
constrain schema, Schema
|
236
|
+
constrain type, PgGraph::Type::Table
|
237
|
+
@schema = schema
|
238
|
+
super(type, dimension: dimension)
|
239
|
+
@impl = {} # A table is implemented as a hash from integer ID to Record object
|
240
|
+
@associations = {} # initialized by #initialize_associations
|
241
|
+
end
|
242
|
+
|
243
|
+
def initialize_associations
|
244
|
+
db = schema.database
|
245
|
+
type.fields.each { |column|
|
246
|
+
next if column.is_a?(PgGraph::Type::SimpleColumn)
|
247
|
+
that_table = db[column.type.schema.name][column.type.table.name]
|
248
|
+
association =
|
249
|
+
case column
|
250
|
+
when PgGraph::Type::KindRecordColumn
|
251
|
+
KindAssociation.new(1,
|
252
|
+
self, that_table,
|
253
|
+
column.this_link_column.to_sym,
|
254
|
+
column.that_link_column.to_sym)
|
255
|
+
when PgGraph::Type::RecordColumn
|
256
|
+
Association.new(1,
|
257
|
+
self, that_table,
|
258
|
+
column.this_link_column.to_sym,
|
259
|
+
column.that_link_column.to_sym)
|
260
|
+
when PgGraph::Type::NmTableColumn, PgGraph::Type::MmTableColumn
|
261
|
+
dimension = column.is_a?(PgGraph::Type::NmTableColumn) ? 2 : 3
|
262
|
+
nm_table = db[column.mm_table.schema.name][column.mm_table.name]
|
263
|
+
LinkAssociation.new(dimension,
|
264
|
+
self, that_table, nm_table,
|
265
|
+
column.this_link_column.to_sym, column.this_mm_column.to_sym,
|
266
|
+
column.that_mm_column.to_sym, column.that_link_column.to_sym)
|
267
|
+
when PgGraph::Type::TableColumn
|
268
|
+
Association.new(2,
|
269
|
+
self, that_table,
|
270
|
+
column.this_link_column.to_sym,
|
271
|
+
column.that_link_column.to_sym)
|
272
|
+
else
|
273
|
+
raise NotHere
|
274
|
+
end
|
275
|
+
@associations[column.name.to_sym] = association
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
# Number of records in the table (including duplicates)
|
280
|
+
def size() records.size end
|
281
|
+
|
282
|
+
# True if table is empty
|
283
|
+
def empty?() @impl.empty? end
|
284
|
+
|
285
|
+
# Return record with the given ID
|
286
|
+
def [](id) @impl[id] end
|
287
|
+
|
288
|
+
# True if the table contains a record with the given ID
|
289
|
+
def id?(id) @impl.key?(id) end
|
290
|
+
|
291
|
+
# List of record IDs (excluding duplicates)
|
292
|
+
def ids() @impl.keys end
|
293
|
+
|
294
|
+
# Max. record ID. Equal to 0 if the table is empty
|
295
|
+
def max_id() @max_id ||= (@impl.keys.max || 0) end
|
296
|
+
|
297
|
+
# :call-seq:
|
298
|
+
# select(id, &block)
|
299
|
+
# select(ids, &block)
|
300
|
+
#
|
301
|
+
# Select a number of records given by an ID or an array of IDs. Returns a
|
302
|
+
# Record object in the first case and a TableSelect otherwise. The
|
303
|
+
# :dimension option can be set to 3 to get a MmTableSelect object
|
304
|
+
# instead
|
305
|
+
#
|
306
|
+
def select(arg = nil, dimension: nil, &block)
|
307
|
+
constrain arg, Integer, [Integer], NilClass
|
308
|
+
constrain dimension, Integer, NilClass
|
309
|
+
case arg
|
310
|
+
when Integer
|
311
|
+
candidates = [self[arg]]
|
312
|
+
dimension ||= 1
|
313
|
+
when Array
|
314
|
+
candidates = arg.map { |id| self[id] }.compact
|
315
|
+
dimension ||= 2
|
316
|
+
when NilClass
|
317
|
+
candidates = records
|
318
|
+
dimension ||= 2
|
319
|
+
end
|
320
|
+
candidates = candidates.select { |record| yield(record) } if block_given?
|
321
|
+
Dimension.value_class(dimension).new(self, candidates)
|
322
|
+
end
|
323
|
+
|
324
|
+
# Iterate records
|
325
|
+
def each(&block) @impl.values.each { |record| yield record } end
|
326
|
+
|
327
|
+
# Return table as a hash from record ID to record hash
|
328
|
+
def data() @impl.map { |k,v| [k, v.data] }.to_h end
|
329
|
+
|
330
|
+
alias_method :to_h, :data
|
331
|
+
|
332
|
+
def to_yaml() @impl.map { |k,v| v.to_h } end
|
333
|
+
|
334
|
+
def inspect_inner() "[" + records.map(&:inspect_inner).join(", ") + "])" end
|
335
|
+
|
336
|
+
protected
|
337
|
+
def add_record(record)
|
338
|
+
!@impl.key?(record.id) or raise "Duplicate record ID: #{record.id}"
|
339
|
+
@impl[record.id] = record
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
class Record < DatabaseObject
|
344
|
+
# Redefine UID to include ID of record
|
345
|
+
def uid() "#{table.uid}[#{id}]" end
|
346
|
+
|
347
|
+
def self.split_uid(uid)
|
348
|
+
uid =~ /^(.*?)\[(\d+)\]$/
|
349
|
+
[$1, $2.to_i]
|
350
|
+
end
|
351
|
+
|
352
|
+
# Redefine GUID to include ID of record
|
353
|
+
def guid() "#{table.guid}[#{id}]" end
|
354
|
+
|
355
|
+
# Redefine name to be the record ID"
|
356
|
+
def name() id end
|
357
|
+
|
358
|
+
# Redefine DatabaseObject#database
|
359
|
+
def database() table.database end
|
360
|
+
|
361
|
+
# Table of record
|
362
|
+
attr_reader :table
|
363
|
+
|
364
|
+
# ID of record (Integer)
|
365
|
+
def id() @impl[:id].value end
|
366
|
+
# def id() @id end
|
367
|
+
|
368
|
+
def initialize(table, columns)
|
369
|
+
constrain table, Table
|
370
|
+
constrain columns, Symbol => PgGraph::RUBY_CLASSES
|
371
|
+
constrain columns, lambda { |h| h.key?(:id) }, "No :id field"
|
372
|
+
@table = table # has to go before super
|
373
|
+
super(table.type.record_type, dimension: 1)
|
374
|
+
@impl = columns.map { |k,v| [k, Column.new(self, k, v)] }.to_h
|
375
|
+
for name, association in table.associations
|
376
|
+
if association.is_a?(KindAssociation) #&& @impl.key?(name)
|
377
|
+
next if !@impl.key?(name)
|
378
|
+
column = @impl.delete(name)
|
379
|
+
@impl[name] = KindRecordField.new(self, name, association, column)
|
380
|
+
else
|
381
|
+
!@impl.key?(name) or raise "Duplicate field: #{name}"
|
382
|
+
field_class = Dimension.field_class(association.dimension)
|
383
|
+
@impl[name] = field_class.new(self, name, association)
|
384
|
+
end
|
385
|
+
end
|
386
|
+
# puts "Record#initialize(#{table.uid.inspect}, #{columns.inspect})"
|
387
|
+
# puts " columns: #{self.columns}"
|
388
|
+
# puts " fields: #{fields}"
|
389
|
+
# puts " names: #{names}"
|
390
|
+
# puts " value_columns: #{value_columns}"
|
391
|
+
table.send(:add_record, self)
|
392
|
+
end
|
393
|
+
|
394
|
+
# Return Field object with the given name
|
395
|
+
def [](name) @impl[name.to_sym] end
|
396
|
+
|
397
|
+
# True if the record contains a field with the given name
|
398
|
+
def field?(name) @impl.key?(name.to_sym) end
|
399
|
+
|
400
|
+
# List of field names
|
401
|
+
def names() @impl.keys end
|
402
|
+
|
403
|
+
# List of Field objects in the record
|
404
|
+
def fields() @impl.values end
|
405
|
+
|
406
|
+
# List of Column objects in the record
|
407
|
+
def columns() @impl.values.select { |field| field.is_a?(Column) } end
|
408
|
+
|
409
|
+
# List of columns excluding generated columns
|
410
|
+
def value_columns()
|
411
|
+
columns.select { |column| !column.type.generated? }.uniq
|
412
|
+
end
|
413
|
+
|
414
|
+
# List of association objects in the record
|
415
|
+
def associations() @impl.values.select { |field| field.is_a?(AssociationField) } end
|
416
|
+
|
417
|
+
# Iterate fields
|
418
|
+
def each(&block) fields.each { |field| yield field } end
|
419
|
+
|
420
|
+
# Return data of record as a hash of from column name to value. Note that
|
421
|
+
# association fields are not included
|
422
|
+
# def data() @impl.select { |k,v| v.is_a?(Column) }.map { |k,v| [k, v.data] }.to_h end
|
423
|
+
|
424
|
+
def data()
|
425
|
+
@impl.select { |k,v| v.is_a?(Column) || v.is_a?(KindRecordField) }.map { |k,v| [k, v.data] }.to_h
|
426
|
+
end
|
427
|
+
|
428
|
+
alias_method :to_h, :data
|
429
|
+
|
430
|
+
# FIXME: Is this in use?
|
431
|
+
# def to_sql()
|
432
|
+
# "(" + type.columns.map { |column_type|
|
433
|
+
# field?(column_type.name) ? self[column_type.name]&.to_sql || 'NULL' : 'DEFAULT'
|
434
|
+
# }.join(", ") + ")"
|
435
|
+
# end
|
436
|
+
|
437
|
+
def to_yaml() data end
|
438
|
+
|
439
|
+
def inspect_inner() "{" + columns.map(&:inspect_inner).join(', ') + "}" end
|
440
|
+
end
|
441
|
+
|
442
|
+
class Field < DatabaseObject
|
443
|
+
# Redefine #uid
|
444
|
+
def uid() "#{record.uid}.#{name}" end
|
445
|
+
|
446
|
+
# Redefine #uid
|
447
|
+
def guid() "#{record.guid}.#{name}" end
|
448
|
+
|
449
|
+
# Redefine #name to return a Symbol
|
450
|
+
def name() super.to_sym end
|
451
|
+
|
452
|
+
# Redefine DatabaseObject#database
|
453
|
+
def database() record.database end
|
454
|
+
|
455
|
+
# Record of field
|
456
|
+
attr_reader :record
|
457
|
+
|
458
|
+
def initialize(record, name, **opts)
|
459
|
+
constrain record, Record
|
460
|
+
constrain name, String, Symbol
|
461
|
+
@record = record # has to go before super
|
462
|
+
super(record.type[name.to_s], **opts)
|
463
|
+
end
|
464
|
+
|
465
|
+
def to_h() { name: value } end
|
466
|
+
end
|
467
|
+
|
468
|
+
class Column < Field
|
469
|
+
def initialize(record, name, value)
|
470
|
+
constrain value, *PgGraph::RUBY_CLASSES
|
471
|
+
super(record, name, dimension: 0)
|
472
|
+
@impl = value
|
473
|
+
end
|
474
|
+
|
475
|
+
# TODO: Escape sequences
|
476
|
+
# FIXME: Not in use (and WRONG!)
|
477
|
+
def to_sql
|
478
|
+
$stderr.puts "* SHOULDN'T BE CALLED IN DATA.RB **************************"
|
479
|
+
raise "Oops"
|
480
|
+
if value.nil?
|
481
|
+
'NULL'
|
482
|
+
else
|
483
|
+
if type.ruby_class <= Numeric
|
484
|
+
value
|
485
|
+
elsif type.ruby_class <= Array
|
486
|
+
if value.empty?
|
487
|
+
"'{}'"
|
488
|
+
else
|
489
|
+
"'{'#{value.join("', '")}'}"
|
490
|
+
end
|
491
|
+
else
|
492
|
+
"'#{PG::Connection.escape_string(value.to_s)}'"
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
def inspect() super value.inspect end
|
498
|
+
def inspect_inner() "#{name}: #{value.is_a?(String) ? "'#{value}'" : value.inspect}" end
|
499
|
+
end
|
500
|
+
|
501
|
+
class AssociationField < Field
|
502
|
+
forward_to :association, :dimension
|
503
|
+
|
504
|
+
# The table of the associated record(s)
|
505
|
+
def table() association.that_table end
|
506
|
+
|
507
|
+
# The association object
|
508
|
+
attr_reader :association
|
509
|
+
|
510
|
+
def initialize(record, name, association)
|
511
|
+
constrain association, Association
|
512
|
+
super(record, name, dimension: association.dimension)
|
513
|
+
@association = association
|
514
|
+
end
|
515
|
+
|
516
|
+
# Return the value of the association. This is either a Record, TableValue,
|
517
|
+
# or MmTableValue depending on the dimension of the association
|
518
|
+
def object()
|
519
|
+
@object ||= Dimension.value_class(dimension).new(table, association.get_records(record))
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
class RecordField < AssociationField
|
524
|
+
forward_to :object, :id, :[], :field?, :names, :fields, :columns, :associations, :value, :data, :to_h
|
525
|
+
end
|
526
|
+
|
527
|
+
class KindRecordField < RecordField
|
528
|
+
def initialize(record, name, association, column)
|
529
|
+
constrain column, Column
|
530
|
+
super(record, name, association)
|
531
|
+
@column = column
|
532
|
+
end
|
533
|
+
|
534
|
+
def value_type() @column.value_type end
|
535
|
+
def value() @column.value end
|
536
|
+
def data() @column.data end
|
537
|
+
end
|
538
|
+
|
539
|
+
class TableField < AssociationField
|
540
|
+
forward_to :object, :schema, :records, :size, :empty?, :[], :id?, :ids, :value, :data, :associations
|
541
|
+
end
|
542
|
+
|
543
|
+
class MmTableField < AssociationField
|
544
|
+
forward_to :object, :schema, :records, :size, :empty?, :[], :id?, :ids, :value, :data, :associations, :count
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
|
549
|
+
|
550
|
+
|
551
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module PgGraph::Data
|
2
|
+
# Dimension of the node (corresponding kind shown in parenthesis):
|
3
|
+
#
|
4
|
+
# 0 A single value (:value)
|
5
|
+
# 1 A single record (:record)
|
6
|
+
# 2 A map from id to record (:table)
|
7
|
+
# 3 A map from id to array of duplicate records (:table)
|
8
|
+
#
|
9
|
+
# The dimension defines class and structures of GraphData objects
|
10
|
+
#
|
11
|
+
class Dimension
|
12
|
+
def self.valid?(dimension, min: 0)
|
13
|
+
dimension.is_a?(Integer) && (min..3).include?(dimension)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.kind(dimension)
|
17
|
+
case dimension
|
18
|
+
when 0; :value
|
19
|
+
when 1; :record
|
20
|
+
when 2, 3; :table
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.field_class(dimension)
|
25
|
+
validate(dimension)
|
26
|
+
case dimension
|
27
|
+
when 0; Column
|
28
|
+
when 1; RecordField
|
29
|
+
when 2; TableField
|
30
|
+
when 3; MmTableField
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.value_class(dimension)
|
35
|
+
validate(dimension, min: 1)
|
36
|
+
case dimension
|
37
|
+
when 1; RecordValue
|
38
|
+
when 2; TableValue
|
39
|
+
when 3; MmTableValue
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.validate(dim, min: 0, unwind: 1)
|
44
|
+
constrain dim, Integer, unwind: unwind
|
45
|
+
constrain dim,
|
46
|
+
lambda { |d| Dimension.valid?(d, min: min) },
|
47
|
+
"Out of range value: #{dim}",
|
48
|
+
unwind: unwind
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/data/read.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
require 'pg_conn'
|
3
|
+
|
4
|
+
module PgGraph::Data
|
5
|
+
class Database
|
6
|
+
# load data from connection
|
7
|
+
def read_connection(conn)
|
8
|
+
constrain conn, PgConn::Connection
|
9
|
+
initialize_impl
|
10
|
+
for schema in type.schemas
|
11
|
+
for table in schema.tables
|
12
|
+
for record in conn.records("select * from #{table.uid}")
|
13
|
+
Record.new(self[schema.name][table.name], record)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
# load data from hash. The hash can be produced by #to_h or #to_yaml
|
21
|
+
#
|
22
|
+
# TODO: Check for unknown keys
|
23
|
+
def read_hash(hash)
|
24
|
+
constrain hash, Hash
|
25
|
+
initialize_impl
|
26
|
+
|
27
|
+
# Works on to #to_h and #to_yaml hashes because it is easier to handle
|
28
|
+
# the two formats dynamically than detecting which format to use
|
29
|
+
for schema in type.schemas
|
30
|
+
next if !hash.key?(schema.name)
|
31
|
+
hash_schema = hash[schema.name]
|
32
|
+
for table in schema.tables
|
33
|
+
next if !hash[schema.name].key?(table.name)
|
34
|
+
hash_table = hash_schema[table.name]
|
35
|
+
records = hash_table.is_a?(Hash) ? hash_table.values : hash_table
|
36
|
+
for record in records
|
37
|
+
Record.new(self[schema.name][table.name], record)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|