linkage 0.0.6 → 0.0.8

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.
Files changed (66) hide show
  1. data/.gitignore +10 -0
  2. data/Gemfile +15 -13
  3. data/Gemfile.lock +67 -37
  4. data/Guardfile +0 -2
  5. data/Rakefile +122 -25
  6. data/lib/linkage/comparator.rb +172 -0
  7. data/lib/linkage/comparators/binary.rb +12 -0
  8. data/lib/linkage/comparators/compare.rb +46 -0
  9. data/lib/linkage/comparators/within.rb +32 -0
  10. data/lib/linkage/configuration.rb +285 -153
  11. data/lib/linkage/data.rb +32 -7
  12. data/lib/linkage/dataset.rb +107 -32
  13. data/lib/linkage/decollation.rb +93 -0
  14. data/lib/linkage/expectation.rb +21 -0
  15. data/lib/linkage/expectations/exhaustive.rb +63 -0
  16. data/lib/linkage/expectations/simple.rb +168 -0
  17. data/lib/linkage/field.rb +30 -4
  18. data/lib/linkage/field_set.rb +6 -3
  19. data/lib/linkage/function.rb +50 -3
  20. data/lib/linkage/functions/binary.rb +30 -0
  21. data/lib/linkage/functions/cast.rb +54 -0
  22. data/lib/linkage/functions/length.rb +29 -0
  23. data/lib/linkage/functions/strftime.rb +12 -11
  24. data/lib/linkage/functions/trim.rb +8 -0
  25. data/lib/linkage/group.rb +20 -0
  26. data/lib/linkage/import_buffer.rb +5 -16
  27. data/lib/linkage/meta_object.rb +139 -0
  28. data/lib/linkage/result_set.rb +74 -17
  29. data/lib/linkage/runner/single_threaded.rb +125 -10
  30. data/lib/linkage/version.rb +3 -0
  31. data/lib/linkage.rb +11 -0
  32. data/linkage.gemspec +16 -121
  33. data/test/config.yml +5 -0
  34. data/test/helper.rb +73 -8
  35. data/test/integration/test_collation.rb +45 -0
  36. data/test/integration/test_configuration.rb +268 -0
  37. data/test/integration/test_cross_linkage.rb +4 -17
  38. data/test/integration/test_dataset.rb +45 -2
  39. data/test/integration/test_dual_linkage.rb +40 -24
  40. data/test/integration/test_functions.rb +22 -0
  41. data/test/integration/test_result_set.rb +85 -0
  42. data/test/integration/test_scoring.rb +84 -0
  43. data/test/integration/test_self_linkage.rb +5 -0
  44. data/test/integration/test_within_comparator.rb +100 -0
  45. data/test/unit/comparators/test_compare.rb +105 -0
  46. data/test/unit/comparators/test_within.rb +57 -0
  47. data/test/unit/expectations/test_exhaustive.rb +111 -0
  48. data/test/unit/expectations/test_simple.rb +303 -0
  49. data/test/unit/functions/test_binary.rb +54 -0
  50. data/test/unit/functions/test_cast.rb +98 -0
  51. data/test/unit/functions/test_length.rb +52 -0
  52. data/test/unit/functions/test_strftime.rb +17 -13
  53. data/test/unit/functions/test_trim.rb +11 -4
  54. data/test/unit/test_comparator.rb +124 -0
  55. data/test/unit/test_configuration.rb +137 -175
  56. data/test/unit/test_data.rb +44 -0
  57. data/test/unit/test_dataset.rb +73 -21
  58. data/test/unit/test_decollation.rb +201 -0
  59. data/test/unit/test_field.rb +38 -14
  60. data/test/unit/test_field_set.rb +12 -8
  61. data/test/unit/test_function.rb +83 -16
  62. data/test/unit/test_group.rb +28 -0
  63. data/test/unit/test_import_buffer.rb +13 -27
  64. data/test/unit/test_meta_object.rb +208 -0
  65. data/test/unit/test_result_set.rb +221 -3
  66. metadata +82 -190
@@ -1,7 +1,6 @@
1
1
  module Linkage
2
2
  class Configuration
3
3
  class DSL
4
-
5
4
  # Class for visually comparing matched records
6
5
  class VisualComparisonWrapper
7
6
  attr_reader :dsl, :lhs, :rhs
@@ -33,119 +32,97 @@ module Linkage
33
32
  :>= => :<
34
33
  }
35
34
 
36
- attr_reader :kind, :side, :lhs, :rhs
37
-
38
- def initialize(dsl, type, lhs)
35
+ def initialize(dsl, type, lhs, *args)
39
36
  @dsl = dsl
40
37
  @type = type
41
38
  @lhs = lhs
42
- @rhs = nil
43
- @side = nil
44
- @kind = nil
45
39
  end
46
40
 
47
- VALID_OPERATORS.each do |operator|
48
- define_method(operator) do |rhs|
49
- # NOTE: lhs is always a DataWrapper
50
-
51
- @rhs = rhs
52
- if !@rhs.is_a?(DataWrapper) || @lhs.static? || @rhs.static? || @lhs.side == @rhs.side
53
- @side = @lhs.side
54
- @side = @rhs.side if @side.nil? && @rhs.is_a?(DataWrapper)
55
- @kind = :filter
56
- elsif @lhs.same_except_side?(@rhs)
57
- @kind = :self
58
- elsif @lhs.dataset == @rhs.dataset
59
- @kind = :cross
60
- else
61
- @kind = :dual
62
- end
63
- @operator = @type == :must_not ? OPERATOR_OPPOSITES[operator] : operator
64
- @dsl.add_expectation(self)
65
- end
66
- end
41
+ def compare_with(operator, rhs)
42
+ # NOTE: lhs is always a DataWrapper
67
43
 
68
- def merged_field
69
- @merged_field ||= @lhs.data.merge(@rhs.data)
70
- end
44
+ if !rhs.is_a?(DataWrapper) || @lhs.static? || rhs.static? || @lhs.side == rhs.side
45
+ @side = !@lhs.static? ? @lhs.side : rhs.side
71
46
 
72
- def filter_expr
73
- if @filter_expr.nil? && @kind == :filter
74
- if @lhs.is_a?(DataWrapper) && !@lhs.static?
75
- target = @lhs
76
- other = @rhs
77
- elsif @rhs.is_a?(DataWrapper) && !@rhs.static?
78
- target = @rhs
79
- other = @lhs
80
- else
81
- raise "Wonky filter"
47
+ # If one of the objects in this comparison is a static function, we need to set the side
48
+ # and the dataset based on the other object
49
+ if rhs.is_a?(DataWrapper) && !rhs.static? && @lhs.is_a?(FunctionWrapper) && @lhs.static?
50
+ @lhs.dataset = rhs.dataset
51
+ @lhs.side = @side
52
+ elsif @lhs.is_a?(DataWrapper) && !@lhs.static? && rhs.is_a?(FunctionWrapper) && rhs.static?
53
+ rhs.dataset = @lhs.dataset
54
+ rhs.side = @side
82
55
  end
56
+ elsif rhs.is_a?(DataWrapper) && operator != :==
57
+ # create an exhaustive expectation with the Compare comparator instead
58
+ comparator = Comparators::Compare.new(@lhs.meta_object,
59
+ MetaObject.new(operator.to_s), rhs.meta_object)
83
60
 
84
- arg1 = target.to_expr(@side)
85
- arg2 = other.is_a?(DataWrapper) ? other.to_expr(@side) : other
86
- @filter_expr =
87
- case @operator
88
- when :==
89
- { arg1 => arg2 }
90
- when :'!='
91
- ~{ arg1 => arg2 }
92
- else
93
- arg1 = Sequel::SQL::Identifier.new(arg1)
94
- arg2 = arg2.is_a?(Symbol) ? Sequel::SQL::Identifier.new(arg2) : arg2
95
- Sequel::SQL::BooleanExpression.new(@operator, arg1, arg2)
96
- end
97
- end
98
- @filter_expr
99
- end
61
+ score_range = Comparators::Compare.score_range
62
+ threshold = @type == :must ? score_range.last : score_range.first
100
63
 
101
- def apply_to(dataset, side)
102
- if @kind == :filter
103
- if @side == side
104
- return dataset.filter(filter_expr)
105
- else
106
- # Doesn't apply
107
- return dataset
108
- end
64
+ expectation = Expectations::Exhaustive.new(comparator, threshold, :equal)
65
+ @dsl.add_exhaustive_expectation(expectation)
66
+ return self
109
67
  end
110
68
 
111
- if @lhs.is_a?(DataWrapper) && @lhs.side == side
112
- target = @lhs
113
- elsif @rhs.is_a?(DataWrapper) && @rhs.side == side
114
- target = @rhs
115
- else
116
- raise "Wonky expectation"
117
- end
69
+ exp_operator = @type == :must_not ? OPERATOR_OPPOSITES[operator] : operator
118
70
 
119
- expr = target.to_expr(side)
120
- aliaz = nil
121
- if expr != merged_field.name
122
- aliaz = merged_field.name
123
- end
71
+ rhs_meta_object = rhs.is_a?(DataWrapper) ? rhs.meta_object : MetaObject.new(rhs)
72
+ @expectation = Expectations::Simple.create(@lhs.meta_object,
73
+ rhs_meta_object, exp_operator)
74
+ @dsl.add_simple_expectation(@expectation)
75
+ self
76
+ end
124
77
 
125
- dataset.match(expr, aliaz)
78
+ VALID_OPERATORS.each do |operator|
79
+ define_method(operator) do |rhs|
80
+ compare_with(operator, rhs)
81
+ end
126
82
  end
127
83
 
128
- def same_filter?(other)
129
- kind == :filter && other.kind == :filter && filter_expr == other.filter_expr
84
+ def exactly
85
+ if !@exact_match
86
+ @expectation.exactly!
87
+ end
130
88
  end
131
89
  end
132
90
 
133
91
  class DataWrapper
134
- attr_reader :side, :dataset
92
+ attr_reader :meta_object
135
93
 
136
94
  def initialize
137
95
  raise NotImplementedError
138
96
  end
139
97
 
140
98
  [:must, :must_not].each do |type|
141
- define_method(type) do
142
- ExpectationWrapper.new(@dsl, type, self)
99
+ define_method(type) do |*args|
100
+ if args.length > 0
101
+ wrapper = args[0]
102
+ comparator = wrapper.to_comparator(self)
103
+
104
+ score_range = wrapper.klass.score_range
105
+ threshold = type == :must ? score_range.last : score_range.first
106
+
107
+ expectation = Expectations::Exhaustive.new(comparator, threshold, :equal)
108
+ @dsl.add_exhaustive_expectation(expectation)
109
+ else
110
+ ExpectationWrapper.new(@dsl, type, self)
111
+ end
143
112
  end
144
113
  end
145
114
 
146
115
  def compare_with(other)
147
116
  VisualComparisonWrapper.new(@dsl, self, other)
148
117
  end
118
+
119
+ def method_missing(m, *args, &block)
120
+ if meta_object.respond_to?(m)
121
+ meta_object.send(m, *args, &block)
122
+ else
123
+ super(m, *args, &block)
124
+ end
125
+ end
149
126
  end
150
127
 
151
128
  class FieldWrapper < DataWrapper
@@ -153,80 +130,51 @@ module Linkage
153
130
 
154
131
  def initialize(dsl, side, dataset, name)
155
132
  @dsl = dsl
156
- @side = side
157
- @dataset = dataset
158
- @name = name
159
- end
160
-
161
- def static?
162
- false
163
- end
164
-
165
- def same_except_side?(other)
166
- other.is_a?(FieldWrapper) && name == other.name
167
- end
168
-
169
- def data
170
- @dataset.field_set[@name]
171
- end
172
-
173
- def to_expr(side = nil)
174
- data.to_expr
133
+ @meta_object = MetaObject.new(dataset.field_set[name], side)
175
134
  end
176
135
  end
177
136
 
178
137
  class FunctionWrapper < DataWrapper
179
- attr_reader :klass, :args
180
-
181
138
  def initialize(dsl, klass, args)
182
139
  @dsl = dsl
183
- @klass = klass
184
- @args = args
185
- @side = nil
186
- @static = true
140
+
141
+ side = dataset = nil
142
+ static = true
143
+ function_args = []
187
144
  args.each do |arg|
188
145
  if arg.kind_of?(DataWrapper)
189
- raise "conflicting sides" if @side && @side != arg.side
190
- @side = arg.side
191
- @static &&= arg.static?
146
+ raise "conflicting sides" if side && side != arg.side
147
+ side = arg.side
148
+ static &&= arg.static?
149
+ dataset = arg.dataset
150
+ function_args << arg.object
151
+ else
152
+ function_args << arg
192
153
  end
193
154
  end
155
+ @meta_object = MetaObject.new(klass.new(*function_args), side)
194
156
  end
157
+ end
195
158
 
196
- def data
197
- @data ||= @klass.new(*@args.collect { |arg| arg.kind_of?(DataWrapper) ? arg.data : arg })
198
- end
199
-
200
- def to_expr(side)
201
- dataset = side == :lhs ? @dsl.lhs : @dsl.rhs
202
- data.to_expr(dataset.dataset.adapter_scheme)
203
- end
159
+ class ComparatorWrapper
160
+ attr_reader :klass, :args
204
161
 
205
- def name
206
- data.name
162
+ def initialize(dsl, klass, args)
163
+ @dsl = dsl
164
+ @klass = klass
165
+ @args = args
207
166
  end
208
167
 
209
- def static?
210
- @static
168
+ def of(*args)
169
+ @args.push(*args)
170
+ self
211
171
  end
212
172
 
213
- def same_except_side?(other)
214
- if other.is_a?(FunctionWrapper) && klass == other.klass
215
- args.each_with_index do |arg, i|
216
- other_arg = other.args[i]
217
- if arg.is_a?(DataWrapper) && other_arg.is_a?(DataWrapper)
218
- if !arg.same_except_side?(other_arg)
219
- return false
220
- end
221
- else
222
- if arg != other_arg
223
- return false
224
- end
225
- end
226
- end
227
- return true
173
+ def to_comparator(receiver)
174
+ comparator_args = ([receiver] + @args).collect do |arg|
175
+ arg.is_a?(DataWrapper) ? arg.meta_object : MetaObject.new(arg)
228
176
  end
229
- false
177
+ comparator = klass.new(*comparator_args)
230
178
  end
231
179
  end
232
180
 
@@ -268,8 +216,12 @@ module Linkage
268
216
  @config.results_uri_options = options
269
217
  end
270
218
 
271
- def add_expectation(expectation)
272
- @config.expectations << expectation
219
+ def set_record_cache_size(num)
220
+ @config.record_cache_size = num
221
+ end
222
+
223
+ def add_simple_expectation(expectation)
224
+ @config.add_simple_expectation(expectation)
273
225
 
274
226
  if @config.linkage_type == :self
275
227
  case expectation.kind
@@ -288,7 +240,7 @@ module Linkage
288
240
 
289
241
  these_filters << expectation
290
242
  other_filters.each do |other|
291
- if !expectation.same_filter?(other)
243
+ if !expectation.same_except_side?(other)
292
244
  @config.linkage_type = :cross
293
245
  break
294
246
  end
@@ -297,36 +249,98 @@ module Linkage
297
249
  end
298
250
  end
299
251
 
252
+ def add_exhaustive_expectation(expectation)
253
+ @config.add_exhaustive_expectation(expectation)
254
+ if @config.linkage_type == :self
255
+ @config.linkage_type = expectation.kind
256
+ end
257
+ end
258
+
300
259
  def add_visual_comparison(visual_comparison)
301
260
  @config.visual_comparisons << visual_comparison
302
261
  end
303
262
 
304
- # For handling functions
263
+ def groups_table_name(new_name)
264
+ @config.groups_table_name = new_name
265
+ end
266
+
267
+ def original_groups_table_name(new_name)
268
+ @config.original_groups_table_name = new_name
269
+ end
270
+
271
+ def scores_table_name(new_name)
272
+ @config.scores_table_name = new_name
273
+ end
274
+
275
+ def matches_table_name(new_name)
276
+ @config.matches_table_name = new_name
277
+ end
278
+
305
279
  def method_missing(name, *args, &block)
306
- klass = Function[name.to_s]
307
- if klass
308
- FunctionWrapper.new(self, klass, args)
280
+ # check for comparators
281
+ md = name.to_s.match(/^be_(.+)$/)
282
+ if md
283
+ klass = Comparator[md[1]]
284
+ if klass
285
+ ComparatorWrapper.new(self, klass, args)
286
+ else
287
+ super
288
+ end
309
289
  else
310
- super
290
+ # check for functions
291
+ klass = Function[name.to_s]
292
+ if klass
293
+ FunctionWrapper.new(self, klass, args)
294
+ else
295
+ super
296
+ end
311
297
  end
312
298
  end
313
299
  end
314
300
 
315
- attr_reader :dataset_1, :dataset_2, :expectations, :visual_comparisons
316
- attr_accessor :linkage_type, :results_uri, :results_uri_options
301
+ attr_reader :dataset_1, :dataset_2, :simple_expectations,
302
+ :exhaustive_expectations, :visual_comparisons
303
+ attr_accessor :linkage_type, :results_uri, :results_uri_options,
304
+ :record_cache_size, :groups_table_name, :original_groups_table_name,
305
+ :scores_table_name, :matches_table_name
317
306
 
318
307
  def initialize(dataset_1, dataset_2)
319
308
  @dataset_1 = dataset_1
320
309
  @dataset_2 = dataset_2
321
310
  @linkage_type = dataset_1 == dataset_2 ? :self : :dual
322
- @expectations = []
311
+ @simple_expectations = []
312
+ @exhaustive_expectations = []
323
313
  @visual_comparisons = []
314
+ @results_uri_options = {}
315
+ @decollation_needed = false
316
+ @record_cache_size = 10_000
317
+ @groups_table_name = :groups
318
+ @original_groups_table_name = :original_groups
319
+ @scores_table_name = :scores
320
+ @matches_table_name = :matches
324
321
  end
325
322
 
326
323
  def configure(&block)
327
324
  DSL.new(self, &block)
328
325
  end
329
326
 
327
+ def results_uri=(uri)
328
+ @results_uri = uri
329
+ if !@decollation_needed
330
+ @simple_expectations.each do |expectation|
331
+ if decollation_needed_for_simple_expectation?(expectation)
332
+ @decollation_needed = true
333
+ break
334
+ end
335
+ end
336
+ end
337
+ uri
338
+ end
339
+
340
+ def decollation_needed?
341
+ @decollation_needed
342
+ end
343
+
330
344
  def groups_table_schema
331
345
  schema = []
332
346
 
@@ -334,29 +348,147 @@ module Linkage
334
348
  schema << [:id, Integer, {:primary_key => true}]
335
349
 
336
350
  # add values
337
- @expectations.each do |exp|
351
+ @simple_expectations.each do |exp|
338
352
  next if exp.kind == :filter
339
353
 
340
354
  merged_field = exp.merged_field
341
355
  merged_type = merged_field.ruby_type
342
- schema << [merged_field.name, merged_type[:type], merged_type[:opts] || {}]
356
+
357
+ # if the merged field's database type is different than the result
358
+ # database, strip collation information
359
+ result_db_type = nil
360
+ result_set.database do |db|
361
+ result_db_type = db.database_type
362
+ end
363
+ if merged_field.database_type != result_db_type && merged_type.has_key?(:opts)
364
+ new_opts = merged_type[:opts].reject { |k, v| k == :collate }
365
+ merged_type = merged_type.merge(:opts => new_opts)
366
+ end
367
+
368
+ col = [merged_field.name, merged_type[:type], merged_type[:opts] || {}]
369
+ schema << col
343
370
  end
344
371
 
345
372
  schema
346
373
  end
347
374
 
375
+ def scores_table_schema
376
+ schema = []
377
+
378
+ # add id
379
+ schema << [:id, Integer, {:primary_key => true}]
380
+
381
+ # add comparator id
382
+ schema << [:comparator_id, Integer, {}]
383
+
384
+ # add record ids
385
+ pk = dataset_1.field_set.primary_key
386
+ ruby_type = pk.ruby_type
387
+ schema << [:record_1_id, ruby_type[:type], ruby_type[:opts] || {}]
388
+
389
+ pk = dataset_2.field_set.primary_key
390
+ ruby_type = pk.ruby_type
391
+ schema << [:record_2_id, ruby_type[:type], ruby_type[:opts] || {}]
392
+
393
+ # add score
394
+ schema << [:score, Integer, {}]
395
+
396
+ schema
397
+ end
398
+
399
+ def matches_table_schema
400
+ schema = []
401
+
402
+ # add id
403
+ schema << [:id, Integer, {:primary_key => true}]
404
+
405
+ # add record ids
406
+ pk = dataset_1.field_set.primary_key
407
+ ruby_type = pk.ruby_type
408
+ schema << [:record_1_id, ruby_type[:type], ruby_type[:opts] || {}]
409
+
410
+ pk = dataset_2.field_set.primary_key
411
+ ruby_type = pk.ruby_type
412
+ schema << [:record_2_id, ruby_type[:type], ruby_type[:opts] || {}]
413
+
414
+ # add score
415
+ schema << [:total_score, Integer, {}]
416
+
417
+ schema
418
+ end
419
+
420
+ def add_simple_expectation(expectation)
421
+ @simple_expectations << expectation
422
+ @decollation_needed ||= decollation_needed_for_simple_expectation?(expectation)
423
+ expectation
424
+ end
425
+
426
+ def add_exhaustive_expectation(expectation)
427
+ @exhaustive_expectations << expectation
428
+ expectation
429
+ end
430
+
348
431
  def result_set
349
432
  @result_set ||= ResultSet.new(self)
350
433
  end
351
434
 
352
- def datasets_with_applied_expectations
435
+ def datasets_with_applied_simple_expectations
353
436
  dataset_1 = @dataset_1
354
437
  dataset_2 = @dataset_2
355
- @expectations.each do |exp|
438
+ @simple_expectations.each do |exp|
356
439
  dataset_1 = exp.apply_to(dataset_1, :lhs)
357
440
  dataset_2 = exp.apply_to(dataset_2, :rhs) if @linkage_type != :self
358
441
  end
359
442
  @linkage_type == :self ? [dataset_1, dataset_1] : [dataset_1, dataset_2]
360
443
  end
444
+
445
+ def datasets_with_applied_exhaustive_expectations
446
+ apply_exhaustive_expectations(@dataset_1, @dataset_2)
447
+ end
448
+
449
+ def apply_exhaustive_expectations(dataset_1, dataset_2)
450
+ dataset_1 = dataset_1.select(dataset_1.field_set.primary_key.to_expr)
451
+ dataset_2 = dataset_2.select(dataset_2.field_set.primary_key.to_expr)
452
+ @exhaustive_expectations.each do |exp|
453
+ dataset_1 = exp.apply_to(dataset_1, :lhs)
454
+ dataset_2 = exp.apply_to(dataset_2, :rhs)
455
+ end
456
+ [dataset_1, dataset_2]
457
+ end
458
+
459
+ def groups_table_needed?
460
+ has_simple_expectations?
461
+ end
462
+
463
+ def scores_table_needed?
464
+ has_exhaustive_expectations?
465
+ end
466
+
467
+ def has_simple_expectations?
468
+ !@simple_expectations.empty?
469
+ end
470
+
471
+ def has_exhaustive_expectations?
472
+ !@exhaustive_expectations.empty?
473
+ end
474
+
475
+ private
476
+
477
+ def decollation_needed_for_simple_expectation?(expectation)
478
+ if expectation.decollation_needed?
479
+ true
480
+ elsif results_uri && expectation.kind != :filter
481
+ result_set_database_type = ResultSet.new(self).database.database_type
482
+ database_types_differ =
483
+ result_set_database_type != dataset_1.database_type ||
484
+ result_set_database_type != dataset_2.database_type
485
+
486
+ merged_field = expectation.merged_field
487
+ merged_field.ruby_type[:type] == String &&
488
+ !merged_field.collation.nil? && database_types_differ
489
+ else
490
+ false
491
+ end
492
+ end
361
493
  end
362
494
  end
data/lib/linkage/data.rb CHANGED
@@ -17,9 +17,14 @@ module Linkage
17
17
  File => nil
18
18
  }
19
19
 
20
- # @return [Symbol] This object's name
20
+ # @!attribute [r] name
21
+ # @return [Symbol] This object's name
21
22
  attr_reader :name
22
23
 
24
+ # @!attribute [r] dataset
25
+ # @return [Linkage::Dataset, nil] This object's dataset, or nil
26
+ attr_reader :dataset
27
+
23
28
  def initialize(name)
24
29
  @name = name
25
30
  end
@@ -28,20 +33,35 @@ module Linkage
28
33
  raise NotImplementedError
29
34
  end
30
35
 
31
- def to_expr(adapter = nil)
36
+ def to_expr
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def collation
41
+ nil
42
+ end
43
+
44
+ def database_type
45
+ ds = dataset
46
+ ds ? ds.database_type : nil
47
+ end
48
+
49
+ def static?
32
50
  raise NotImplementedError
33
51
  end
34
52
 
35
- # Create a data object that can hold data from two other fields. If the fields
36
- # have different types, the resulting type is determined via a
53
+ # Create a merge field that can hold data from two data sources. If the
54
+ # fields have different types, the resulting type is determined via a
37
55
  # type-conversion tree.
38
56
  #
39
57
  # @param [Linkage::Data] other
40
- # @return [Linkage::Field]
58
+ # @return [Linkage::MergeField]
41
59
  def merge(other, new_name = nil)
42
60
  schema_1 = self.ruby_type
61
+ db_type_1 = self.database_type
43
62
  schema_2 = other.ruby_type
44
- if schema_1 == schema_2
63
+ db_type_2 = other.database_type
64
+ if schema_1 == schema_2 && db_type_1 == db_type_2
45
65
  result = schema_1
46
66
  else
47
67
  type_1 = schema_1[:type]
@@ -113,6 +133,11 @@ module Linkage
113
133
  result_opts[:fixed] = true
114
134
  end
115
135
 
136
+ # collation
137
+ if opts_1[:collate] != opts_2[:collate] || db_type_1 != db_type_2
138
+ result_opts.delete(:collate)
139
+ end
140
+
116
141
  result = {:type => result_type}
117
142
  result[:opts] = result_opts unless result_opts.empty?
118
143
  end
@@ -122,7 +147,7 @@ module Linkage
122
147
  else
123
148
  name = self.name == other.name ? self.name : :"#{self.name}_#{other.name}"
124
149
  end
125
- Field.new(name, nil, result)
150
+ MergeField.new(name, result, db_type_1 == db_type_2 ? db_type_1 : nil)
126
151
  end
127
152
 
128
153
  private