linkage 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,13 +2,28 @@ module Linkage
2
2
  # Delegator around Sequel::Dataset with some extra functionality.
3
3
  class Dataset
4
4
  attr_reader :field_set, :table_name
5
+ attr_accessor :linkage_options
5
6
 
6
- def initialize(uri, table, options = {})
7
- @table_name = table.to_sym
8
- db = Sequel.connect(uri, options)
9
- @dataset = db[@table_name]
10
- @field_set = FieldSet.new(db.schema(@table_name))
11
- @_match = []
7
+ def initialize(*args)
8
+ if args.length == 1
9
+ @dataset = args[0]
10
+ @db = @dataset.db
11
+ @table_name = @dataset.first_source_table
12
+
13
+ if !@db.kind_of?(Sequel::Collation)
14
+ @db.extend(Sequel::Collation)
15
+ end
16
+ else
17
+ uri, table, options = args
18
+ options ||= {}
19
+
20
+ @table_name = table.to_sym
21
+ @db = Sequel.connect(uri, options)
22
+ @db.extend(Sequel::Collation)
23
+ @dataset = @db[@table_name]
24
+ end
25
+ @field_set = FieldSet.new(self)
26
+ @linkage_options = {}
12
27
  end
13
28
 
14
29
  def obj
@@ -28,67 +43,127 @@ module Linkage
28
43
  conf
29
44
  end
30
45
 
31
- def adapter_scheme
32
- @dataset.db.adapter_scheme
46
+ def database_type
47
+ @db.database_type
33
48
  end
34
49
 
35
- def match(expr, aliaz = nil)
36
- clone(:match => {:expr => expr, :alias => aliaz})
50
+ # Set objects to use for group matching. Accepts either {Linkage::MetaObject} or a
51
+ # hash with options (valid options are :meta_object, :alias, and :cast).
52
+ #
53
+ # @example
54
+ # dataset.group_match(meta_object_1,
55
+ # {:meta_object => meta_object_2, :alias => :foo})
56
+ def group_match(*args)
57
+ args.collect! do |arg|
58
+ case arg
59
+ when Linkage::MetaObject
60
+ { :meta_object => arg }
61
+ when Hash
62
+ if !arg.has_key?(:meta_object)
63
+ raise ArgumentError, "Invalid option hash, missing :meta_object key"
64
+ end
65
+ (arg.keys - [:meta_object, :alias, :cast]).each do |invalid_key|
66
+ warn "Invalid key in option hash: #{invalid_key}"
67
+ end
68
+ arg
69
+ else
70
+ raise ArgumentError, "expected Hash or MetaObject, got #{arg.class}"
71
+ end
72
+ end
73
+ clone(:group_match => args)
74
+ end
75
+
76
+ # Add additional objects to use for group matching.
77
+ def group_match_more(*args)
78
+ args = @linkage_options[:group_match] + args if @linkage_options[:group_match]
79
+ group_match(*args)
37
80
  end
38
81
 
39
- def clone(new_opts={})
40
- new_opts = new_opts.dup
41
- new_obj = new_opts.delete(:new_obj)
82
+ def clone(new_options = {})
83
+ new_linkage_options = {}
84
+ new_obj_options = {}
85
+ new_options.each_pair do |k, v|
86
+ case k
87
+ when :group_match
88
+ new_linkage_options[k] = v
89
+ else
90
+ new_obj_options[k] = v
91
+ end
92
+ end
93
+ new_obj = new_options[:new_obj]
42
94
 
43
- match = new_opts.delete(:match)
44
95
  result = super()
45
- result.send(:_match, match)
96
+ result.linkage_options = @linkage_options.merge(new_linkage_options)
46
97
 
47
98
  if new_obj
48
99
  result.obj = new_obj
49
100
  else
50
- result.obj = obj.clone(new_opts)
101
+ result.obj = obj.clone(new_options)
51
102
  end
103
+
52
104
  result
53
105
  end
54
106
 
55
107
  def each_group(min = 2)
56
- @dataset.group_and_count(*aliased_match_expressions).having{count >= min}.each do |row|
108
+ group_match = @linkage_options[:group_match] || []
109
+ ruby_types = group_match.inject({}) do |hsh, m|
110
+ key = m[:alias] || m[:meta_object].to_expr
111
+ hsh[key] = m[:meta_object].ruby_type
112
+ hsh
113
+ end
114
+ options = {:database_type => database_type, :ruby_types => ruby_types }
115
+ @dataset.group_and_count(*match_expressions).having{count >= min}.each do |row|
57
116
  count = row.delete(:count)
58
- yield Group.new(row, {:count => count})
117
+ group = Group.new(row, options.merge(:count => count))
118
+ yield group
59
119
  end
60
120
  end
61
121
 
62
- def group_by_matches(aliased = false)
63
- expr = aliased ? aliased_match_expressions : match_expressions
122
+ def group_by_matches(raw = true)
123
+ expr = raw ? raw_match_expressions : match_expressions
64
124
  group(*expr)
65
125
  end
66
126
 
67
127
  def dataset_for_group(group)
68
128
  filters = []
129
+ group_match = @linkage_options[:group_match] || []
69
130
  group.values.each_pair do |key, value|
70
131
  # find a matched expression with this alias
71
- m = @_match.detect { |h| h[:alias] ? h[:alias] == key : h[:expr] == key }
72
- raise "this dataset isn't compatible with the given group" if !m
73
- filters << {m[:expr] => value}
132
+ found = false
133
+ group_match.each do |m|
134
+ expr = m[:meta_object].to_expr
135
+ if (m[:alias] && m[:alias] == key) || expr == key
136
+ found = true
137
+ filters << {expr => value}
138
+ break
139
+ end
140
+ end
141
+ if !found
142
+ raise "this dataset isn't compatible with the given group"
143
+ end
74
144
  end
75
145
  filter(*filters)
76
146
  end
77
147
 
148
+ def schema
149
+ @db.schema(@table_name)
150
+ end
151
+
78
152
  private
79
153
 
80
- def _match(opts)
81
- if opts
82
- @_match += [opts]
83
- end
154
+ def raw_match_expressions
155
+ group_match = @linkage_options[:group_match] || []
156
+ group_match.collect { |m| m[:meta_object].to_expr }
84
157
  end
85
158
 
86
159
  def match_expressions
87
- @_match.collect { |m| m[:expr] }
88
- end
89
-
90
- def aliased_match_expressions
91
- @_match.collect { |m| m[:alias] ? m[:expr].as(m[:alias]) : m[:expr] }
160
+ group_match = @linkage_options[:group_match] || []
161
+ group_match.collect do |m|
162
+ expr = m[:meta_object].to_expr
163
+ expr = expr.as(m[:alias]) if m[:alias]
164
+ expr = expr.cast(m[:cast]) if m[:cast]
165
+ expr
166
+ end
92
167
  end
93
168
 
94
169
  def method_missing(name, *args, &block)
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+ module Linkage
3
+ module Decollation
4
+ def decollate(string, database_type, collation)
5
+ case database_type
6
+ when :mysql
7
+ decollate_mysql(string, collation)
8
+ else
9
+ string
10
+ end
11
+ end
12
+
13
+ def decollate_mysql(string, collation)
14
+ case collation
15
+ when "latin1_swedish_ci"
16
+ decollate_mysql_latin1_swedish_ci(string)
17
+ else
18
+ string
19
+ end
20
+ end
21
+
22
+ def decollate_mysql_latin1_swedish_ci(string)
23
+ result = string.strip
24
+ result.each_char.with_index do |char, i|
25
+ case char
26
+ when 'A', 'a', 'À', 'Á', 'Â', 'Ã', 'à', 'á', 'â', 'ã'
27
+ result[i] = 'A'
28
+ when 'B', 'b'
29
+ result[i] = 'B'
30
+ when 'C', 'c', 'Ç', 'ç'
31
+ result[i] = 'C'
32
+ when 'D', 'd', 'Ð', 'ð'
33
+ result[i] = 'D'
34
+ when 'E', 'e', 'È', 'É', 'Ê', 'Ë', 'è', 'é', 'ê', 'ë'
35
+ result[i] = 'E'
36
+ when 'F', 'f'
37
+ result[i] = 'F'
38
+ when 'G', 'g'
39
+ result[i] = 'G'
40
+ when 'H', 'h'
41
+ result[i] = 'H'
42
+ when 'I', 'i', 'Ì', 'Í', 'Î', 'Ï', 'ì', 'í', 'î', 'ï'
43
+ result[i] = 'I'
44
+ when 'J', 'j'
45
+ result[i] = 'J'
46
+ when 'K', 'k'
47
+ result[i] = 'K'
48
+ when 'L', 'l'
49
+ result[i] = 'L'
50
+ when 'M', 'm'
51
+ result[i] = 'M'
52
+ when 'N', 'n', 'Ñ', 'ñ'
53
+ result[i] = 'N'
54
+ when 'O', 'o', 'Ò', 'Ó', 'Ô', 'Õ', 'ò', 'ó', 'ô', 'õ'
55
+ result[i] = 'O'
56
+ when 'P', 'p'
57
+ result[i] = 'P'
58
+ when 'Q', 'q'
59
+ result[i] = 'Q'
60
+ when 'R', 'r'
61
+ result[i] = 'R'
62
+ when 'S', 's'
63
+ result[i] = 'S'
64
+ when 'T', 't'
65
+ result[i] = 'T'
66
+ when 'U', 'u', 'Ù', 'Ú', 'Û', 'ù', 'ú', 'û'
67
+ result[i] = 'U'
68
+ when 'V', 'v'
69
+ result[i] = 'V'
70
+ when 'W', 'w'
71
+ result[i] = 'W'
72
+ when 'X', 'x'
73
+ result[i] = 'X'
74
+ when 'Y', 'y', 'Ü', 'Ý', 'ü', 'ý'
75
+ result[i] = 'Y'
76
+ when 'Z', 'z'
77
+ result[i] = 'Z'
78
+ when '[', 'Å', 'å'
79
+ result[i] = '['
80
+ when '\\', 'Ä', 'Æ', 'ä', 'æ'
81
+ result[i] = '\\'
82
+ when ']', 'Ö', 'ö'
83
+ result[i] = ']'
84
+ when 'Ø', 'ø'
85
+ result[i] = 'Ø'
86
+ when 'Þ', 'þ'
87
+ result[i] = 'Þ'
88
+ end
89
+ end
90
+ result
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,21 @@
1
+ module Linkage
2
+ # The Expectation class contains information about how two datasets
3
+ # should be linked.
4
+ class Expectation
5
+ def kind
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def apply_to(*args)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def decollation_needed?
14
+ false
15
+ end
16
+ end
17
+ end
18
+
19
+ Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), "expectations", "*.rb"))).each do |filename|
20
+ require filename
21
+ end
@@ -0,0 +1,63 @@
1
+ module Linkage
2
+ module Expectations
3
+ class Exhaustive < Expectation
4
+ attr_reader :comparator, :threshold, :mode
5
+
6
+ def initialize(comparator, threshold, mode)
7
+ @comparator = comparator
8
+ @threshold = threshold
9
+ @mode = mode
10
+ end
11
+
12
+ def kind
13
+ if @kind.nil?
14
+ if @comparator.lhs_args.length != @comparator.rhs_args.length
15
+ @kind = :cross
16
+ else
17
+ @kind = :self
18
+ @comparator.lhs_args.each_with_index do |lhs_arg, index|
19
+ rhs_arg = @comparator.rhs_args[index]
20
+ if !lhs_arg.objects_equal?(rhs_arg)
21
+ @kind = :cross
22
+ break
23
+ end
24
+ end
25
+ end
26
+
27
+ # Check for dual-linkage.
28
+ if @kind == :cross
29
+ # Assume that all lhs arguments have the same dataset, as well
30
+ # as all the rhs arguments. Only check the first argument of each
31
+ # side.
32
+ lhs_arg = @comparator.lhs_args[0]
33
+ rhs_arg = @comparator.rhs_args[0]
34
+ if !lhs_arg.datasets_equal?(rhs_arg)
35
+ @kind = :dual
36
+ end
37
+ end
38
+ end
39
+ @kind
40
+ end
41
+
42
+ def apply_to(dataset, side)
43
+ exprs =
44
+ case side
45
+ when :lhs
46
+ comparator.lhs_args.collect { |arg| arg.to_expr.as(arg.name) }
47
+ when :rhs
48
+ comparator.rhs_args.collect { |arg| arg.to_expr.as(arg.name) }
49
+ end
50
+ dataset.select_more(*exprs)
51
+ end
52
+
53
+ def satisfied?(score)
54
+ case mode
55
+ when :equal
56
+ score == threshold
57
+ when :min
58
+ score >= threshold
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,168 @@
1
+ module Linkage
2
+ module Expectations
3
+ class Simple < Expectation
4
+ # The dataset this expectation applies to: `:lhs` or `:rhs`. This
5
+ # only applies to filter expectations.
6
+ # @return [Symbol]
7
+ attr_reader :side
8
+
9
+ attr_reader :meta_object_1, :meta_object_2, :operator
10
+
11
+ VALID_OPERATORS = [:==, :'!=', :>, :<, :>=, :<=]
12
+
13
+ # Automatically create an expectation type depending on the arguments.
14
+ #
15
+ # @param [Linkage::MetaObject] meta_object_1
16
+ # @param [Linkage::MetaObject] meta_object_2
17
+ # @param [Symbol] operator Valid operators: `:==`, `:'!='`, `:>`, `:<`, `:>=`, `:<=`
18
+ def self.create(meta_object_1, meta_object_2, operator)
19
+ klass =
20
+ if meta_object_1.static? && meta_object_2.static?
21
+ raise ArgumentError, "An expectation with two static objects is invalid"
22
+ elsif meta_object_1.static? || meta_object_2.static?
23
+ Filter
24
+ elsif meta_object_1.side == meta_object_2.side
25
+ if !meta_object_1.datasets_equal?(meta_object_2)
26
+ raise ArgumentError, "An expectation with two dynamic objects with the same side but different datasets is invalid"
27
+ end
28
+ Filter
29
+ elsif meta_object_1.objects_equal?(meta_object_2)
30
+ Self
31
+ elsif meta_object_1.datasets_equal?(meta_object_2)
32
+ Cross
33
+ else
34
+ Dual
35
+ end
36
+
37
+ klass.new(meta_object_1, meta_object_2, operator)
38
+ end
39
+
40
+ # Creates a new Simple.
41
+ #
42
+ # @param [Linkage::MetaObject] meta_object_1
43
+ # @param [Linkage::MetaObject] meta_object_2
44
+ # @param [Symbol] operator Valid operators: `:==`, `:'!='`, `:>`, `:<`, `:>=`, `:<=`
45
+ def initialize(meta_object_1, meta_object_2, operator)
46
+ @meta_object_1 = meta_object_1
47
+ @meta_object_2 = meta_object_2
48
+ @operator = operator
49
+
50
+ if !VALID_OPERATORS.include?(operator)
51
+ raise ArgumentError, "Invalid operator: #{operator.inspect}"
52
+ end
53
+
54
+ after_initialize
55
+ end
56
+
57
+ def same_except_side?(other)
58
+ other.is_a?(Simple) &&
59
+ operator == other.operator &&
60
+ meta_object_1.objects_equal?(other.meta_object_1) &&
61
+ meta_object_2.objects_equal?(other.meta_object_2)
62
+ end
63
+
64
+ def exactly!
65
+ function_1 = Function['binary'].new(@meta_object_1.object, :dataset => @meta_object_1.dataset)
66
+ function_2 = Function['binary'].new(@meta_object_2.object, :dataset => @meta_object_2.dataset)
67
+ @meta_object_1 = MetaObject.new(function_1, @meta_object_1.side)
68
+ @meta_object_2 = MetaObject.new(function_2, @meta_object_2.side)
69
+ end
70
+
71
+ # Display any warnings about this expectation.
72
+ def display_warnings
73
+ end
74
+
75
+ def decollation_needed?
76
+ merged_field.ruby_type[:type] == String && (
77
+ @meta_object_1.collation != @meta_object_2.collation ||
78
+ @meta_object_1.database_type != @meta_object_2.database_type
79
+ )
80
+ end
81
+
82
+ protected
83
+
84
+ def after_initialize
85
+ end
86
+ end
87
+
88
+ class Filter < Simple
89
+ def kind; :filter; end
90
+
91
+ def to_expr
92
+ case @operator
93
+ when :==, :'!='
94
+ expr = { @meta_object_1.to_expr => @meta_object_2.to_expr }
95
+ @operator == :== ? expr : ~expr
96
+ else
97
+ Sequel::SQL::BooleanExpression.new(@operator,
98
+ @meta_object_1.to_identifier, @meta_object_2.to_identifier)
99
+ end
100
+ end
101
+
102
+ def apply_to(dataset, side)
103
+ if side != @side
104
+ return dataset
105
+ end
106
+
107
+ dataset.filter(self.to_expr)
108
+ end
109
+
110
+ def decollation_needed?
111
+ false
112
+ end
113
+
114
+ private
115
+
116
+ def after_initialize
117
+ super
118
+ @side = @meta_object_1.static? ? @meta_object_2.side : @meta_object_1.side
119
+ end
120
+ end
121
+
122
+ class Match < Simple
123
+ def apply_to(dataset, side)
124
+ target =
125
+ if @meta_object_1.side == side
126
+ @meta_object_1
127
+ elsif @meta_object_2.side == side
128
+ @meta_object_2
129
+ else
130
+ raise ArgumentError, "Invalid `side` argument: #{side}"
131
+ end
132
+
133
+ dataset.group_match_more({
134
+ :meta_object => target,
135
+ :alias => merged_field.name
136
+ })
137
+ end
138
+
139
+ def merged_field
140
+ @merged_field ||= @meta_object_1.merge(@meta_object_2)
141
+ end
142
+
143
+ def display_warnings
144
+ object_1 = @meta_object_1.object
145
+ object_2 = @meta_object_2.object
146
+ if object_1.ruby_type[:type] == String && object_2.ruby_type[:type] == String
147
+ if @meta_object_1.dataset.database_type != @meta_object_2.dataset.database_type
148
+ warn "NOTE: You are comparing two string fields (#{object_1.name} and #{object_2.name}) from different databases. This may result in unexpected results, as different databases compare strings differently. Consider using the =binary= function."
149
+ elsif object_1.respond_to?(:collation) && object_1.respond_to?(:collation) && object_1.collation != object_2.collation
150
+ warn "NOTE: The two string fields you are comparing (#{object_1.name} and #{object_2.name}) have different collations (#{ldata.collation} vs. #{rdata.collation}). This may result in unexpected results, as the database may compare them differently. Consider using the =exactly= method."
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ class Self < Match
157
+ def kind; :self; end
158
+ end
159
+
160
+ class Cross < Match
161
+ def kind; :cross; end
162
+ end
163
+
164
+ class Dual < Match
165
+ def kind; :dual; end
166
+ end
167
+ end
168
+ end
data/lib/linkage/field.rb CHANGED
@@ -7,13 +7,13 @@ module Linkage
7
7
 
8
8
  # Create a new instance of Field.
9
9
  #
10
+ # @param [Linkage::Dataset] dataset
10
11
  # @param [Symbol] name The field's name
11
12
  # @param [Hash] schema The field's schema information
12
- # @param [Hash] ruby_type The field's ruby type
13
- def initialize(name, schema, ruby_type = nil)
13
+ def initialize(dataset, name, schema)
14
+ @dataset = dataset
14
15
  @name = name
15
16
  @schema = schema
16
- @ruby_type = ruby_type
17
17
  end
18
18
 
19
19
  # Convert the column schema information to a hash of column options, one of
@@ -63,6 +63,8 @@ module Linkage
63
63
  else
64
64
  {:type=>String}
65
65
  end
66
+ hsh[:collate] = collation
67
+
66
68
  hsh.delete_if { |k, v| v.nil? }
67
69
  @ruby_type = {:type => hsh.delete(:type)}
68
70
  @ruby_type[:opts] = hsh if !hsh.empty?
@@ -70,7 +72,7 @@ module Linkage
70
72
  @ruby_type
71
73
  end
72
74
 
73
- def to_expr(adapter = nil)
75
+ def to_expr(options = {})
74
76
  @name
75
77
  end
76
78
 
@@ -81,5 +83,29 @@ module Linkage
81
83
  def primary_key?
82
84
  schema && schema[:primary_key]
83
85
  end
86
+
87
+ def collation
88
+ schema[:collation]
89
+ end
90
+ end
91
+
92
+ # A special field used for merging two {Data} objects together. It
93
+ # has no dataset or schema.
94
+ class MergeField < Field
95
+ attr_reader :database_type
96
+
97
+ # Create a new instance of MergeField.
98
+ #
99
+ # @param [Symbol] name The field's name
100
+ # @param [Hash] ruby_type The field's schema information
101
+ def initialize(name, ruby_type, database_type = nil)
102
+ @name = name
103
+ @ruby_type = ruby_type
104
+ @database_type = database_type
105
+ end
106
+
107
+ def collation
108
+ @ruby_type.has_key?(:opts) ? @ruby_type[:opts][:collate] : nil
109
+ end
84
110
  end
85
111
  end
@@ -2,9 +2,12 @@ module Linkage
2
2
  class FieldSet < Hash
3
3
  attr_reader :primary_key
4
4
 
5
- def initialize(schema)
6
- schema.each do |(name, column_schema)|
7
- f = Field.new(name, column_schema)
5
+ # Create a new FieldSet.
6
+ #
7
+ # @param [Linkage::Dataset] dataset
8
+ def initialize(dataset)
9
+ dataset.schema.each do |(name, column_schema)|
10
+ f = Field.new(dataset, name, column_schema)
8
11
  self[name] = f
9
12
 
10
13
  if @primary_key.nil? && column_schema[:primary_key]