activefacts-generators 1.8.3 → 1.9.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.
@@ -24,288 +24,288 @@ module ActiveFacts
24
24
  module Generators #:nodoc:
25
25
  module Transform #:nodoc:
26
26
  class DataVault
27
- def initialize(vocabulary, *options)
28
- @vocabulary = vocabulary
29
- @constellation = vocabulary.constellation
30
- end
31
-
32
- def classify_tables
33
- initial_tables = @vocabulary.tables
34
- non_reference_tables = initial_tables.reject do |table|
35
- table.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'static'} or
36
- !table.is_a?(ActiveFacts::Metamodel::EntityType)
37
- end
38
- @reference_tables = initial_tables-non_reference_tables
39
-
40
- @link_tables, @hub_tables = non_reference_tables.partition do |table|
41
- identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
42
- # Which identifying_references are played by other tables?
43
- ir_tables =
44
- identifying_references.select do |r|
45
- table_referred_to = r.to
46
- # I have no examples of multi-level absorption, but it's possible, so loop
47
- while av = table_referred_to.absorbed_via
48
- table_referred_to = av.from
49
- end
50
- table_referred_to.is_table
51
- end
52
- ir_tables.size > 1
53
- end
54
- trace_table_classifications
55
- end
56
-
57
- def trace_table_classifications
58
- # Trace the decisions about table types:
59
- if trace :datavault
60
- [@reference_tables, @hub_tables, @link_tables].zip(['Reference', 'Hub', 'Link']).each do |tables, kind|
61
- trace :datavault, kind+' tables:' do
62
- tables.each do |table|
63
- identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
64
- trace :datavault, "#{table.name}(#{identifying_references.map{|r| (t = r.to) && t.name || 'self'}*', '})"
65
- end
66
- end
67
- end
68
- end
69
- end
70
-
71
- def detect_required_surrogates
72
- trace :datavault, "Detecting required surrogates" do
73
- @required_surrogates =
74
- (@hub_tables+@link_tables).select do |table|
75
- table.dv_needs_surrogate
76
- end
77
- end
78
- end
79
-
80
- def inject_required_surrogates
81
- trace :datavault, "Injecting any required surrogates" do
82
- trace :datavault, "Need to inject surrogates into #{@required_surrogates.map(&:name)*', '}"
83
- @required_surrogates.each do |table|
84
- table.dv_inject_surrogate
85
- end
86
- end
87
- end
88
-
89
- def classify_satellite_references table
90
- identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
91
- non_identifying_references = table.columns.map{|c| c.references[0]}.uniq - identifying_references
92
-
93
- # Skip this table if no satellite data is needed
94
- # REVISIT: Needed anyway for a link?
95
- if non_identifying_references.size == 0
96
- return nil
97
- end
98
-
99
- satellites = non_identifying_references.inject({}) do |hash, ref|
100
- # Extract the declared satellite name, or use just "satellite"
101
- satellite_subname =
102
- ref.fact_type.internal_presence_constraints.map do |pc|
103
- next if !pc.max_frequency || pc.max_frequency > 1 # Not a Uniqueness Constraint
104
- next if pc.role_sequence.all_role_ref.size > 1 # Covers more than one role
105
- next if pc.role_sequence.all_role_ref.single.role.object_type != table # Not a unique attribute
106
- pc.concept.all_concept_annotation.map do |ca|
107
- if ca.mapping_annotation =~ /^satellite */
108
- ca.mapping_annotation.sub(/^satellite +/, '')
109
- else
110
- nil
111
- end
112
- end
113
- end.flatten.compact.uniq[0] || table.name
114
- satellite_name = satellite_subname
115
- (hash[satellite_name] ||= []) << ref
116
- hash
117
- end
118
- trace :datavault, "#{table.name} satellites are #{satellites.inspect}"
119
- satellites
120
- end
121
-
122
- def create_one_to_many(one, many, predicate_1 = 'has', predicate_2 = 'is of', one_adj = nil)
123
- # Create a fact type
124
- fact_type = @constellation.FactType(:concept => :new)
125
- one_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 0, :object_type => one)
126
- many_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 1, :object_type => many)
127
-
128
- # Create two readings
129
- reading2 = @constellation.Reading(:fact_type => fact_type, :ordinal => 0, :role_sequence => [:new], :text => "{0} #{predicate_2} {1}")
130
- @constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 0, :role => many_role)
131
- @constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 1, :role => one_role, :leading_adjective => one_adj)
132
-
133
- reading1 = @constellation.Reading(:fact_type => fact_type, :ordinal => 1, :role_sequence => [:new], :text => "{0} #{predicate_1} {1}")
134
- @constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 0, :role => one_role, :leading_adjective => one_adj)
135
- @constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 1, :role => many_role)
136
-
137
- one_id = @constellation.PresenceConstraint(
138
- :concept => :new,
139
- :vocabulary => @vocabulary,
140
- :name => one.name+'HasOne'+many.name,
141
- :role_sequence => [:new],
142
- :is_mandatory => true,
143
- :min_frequency => 1,
144
- :max_frequency => 1,
145
- :is_preferred_identifier => false
146
- )
147
- @constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => many_role)
148
- one_role
149
- end
150
-
151
- def assert_value_type name, supertype = nil
152
- @vocabulary.valid_value_type_name(name) ||
153
- @constellation.ValueType(:vocabulary => @vocabulary, :name => name, :supertype => supertype, :concept => :new)
154
- end
155
-
156
- def assert_record_source
157
- assert_value_type('Record Source', assert_value_type('String'))
158
- end
159
-
160
- def assert_date_time
161
- assert_value_type('Date Time')
162
- end
163
-
164
- # Create a PresenceConstraint with two roles, marked as preferred_identifier
165
- def create_two_role_identifier(r1, r2)
166
- pc = @constellation.PresenceConstraint(
167
- :concept => :new,
168
- :vocabulary => @vocabulary,
169
- :name => r1.object_type.name+' '+r1.object_type.name+'PK',
170
- :role_sequence => [:new],
171
- :is_mandatory => true,
172
- :min_frequency => 1,
173
- :max_frequency => 1,
174
- :is_preferred_identifier => true
175
- )
176
- @constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 0, :role => r1)
177
- @constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 1, :role => r2)
178
- end
179
-
180
- def lift_role_to_link(ref, table_role)
181
- trace :datavault, "Broaden #{ref} into a new link"
182
- uc = table_role.uniqueness_constraint
183
- one_to_one_constraint = ref.fact_type.internal_presence_constraints.detect{|pc| pc != uc }
184
-
185
- # Any query Step or Reading on this fact type should be unaffected
186
-
187
- # Make a new RoleRef for the uniqueness constraint so it spans
188
- uc.constellation.RoleRef(uc.role_sequence, 1, :role => ref.to_role)
189
- one_to_one_constraint.retract if one_to_one_constraint
190
-
191
- # Add the objectifying entity type:
192
- et = uc.constellation.EntityType(
193
- uc.vocabulary,
194
- "#{ref.from.name} #{ref.to_names*' '}",
195
- :fact_type => ref.fact_type,
196
- :concept => :new
197
- )
198
-
199
- # REVISIT: Objectifying requires creation of LinkFactTypes.
200
-
201
- @link_tables << et
202
- end
203
-
204
- def create_satellite(table, satellite_name, references)
205
- satellite_name = satellite_name.words.titlewords*' '+' SAT'
206
-
207
- # Create a new entity type with record-date fields in its identifier
208
- trace :datavault, "Creating #{satellite_name} with #{references.size} references"
209
- satellite = @constellation.EntityType(:vocabulary => @vocabulary, :name => "#{satellite_name}", :concept => [:new, :implication_rule => "datavault"])
210
- satellite.definitely_table
211
-
212
- table_role = create_one_to_many(table, satellite)
213
-
214
- date_time = assert_date_time
215
- date_time_role = create_one_to_many(date_time, satellite, 'is of', 'was loaded at', 'load')
216
- create_two_role_identifier(table_role, date_time_role)
217
-
218
- record_source = assert_record_source
219
- record_source.length = 64
220
- record_source_role = create_one_to_many(record_source, satellite, 'is of', 'was loaded from')
221
-
222
- # Move all roles across to it from the parent table.
223
- references.each do |ref|
224
- trace :datavault, "Moving #{ref} across to #{table.name}_#{satellite_name}" do
225
- table_role = ref.fact_type.all_role.detect{|r| r.object_type == table}
226
- if table_role
227
- remote_table = ref.to
228
- while remote_table && remote_table.absorbed_via
229
- absorbed_into = remote_table.absorbed_via.from
230
- remote_table = absorbed_into
231
- end
232
- if @hub_tables.include?(remote_table)
233
- lift_role_to_link(ref, table_role)
234
- else
235
- # Reassign the role player to the satellite:
236
- table_role.object_type = satellite
237
- end
238
- else
239
- #debugger # Bum, the crappy Reference object bites again.
240
- $stderr.puts "REVISIT: Can't move the objectified role for #{ref.inspect}. This column will remain in the hub instead of moving to the satellite"
241
- end
242
- end
243
- end
244
- satellite
245
- end
246
-
247
- def generate(out = $stdout)
248
- @out = out
249
-
250
- # Strategy:
251
- # Determine list of ER tables
252
- # Partition tables into reference tables (annotated), link tables (two+ FKs in PK), and hub tables
253
- # For each hub and link table
254
- # Apply a surrogate key if needed (all links, hubs lacking a simple surrogate)
255
- # Detect references (fact types) leading to all attributes (non-identifying columns)
256
- # Group attribute facts into satellites (use the satellite annotation if present)
257
- # For each satellite
258
- # Create a new entity type with a (hub-key, record-date key)
259
- # Make new one->many fact type between hub and satellite
260
- # Modify all attribute facts in this group to attach to the satellite
261
- # Compute a gresh relational mapping
262
- # Exclude reference tables and disable enforcement to them
263
-
264
- classify_tables
265
-
266
- detect_required_surrogates
267
-
268
- @sat_tables = []
269
- trace :datavault, "Creating satellites" do
270
- (@hub_tables+@link_tables).each do |table|
271
- satellites = classify_satellite_references table
272
- next unless satellites
273
-
274
- trace :datavault, "Creating #{satellites.size} satellites for #{table.name}" do
275
- satellites.each do |satellite_name, references|
276
- @sat_tables << create_satellite(table, satellite_name, references)
277
- end
278
- end
279
- end
280
- end
281
- trace :datavault, "#{@sat_tables.size} satellite tables created"
282
-
283
- inject_required_surrogates
284
-
285
- trace :datavault, "Adding standard fields to hubs and links" do
286
- (@hub_tables+@link_tables).each do |table|
287
- date_time = assert_date_time
288
- date_time_role = create_one_to_many(date_time, table, 'is of', 'was loaded at', 'load')
289
-
290
- record_source = assert_record_source
291
- record_source_role = create_one_to_many(record_source, table, 'is of', 'was loaded from')
292
- end
293
- end
294
-
295
- # Now, redo the E-R mapping using the revised schema:
296
- @vocabulary.decide_tables
297
-
298
- # Suffix Hub and Link tables with HUB and LINK
299
- @hub_tables.each { |h| h.name = "#{h.name} HUB"}
300
- @link_tables.each { |l| l.name = "#{l.name} LINK"}
301
-
302
- # Before departing, ensure we don't emit the reference tables!
303
- @reference_tables.each do |table|
304
- table.definitely_not_table
305
- @vocabulary.tables.delete(table)
306
- end
307
-
308
- end # generate
27
+ def initialize(vocabulary, *options)
28
+ @vocabulary = vocabulary
29
+ @constellation = vocabulary.constellation
30
+ end
31
+
32
+ def classify_tables
33
+ initial_tables = @vocabulary.tables
34
+ non_reference_tables = initial_tables.reject do |table|
35
+ table.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'static'} or
36
+ !table.is_a?(ActiveFacts::Metamodel::EntityType)
37
+ end
38
+ @reference_tables = initial_tables-non_reference_tables
39
+
40
+ @link_tables, @hub_tables = non_reference_tables.partition do |table|
41
+ identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
42
+ # Which identifying_references are played by other tables?
43
+ ir_tables =
44
+ identifying_references.select do |r|
45
+ table_referred_to = r.to
46
+ # I have no examples of multi-level absorption, but it's possible, so loop
47
+ while av = table_referred_to.absorbed_via
48
+ table_referred_to = av.from
49
+ end
50
+ table_referred_to.is_table
51
+ end
52
+ ir_tables.size > 1
53
+ end
54
+ trace_table_classifications
55
+ end
56
+
57
+ def trace_table_classifications
58
+ # Trace the decisions about table types:
59
+ if trace :datavault
60
+ [@reference_tables, @hub_tables, @link_tables].zip(['Reference', 'Hub', 'Link']).each do |tables, kind|
61
+ trace :datavault, kind+' tables:' do
62
+ tables.each do |table|
63
+ identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
64
+ trace :datavault, "#{table.name}(#{identifying_references.map{|r| (t = r.to) && t.name || 'self'}*', '})"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def detect_required_surrogates
72
+ trace :datavault, "Detecting required surrogates" do
73
+ @required_surrogates =
74
+ (@hub_tables+@link_tables).select do |table|
75
+ table.dv_needs_surrogate
76
+ end
77
+ end
78
+ end
79
+
80
+ def inject_required_surrogates
81
+ trace :datavault, "Injecting any required surrogates" do
82
+ trace :datavault, "Need to inject surrogates into #{@required_surrogates.map(&:name)*', '}"
83
+ @required_surrogates.each do |table|
84
+ table.dv_inject_surrogate
85
+ end
86
+ end
87
+ end
88
+
89
+ def classify_satellite_references table
90
+ identifying_references = table.identifier_columns.map{|c| c.references.first}.uniq
91
+ non_identifying_references = table.columns.map{|c| c.references[0]}.uniq - identifying_references
92
+
93
+ # Skip this table if no satellite data is needed
94
+ # REVISIT: Needed anyway for a link?
95
+ if non_identifying_references.size == 0
96
+ return nil
97
+ end
98
+
99
+ satellites = non_identifying_references.inject({}) do |hash, ref|
100
+ # Extract the declared satellite name, or use just "satellite"
101
+ satellite_subname =
102
+ ref.fact_type.internal_presence_constraints.map do |pc|
103
+ next if !pc.max_frequency || pc.max_frequency > 1 # Not a Uniqueness Constraint
104
+ next if pc.role_sequence.all_role_ref.size > 1 # Covers more than one role
105
+ next if pc.role_sequence.all_role_ref.single.role.object_type != table # Not a unique attribute
106
+ pc.concept.all_concept_annotation.map do |ca|
107
+ if ca.mapping_annotation =~ /^satellite */
108
+ ca.mapping_annotation.sub(/^satellite +/, '')
109
+ else
110
+ nil
111
+ end
112
+ end
113
+ end.flatten.compact.uniq[0] || table.name
114
+ satellite_name = satellite_subname
115
+ (hash[satellite_name] ||= []) << ref
116
+ hash
117
+ end
118
+ trace :datavault, "#{table.name} satellites are #{satellites.inspect}"
119
+ satellites
120
+ end
121
+
122
+ def create_one_to_many(one, many, predicate_1 = 'has', predicate_2 = 'is of', one_adj = nil)
123
+ # Create a fact type
124
+ fact_type = @constellation.FactType(:concept => :new)
125
+ one_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 0, :object_type => one)
126
+ many_role = @constellation.Role(:concept => :new, :fact_type => fact_type, :ordinal => 1, :object_type => many)
127
+
128
+ # Create two readings
129
+ reading2 = @constellation.Reading(:fact_type => fact_type, :ordinal => 0, :role_sequence => [:new], :text => "{0} #{predicate_2} {1}")
130
+ @constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 0, :role => many_role)
131
+ @constellation.RoleRef(:role_sequence => reading2.role_sequence, :ordinal => 1, :role => one_role, :leading_adjective => one_adj)
132
+
133
+ reading1 = @constellation.Reading(:fact_type => fact_type, :ordinal => 1, :role_sequence => [:new], :text => "{0} #{predicate_1} {1}")
134
+ @constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 0, :role => one_role, :leading_adjective => one_adj)
135
+ @constellation.RoleRef(:role_sequence => reading1.role_sequence, :ordinal => 1, :role => many_role)
136
+
137
+ one_id = @constellation.PresenceConstraint(
138
+ :concept => :new,
139
+ :vocabulary => @vocabulary,
140
+ :name => one.name+'HasOne'+many.name,
141
+ :role_sequence => [:new],
142
+ :is_mandatory => true,
143
+ :min_frequency => 1,
144
+ :max_frequency => 1,
145
+ :is_preferred_identifier => false
146
+ )
147
+ @constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => many_role)
148
+ one_role
149
+ end
150
+
151
+ def assert_value_type name, supertype = nil
152
+ @vocabulary.valid_value_type_name(name) ||
153
+ @constellation.ValueType(:vocabulary => @vocabulary, :name => name, :supertype => supertype, :concept => :new)
154
+ end
155
+
156
+ def assert_record_source
157
+ assert_value_type('Record Source', assert_value_type('String'))
158
+ end
159
+
160
+ def assert_date_time
161
+ assert_value_type('Date Time')
162
+ end
163
+
164
+ # Create a PresenceConstraint with two roles, marked as preferred_identifier
165
+ def create_two_role_identifier(r1, r2)
166
+ pc = @constellation.PresenceConstraint(
167
+ :concept => :new,
168
+ :vocabulary => @vocabulary,
169
+ :name => r1.object_type.name+' '+r1.object_type.name+'PK',
170
+ :role_sequence => [:new],
171
+ :is_mandatory => true,
172
+ :min_frequency => 1,
173
+ :max_frequency => 1,
174
+ :is_preferred_identifier => true
175
+ )
176
+ @constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 0, :role => r1)
177
+ @constellation.RoleRef(:role_sequence => pc.role_sequence, :ordinal => 1, :role => r2)
178
+ end
179
+
180
+ def lift_role_to_link(ref, table_role)
181
+ trace :datavault, "Broaden #{ref} into a new link"
182
+ uc = table_role.uniqueness_constraint
183
+ one_to_one_constraint = ref.fact_type.internal_presence_constraints.detect{|pc| pc != uc }
184
+
185
+ # Any query Step or Reading on this fact type should be unaffected
186
+
187
+ # Make a new RoleRef for the uniqueness constraint so it spans
188
+ uc.constellation.RoleRef(uc.role_sequence, 1, :role => ref.to_role)
189
+ one_to_one_constraint.retract if one_to_one_constraint
190
+
191
+ # Add the objectifying entity type:
192
+ et = uc.constellation.EntityType(
193
+ uc.vocabulary,
194
+ "#{ref.from.name} #{ref.to_names*' '}",
195
+ :fact_type => ref.fact_type,
196
+ :concept => :new
197
+ )
198
+
199
+ # REVISIT: Objectifying requires creation of LinkFactTypes.
200
+
201
+ @link_tables << et
202
+ end
203
+
204
+ def create_satellite(table, satellite_name, references)
205
+ satellite_name = satellite_name.words.titlewords*' '+' SAT'
206
+
207
+ # Create a new entity type with record-date fields in its identifier
208
+ trace :datavault, "Creating #{satellite_name} with #{references.size} references"
209
+ satellite = @constellation.EntityType(:vocabulary => @vocabulary, :name => "#{satellite_name}", :concept => [:new, :implication_rule => "datavault"])
210
+ satellite.definitely_table
211
+
212
+ table_role = create_one_to_many(table, satellite)
213
+
214
+ date_time = assert_date_time
215
+ date_time_role = create_one_to_many(date_time, satellite, 'is of', 'was loaded at', 'load')
216
+ create_two_role_identifier(table_role, date_time_role)
217
+
218
+ record_source = assert_record_source
219
+ record_source.length = 64
220
+ record_source_role = create_one_to_many(record_source, satellite, 'is of', 'was loaded from')
221
+
222
+ # Move all roles across to it from the parent table.
223
+ references.each do |ref|
224
+ trace :datavault, "Moving #{ref} across to #{table.name}_#{satellite_name}" do
225
+ table_role = ref.fact_type.all_role.detect{|r| r.object_type == table}
226
+ if table_role
227
+ remote_table = ref.to
228
+ while remote_table && remote_table.absorbed_via
229
+ absorbed_into = remote_table.absorbed_via.from
230
+ remote_table = absorbed_into
231
+ end
232
+ if @hub_tables.include?(remote_table)
233
+ lift_role_to_link(ref, table_role)
234
+ else
235
+ # Reassign the role player to the satellite:
236
+ table_role.object_type = satellite
237
+ end
238
+ else
239
+ #debugger # Bum, the crappy Reference object bites again.
240
+ $stderr.puts "REVISIT: Can't move the objectified role for #{ref.inspect}. This column will remain in the hub instead of moving to the satellite"
241
+ end
242
+ end
243
+ end
244
+ satellite
245
+ end
246
+
247
+ def generate(out = $stdout)
248
+ @out = out
249
+
250
+ # Strategy:
251
+ # Determine list of ER tables
252
+ # Partition tables into reference tables (annotated), link tables (two+ FKs in PK), and hub tables
253
+ # For each hub and link table
254
+ # Apply a surrogate key if needed (all links, hubs lacking a simple surrogate)
255
+ # Detect references (fact types) leading to all attributes (non-identifying columns)
256
+ # Group attribute facts into satellites (use the satellite annotation if present)
257
+ # For each satellite
258
+ # Create a new entity type with a (hub-key, record-date key)
259
+ # Make new one->many fact type between hub and satellite
260
+ # Modify all attribute facts in this group to attach to the satellite
261
+ # Compute a gresh relational mapping
262
+ # Exclude reference tables and disable enforcement to them
263
+
264
+ classify_tables
265
+
266
+ detect_required_surrogates
267
+
268
+ @sat_tables = []
269
+ trace :datavault, "Creating satellites" do
270
+ (@hub_tables+@link_tables).each do |table|
271
+ satellites = classify_satellite_references table
272
+ next unless satellites
273
+
274
+ trace :datavault, "Creating #{satellites.size} satellites for #{table.name}" do
275
+ satellites.each do |satellite_name, references|
276
+ @sat_tables << create_satellite(table, satellite_name, references)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ trace :datavault, "#{@sat_tables.size} satellite tables created"
282
+
283
+ inject_required_surrogates
284
+
285
+ trace :datavault, "Adding standard fields to hubs and links" do
286
+ (@hub_tables+@link_tables).each do |table|
287
+ date_time = assert_date_time
288
+ date_time_role = create_one_to_many(date_time, table, 'is of', 'was loaded at', 'load')
289
+
290
+ record_source = assert_record_source
291
+ record_source_role = create_one_to_many(record_source, table, 'is of', 'was loaded from')
292
+ end
293
+ end
294
+
295
+ # Now, redo the E-R mapping using the revised schema:
296
+ @vocabulary.decide_tables
297
+
298
+ # Suffix Hub and Link tables with HUB and LINK
299
+ @hub_tables.each { |h| h.name = "#{h.name} HUB"}
300
+ @link_tables.each { |l| l.name = "#{l.name} LINK"}
301
+
302
+ # Before departing, ensure we don't emit the reference tables!
303
+ @reference_tables.each do |table|
304
+ table.definitely_not_table
305
+ @vocabulary.tables.delete(table)
306
+ end
307
+
308
+ end # generate
309
309
 
310
310
  end
311
311
  end