activefacts-rmap 1.7.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f6127c77cb2acadd6a25acbd42a0ceff08353fdd
4
+ data.tar.gz: 254d937ce5e28fdf4648c59d0ad3d718e47571bf
5
+ SHA512:
6
+ metadata.gz: 7079737917e3dbf2313cc7c0ae0e9d6accdeffcf3bc993d76110a55c1d99f2d0fbc6ee10d269d2d4e0a421b7890d32d162249db025f02c3991de3e57ee129f8a
7
+ data.tar.gz: 5bb8a3ff2d7515a5d047f324f0c2b4e924fe88ab6fa322680f98e8a7340f665acc3b61a743ef53682b157158a9d42bf34aeef776177244c59066a411327c20d5
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ before_install: gem install bundler -v 1.10.0.rc
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ if ENV['PWD'] =~ %r{\A/Users/cjh/work/activefacts}
6
+ gem 'activefacts-metamodel', path: '/Users/cjh/work/activefacts/metamodel'
7
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Clifford Heath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Activefacts::Rmap
2
+
3
+ This gem provides a relational mapping that generates 5NF relation schemas
4
+ for a fact model in ActiveFacts. Usually the model will have been compiled
5
+ from a Constellation Query Language (CQL) source file or loaded from an ORM
6
+ file. The code is tested in dependent gems that use this gem.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'activefacts-rmap'
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ Check out the activefacts-examples tests for API usage, or just use the
19
+ acticefacts-generators to generate SQL, Rails, or other relational code.
20
+
21
+ ## Development
22
+
23
+ After checking out the repo, run `bundle install` to install dependencies.
24
+
25
+ ## Contributing
26
+
27
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cjheath/activefacts-rmap.
28
+
29
+ ## License
30
+
31
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
32
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activefacts/rmap/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activefacts-rmap"
8
+ spec.version = Activefacts::RMap::VERSION
9
+ spec.authors = ["Clifford Heath"]
10
+ spec.email = ["clifford.heath@gmail.com"]
11
+
12
+ spec.summary = %q{Relational mapping for ActiveFacts}
13
+ spec.description = %q{Relational mapping for fact models. Part of the ActiveFacts suite.}
14
+ spec.homepage = "https://github.com/cjheath/activefacts-rmap"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.10.a"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "rspec"
23
+
24
+ spec.add_runtime_dependency "activefacts-metamodel"
25
+ end
@@ -0,0 +1,15 @@
1
+ #
2
+ # ActiveFacts Relational mapping
3
+ #
4
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
5
+ #
6
+
7
+ # These files are concerned with calculating a relational schema for a vocabulary:
8
+ require 'activefacts/rmap/reference'
9
+ require 'activefacts/rmap/tables'
10
+ require 'activefacts/rmap/columns'
11
+ require 'activefacts/rmap/foreignkey'
12
+ require 'activefacts/rmap/index'
13
+
14
+ # These extend the API classes with relational awareness:
15
+ require 'activefacts/rmap/object_type'
@@ -0,0 +1,444 @@
1
+ #
2
+ # ActiveFacts Relational mapping.
3
+ # Columns in a relational table; each is derived from a sequence of References.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ # Each Reference from a ObjectType creates one or more Columns.
8
+ # A reference to a simple valuetype creates a single column, as
9
+ # does a reference to a table entity identified by a single value.
10
+ #
11
+ # When referring to a object_type that doesn't have its own table,
12
+ # all references from that object_type are absorbed into this one.
13
+ #
14
+ # When multiple values identify an entity that does have its own
15
+ # table, a reference to that entity creates multiple columns,
16
+ # a multi-part foreign key.
17
+ #
18
+
19
+ module ActiveFacts
20
+ module RMap #:nodoc:
21
+
22
+ class Column
23
+ def initialize(reference = nil) #:nodoc:
24
+ references << reference if reference
25
+ end
26
+
27
+ # A Column is created from a path through an array of References to a ValueType
28
+ def references
29
+ @references ||= []
30
+ end
31
+
32
+ # All references up to and including the first non-absorbing reference
33
+ def absorption_references
34
+ @references.inject([]) do |array, ref|
35
+ array << ref
36
+ # puts "Column #{name} spans #{ref}, #{ref.is_absorbing ? "" : "not "} absorbing (#{ref.to.name} absorbs via #{ref.to.absorbed_via.inspect})"
37
+ break array unless ref.is_absorbing
38
+ array
39
+ end
40
+ end
41
+
42
+ # How many of the initial references are involved in full absorption of an EntityType into this column's table
43
+ def absorption_level
44
+ l = 0
45
+ @references.detect do |ref|
46
+ l += 1 if ref.is_absorbing
47
+ false
48
+ end
49
+ l
50
+ end
51
+
52
+ def prepend reference #:nodoc:
53
+ references.insert 0, reference
54
+ self
55
+ end
56
+
57
+ # A Column name is a sequence of names (derived from the to_roles of the References)
58
+ # appended by a separator string (pass nil to get the original array of names)
59
+ # The names to use is derived from the to_names of each Reference,
60
+ # modified by these rules:
61
+ # * A reference after the first one which is not a TypeInheritance but where the _from_ object plays the sole role in the preferred identifier of the _to_ entity is ignored,
62
+ # * A reference (after a name has been retained) which is a TypeInheritance retains the names of the subtype,
63
+ # * If the names retained so far end in XYZ and the to_names start with XYZ, remove the duplication
64
+ # * If we have retained the name of an entity, and this reference is the sole identifying role of an entity, and the identifying object has a name that is prefixed by the name of the object it identifies, remove the prefix and use just the suffix.
65
+ def name(separator = "")
66
+ self.class.name(@references, separator)
67
+ end
68
+
69
+ def self.name(refs, separator = "")
70
+ last_names = []
71
+ names = refs.
72
+ inject([]) do |a, ref|
73
+
74
+ # Skip any object after the first which is identified by this reference
75
+ if ref != refs[0] and
76
+ !ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) and
77
+ ref.to and
78
+ ref.to.is_a?(ActiveFacts::Metamodel::EntityType) and
79
+ (role_ref = ref.to.preferred_identifier.role_sequence.all_role_ref.single) and
80
+ role_ref.role == ref.from_role
81
+ trace :columns, "Skipping #{ref}, identifies non-initial object"
82
+ next a
83
+ end
84
+
85
+ names = ref.to_names(ref != refs.last)
86
+
87
+ # When traversing type inheritances, keep the subtype name, not the supertype names as well:
88
+ if a.size > 0 && ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
89
+ if ref.to != ref.fact_type.subtype # Did we already have the subtype?
90
+ trace :columns, "Skipping supertype #{ref}"
91
+ next a
92
+ end
93
+ trace :columns, "Eliding supertype in #{ref}"
94
+ last_names.size.times { a.pop } # Remove the last names added
95
+ elsif last_names.last && last_names.last == names[0][0...last_names.last.size]
96
+ # When Xyz is followed by XyzID, truncate that to just ID
97
+ trace :columns, "truncating repeated #{last_names.last} in #{names[0]}"
98
+ names[0] = names[0][last_names.last.size..-1]
99
+ names.shift if names[0] == ''
100
+ elsif last_names.last == names[0]
101
+ # Same, but where an underscore split up the words
102
+ trace :columns, "truncating repeated name in #{names.inspect}"
103
+ names.shift
104
+ end
105
+
106
+ # If the reference is to the single identifying role of the object_type making the reference,
107
+ # strip the object_type name from the start of the reference role
108
+ if a.size > 0 and
109
+ (et = ref.from).is_a?(ActiveFacts::Metamodel::EntityType) and
110
+ # This instead of the next 2 would apply to all identifying roles, but breaks some examples:
111
+ # (role_ref = et.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == ref.to_role}) and
112
+ (role_ref = et.preferred_identifier.role_sequence.all_role_ref.single) and
113
+ role_ref.role == ref.to_role and
114
+ names[0][0...et.name.size].downcase == et.name.downcase
115
+
116
+ trace :columns, "truncating transitive identifying role #{names.inspect}"
117
+ names[0] = names[0][et.name.size..-1]
118
+ names.shift if names[0] == ""
119
+ end
120
+
121
+ last_names = names
122
+
123
+ a += names
124
+ a
125
+ end.elide_repeated_subsequences { |a, b|
126
+ if a.is_a?(Array)
127
+ a.map{|e| e.downcase} == b.map{|e| e.downcase}
128
+ else
129
+ a.downcase == b.downcase
130
+ end
131
+ }
132
+
133
+ name_array = names.map{|n| n.sub(/^[a-z]/){|s| s.upcase}}
134
+ separator ? name_array * separator : name_array
135
+ end
136
+
137
+ # Is this column mandatory or nullable?
138
+ def is_mandatory
139
+ # Uncomment the following line for CWA unaries (not nullable, just T/F)
140
+ # @references[-1].is_unary ||
141
+ !@references.detect{|ref| !ref.is_mandatory || ref.is_unary }
142
+ end
143
+
144
+ # This column is auto-assigned if it's an auto-assigned value type and is not a foreign key
145
+ def is_auto_assigned
146
+ last_table_ref = references.reverse.detect{|r| r.from && r.from.is_table}
147
+ (to = references[-1].to) &&
148
+ to.is_auto_assigned &&
149
+ references[0].from.identifier_columns.size == 1 &&
150
+ references[0].from == last_table_ref.from
151
+ end
152
+
153
+ # What's the underlying SQL data type of this column?
154
+ def type
155
+ params = {}
156
+ constraints = []
157
+ return ["BIT", params, constraints] if references[-1].is_unary # It's a unary
158
+
159
+ # Add a role value constraint
160
+ # REVISIT: Can add join-role-value-constraints here, if we ever provide a way to define them
161
+ if references[-1].to_role && references[-1].to_role.role_value_constraint
162
+ constraints << references[-1].to_role.role_value_constraint
163
+ end
164
+
165
+ vt = references[-1].is_self_value ? references[-1].from : references[-1].to
166
+ begin
167
+ params[:length] ||= vt.length if vt.length.to_i != 0
168
+ params[:scale] ||= vt.scale if vt.scale.to_i != 0
169
+ constraints << vt.value_constraint if vt.value_constraint
170
+ last_vt = vt
171
+ vt = vt.supertype
172
+ end while vt
173
+ params[:underlying_type] = last_vt
174
+ return [last_vt.name, params, constraints]
175
+ end
176
+
177
+ # The comment is the readings from the References expressed as a series of steps (not a full verbalisation)
178
+ def comment
179
+ @references.map do |ref|
180
+ ref.verbalised_path
181
+ end.compact * " and "
182
+ end
183
+
184
+ def to_s #:nodoc:
185
+ "#{@references[0].from.name} column #{name('.')}"
186
+ end
187
+ end
188
+
189
+ class Reference
190
+ def columns(excluded_supertypes) #:nodoc:
191
+ kind = ""
192
+ cols =
193
+ if is_unary
194
+ kind = "unary "
195
+ objectified_unary_columns =
196
+ ((@to && @to.fact_type) ? @to.all_columns(excluded_supertypes) : [])
197
+
198
+ =begin
199
+ # This code omits the unary if it's objectified and that plays a mandatory role
200
+ first_mandatory_column = nil
201
+ if (@to && @to.fact_type)
202
+ trace :unary_col, "Deciding whether to skip unary column for #{inspect}" do
203
+ first_mandatory_column =
204
+ objectified_unary_columns.detect do |col| # Detect a mandatory column for the unary
205
+ trace :unary_col, "checking column #{col.name}" do
206
+ !col.references.detect do |ref|
207
+ trace :unary_col, "#{ref} is mandatory=#{ref.is_mandatory.inspect}"
208
+ !ref.is_mandatory
209
+ end
210
+ end
211
+ end
212
+ if is_from_objectified_fact && first_mandatory_column
213
+ trace :unary_col, "Skipping unary column for #{inspect} because #{first_mandatory_column.name} is mandatory"
214
+ end
215
+ end
216
+ end
217
+
218
+ (is_from_objectified_fact && first_mandatory_column ? [] : [Column.new()]) + # The unary itself, unless its objectified
219
+ =end
220
+
221
+ [Column.new()] + # The unary itself
222
+ objectified_unary_columns
223
+ elsif is_self_value
224
+ kind = "self-role "
225
+ [Column.new()]
226
+ elsif is_simple_reference
227
+ @to.reference_columns(excluded_supertypes)
228
+ else
229
+ kind = "absorbing "
230
+ @to.all_columns(excluded_supertypes)
231
+ end
232
+
233
+ cols.each do |c|
234
+ c.prepend self
235
+ end
236
+
237
+ trace :columns, "Columns from #{kind}#{self}" do
238
+ cols.each {|c|
239
+ trace :columns, "#{c}"
240
+ }
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ module Metamodel #:nodoc:
247
+ # The ObjectType class is defined in the metamodel; full documentation is not generated.
248
+ # This section shows the features relevant to relational mapping.
249
+ class ObjectType
250
+ # The array of columns for this ObjectType's table
251
+ def columns
252
+ @columns || populate_columns
253
+ end
254
+
255
+ def populate_columns #:nodoc:
256
+ @columns =
257
+ all_columns({})
258
+ end
259
+
260
+ def wipe_columns
261
+ @columns = nil
262
+ end
263
+ end
264
+
265
+ # The ValueType class is defined in the metamodel; full documentation is not generated.
266
+ # This section shows the features relevant to relational mapping.
267
+ class ValueType < DomainObjectType
268
+ # The identifier_columns for a ValueType can only ever be the self-value role that was injected
269
+ def identifier_columns
270
+ trace :columns, "Identifier Columns for #{name}" do
271
+ raise "Illegal call to identifier_columns for absorbed ValueType #{name}" unless is_table
272
+ if isr = injected_surrogate_role
273
+ columns.select{|column| column.references[0].from_role == isr }
274
+ else
275
+ columns.select{|column| column.references[0] == self_value_reference}
276
+ end
277
+ end
278
+ end
279
+
280
+ # When creating a foreign key to this ValueType, what columns must we include?
281
+ # This must be a fresh copy, because the columns will have References prepended
282
+ def reference_columns(excluded_supertypes) #:nodoc:
283
+ trace :columns, "Reference Columns for #{name}" do
284
+ if is_table
285
+ if isr = injected_surrogate_role
286
+ ref_from = references_from.detect{|ref| ref.from_role == isr}
287
+ [ActiveFacts::RMap::Column.new(ref_from)]
288
+ else
289
+ [ActiveFacts::RMap::Column.new(self_value_reference)]
290
+ end
291
+ else
292
+ [ActiveFacts::RMap::Column.new]
293
+ end
294
+ end
295
+ end
296
+
297
+ # When absorbing this ValueType, what columns must be absorbed?
298
+ # This must be a fresh copy, because the columns will have References prepended.
299
+ def all_columns(excluded_supertypes) #:nodoc:
300
+ columns = []
301
+ trace :columns, "All Columns for #{name}" do
302
+ if is_table
303
+ self_value_reference
304
+ else
305
+ columns << ActiveFacts::RMap::Column.new
306
+ end
307
+ references_from.each do |ref|
308
+ trace :columns, "Columns absorbed via #{ref}" do
309
+ columns += ref.columns({})
310
+ end
311
+ end
312
+ end
313
+ columns
314
+ end
315
+
316
+ # If someone asks for this, it's because it's needed, so create it.
317
+ def self_value_reference #:nodoc:
318
+ # Make a reference for the self-value column
319
+ @self_value_reference ||= ActiveFacts::RMap::Reference.new(self, nil).tabulate
320
+ end
321
+ end
322
+
323
+ # The EntityType class is defined in the metamodel; full documentation is not generated.
324
+ # This section shows the features relevant to relational mapping.
325
+ class EntityType < DomainObjectType
326
+ # The identifier_columns for an EntityType are the columns that result from the identifying roles
327
+ def identifier_columns
328
+ trace :columns, "Identifier Columns for #{name}" do
329
+ if absorbed_via and
330
+ # If this is a subtype that has its own identification, use that.
331
+ (all_type_inheritance_as_subtype.size == 0 ||
332
+ all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification })
333
+ return absorbed_via.from.identifier_columns
334
+ end
335
+
336
+ preferred_identifier.role_sequence.all_role_ref.map do |role_ref|
337
+ ref = references_from.detect {|ref| ref.to_role == role_ref.role}
338
+
339
+ columns.select{|column| column.references[0] == ref}
340
+ end.flatten
341
+ end
342
+ end
343
+
344
+ # When creating a foreign key to this EntityType, what columns must we include (the identifier columns)?
345
+ # This must be a fresh copy, because the columns will have References prepended
346
+ def reference_columns(excluded_supertypes) #:nodoc:
347
+ trace :columns, "Reference Columns for #{name}" do
348
+
349
+ if absorbed_via and
350
+ # If this is not a subtype, or is a subtype that has its own identification, use the id.
351
+ (all_type_inheritance_as_subtype.size == 0 ||
352
+ all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification })
353
+ rc = absorbed_via.from.reference_columns(excluded_supertypes)
354
+ # The absorbed_via reference gets skipped here, and also in object_type.rb
355
+ trace :columns, "Skipping #{absorbed_via}"
356
+ absorbed_mirror ||= absorbed_via.reversed
357
+ rc.each{|col| col.prepend(absorbed_mirror)}
358
+ return rc
359
+ end
360
+
361
+ # REVISIT: Should have built preferred_identifier_references
362
+ preferred_identifier.role_sequence.all_role_ref.map do |role_ref|
363
+ # REVISIT: Should index references by to_role:
364
+ ref = references_from.detect {|ref| ref.to_role == role_ref.role}
365
+
366
+ raise "reference for role #{role_ref.describe} not found on #{name} in #{references_from.size} references:\n\t#{references_from.map(&:to_s)*"\n\t"}" unless ref
367
+
368
+ ref.columns({})
369
+ end.flatten
370
+ end
371
+ end
372
+
373
+ # When absorbing this EntityType, what columns must be absorbed?
374
+ # This must be a fresh copy, because the columns will have References prepended.
375
+ def all_columns(excluded_supertypes) #:nodoc:
376
+ trace :columns, "All Columns for #{name}" do
377
+ columns = []
378
+ sups = supertypes
379
+ pi_roles = preferred_identifier.role_sequence.all_role_ref.map{|rr| rr.role}
380
+ references_from.sort_by do |ref|
381
+ # Put supertypes first, in order, then PI roles, non-subtype references by name, then subtypes by name:
382
+ next [0, p] if p = sups.index(ref.to)
383
+ if !ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
384
+ next [1, p] if p = pi_roles.index(ref.to_role)
385
+ next [2, ref.to_names]
386
+ end
387
+ [3, ref.to_names]
388
+ end.each do |ref|
389
+ trace :columns, "Columns absorbed via #{ref}" do
390
+ if (ref.role_type == :supertype)
391
+ if excluded_supertypes[ref.to]
392
+ trace :columns, "Exclude #{ref.to.name}, we already inherited it"
393
+ next
394
+ end
395
+
396
+ next if (ref.to.absorbed_via != ref)
397
+ excluded_supertypes[ref.to] = true
398
+ columns += ref.columns(excluded_supertypes)
399
+ else
400
+ columns += ref.columns({})
401
+ end
402
+ end
403
+ end
404
+ columns
405
+ end
406
+ end
407
+ end
408
+
409
+ # The Vocabulary class is defined in the metamodel; full documentation is not generated.
410
+ # This section shows the features relevant to relational mapping.
411
+ class Vocabulary
412
+ # Make schema transformations like adding ValueType self-value columns (and later, Rails-friendly ID fields).
413
+ # Override this method to change the transformations
414
+ def finish_schema
415
+ all_object_type.each do |object_type|
416
+ object_type.self_value_reference if object_type.is_a?(ActiveFacts::Metamodel::ValueType) && object_type.is_table
417
+ end
418
+ end
419
+
420
+ def populate_all_columns #:nodoc:
421
+ # REVISIT: Is now a good time to apply schema transforms or should this be more explicit?
422
+ finish_schema
423
+
424
+ trace :columns, "Populating all columns" do
425
+ tables.each do |object_type|
426
+ trace :columns, "Populating columns for table #{object_type.name}" do
427
+ object_type.populate_columns
428
+ end
429
+ end
430
+ end
431
+ trace :columns, "Finished columns" do
432
+ tables.each do |object_type|
433
+ trace :columns, "Finished columns for table #{object_type.name}" do
434
+ object_type.columns.each do |column|
435
+ trace :columns, "#{column}"
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
442
+
443
+ end
444
+ end