activefacts-rmap 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
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