linkage 0.0.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.
- data/.document +5 -0
- data/.vimrc +34 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +44 -0
- data/Guardfile +12 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +64 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/lib/linkage.rb +15 -0
- data/lib/linkage/configuration.rb +178 -0
- data/lib/linkage/dataset.rb +205 -0
- data/lib/linkage/expectation.rb +138 -0
- data/lib/linkage/field.rb +227 -0
- data/lib/linkage/group.rb +43 -0
- data/lib/linkage/import_buffer.rb +39 -0
- data/lib/linkage/runner.rb +59 -0
- data/lib/linkage/runner/single_threaded.rb +114 -0
- data/lib/linkage/utils.rb +164 -0
- data/linkage.gemspec +106 -0
- data/test/helper.rb +43 -0
- data/test/integration/test_cross_linkage.rb +68 -0
- data/test/integration/test_dual_linkage.rb +85 -0
- data/test/integration/test_self_linkage.rb +209 -0
- data/test/unit/test_configuration.rb +145 -0
- data/test/unit/test_dataset.rb +274 -0
- data/test/unit/test_expectation.rb +294 -0
- data/test/unit/test_field.rb +447 -0
- data/test/unit/test_group.rb +21 -0
- data/test/unit/test_import_buffer.rb +51 -0
- data/test/unit/test_linkage.rb +6 -0
- data/test/unit/test_runner.rb +14 -0
- data/test/unit/test_single_threaded_runner.rb +12 -0
- data/test/unit/test_utils.rb +341 -0
- metadata +272 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
module Linkage
|
2
|
+
# Wrapper for a Sequel dataset
|
3
|
+
class Dataset
|
4
|
+
@@next_id = 1 # Internal ID used for expectations
|
5
|
+
@@next_id_mutex = Mutex.new
|
6
|
+
|
7
|
+
# @private
|
8
|
+
def self.next_id
|
9
|
+
result = nil
|
10
|
+
@@next_id_mutex.synchronize do
|
11
|
+
result = @@next_id
|
12
|
+
@@next_id += 1
|
13
|
+
end
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Array] Schema information about the dataset's primary key
|
18
|
+
attr_reader :primary_key
|
19
|
+
|
20
|
+
# @return [Array] Schema information for this dataset
|
21
|
+
attr_reader :schema
|
22
|
+
|
23
|
+
# @return [String] Database URI
|
24
|
+
attr_reader :uri
|
25
|
+
|
26
|
+
# @return [Symbol] Database table name
|
27
|
+
attr_reader :table
|
28
|
+
|
29
|
+
# @return [Array<Linkage::Field>] List of {Linkage::Field}'s
|
30
|
+
attr_reader :fields
|
31
|
+
|
32
|
+
# @private
|
33
|
+
attr_reader :id
|
34
|
+
|
35
|
+
# @param [String] uri Sequel-style database URI
|
36
|
+
# @param [String, Symbol] table Database table name
|
37
|
+
# @param [Hash] options Options to pass to Sequel.connect
|
38
|
+
# @see http://sequel.rubyforge.org/rdoc/files/doc/opening_databases_rdoc.html Sequel: Connecting to a database
|
39
|
+
def initialize(uri, table, options = {})
|
40
|
+
@id = self.class.next_id
|
41
|
+
@uri = uri
|
42
|
+
@table = table.to_sym
|
43
|
+
@options = options
|
44
|
+
schema = nil
|
45
|
+
database { |db| schema = db.schema(@table) }
|
46
|
+
@schema = schema
|
47
|
+
@order = []
|
48
|
+
@select = []
|
49
|
+
@filter = []
|
50
|
+
create_fields
|
51
|
+
end
|
52
|
+
|
53
|
+
# Setup a linkage with another dataset
|
54
|
+
#
|
55
|
+
# @return [Linkage::Configuration]
|
56
|
+
def link_with(dataset, &block)
|
57
|
+
conf = Configuration.new(self, dataset)
|
58
|
+
conf.instance_eval(&block)
|
59
|
+
conf
|
60
|
+
end
|
61
|
+
|
62
|
+
# Compare URI and database table name
|
63
|
+
#
|
64
|
+
# @return [Boolean]
|
65
|
+
def ==(other)
|
66
|
+
if !other.is_a?(Dataset)
|
67
|
+
super
|
68
|
+
else
|
69
|
+
uri == other.uri && table == other.table
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Create a copy of this instance of Dataset, using {Dataset#initialize}.
|
74
|
+
#
|
75
|
+
# @return [Linkage::Dataset]
|
76
|
+
def dup
|
77
|
+
self.class.new(uri, table)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Clone the dataset and its associated {Linkage::Field}'s (without hitting
|
81
|
+
# the database).
|
82
|
+
#
|
83
|
+
# @return [Linkage::Dataset]
|
84
|
+
def clone
|
85
|
+
other = self.class.allocate
|
86
|
+
other.send(:initialize_copy, self, {
|
87
|
+
:order => @order.clone, :select => @select.clone,
|
88
|
+
:filter => @filter.clone, :options => @options.clone
|
89
|
+
})
|
90
|
+
end
|
91
|
+
|
92
|
+
# Add a field to use for ordering the dataset.
|
93
|
+
#
|
94
|
+
# @param [Linkage::Field] field
|
95
|
+
# @param [nil, Symbol] desc nil or :desc (for descending order)
|
96
|
+
def add_order(field, desc = nil)
|
97
|
+
expr = desc == :desc ? field.name.desc : field.name
|
98
|
+
unless @order.include?(expr)
|
99
|
+
@order << expr
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add a field to be selected on the dataset. If you don't add any
|
104
|
+
# selects, all fields will be selected. The primary key is always
|
105
|
+
# selected in either case.
|
106
|
+
#
|
107
|
+
# @param [Linkage::Field] field
|
108
|
+
# @param [Symbol] as Optional field alias
|
109
|
+
def add_select(field, as = nil)
|
110
|
+
expr = as ? field.name.as(as) : field.name
|
111
|
+
unless @select.include?(expr)
|
112
|
+
@select << expr
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Add a filter (SQL WHERE) condition to the dataset.
|
117
|
+
#
|
118
|
+
# @param [Linkage::Field] field
|
119
|
+
# @param [Symbol] operator
|
120
|
+
# @param [Linkage::Field, Object] other
|
121
|
+
def add_filter(field, operator, other)
|
122
|
+
arg1 = field.name
|
123
|
+
arg2 = other.is_a?(Field) ? other.name : other
|
124
|
+
expr =
|
125
|
+
case operator
|
126
|
+
when :==
|
127
|
+
{ arg1 => arg2 }
|
128
|
+
when :'!='
|
129
|
+
~{ arg1 => arg2 }
|
130
|
+
else
|
131
|
+
arg1 = Sequel::SQL::Identifier.new(arg1)
|
132
|
+
arg2 = arg2.is_a?(Symbol) ? Sequel::SQL::Identifier.new(arg2) : arg2
|
133
|
+
Sequel::SQL::BooleanExpression.new(operator, arg1, arg2)
|
134
|
+
end
|
135
|
+
@filter << expr
|
136
|
+
end
|
137
|
+
|
138
|
+
# Yield each row of the dataset in a block.
|
139
|
+
#
|
140
|
+
# @yield [row] A Hash of two elements, :pk and :values, where row[:pk] is
|
141
|
+
# the row's primary key value, and row[:values] is an array of all
|
142
|
+
# selected values (except the primary key).
|
143
|
+
def each
|
144
|
+
database do |db|
|
145
|
+
ds = db[@table]
|
146
|
+
|
147
|
+
pk = @primary_key.name
|
148
|
+
if !@select.empty?
|
149
|
+
ds = ds.select(pk, *@select)
|
150
|
+
end
|
151
|
+
if !@order.empty?
|
152
|
+
ds = ds.order(*@order)
|
153
|
+
end
|
154
|
+
if !@filter.empty?
|
155
|
+
ds = ds.filter(*@filter)
|
156
|
+
end
|
157
|
+
ds.each do |row|
|
158
|
+
yield({:pk => row.delete(pk), :values => row})
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def initialize_copy(dataset, options = {})
|
166
|
+
@id = dataset.id
|
167
|
+
@uri = dataset.uri
|
168
|
+
@table = dataset.table
|
169
|
+
@schema = dataset.schema
|
170
|
+
@options = options[:options]
|
171
|
+
@order = options[:order]
|
172
|
+
@select = options[:select]
|
173
|
+
@filter = options[:filter]
|
174
|
+
@fields = dataset.fields.inject({}) do |hsh, (name, field)|
|
175
|
+
new_field = field.clone
|
176
|
+
new_field.dataset = self
|
177
|
+
hsh[name] = new_field
|
178
|
+
hsh
|
179
|
+
end
|
180
|
+
@primary_key = @fields[dataset.primary_key.name]
|
181
|
+
self
|
182
|
+
end
|
183
|
+
|
184
|
+
def database(&block)
|
185
|
+
Sequel.connect(uri, @options, &block)
|
186
|
+
end
|
187
|
+
|
188
|
+
def create_fields
|
189
|
+
@fields = {}
|
190
|
+
@schema.each do |(name, column_schema)|
|
191
|
+
f = Field.new(name, column_schema)
|
192
|
+
f.dataset = self
|
193
|
+
@fields[name] = f
|
194
|
+
|
195
|
+
if @primary_key.nil? && column_schema[:primary_key]
|
196
|
+
@primary_key = f
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def set_new_id
|
202
|
+
@id = self.class.next_id
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Linkage
|
2
|
+
class Expectation
|
3
|
+
VALID_OPERATORS = [:==, :>, :<, :>=, :<=, :'!=']
|
4
|
+
|
5
|
+
def self.get(type)
|
6
|
+
TYPES[type]
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :operator, :field_1, :field_2
|
10
|
+
|
11
|
+
# @param [Symbol] operator Currently, only :==
|
12
|
+
# @param [Linkage::Field, Object] field_1
|
13
|
+
# @param [Linkage::Field, Object] field_2
|
14
|
+
# @param [Symbol] force_kind Manually set type of expectation (useful for
|
15
|
+
# a filter between two fields)
|
16
|
+
def initialize(operator, field_1, field_2, force_kind = nil)
|
17
|
+
if !(field_1.is_a?(Field) || field_2.is_a?(Field))
|
18
|
+
raise ArgumentError, "You must have at least one Linkage::Field"
|
19
|
+
end
|
20
|
+
|
21
|
+
if !VALID_OPERATORS.include?(operator)
|
22
|
+
raise ArgumentError, "Invalid operator: #{operator.inspect}"
|
23
|
+
end
|
24
|
+
|
25
|
+
@operator = operator
|
26
|
+
@field_1 = field_1
|
27
|
+
@field_2 = field_2
|
28
|
+
@kind = force_kind
|
29
|
+
|
30
|
+
if kind == :filter
|
31
|
+
if @field_1.is_a?(Field)
|
32
|
+
@filter_field = @field_1
|
33
|
+
@filter_value = @field_2
|
34
|
+
else
|
35
|
+
@filter_field = @field_2
|
36
|
+
@filter_value = @field_1
|
37
|
+
end
|
38
|
+
elsif @operator != :==
|
39
|
+
raise ArgumentError, "Inequality operators are not allowed for non-filter expectations"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(other)
|
44
|
+
if other.is_a?(Expectation)
|
45
|
+
@operator == other.operator && @field_1 == other.field_1 &&
|
46
|
+
@field_2 == other.field_2
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Symbol] :self, :dual, :cross, or :filter
|
53
|
+
def kind
|
54
|
+
@kind ||=
|
55
|
+
if !(@field_1.is_a?(Field) && @field_2.is_a?(Field))
|
56
|
+
:filter
|
57
|
+
elsif @field_1 == @field_2
|
58
|
+
:self
|
59
|
+
elsif @field_1.dataset == @field_2.dataset
|
60
|
+
:cross
|
61
|
+
else
|
62
|
+
:dual
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Symbol] name of the merged field type
|
67
|
+
def name
|
68
|
+
merged_field.name
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Linkage::Field] result of Field#merge between the two fields
|
72
|
+
def merged_field
|
73
|
+
@merged_field ||= @field_1.merge(@field_2)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Boolean] Whether or not this expectation involves a field in
|
77
|
+
# the given dataset (Only useful for :filter expressions)
|
78
|
+
def applies_to?(dataset)
|
79
|
+
if kind == :filter
|
80
|
+
@filter_field.belongs_to?(dataset)
|
81
|
+
else
|
82
|
+
@field_1.belongs_to?(dataset) || @field_2.belongs_to?(dataset)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Apply changes to a dataset based on the expectation, such as calling
|
87
|
+
# {Dataset#add_order}, {Dataset#add_select}, and {Dataset#add_filter}
|
88
|
+
# with the appropriate arguments.
|
89
|
+
def apply_to(dataset)
|
90
|
+
case kind
|
91
|
+
when :filter
|
92
|
+
if @filter_field.belongs_to?(dataset)
|
93
|
+
dataset.add_filter(@filter_field, @operator, @filter_value)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
as =
|
97
|
+
if kind == :self
|
98
|
+
nil
|
99
|
+
else
|
100
|
+
name != @field_1.name ? name : nil
|
101
|
+
end
|
102
|
+
|
103
|
+
if @field_1.belongs_to?(dataset)
|
104
|
+
dataset.add_order(@field_1)
|
105
|
+
dataset.add_select(@field_1, as)
|
106
|
+
end
|
107
|
+
if @field_2.belongs_to?(dataset)
|
108
|
+
dataset.add_order(@field_2)
|
109
|
+
dataset.add_select(@field_2, as)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class MustExpectation < Expectation
|
116
|
+
end
|
117
|
+
|
118
|
+
class MustNotExpectation < Expectation
|
119
|
+
OPERATOR_OPPOSITES = {
|
120
|
+
:== => :'!=',
|
121
|
+
:'!=' => :==,
|
122
|
+
:> => :<=,
|
123
|
+
:<= => :>,
|
124
|
+
:< => :>=,
|
125
|
+
:>= => :<
|
126
|
+
}
|
127
|
+
|
128
|
+
# Same as Expectation, except it negates the operator.
|
129
|
+
def initialize(operator, field_1, field_2, force_kind = nil)
|
130
|
+
super(OPERATOR_OPPOSITES[operator], field_1, field_2, force_kind)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
Expectation::TYPES = {
|
135
|
+
:must => MustExpectation,
|
136
|
+
:must_not => MustNotExpectation
|
137
|
+
}
|
138
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Linkage
|
2
|
+
# This class is for holding information about a particular field in a
|
3
|
+
# dataset.
|
4
|
+
class Field
|
5
|
+
# A "tree" used to find compatible types.
|
6
|
+
TYPE_CONVERSION_TREE = {
|
7
|
+
TrueClass => [Integer],
|
8
|
+
Integer => [Bignum, Float],
|
9
|
+
Bignum => [BigDecimal],
|
10
|
+
Float => [BigDecimal],
|
11
|
+
BigDecimal => [String],
|
12
|
+
String => nil,
|
13
|
+
DateTime => nil,
|
14
|
+
Date => nil,
|
15
|
+
Time => nil,
|
16
|
+
File => nil
|
17
|
+
}
|
18
|
+
|
19
|
+
# @return [Symbol] This field's name
|
20
|
+
attr_reader :name
|
21
|
+
|
22
|
+
# @return [Symbol] This field's schema information
|
23
|
+
attr_reader :schema
|
24
|
+
|
25
|
+
# @attr [Linkage::Dataset] This field's associated dataset
|
26
|
+
attr_accessor :dataset
|
27
|
+
|
28
|
+
# Create a new instance of Field.
|
29
|
+
#
|
30
|
+
# @param [Symbol] name The field's name
|
31
|
+
# @param [Hash] schema The field's schema information
|
32
|
+
# @param [Hash] ruby_type The field's ruby type
|
33
|
+
def initialize(name, schema, ruby_type = nil)
|
34
|
+
@name = name
|
35
|
+
@schema = schema
|
36
|
+
@ruby_type = ruby_type
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convert the column schema information to a hash of column options, one of
|
40
|
+
# which must be :type. The other options added should modify that type
|
41
|
+
# (e.g. :size). If a database type is not recognized, return it as a String
|
42
|
+
# type.
|
43
|
+
#
|
44
|
+
# @note This method comes more or less straight from Sequel
|
45
|
+
# (lib/sequel/extensions/schema_dumper.rb).
|
46
|
+
def ruby_type
|
47
|
+
unless @ruby_type
|
48
|
+
hsh =
|
49
|
+
case t = @schema[:db_type].downcase
|
50
|
+
when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?(?: unsigned)?\z/o
|
51
|
+
{:type=>Integer}
|
52
|
+
when /\Atinyint(?:\((\d+)\))?\z/o
|
53
|
+
{:type =>@schema[:type] == :boolean ? TrueClass : Integer}
|
54
|
+
when /\Abigint(?:\((?:\d+)\))?(?: unsigned)?\z/o
|
55
|
+
{:type=>Bignum}
|
56
|
+
when /\A(?:real|float|double(?: precision)?)\z/o
|
57
|
+
{:type=>Float}
|
58
|
+
when 'boolean'
|
59
|
+
{:type=>TrueClass}
|
60
|
+
when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/o
|
61
|
+
{:type=>String, :text=>true}
|
62
|
+
when 'date'
|
63
|
+
{:type=>Date}
|
64
|
+
when /\A(?:small)?datetime\z/o
|
65
|
+
{:type=>DateTime}
|
66
|
+
when /\Atimestamp(?:\((\d+)\))?(?: with(?:out)? time zone)?\z/o
|
67
|
+
{:type=>DateTime, :size=>($1.to_i if $1)}
|
68
|
+
when /\Atime(?: with(?:out)? time zone)?\z/o
|
69
|
+
{:type=>Time, :only_time=>true}
|
70
|
+
when /\An?char(?:acter)?(?:\((\d+)\))?\z/o
|
71
|
+
{:type=>String, :size=>($1.to_i if $1), :fixed=>true}
|
72
|
+
when /\A(?:n?varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/o
|
73
|
+
{:type=>String, :size=>($1.to_i if $1)}
|
74
|
+
when /\A(?:small)?money\z/o
|
75
|
+
{:type=>BigDecimal, :size=>[19,2]}
|
76
|
+
when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/o
|
77
|
+
s = [($1.to_i if $1), ($2.to_i if $2)].compact
|
78
|
+
{:type=>BigDecimal, :size=>(s.empty? ? nil : s)}
|
79
|
+
when /\A(?:bytea|(?:tiny|medium|long)?blob|(?:var)?binary)(?:\((\d+)\))?\z/o
|
80
|
+
{:type=>File, :size=>($1.to_i if $1)}
|
81
|
+
when 'year'
|
82
|
+
{:type=>Integer}
|
83
|
+
else
|
84
|
+
{:type=>String}
|
85
|
+
end
|
86
|
+
hsh.delete_if { |k, v| v.nil? }
|
87
|
+
@ruby_type = {:type => hsh.delete(:type)}
|
88
|
+
@ruby_type[:opts] = hsh if !hsh.empty?
|
89
|
+
end
|
90
|
+
@ruby_type
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create a field that can hold data from two other fields. If the fields
|
94
|
+
# have different types, the resulting type is determined via a
|
95
|
+
# type-conversion tree.
|
96
|
+
#
|
97
|
+
# @param [Linkage::Field] other
|
98
|
+
# @return [Linkage::Field]
|
99
|
+
def merge(other, new_name = nil)
|
100
|
+
schema_1 = self.ruby_type
|
101
|
+
schema_2 = other.ruby_type
|
102
|
+
if schema_1 == schema_2
|
103
|
+
result = schema_1
|
104
|
+
else
|
105
|
+
type_1 = schema_1[:type]
|
106
|
+
opts_1 = schema_1[:opts] || {}
|
107
|
+
type_2 = schema_2[:type]
|
108
|
+
opts_2 = schema_2[:opts] || {}
|
109
|
+
result_type = type_1
|
110
|
+
result_opts = schema_1[:opts] ? schema_1[:opts].dup : {}
|
111
|
+
|
112
|
+
# type
|
113
|
+
if type_1 != type_2
|
114
|
+
result_type = first_common_type(type_1, type_2)
|
115
|
+
end
|
116
|
+
|
117
|
+
# text
|
118
|
+
if opts_1[:text] != opts_2[:text]
|
119
|
+
# This can only be of type String.
|
120
|
+
result_opts[:text] = true
|
121
|
+
result_opts.delete(:size)
|
122
|
+
end
|
123
|
+
|
124
|
+
# size
|
125
|
+
if !result_opts[:text] && opts_1[:size] != opts_2[:size]
|
126
|
+
types = [type_1, type_2].uniq
|
127
|
+
if types.length == 1 && types[0] == BigDecimal
|
128
|
+
# Two decimals
|
129
|
+
if opts_1.has_key?(:size) && opts_2.has_key?(:size)
|
130
|
+
s_1 = opts_1[:size]
|
131
|
+
s_2 = opts_2[:size]
|
132
|
+
result_opts[:size] = [ s_1[0] > s_2[0] ? s_1[0] : s_2[0] ]
|
133
|
+
|
134
|
+
if s_1[1] && s_2[1]
|
135
|
+
result_opts[:size][1] = s_1[1] > s_2[1] ? s_1[1] : s_2[1]
|
136
|
+
else
|
137
|
+
result_opts[:size][1] = s_1[1] ? s_1[1] : s_2[1]
|
138
|
+
end
|
139
|
+
else
|
140
|
+
result_opts[:size] = opts_1.has_key?(:size) ? opts_1[:size] : opts_2[:size]
|
141
|
+
end
|
142
|
+
elsif types.include?(String) && types.include?(BigDecimal)
|
143
|
+
# Add one to the precision of the BigDecimal (for the dot)
|
144
|
+
if opts_1.has_key?(:size) && opts_2.has_key?(:size)
|
145
|
+
s_1 = opts_1[:size].is_a?(Array) ? opts_1[:size][0] + 1 : opts_1[:size]
|
146
|
+
s_2 = opts_2[:size].is_a?(Array) ? opts_2[:size][0] + 1 : opts_2[:size]
|
147
|
+
result_opts[:size] = s_1 > s_2 ? s_1 : s_2
|
148
|
+
elsif opts_1.has_key?(:size)
|
149
|
+
result_opts[:size] = opts_1[:size].is_a?(Array) ? opts_1[:size][0] + 1 : opts_1[:size]
|
150
|
+
elsif opts_2.has_key?(:size)
|
151
|
+
result_opts[:size] = opts_2[:size].is_a?(Array) ? opts_2[:size][0] + 1 : opts_2[:size]
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# Treat as two strings
|
155
|
+
if opts_1.has_key?(:size) && opts_2.has_key?(:size)
|
156
|
+
result_opts[:size] = opts_1[:size] > opts_2[:size] ? opts_1[:size] : opts_2[:size]
|
157
|
+
elsif opts_1.has_key?(:size)
|
158
|
+
result_opts[:size] = opts_1[:size]
|
159
|
+
else
|
160
|
+
result_opts[:size] = opts_2[:size]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# fixed
|
166
|
+
if opts_1[:fixed] != opts_2[:fixed]
|
167
|
+
# This can only be of type String.
|
168
|
+
result_opts[:fixed] = true
|
169
|
+
end
|
170
|
+
|
171
|
+
result = {:type => result_type}
|
172
|
+
result[:opts] = result_opts unless result_opts.empty?
|
173
|
+
end
|
174
|
+
|
175
|
+
if new_name
|
176
|
+
name = new_name.to_sym
|
177
|
+
else
|
178
|
+
name = self.name == other.name ? self.name : :"#{self.name}_#{other.name}"
|
179
|
+
end
|
180
|
+
Field.new(name, nil, result)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Returns true if this field's name and dataset match the other's name
|
184
|
+
# and dataset (using {Dataset#==})
|
185
|
+
def ==(other)
|
186
|
+
if !other.is_a?(Field)
|
187
|
+
super
|
188
|
+
else
|
189
|
+
self.name == other.name && self.dataset == other.dataset
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns true if this field's dataset is equal to the given dataset
|
194
|
+
# (using Dataset#id).
|
195
|
+
#
|
196
|
+
# @param [Linkage::Dataset]
|
197
|
+
def belongs_to?(dataset)
|
198
|
+
self.dataset.id == dataset.id
|
199
|
+
end
|
200
|
+
|
201
|
+
def primary_key?
|
202
|
+
schema && schema[:primary_key]
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
def first_common_type(type_1, type_2)
|
208
|
+
types_1 = [type_1] + get_types(type_1)
|
209
|
+
types_2 = [type_2] + get_types(type_2)
|
210
|
+
(types_1 & types_2).first
|
211
|
+
end
|
212
|
+
|
213
|
+
# Get all types that the specified type can be converted to. Order
|
214
|
+
# matters.
|
215
|
+
def get_types(type)
|
216
|
+
result = []
|
217
|
+
types = TYPE_CONVERSION_TREE[type]
|
218
|
+
if types
|
219
|
+
result += types
|
220
|
+
types.each do |t|
|
221
|
+
result |= get_types(t)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
result
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|