pg_graph 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,119 @@
1
+
2
+ module Timer
3
+ class TimerNode
4
+ attr_reader :timer
5
+ attr_reader :label
6
+
7
+ def initialize(timer, label)
8
+ @timer, @label = timer, label
9
+ end
10
+
11
+ def label_width() label&.size || 0 end
12
+ def value_width() raise NotThis end
13
+
14
+ def total() raise NotThis end
15
+
16
+ protected
17
+ def format(time) sprintf "%.#{timer.scale}f", timer.factor * time end
18
+ end
19
+
20
+ class TimerElement < TimerNode
21
+ attr_accessor :time
22
+
23
+ def initialize(timer, label, time)
24
+ super(timer, label)
25
+ @time = time
26
+ end
27
+
28
+ def value_width() value.size end
29
+ def value() @value ||= format time end
30
+
31
+ def total() @time end
32
+
33
+ protected
34
+ def dump_element(file, indent, label_width, value_width)
35
+ file.printf "#{indent}%-#{label_width}s: %#{value_width}s#{timer.unit}\n", label, value
36
+ end
37
+ end
38
+
39
+ class TimerGroup < TimerNode
40
+ attr_accessor :unit # Currently only :s, or :ms. Default is :ms
41
+ def factor() { s: 1, ms: 1000 }[unit] end
42
+ attr_accessor :scale # Number of digits after the decimal point
43
+ attr_reader :nodes # Array of tuples of label and time or TimerGroup objects
44
+
45
+ def initialize(timer, label = nil, unit: :ms, scale: 2)
46
+ super(timer, label)
47
+ @unit = unit.to_sym
48
+ @scale = scale
49
+ @nodes = []
50
+ @nodes_by_label = {}
51
+ end
52
+
53
+ def time(label, &block)
54
+ t0 = Time.now
55
+ r = yield
56
+ dt = Time.now - t0
57
+ if @nodes_by_label.key?(label)
58
+ @nodes_by_label[label].time += dt
59
+ else
60
+ element = TimerElement.new(self, label, dt)
61
+ @nodes << element
62
+ @nodes_by_label[label] = element
63
+ end
64
+ r
65
+ end
66
+
67
+ def group(label, **opts)
68
+ r = TimerGroup.new(self, label, **{ unit: unit, scale: scale }.merge(opts))
69
+ @nodes << r
70
+ r
71
+ end
72
+
73
+ def total()
74
+ @nodes.inject(0) { |a,e| a + e.total }
75
+ end
76
+
77
+ def dump(file = $stdout, indent = "")
78
+ dump_element(file, indent, label_width, value_width)
79
+ end
80
+
81
+ protected
82
+ def label_width()
83
+ if label
84
+ ([label.size - 2] + @nodes.map { |node| node.send(:label_width) }).max + 2
85
+ else
86
+ @nodes.map { |node| node.send :label_width }.max
87
+ end
88
+ end
89
+
90
+ def value_width() @value_width ||= @nodes.map { |node| node.send :value_width }.max end
91
+
92
+ def dump_element(file, indent, label_width, value_width)
93
+ if label
94
+ file.puts "#{indent}#{label} (#{format(total)}#{timer.unit})"
95
+ nodes.each { |node|
96
+ node.send :dump_element, file, indent + " ", label_width - 2, value_width
97
+ }
98
+ else
99
+ nodes.each { |node|
100
+ node.send :dump_element, file, indent, label_width, value_width
101
+ }
102
+ end
103
+ end
104
+ end
105
+
106
+ class Timer < TimerGroup
107
+ def initialize(label = nil, **opts)
108
+ super(self, label, **opts)
109
+ end
110
+
111
+ def dump(file = $stdout)
112
+ super
113
+ if @nodes.size > 1
114
+ file.printf "%-#{label_width}s: %#{value_width}s#{timer.unit}\n", "Total", format(total)
115
+ end
116
+ end
117
+ end
118
+ end
119
+
@@ -0,0 +1,3 @@
1
+ module PgGraph
2
+ VERSION = "0.1.0"
3
+ end
data/lib/pg_graph.rb ADDED
@@ -0,0 +1,124 @@
1
+
2
+ require "pg_graph/version"
3
+
4
+ require "boolean"
5
+ require "constrain"
6
+ #def constrain(*args) end
7
+ require "developer_exceptions"
8
+
9
+ require "hash_tree"
10
+
11
+ require "ext/meta.rb"
12
+ require "ext/module.rb"
13
+
14
+ require "pg_graph/version.rb"
15
+ require "pg_graph/inflector.rb"
16
+ require "pg_graph/reflector.rb"
17
+
18
+ require "type/type.rb"
19
+ require "type/read.rb"
20
+ require "type/dump_type.rb"
21
+
22
+ require "data/data.rb"
23
+ require "data/association.rb"
24
+ require "data/value.rb"
25
+ require "data/dimension.rb"
26
+ require "data/read.rb"
27
+ require "data/render.rb"
28
+
29
+ module PgGraph
30
+ include DeveloperExceptions
31
+
32
+ class Error < StandardError; end
33
+
34
+ # The supported ruby classes
35
+ RUBY_CLASSES = Inflector::SUPPORTED_RUBY_CLASSES
36
+
37
+ module Type
38
+ # :call-seq:
39
+ # new(meta, reflect = nil)
40
+ # new(pg_conn, reflect = nil)
41
+ #
42
+ # The +reflect+ argument can be a Reflector object, an yaml array, a file
43
+ # name or nil. The +ignore+ option is a list of schema names to exclude
44
+ # from the type system
45
+ #
46
+ # Note that together with ::=== and Database#is_a? this makes
47
+ # Type::Database pretend it is an instance of the Type module
48
+ #
49
+ def self.new(arg, reflect = nil, ignore: [])
50
+ constrain arg, PgMeta, PgConn
51
+ constrain reflect, Reflector, Array, String, NilClass
52
+ meta =
53
+ case arg
54
+ when PgMeta; arg
55
+ when PgConn; PgMeta.new(arg)
56
+ end
57
+ reflector =
58
+ case reflect
59
+ when Reflector; reflect
60
+ when Array; Reflector.load_yaml(reflect)
61
+ when String; Reflector.load_file(reflect)
62
+ when NilClass; Reflector.new
63
+ end
64
+ Database.new(meta.name, reflector).read(meta, ignore: ignore)
65
+ end
66
+
67
+ # Make the Type module pretend to have Database object instances
68
+ def self.===(element) element.is_a?(PgGraph::Type::Connection) or super end
69
+
70
+ class Database
71
+ # :call-seq:
72
+ # instantiate()
73
+ # instantiate(hash)
74
+ # instantiate(yaml)
75
+ # instantiate(pg_conn)
76
+ #
77
+ def instantiate(arg = nil)
78
+ constrain arg, NilClass, Hash, PgConn
79
+ Data.new(self, arg)
80
+ end
81
+
82
+ # Let Type::Database objects pretend to be-a module Type object
83
+ def is_a?(arg) arg == PgGraph::Type || super end
84
+ end
85
+ end
86
+
87
+ module Data
88
+ # :call-seq:
89
+ # new(type, hash = {})
90
+ # new(type, yaml = {})
91
+ # new(type, pg_conn = nil)
92
+ #
93
+ # Note that together with ::=== and Data::Database#is_a? this makes
94
+ # Data::Database pretend it is an instance of the Data module
95
+ def self.new(type, arg)
96
+ constrain type, PgGraph::Type
97
+ constrain arg, Hash, PgConn, NilClass
98
+ Database.new(type, arg)
99
+ end
100
+
101
+ # Make the Data module pretend to have Database object instances
102
+ def self.===(element) element.is_a?(PgGraph::Data::Connection) or super end
103
+
104
+ class Database
105
+ # Let Data::Database objects pretend to be-a module Type object
106
+ def is_a?(arg) arg == PgGraph::Data || super end
107
+ end
108
+ end
109
+
110
+ # Non-public namespace
111
+ module PrivatePgGraph
112
+ # Note that this changes the +args+ argument
113
+ def self.extract_reflections(args)
114
+ if args.last.is_a?(Hash)
115
+ reflections = args.last.delete(:reflections) || []
116
+ args.pop if args.last.empty? && !reflections.empty?
117
+ reflections
118
+ else
119
+ []
120
+ end
121
+ end
122
+ end
123
+ end
124
+
@@ -0,0 +1,69 @@
1
+ require 'indented_io'
2
+
3
+ module PgGraph::Type
4
+ class Node
5
+ def dump(children = self.children.values.sort_by(&:name), link_info: false, supertable: nil)
6
+ print identifier + (supertable ? " < #{supertable}" : "")
7
+ puts
8
+ indent { children.each { |child| child.dump(link_info: link_info) } }
9
+ end
10
+ end
11
+
12
+ class Schema
13
+ def dump(link_info: false)
14
+ super(record_types.sort_by(&:name), link_info: link_info)
15
+ end
16
+ end
17
+
18
+ class RecordType
19
+ def dump(**opts)
20
+ supertable = table.supertable&.identifier
21
+ columns = fields.reject { |column|
22
+ column.primary_key? || column.name =~ /_id$/ || column.name =~ /_kind$/ # FIXME
23
+ }
24
+ super(columns, supertable: supertable, **opts)
25
+ end
26
+ end
27
+
28
+ class Column
29
+ def dump(link_info: false)
30
+ print "#{identifier}: "
31
+ method = type.schema == record_type.schema ? :identifier : :schema_identifier
32
+ if self.is_a?(MmTableColumn) && !self.is_a?(NmTableColumn)
33
+ type_identifier = "{[#{type.element_type.send(method)}]}"
34
+ else
35
+ type_identifier = type.send(method)
36
+ end
37
+ print type_identifier
38
+ print "()" if generated?
39
+ print "[#{that_link_column}]" if kind?
40
+ print "?" if nullable?
41
+ print "!" if unique?
42
+ dump_type if link_info
43
+ puts
44
+ end
45
+
46
+ def dump_type() end
47
+ end
48
+
49
+ class RecordColumn
50
+ def dump_type
51
+ print " (#{this_link_column} -> #{type.table.name}.#{that_link_column}) (RecordColumn)"
52
+ end
53
+ end
54
+
55
+ class TableColumn
56
+ def dump_type
57
+ print " (#{this_link_column} -> #{type.table.name}.#{that_link_column}) (TableColumn)"
58
+ end
59
+ end
60
+
61
+ class MmTableColumn
62
+ def dump_type
63
+ print \
64
+ " (#{this_link_column} -> #{mm_table.name}.#{this_mm_column}," +
65
+ " #{mm_table.name}.#{that_mm_column} -> #{that_table.name}.#{that_link_column}) (MmTableColumn)"
66
+ end
67
+ end
68
+ end
69
+
data/lib/type/read.rb ADDED
@@ -0,0 +1,269 @@
1
+ module PgGraph::Type
2
+ class Error < StandardError; end
3
+ class KeyNotFound < Error; end
4
+
5
+ class Database
6
+ def read_meta(meta, ignore: [])
7
+ # Array of [record, meta_column] tuples
8
+ field_columns = []
9
+ link_columns = []
10
+ kind_columns = [] # The link side
11
+ id_link_columns = [] # sub tables
12
+
13
+ # Temporary arrays of meta tables
14
+ tables = [] # Ordinary tables
15
+ mm_tables = [] # M:M link tables
16
+
17
+ # Create schemas and tables and initialize lists of objects
18
+ meta.schemas.values.each { |meta_schema|
19
+ next if ignore.include?(meta_schema.name)
20
+ schema = Schema.new(self, meta_schema.name)
21
+ meta_schema.tables.values.select { |t| t.table? }.each { |meta_table| # FIXME Ignore views for now
22
+ (meta_table.mm_table? ? mm_tables : tables) << meta_table
23
+ table = Table.new(
24
+ schema, meta_table.name,
25
+ mm_table: meta_table.mm_table?,
26
+ depending_materialized_views: meta_table.depending_views.select(&:materialized?))
27
+ record_type = RecordType.new(table)
28
+
29
+ # Process columns
30
+ array_columns = []
31
+ meta_table.columns.values.each { |meta_column|
32
+ # Create basic types needed by columns
33
+ type_name = meta_column.type
34
+ if !schema.key?(type_name) && !catalog.key?(type_name)
35
+ if meta_column.array?
36
+ element_name = meta_column.element_type
37
+ dimensions = meta_column.dimensions
38
+ if !schema.key?(element_name) && !catalog.key?(element_name)
39
+ element_type = SimpleType.new(catalog, element_name)
40
+ else
41
+ element_type = schema[element_name] || catalog[element_name]
42
+ end
43
+ ArrayType.new(catalog, type_name, element_type, dimensions)
44
+ else
45
+ SimpleType.new(catalog, type_name)
46
+ end
47
+ end
48
+
49
+ # Collect columns
50
+ if meta_column.reference?
51
+ if meta_column.name == "id"
52
+ id_link_columns
53
+ elsif meta_column.name =~ /^(?:.*_)?kind$/
54
+ kind_columns
55
+ else
56
+ link_columns
57
+ end
58
+ else
59
+ field_columns
60
+ end << [record_type, meta_column]
61
+ }
62
+ }
63
+ }
64
+
65
+ # Build list of depending tables
66
+ (tables + mm_tables).each { |meta_table|
67
+ table = dot(meta_table.path)
68
+ meta_table.depending_tables.each { |meta_depending_table|
69
+ depending_table = dot(meta_depending_table.path)
70
+ table.depending_tables << depending_table
71
+ # puts "#{table.uid} -> #{depending_table.uid}"
72
+ }
73
+
74
+ }
75
+
76
+ # Create postgres columns except kind_columns
77
+ (id_link_columns + link_columns + field_columns).each { |record_type, meta_column|
78
+ next if meta_column.kind?
79
+ type = record_type.schema[meta_column.type] || catalog[meta_column.type]
80
+ SimpleColumn.new(
81
+ record_type, meta_column.name, meta_column.name, type,
82
+ ordinal: meta_column.ordinal,
83
+ **column_options(meta_column))
84
+ }
85
+
86
+
87
+ # Create and collect forward-references. link_fields is a list of [uid, record_column] tuples
88
+ link_fields = []
89
+ (link_columns + kind_columns).each { |record_type, meta_column|
90
+ meta_column.references.each { |constraint|
91
+ constraint.referencing_columns.size == 1 or raise Error, "Can't handle multi-column keys (for now)"
92
+ type = dot(constraint.referenced_table.path).type.record_type
93
+ this_link_column = constraint.referencing_columns.first.name
94
+ that_link_column = constraint.referenced_columns.first.name
95
+
96
+ field =
97
+ if meta_column.kind?
98
+ name = reflector.this(meta_column.uid) || meta_column.name
99
+ name = meta_column.name if record_type[name] # Check for duplicates
100
+ !record_type.nil? or raise Error, "Duplicate column name: #{name}" # Check again
101
+ column_type = record_type.schema[meta_column.type] || catalog[meta_column.type]
102
+
103
+ parent = (name != meta_column.name ? record_type : nil)
104
+ kind_column = SimpleColumn.new(
105
+ parent, meta_column.name, meta_column.name, column_type,
106
+ ordinal: meta_column.ordinal,
107
+ **column_options(meta_column))
108
+
109
+ KindRecordColumn.new(
110
+ record_type, name, meta_column.name, type, this_link_column, that_link_column,
111
+ kind_column, **column_options(meta_column))
112
+ else
113
+ name = reflector.this(meta_column.uid)
114
+ RecordColumn.new(
115
+ record_type, name, meta_column.name, type, this_link_column, that_link_column,
116
+ **column_options(meta_column))
117
+ end
118
+ link_fields << [meta_column.uid, field]
119
+ }
120
+ }
121
+
122
+ # Detect derived tables
123
+ # link_fields.each { |uid, record_column|
124
+ # if record_column.this_link_column.primary_key? && that_link_column.primary_key?
125
+
126
+ # Create back-reference fields
127
+ (link_fields).each { |uid, this_column|
128
+ this_record_type = this_column.record_type
129
+ this_table = this_record_type.table
130
+ that_record_type = this_column.type
131
+ next if this_table.mm_table?
132
+
133
+ # Field name of back-reference
134
+ if this_column.primary_key? # child record
135
+ this_column.postgres_column == "id" or raise Error, "Primary keys should be named 'id'"
136
+ name = this_record_type.name
137
+ else
138
+ name = reflector.that(uid, this_column.unique?, table: this_record_type.name)
139
+ name ||= PgGraph.inflector.pluralize(this_column.table.name) if this_column.kind?
140
+ end
141
+
142
+ next if name.nil?
143
+
144
+ # Check for name collisions
145
+ if that_record_type.key?(name)
146
+ # Check if this is a 1:1 relation with keys on both sides. In that
147
+ # case, the back-reference is already created
148
+ that_column = that_record_type[name]
149
+ if that_column.is_a?(RecordColumn) && that_column.type == this_record_type
150
+ next # Do nothing
151
+
152
+ # Check if the reference spans across schemes so we can disambiguate
153
+ # by prefixing the schema name
154
+ elsif this_column.table.schema != that_column.table.schema
155
+ name = "#{this_column.table.schema.name}_#{name}"
156
+ end
157
+ end
158
+
159
+ # Final check for name collisions
160
+ !that_record_type.key?(name) or
161
+ raise Error, "Name collision in reverse map for #{this_column.uid}, trying #{name}"
162
+
163
+ # Create back-references
164
+ if this_column.unique?
165
+ RecordColumn.new(
166
+ that_record_type, name, nil, this_record_type,
167
+ this_column.that_link_column, this_column.this_link_column)
168
+ else
169
+ TableColumn.new(that_record_type, name, this_table.type,
170
+ this_column.that_link_column, this_column.this_link_column)
171
+ end
172
+ }
173
+
174
+ # Setup super/subtables
175
+ id_link_columns.each { |this_record_type, meta_column|
176
+ # Create forward and backward references for id links
177
+ constraint = meta_column.references.first
178
+ that_record_type = dot(constraint.referenced_table.path).type.record_type
179
+ SuperRecordColumn.new(this_record_type, that_record_type)
180
+ SubRecordColumn.new(that_record_type, this_record_type)
181
+
182
+ # Create hierarchy
183
+ this_table = this_record_type.table
184
+ that_table = that_record_type.table
185
+ this_table.instance_variable_set(:@supertable, that_table)
186
+ that_table.instance_variable_set(:@has_subtables, true)
187
+ }
188
+
189
+ # Create N:M or M:M reference fields
190
+ mm_tables.each { |link_meta_table|
191
+ constraint1 = link_meta_table.referential_constraints.values[0]
192
+ constraint2 = link_meta_table.referential_constraints.values[1]
193
+ table1 = dot(constraint1.referenced_table.path)
194
+ table2 = dot(constraint2.referenced_table.path)
195
+ mm_table = dot(link_meta_table.path)
196
+
197
+ link_column1 = constraint1.referenced_columns.first.name
198
+ mm_column1 = constraint1.referencing_columns.first.name
199
+ mm_column1_uid = constraint1.referencing_columns.first.uid
200
+
201
+ link_column2 = constraint2.referenced_columns.first.name
202
+ mm_column2 = constraint2.referencing_columns.first.name
203
+ mm_column2_uid = constraint2.referencing_columns.first.uid
204
+
205
+ column1_name = reflector.that(mm_column1_uid, false, table: table2.record_type.name)
206
+ column2_name = reflector.that(mm_column2_uid, false, table: table1.record_type.name)
207
+
208
+ # FIXME: DAGs over an super table creates problems if reflections
209
+ # doesn't match (eg. role_id/group_id instead of
210
+ # child_group_id/parent_group_id)
211
+
212
+ # Detect DAGs. This check is performed after column names have been
213
+ # computed to allow for user defined reflections
214
+ # if column1_name == column2_name && table1 == table2
215
+ # column1_name = reflector.that(mm_column1_uid, false)
216
+ # column2_name = reflector.that(mm_column2_uid, false)
217
+ # puts "BINGO"
218
+ # puts column1_name
219
+ # puts column2_name
220
+ # exit 1
221
+ # else
222
+ # end
223
+
224
+
225
+ # puts "mm_tables.each"
226
+ # indent {
227
+ # puts "mm_column1_uid: #{mm_column1_uid}"
228
+ # puts "mm_column2_uid: #{mm_column2_uid}"
229
+ # puts "table1: #{table1}"
230
+ # puts "column1_name: #{column1_name}"
231
+ # puts "table2: #{table2}"
232
+ # puts "column2_name: #{column2_name}"
233
+ # }
234
+
235
+ if table1.type.record_type.key?(column1_name)
236
+ raise Error, "Duplicate column name in #{table1.identifier}: #{column1_name}"
237
+ end
238
+
239
+ if table2.type.record_type.key?(column2_name)
240
+ raise Error, "Duplicate column name in #{table2.identifier}: #{column2_name}"
241
+ end
242
+
243
+ klass = link_meta_table.nm_table? ? NmTableColumn : MmTableColumn
244
+ klass.new(
245
+ table1.type.record_type, column1_name, table2.type, mm_table.type,
246
+ link_column1, mm_column1, mm_column2, link_column2)
247
+ klass.new(
248
+ table2.type.record_type, column2_name, table1.type, mm_table.type,
249
+ link_column2, mm_column2, mm_column1, link_column1)
250
+ }
251
+
252
+ self
253
+ end
254
+
255
+ private
256
+ # Create a options Hash for ColumnField objects
257
+ def column_options(meta_column)
258
+ {
259
+ primary_key: meta_column.primary_key?,
260
+ identity: meta_column.identity?,
261
+ nullable: meta_column.nullable?,
262
+ unique: meta_column.unique?,
263
+ readonly: !meta_column.updatable? || meta_column.generated?,
264
+ generated: meta_column.generated?
265
+ }
266
+ end
267
+ end
268
+ end
269
+