fixture_fox 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +20 -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 +79 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/diagram.drawio +1 -0
- data/examples/1-1-subrecord.fox +15 -0
- data/examples/1-1-subrecord.sql +22 -0
- data/examples/N-M.fox +25 -0
- data/examples/N-M.sql +34 -0
- data/examples/anchors.sql +13 -0
- data/examples/anchors.yml +4 -0
- data/examples/anchors1.fox +4 -0
- data/examples/anchors2.fox +6 -0
- data/examples/array.fox +84 -0
- data/examples/array.sql +53 -0
- data/examples/base.fox +81 -0
- data/examples/base.sql +52 -0
- data/examples/empty.fox +8 -0
- data/examples/empty.sql +33 -0
- data/examples/include/schema-included.fox +23 -0
- data/examples/inherit.fox +26 -0
- data/examples/inherit.sql +28 -0
- data/examples/kind.fox +17 -0
- data/examples/kind.sql +31 -0
- data/examples/link.fox +35 -0
- data/examples/link.sql +32 -0
- data/examples/root.fox +22 -0
- data/examples/schema-fragment-1.fox +9 -0
- data/examples/schema-fragment-2.fox +21 -0
- data/examples/schema-include.fox +10 -0
- data/examples/schema-indent.fox +29 -0
- data/examples/schema.fox +29 -0
- data/examples/schema.sql +33 -0
- data/examples/types.fox +8 -0
- data/examples/types.sql +17 -0
- data/examples/views.fox +15 -0
- data/examples/views.sql +38 -0
- data/exe/fox +178 -0
- data/fixture_fox.gemspec +37 -0
- data/lib/fixture_fox/analyzer.rb +371 -0
- data/lib/fixture_fox/anchor.rb +93 -0
- data/lib/fixture_fox/ast.rb +176 -0
- data/lib/fixture_fox/error.rb +23 -0
- data/lib/fixture_fox/hash_parser.rb +111 -0
- data/lib/fixture_fox/idr.rb +62 -0
- data/lib/fixture_fox/line.rb +217 -0
- data/lib/fixture_fox/parser.rb +153 -0
- data/lib/fixture_fox/token.rb +173 -0
- data/lib/fixture_fox/tokenizer.rb +78 -0
- data/lib/fixture_fox/version.rb +3 -0
- data/lib/fixture_fox.rb +216 -0
- metadata +227 -0
@@ -0,0 +1,371 @@
|
|
1
|
+
module FixtureFox
|
2
|
+
class Analyzer
|
3
|
+
attr_reader :ast
|
4
|
+
attr_reader :type
|
5
|
+
attr_reader :idr # Initialized by #call
|
6
|
+
|
7
|
+
# List of all known tables whether empty or not (PgGraph::Type::Table objects)
|
8
|
+
def tables() @tables.values end
|
9
|
+
|
10
|
+
# List of non-empty tables (PgGraph::Type::Table objects)
|
11
|
+
def data_tables() @data_tables.values end
|
12
|
+
|
13
|
+
attr_reader :records # [AstRecord]
|
14
|
+
attr_reader :record_refs # [AstRecordRef]
|
15
|
+
attr_reader :fields # [AstField]
|
16
|
+
|
17
|
+
# Map from qualified table name to maximum record ID
|
18
|
+
attr_reader :ids
|
19
|
+
|
20
|
+
# Like ids but includes only tables with records from the sources
|
21
|
+
# attr_reader :referenced_ids
|
22
|
+
|
23
|
+
# Anchors object. This includes both anchors defined in the sources and
|
24
|
+
# anchors supplied to #initialize
|
25
|
+
attr_reader :anchors
|
26
|
+
|
27
|
+
# Map from anchor name to anchors defined by the sources
|
28
|
+
attr_reader :defined_anchors
|
29
|
+
|
30
|
+
# Map from anchor name to anchors referenced by the sources. FIXME: Unused
|
31
|
+
attr_reader :referenced_anchors
|
32
|
+
|
33
|
+
# True iff types have been assigned
|
34
|
+
def assigned?() @assigned end
|
35
|
+
|
36
|
+
# True iff types have been checked
|
37
|
+
def checked?() @checked end
|
38
|
+
|
39
|
+
# True iff data has been generated
|
40
|
+
def generated?() !@idr.nil? end
|
41
|
+
|
42
|
+
def initialize(type, ast, ids: {}, anchors: Anchors.new(type))
|
43
|
+
constrain type, PgGraph::Type
|
44
|
+
constrain ast, Ast
|
45
|
+
constrain anchors, Anchors
|
46
|
+
@type = type
|
47
|
+
@ast = ast
|
48
|
+
@tables = {}
|
49
|
+
@data_tables = {}
|
50
|
+
@records = []
|
51
|
+
@record_refs = []
|
52
|
+
@fields = [] # List of all fields
|
53
|
+
|
54
|
+
@ids = Hash.new(0)
|
55
|
+
ids.each { |k,v| @ids[k] = v } # copy but preserve default value
|
56
|
+
@anchors = anchors
|
57
|
+
@defined_anchors = {}
|
58
|
+
@referenced_anchors = {}
|
59
|
+
|
60
|
+
@assigned = false
|
61
|
+
@checked = false
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO: Also generate data
|
65
|
+
def call
|
66
|
+
assign_types
|
67
|
+
check_types
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# Assign types and collect fields, records, and anchors. Note that
|
72
|
+
# #assign_types and #check_types are separate methods to make it possible
|
73
|
+
# to analyze the AST just up to the point where external anchors are needed
|
74
|
+
# for further processing. This is used in the caching mechanism in Postspec
|
75
|
+
def assign_types
|
76
|
+
@assigned = true
|
77
|
+
assign_table_types
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check types. Merges anchors: into self.anchors
|
82
|
+
def check_types(anchors: nil)
|
83
|
+
@checked = true
|
84
|
+
@anchors.merge!(anchors) if anchors
|
85
|
+
check_field_types
|
86
|
+
check_record_ref_types
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
# Generate IDR. Use self.ids if ids: is nil
|
91
|
+
def generate(ids: nil)
|
92
|
+
ids.each { |k,v| @ids[k] = v } if ids # copy while preserving default value in hash
|
93
|
+
generate_record_ids
|
94
|
+
generate_idr
|
95
|
+
@idr
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def merge_anchor(record)
|
100
|
+
constrain record, AstRecord
|
101
|
+
name = record.anchor&.value
|
102
|
+
if !@anchors.key?(name)
|
103
|
+
@defined_anchors[name] = @anchors.create(name, record.type)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def assign_table_types
|
108
|
+
ast.tables.each { |ast_table|
|
109
|
+
@type.key?(ast_table.schema.value.downcase) or
|
110
|
+
ast_table.schema.error("Can't find schema '#{ast_table.schema}'")
|
111
|
+
type = ast_table.type = @type[ast_table.schema.value.downcase][ast_table.name.downcase] or
|
112
|
+
ast_table.ident.error(
|
113
|
+
"Can't find ast_table '#{ast_table.ident}' (maybe you forgot to declare a schema?)")
|
114
|
+
@tables[type.uid] = type
|
115
|
+
assign_record_types(ast_table)
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
119
|
+
def assign_record_types(ast_table)
|
120
|
+
constrain ast_table, AstTable, AstTableMember
|
121
|
+
ast_table.elements.each { |ast_record|
|
122
|
+
ast_record.type = ast_table.type.record_type
|
123
|
+
@data_tables[ast_record.type.table.uid] ||= ast_record.type.table
|
124
|
+
case ast_record
|
125
|
+
when AstRecordElement
|
126
|
+
records << ast_record
|
127
|
+
merge_anchor(ast_record) if ast_record.anchor
|
128
|
+
assign_field_types(ast_record)
|
129
|
+
when AstReferenceElement
|
130
|
+
record_refs << ast_record
|
131
|
+
else
|
132
|
+
raise "Oops"
|
133
|
+
end
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
def assign_field_types(ast_record)
|
138
|
+
ast_record.members.each { |field|
|
139
|
+
field.column = ast_record.type[field.ident.litt.downcase] or
|
140
|
+
field.ident.error("Can't find field '#{ast_record.type.table.name}.#{field.ident}'")
|
141
|
+
field.type = field.column.type
|
142
|
+
case field
|
143
|
+
when AstTableMember
|
144
|
+
assign_record_types(field)
|
145
|
+
when AstRecordMember
|
146
|
+
records << field
|
147
|
+
merge_anchor(field) if field.anchor
|
148
|
+
assign_field_types(field)
|
149
|
+
when AstFieldMember, AstReferenceMember
|
150
|
+
fields << field
|
151
|
+
else
|
152
|
+
raise "Oops"
|
153
|
+
end
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
def check_field_types
|
158
|
+
fields.each { |f|
|
159
|
+
case f
|
160
|
+
when AstFieldMember # FIXME: Doesn't handle multidimensional arrays
|
161
|
+
if f.type.array?
|
162
|
+
values = f.value.value
|
163
|
+
klass = f.type.element_type.ruby_class
|
164
|
+
elsif f.type.is_a?(PgGraph::Type::RecordType)
|
165
|
+
if f.column.is_a?(PgGraph::Type::KindRecordColumn)
|
166
|
+
values = [f.value.value]
|
167
|
+
klass = f.column.kind_column.type.ruby_class
|
168
|
+
else
|
169
|
+
f.value.error("Expected a record or a reference, got #{f.value.value.inspect}")
|
170
|
+
end
|
171
|
+
else
|
172
|
+
values = [f.value.value]
|
173
|
+
klass = f.type.ruby_class
|
174
|
+
end
|
175
|
+
|
176
|
+
values.each { |value|
|
177
|
+
if value.is_a?(klass) || !f.type.array? &&
|
178
|
+
klass == Time &&
|
179
|
+
value.is_a?(String) &&
|
180
|
+
value =~ /^\d\d\d\d-\d\d-\d\d(?: \d\d:\d\d(?::\d\d)?)?$/
|
181
|
+
;
|
182
|
+
else
|
183
|
+
f.value.error("Data type mismatch - expected #{klass}, got #{value.class}")
|
184
|
+
end
|
185
|
+
}
|
186
|
+
when AstReferenceMember
|
187
|
+
check_ref(f)
|
188
|
+
else
|
189
|
+
raise "Oops"
|
190
|
+
end
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
def check_ref(ast_ref)
|
195
|
+
anchor = ast_ref.referenced_anchor = @anchors[ast_ref.reference.value.to_sym] or
|
196
|
+
ast_ref.reference.error("Can't find anchor for reference '#{ast_ref.reference.litt}'")
|
197
|
+
anchor_types =
|
198
|
+
[anchor.type] +
|
199
|
+
(anchor.type.table.subtable? ? [anchor.type.table.supertable.record_type] : [])
|
200
|
+
anchor_types.any? { |anchor_type| ast_ref.type == anchor_type } or
|
201
|
+
ast_ref.reference.error(
|
202
|
+
"Data type mismatch - expected a reference to a " \
|
203
|
+
"#{ast_ref.type.identifier} record, got #{anchor.type.identifier}")
|
204
|
+
end
|
205
|
+
|
206
|
+
def check_record_ref_types
|
207
|
+
record_refs.each { |record| check_ref(record) }
|
208
|
+
end
|
209
|
+
|
210
|
+
def check_record_kind_ref_types
|
211
|
+
end
|
212
|
+
|
213
|
+
def generate_record_ids
|
214
|
+
# puts "generate_record_ids"
|
215
|
+
# puts " ids: #{@ids}"
|
216
|
+
@supertable_records = [] # Array of pairs of supertable/id
|
217
|
+
records.each { |ast_record|
|
218
|
+
table = ast_record.type.table
|
219
|
+
table_uid = table.subtable? ? table.supertable.uid : table.uid
|
220
|
+
|
221
|
+
# If enclosing record/table is the supertable, then take the ID from there
|
222
|
+
if table.subtable?
|
223
|
+
parent_table = ast_record.parent.type.table
|
224
|
+
if table.supertable == parent_table # works for both Record and Table objects
|
225
|
+
id = @ids[table.uid] = @ids[parent_table.uid]
|
226
|
+
|
227
|
+
# Enclosing record/table is not the supertable so generate a new ID
|
228
|
+
# based on the current supertable ID and prepare a new supertable
|
229
|
+
# record
|
230
|
+
else
|
231
|
+
id = @ids[table.uid] = @ids[table.supertable.uid] += 1
|
232
|
+
@supertable_records << [table.supertable, id]
|
233
|
+
end
|
234
|
+
ast_record.id = id
|
235
|
+
|
236
|
+
# If record is a (labelled) root record then check if it has already been defined
|
237
|
+
# and take the ID from there
|
238
|
+
elsif name = ast_record.anchor&.value
|
239
|
+
@anchors.key?(name) or raise "Oops"
|
240
|
+
@referenced_anchors[name] ||= @anchors[name] if !@defined_anchors.key?(name)
|
241
|
+
ast_record.id = @anchors[name].id ||= @ids[table_uid] += 1
|
242
|
+
|
243
|
+
else
|
244
|
+
# Assign a new ID to the record
|
245
|
+
ast_record.id = @ids[table_uid] += 1
|
246
|
+
end
|
247
|
+
|
248
|
+
# Register anchor
|
249
|
+
if name = ast_record.anchor&.value
|
250
|
+
@anchors.key?(name) or raise "Oops"
|
251
|
+
@anchors[name].id = ast_record.id
|
252
|
+
# @referenced_anchors[name] ||= @anchors[name] if !@defined_anchors.key?(name)
|
253
|
+
# ast_record.id = @anchors[name].id ||= @ids[table_uid] += 1
|
254
|
+
end
|
255
|
+
}
|
256
|
+
end
|
257
|
+
|
258
|
+
def generate_idr
|
259
|
+
@idr = Idr.new
|
260
|
+
|
261
|
+
# Build all root tables. This makes sure empty tables are registered
|
262
|
+
ast.tables.each { |t| idr.put(t.schema, t.type.table.name) }
|
263
|
+
|
264
|
+
# Create implicit supertable records
|
265
|
+
@supertable_records.each { |table, id|
|
266
|
+
idr.put(table.schema.name, table.name, id)
|
267
|
+
}
|
268
|
+
|
269
|
+
# Create records by filling in the Idr field-by-field
|
270
|
+
records.each { |record|
|
271
|
+
record.members.each { |field|
|
272
|
+
table = nil # To bring table into scope. FIXME: Yt?
|
273
|
+
case field
|
274
|
+
when AstFieldMember
|
275
|
+
# puts "AstFieldMember: #{field}"
|
276
|
+
table = record.type.table
|
277
|
+
column = field.column.postgres_column
|
278
|
+
idr.put(table.schema.name, table.name, record.id, column, field.value.value)
|
279
|
+
when AstReferenceMember
|
280
|
+
# puts "AstReferenceMember: #{field}"
|
281
|
+
table = record.type.table
|
282
|
+
column = field.column.postgres_column
|
283
|
+
idr.put(table.schema.name, table.name, record.id, column, field.referenced_anchor.id)
|
284
|
+
when AstRecordMember
|
285
|
+
# puts "AstRecordMember: #{record.type}"
|
286
|
+
column = field.column
|
287
|
+
|
288
|
+
this_record_id = field.record.id
|
289
|
+
that_record_id = field.id
|
290
|
+
|
291
|
+
if column.reference? # The key is on this side
|
292
|
+
table = record.type.table
|
293
|
+
schema = table.schema
|
294
|
+
link_column = column.this_link_column
|
295
|
+
idr.put(schema.name, table.name, this_record_id, link_column, that_record_id)
|
296
|
+
else # The key is on the other side
|
297
|
+
table = column.type.table
|
298
|
+
schema = table.schema
|
299
|
+
link_column = column.that_link_column
|
300
|
+
idr.put(schema.name, table.name, that_record_id, link_column, this_record_id)
|
301
|
+
end
|
302
|
+
when AstTableMember
|
303
|
+
# puts "AstTableMember: #{field}"
|
304
|
+
if field.column.is_a?(PgGraph::Type::MmTableColumn)
|
305
|
+
table = mm_table = field.column.mm_table
|
306
|
+
this_mm_column = field.column.this_mm_column
|
307
|
+
that_mm_column = field.column.that_mm_column
|
308
|
+
|
309
|
+
field.elements.each { |r|
|
310
|
+
mm_id = @ids[mm_table.path] += 1
|
311
|
+
this_mm_column_id = record.id
|
312
|
+
that_mm_column_id = r.is_a?(AstReferenceElement) ? r.referenced_anchor.id : r.id
|
313
|
+
idr.put(mm_table.schema.name, mm_table.name, mm_id, this_mm_column, this_mm_column_id)
|
314
|
+
idr.put(mm_table.schema.name, mm_table.name, mm_id, that_mm_column, that_mm_column_id)
|
315
|
+
}
|
316
|
+
else
|
317
|
+
table = record_table = field.column.type.table
|
318
|
+
record_column = field.column
|
319
|
+
field.elements.each { |r|
|
320
|
+
record_id = r.is_a?(AstReferenceElement) ? r.referenced_anchor.id : r.id
|
321
|
+
idr.put(
|
322
|
+
record_table.schema.name, record_table.name,
|
323
|
+
record_id, record_column.that_link_column, record.id)
|
324
|
+
}
|
325
|
+
end
|
326
|
+
else
|
327
|
+
raise "Oops: Unhandled case - #{field.class}"
|
328
|
+
end
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
idr.materialized_views = @tables.values.map(&:depending_materialized_views).flatten.uniq
|
333
|
+
end
|
334
|
+
|
335
|
+
def dump
|
336
|
+
records.sort { |l,r| l.type.identifier <=> r.type.identifier }.each { |r|
|
337
|
+
puts r.type.identifier
|
338
|
+
indent {
|
339
|
+
puts "id: #{r.id}"
|
340
|
+
r.members.each { |f|
|
341
|
+
print "#{f.ident}: #{f.type.identifier}"
|
342
|
+
case f
|
343
|
+
when AstField
|
344
|
+
if f.value.is_a?(Value)
|
345
|
+
print " = #{f.value.to_s}"
|
346
|
+
elsif f.value.is_a?(Reference)
|
347
|
+
print " = #{f.type.table.identifier}[#{f.referenced_anchor.id}]"
|
348
|
+
else
|
349
|
+
raise "Oops"
|
350
|
+
end
|
351
|
+
puts " (#{f.type.class})"
|
352
|
+
when AstRecord
|
353
|
+
puts " = #{r.type.table.identifier}[#{f.id}] (#{f.type.class})"
|
354
|
+
when AstTable
|
355
|
+
print " = ["
|
356
|
+
print f.records.map { |r| "#{r.type.table.identifier}[#{r.id}]" }.join(", ")
|
357
|
+
puts "] (#{f.class}, #{f.type.class})"
|
358
|
+
else
|
359
|
+
puts
|
360
|
+
end
|
361
|
+
}
|
362
|
+
}
|
363
|
+
}
|
364
|
+
end
|
365
|
+
|
366
|
+
def dump_anchors
|
367
|
+
@anchor_uids.map { |k,v| puts "#{k}: #{v}" }
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
module FixtureFox
|
3
|
+
class Anchor
|
4
|
+
attr_reader :name
|
5
|
+
attr_accessor :type
|
6
|
+
attr_accessor :id
|
7
|
+
|
8
|
+
def initialize(name, type, id: nil)
|
9
|
+
constrain name, Symbol
|
10
|
+
constrain type, PgGraph::Type::RecordType
|
11
|
+
constrain id, NilClass, Integer
|
12
|
+
@name, @type, @id = name, type, id
|
13
|
+
end
|
14
|
+
|
15
|
+
def uid() @uid ||= "#{table_uid}[#{id}]" end
|
16
|
+
def table_uid() type.table.uid end
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: Remove Anchors and replace with a simple hash
|
20
|
+
class Anchors
|
21
|
+
attr_reader :type
|
22
|
+
|
23
|
+
# Initialize an Anchors object
|
24
|
+
#
|
25
|
+
# +anchors+ is an array of hashes of name, table, and id values. It is the
|
26
|
+
# same as the output from #to_yaml
|
27
|
+
def initialize(type, anchors = [])
|
28
|
+
constrain type, PgGraph::Type
|
29
|
+
constrain anchors, [Hash], NilClass
|
30
|
+
@type = type
|
31
|
+
@anchors = {}
|
32
|
+
(anchors || []).each { |anchor| create(anchor[:name], anchor[:table], id: anchor[:id]) }
|
33
|
+
|
34
|
+
# @anchors = anchors.map { |name, record_uid|
|
35
|
+
# raise "This is actually used!"
|
36
|
+
# table_uid, id = PgGraph::Data::Record.split_uid(record_uid)
|
37
|
+
# Anchor.new(name, type.dot(table_uid).record_type, id)
|
38
|
+
# }.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
forward_to :@anchors, :size, :empty?, :key?, :keys, :values, :each, :map
|
42
|
+
|
43
|
+
def create(name, arg, id: nil)
|
44
|
+
constrain name, Symbol
|
45
|
+
constrain arg, PgGraph::Type::RecordType, String
|
46
|
+
constrain id, NilClass, Integer # id is initially nil for anchors declared in the source
|
47
|
+
!@anchors.key?(name) or raise Error, "Duplicate anchor: #{name.inspect}"
|
48
|
+
type =
|
49
|
+
case arg
|
50
|
+
when PgGraph::Type::RecordType; arg
|
51
|
+
when String; @type.dot(arg) or raise Error, "Illegal path: #{path.inspect}"
|
52
|
+
end
|
53
|
+
@anchors[name] = Anchor.new(name, type.record_type, id: id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def [](name)
|
57
|
+
constrain name, Symbol
|
58
|
+
@anchors[name]
|
59
|
+
end
|
60
|
+
|
61
|
+
def merge!(anchors)
|
62
|
+
constrain anchors, Anchors
|
63
|
+
type == anchors.type or raise Error, "anchor type mismatch"
|
64
|
+
@anchors.merge!(anchors.instance_variable_get(:@anchors))
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_yaml
|
69
|
+
@anchors.values.map { |anchor| { name: anchor.name, table: anchor.table_uid, id: anchor.id } }
|
70
|
+
end
|
71
|
+
|
72
|
+
def save(filename)
|
73
|
+
IO.write(filename, YAML.dump(to_yaml))
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.load(type, filename)
|
77
|
+
anchors = Anchors.new(type)
|
78
|
+
YAML.load(IO.read(filename)).each { |entry|
|
79
|
+
anchors.create(entry[:name], entry[:table], id: entry[:id])
|
80
|
+
}
|
81
|
+
anchors
|
82
|
+
end
|
83
|
+
|
84
|
+
def dump
|
85
|
+
puts "Anchors"
|
86
|
+
indent {
|
87
|
+
puts "type: #{type.uid}"
|
88
|
+
puts "anchors: #{@anchors.inspect}"
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "indented_io"
|
2
|
+
|
3
|
+
module FixtureFox
|
4
|
+
class AstNode
|
5
|
+
# Parent node. nil for the top-level Ast node
|
6
|
+
attr_reader :parent
|
7
|
+
|
8
|
+
# List of elements or members. Only non-empty for table and records nodes.
|
9
|
+
# Assigned by the parser
|
10
|
+
attr_reader :children
|
11
|
+
|
12
|
+
# Type of this node or of the node referenced by this node. nil for the
|
13
|
+
# top-level Ast node. Assigned by the analyzer
|
14
|
+
attr_accessor :type
|
15
|
+
|
16
|
+
def initialize(parent)
|
17
|
+
@parent = parent
|
18
|
+
@children = []
|
19
|
+
@parent.children << self if @parent
|
20
|
+
end
|
21
|
+
|
22
|
+
def dump
|
23
|
+
puts self.to_s
|
24
|
+
indent { children.each(&:dump) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def inspect
|
28
|
+
"#<#{self.class}:#{object_id} @type=#{type&.uid.inspect}>"
|
29
|
+
# raise
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Ast < AstNode
|
34
|
+
alias_method :tables, :children
|
35
|
+
|
36
|
+
attr_reader :file
|
37
|
+
|
38
|
+
def initialize(file = nil) # file = nil is used in tests
|
39
|
+
super(nil)
|
40
|
+
@file = file
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s() "Ast" end
|
44
|
+
end
|
45
|
+
|
46
|
+
class AstTable < AstNode # Root table
|
47
|
+
alias_method :ast, :parent
|
48
|
+
alias_method :elements, :children # Initialized by the parser
|
49
|
+
|
50
|
+
attr_reader :ident # Ident token
|
51
|
+
|
52
|
+
attr_reader :schema # Ident token
|
53
|
+
attr_reader :name # String
|
54
|
+
|
55
|
+
attr_reader :dependent_materialized_views # Initialized by the analyzer
|
56
|
+
|
57
|
+
def initialize(ast, schema, ident)
|
58
|
+
super(ast)
|
59
|
+
@ident = ident
|
60
|
+
if @ident.value =~ /^(\w+)\.(\w+)$/
|
61
|
+
@schema = Ident.new(ident.file, ident.lineno, ident.initial_indent, ident.pos, $1)
|
62
|
+
@name = $2
|
63
|
+
else
|
64
|
+
@schema = schema
|
65
|
+
@name = ident.value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_s() "#{schema.value}.#{name}: AstTable" end
|
70
|
+
end
|
71
|
+
|
72
|
+
class AstElement < AstNode
|
73
|
+
alias_method :table, :parent
|
74
|
+
end
|
75
|
+
|
76
|
+
# Make AstRecordElement and AstRecordMember act as they're instances of AstRecord
|
77
|
+
module AstRecord; end
|
78
|
+
|
79
|
+
class AstRecordElement < AstElement # Table record element
|
80
|
+
include AstRecord
|
81
|
+
|
82
|
+
alias_method :members, :children
|
83
|
+
|
84
|
+
attr_reader :anchor # Anchor token. Can be nil
|
85
|
+
attr_accessor :id # Assigned by the analyzer
|
86
|
+
|
87
|
+
def initialize(table, anchor = nil)
|
88
|
+
super(table)
|
89
|
+
@anchor = anchor
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s() "AstRecordElement" + (anchor ? " #{anchor.litt}" : "") end
|
93
|
+
end
|
94
|
+
|
95
|
+
class AstReferenceElement < AstElement # Table reference element
|
96
|
+
attr_reader :reference
|
97
|
+
# attr_accessor :referenced_node # Initialized by the analyzer
|
98
|
+
attr_accessor :referenced_anchor # Tuple of [table_uid, id]
|
99
|
+
|
100
|
+
def initialize(table, reference)
|
101
|
+
super(table)
|
102
|
+
@reference = reference
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_s() "AstReferenceElement #{reference.litt}" end
|
106
|
+
end
|
107
|
+
|
108
|
+
# A member of a record
|
109
|
+
class AstMember < AstNode
|
110
|
+
alias_method :record, :parent
|
111
|
+
|
112
|
+
attr_reader :ident # Ident token
|
113
|
+
attr_accessor :column # Initialized by the analyzer
|
114
|
+
|
115
|
+
def initialize(record, ident)
|
116
|
+
super(record)
|
117
|
+
@ident = ident
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class AstTableMember < AstMember # Also root table
|
122
|
+
alias_method :elements, :children # Initialized by the parser
|
123
|
+
|
124
|
+
def to_s() "#{ident}: AstTableMember" end
|
125
|
+
end
|
126
|
+
|
127
|
+
class AstRecordMember < AstMember
|
128
|
+
include AstRecord
|
129
|
+
|
130
|
+
alias_method :members, :children
|
131
|
+
|
132
|
+
attr_reader :anchor # Can be nil
|
133
|
+
attr_accessor :id # Assigned by the analyzer
|
134
|
+
|
135
|
+
def initialize(record, ident, anchor = nil)
|
136
|
+
super(record, ident)
|
137
|
+
@anchor = anchor
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_s() "#{ident}: AstRecordMember" + (anchor ? " #{anchor.litt}" : "") end
|
141
|
+
end
|
142
|
+
|
143
|
+
class AstReferenceMember < AstMember
|
144
|
+
attr_reader :reference # Reference token
|
145
|
+
# attr_accessor :referenced_node # Initialized by the analyzer
|
146
|
+
attr_accessor :referenced_anchor # Tuple of [table_uid, id]
|
147
|
+
|
148
|
+
def initialize(record, ident, reference)
|
149
|
+
super(record, ident)
|
150
|
+
@reference = reference
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_s() "#{ident}: AstReferenceMember #{reference.litt}" end
|
154
|
+
end
|
155
|
+
|
156
|
+
class AstFieldMember < AstMember
|
157
|
+
attr_reader :value
|
158
|
+
|
159
|
+
def initialize(record, ident, value)
|
160
|
+
super(record, ident)
|
161
|
+
@value = value
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_s() "#{ident}: #{value}" end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
|
175
|
+
|
176
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module FixtureFox
|
2
|
+
class Error < StandardError
|
3
|
+
def error_message() message end
|
4
|
+
end
|
5
|
+
|
6
|
+
class ParseError < Error
|
7
|
+
attr_reader :file
|
8
|
+
attr_reader :lineno
|
9
|
+
attr_reader :pos
|
10
|
+
|
11
|
+
def initialize(file, lineno, pos, msg)
|
12
|
+
super(msg)
|
13
|
+
@file = file
|
14
|
+
@lineno = lineno
|
15
|
+
@pos = pos
|
16
|
+
end
|
17
|
+
|
18
|
+
def error_message()
|
19
|
+
[file, lineno, pos].compact.join(":") + " " + message
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|