google-cloud-spanner 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -99,7 +99,7 @@ module Google
99
99
  Google::Spanner::V1::Mutation.new(
100
100
  insert_or_update: Google::Spanner::V1::Mutation::Write.new(
101
101
  table: table, columns: row.keys.map(&:to_s),
102
- values: [Convert.raw_to_value(row.values).list_value]
102
+ values: [Convert.object_to_grpc_value(row.values).list_value]
103
103
  )
104
104
  )
105
105
  end
@@ -157,7 +157,7 @@ module Google
157
157
  Google::Spanner::V1::Mutation.new(
158
158
  insert: Google::Spanner::V1::Mutation::Write.new(
159
159
  table: table, columns: row.keys.map(&:to_s),
160
- values: [Convert.raw_to_value(row.values).list_value]
160
+ values: [Convert.object_to_grpc_value(row.values).list_value]
161
161
  )
162
162
  )
163
163
  end
@@ -214,7 +214,7 @@ module Google
214
214
  Google::Spanner::V1::Mutation.new(
215
215
  update: Google::Spanner::V1::Mutation::Write.new(
216
216
  table: table, columns: row.keys.map(&:to_s),
217
- values: [Convert.raw_to_value(row.values).list_value]
217
+ values: [Convert.object_to_grpc_value(row.values).list_value]
218
218
  )
219
219
  )
220
220
  end
@@ -273,7 +273,7 @@ module Google
273
273
  Google::Spanner::V1::Mutation.new(
274
274
  replace: Google::Spanner::V1::Mutation::Write.new(
275
275
  table: table, columns: row.keys.map(&:to_s),
276
- values: [Convert.raw_to_value(row.values).list_value]
276
+ values: [Convert.object_to_grpc_value(row.values).list_value]
277
277
  )
278
278
  )
279
279
  end
@@ -334,7 +334,7 @@ module Google
334
334
  end
335
335
  key_list = keys.map do |key|
336
336
  key = [key] unless key.is_a? Array
337
- Convert.raw_to_value(key).list_value
337
+ Convert.object_to_grpc_value(key).list_value
338
338
  end
339
339
  Google::Spanner::V1::KeySet.new keys: key_list
340
340
  end
@@ -31,106 +31,43 @@ module Google
31
31
  def to_query_params params, types = nil
32
32
  types ||= {}
33
33
  formatted_params = params.map do |key, obj|
34
- [String(key), raw_to_value_and_type(obj, types[key])]
34
+ [String(key), object_to_grpc_value_and_type(obj, types[key])]
35
35
  end
36
36
  Hash[formatted_params]
37
37
  end
38
38
 
39
- def raw_to_value_and_type obj, type = nil
39
+ def object_to_grpc_value_and_type obj, field = nil
40
40
  obj = obj.to_column_value if obj.respond_to? :to_column_value
41
- if NilClass === obj
42
- if type
43
- if type.is_a?(Array) && type.count == 1
44
- [Google::Protobuf::Value.new(null_value: :NULL_VALUE),
45
- Google::Spanner::V1::Type.new(
46
- code: :ARRAY, array_element_type:
47
- Google::Spanner::V1::Type.new(code: type.first))]
48
- # elsif type.is_a? Fields
49
- else
50
- [Google::Protobuf::Value.new(null_value: :NULL_VALUE),
51
- Google::Spanner::V1::Type.new(code: type)]
52
- end
53
- else
54
- raise ArgumentError, "Must provide type for nil values."
55
- end
56
- elsif String === obj
57
- [raw_to_value(obj), Google::Spanner::V1::Type.new(code: :STRING)]
58
- elsif Symbol === obj
59
- [raw_to_value(obj.to_s),
60
- Google::Spanner::V1::Type.new(code: :STRING)]
61
- elsif TrueClass === obj
62
- [raw_to_value(obj), Google::Spanner::V1::Type.new(code: :BOOL)]
63
- elsif FalseClass === obj
64
- [raw_to_value(obj), Google::Spanner::V1::Type.new(code: :BOOL)]
65
- elsif Integer === obj
66
- [raw_to_value(obj.to_s),
67
- Google::Spanner::V1::Type.new(code: :INT64)]
68
- elsif Numeric === obj # Any number not an integer gets to be a float
69
- [raw_to_value(obj),
70
- Google::Spanner::V1::Type.new(code: :FLOAT64)]
71
- elsif Time === obj || DateTime === obj
72
- [raw_to_value(obj),
73
- Google::Spanner::V1::Type.new(code: :TIMESTAMP)]
74
- elsif Date === obj
75
- [raw_to_value(obj.to_s),
76
- Google::Spanner::V1::Type.new(code: :DATE)]
77
- elsif Array === obj
78
- if type && !type.is_a?(Array)
79
- raise ArgumentError, "Array values must have an Array type."
80
- end
81
- type ||= begin
82
- # Find the param type for the first non-nil item the list
83
- nested_param_value = obj.detect { |x| !x.nil? }
84
- if nested_param_value.nil?
85
- raise ArgumentError, "Array values must have an Array type."
86
- end
87
- [raw_to_value_and_type(nested_param_value).last.code]
88
- end
89
- [raw_to_value(obj),
90
- Google::Spanner::V1::Type.new(
91
- code: :ARRAY, array_element_type:
92
- Google::Spanner::V1::Type.new(code: type.first))]
93
- elsif Hash === obj
94
- field_pairs = obj.map do |key, value|
95
- [key, raw_to_value_and_type(value).last]
96
- end
97
- formatted_fields = field_pairs.map do |name, param_type|
98
- Google::Spanner::V1::StructType::Field.new(
99
- name: String(name), type: param_type
100
- )
101
- end
102
- [raw_to_value(obj),
103
- Google::Spanner::V1::Type.new(
104
- code: :STRUCT,
105
- struct_type: Google::Spanner::V1::StructType.new(
106
- fields: formatted_fields
107
- ))]
108
- elsif obj.respond_to?(:read) && obj.respond_to?(:rewind)
109
- obj.rewind
110
- [raw_to_value(obj),
111
- Google::Spanner::V1::Type.new(code: :BYTES)]
112
- else
113
- raise ArgumentError,
114
- "A parameter of type #{obj.class} is not supported."
41
+
42
+ if obj.respond_to? :to_grpc_value_and_type
43
+ return obj.to_grpc_value_and_type
115
44
  end
45
+
46
+ field ||= field_for_object obj
47
+ [object_to_grpc_value(obj, field), grpc_type_for_field(field)]
116
48
  end
117
49
 
118
- def raw_to_value obj
50
+ def object_to_grpc_value obj, field = nil
119
51
  obj = obj.to_column_value if obj.respond_to? :to_column_value
120
52
 
121
- if NilClass === obj
53
+ if obj.respond_to? :to_grpc_value_and_type
54
+ return obj.to_grpc_value_and_type.first
55
+ end
56
+
57
+ case obj
58
+ when NilClass
122
59
  Google::Protobuf::Value.new null_value: :NULL_VALUE
123
- elsif String === obj
124
- Google::Protobuf::Value.new string_value: obj
125
- elsif Symbol === obj
60
+ when String
126
61
  Google::Protobuf::Value.new string_value: obj.to_s
127
- elsif TrueClass === obj
62
+ when Symbol
63
+ Google::Protobuf::Value.new string_value: obj.to_s
64
+ when TrueClass
128
65
  Google::Protobuf::Value.new bool_value: true
129
- elsif FalseClass === obj
66
+ when FalseClass
130
67
  Google::Protobuf::Value.new bool_value: false
131
- elsif Integer === obj
68
+ when Integer
132
69
  Google::Protobuf::Value.new string_value: obj.to_s
133
- elsif Numeric === obj # Any number not an integer gets to be a float
70
+ when Numeric
134
71
  if obj == Float::INFINITY
135
72
  Google::Protobuf::Value.new string_value: "Infinity"
136
73
  elsif obj == -Float::INFINITY
@@ -140,42 +77,110 @@ module Google
140
77
  else
141
78
  Google::Protobuf::Value.new number_value: obj.to_f
142
79
  end
143
- elsif Time === obj || DateTime === obj
80
+ when Time
81
+ Google::Protobuf::Value.new(string_value:
82
+ obj.to_time.utc.strftime("%FT%T.%NZ"))
83
+ when DateTime
144
84
  Google::Protobuf::Value.new(string_value:
145
85
  obj.to_time.utc.strftime("%FT%T.%NZ"))
146
- elsif Date === obj
86
+ when Date
147
87
  Google::Protobuf::Value.new string_value: obj.to_s
148
- elsif Array === obj
88
+ when Array
89
+ arr_field = nil
90
+ arr_field = field.first if Array === field
149
91
  Google::Protobuf::Value.new list_value:
150
92
  Google::Protobuf::ListValue.new(values:
151
- obj.map { |o| raw_to_value(o) })
152
- elsif Hash === obj
153
- Google::Protobuf::Value.new struct_value:
154
- Google::Protobuf::Struct.new(fields:
155
- Hash[obj.map { |k, v| [String(k), raw_to_value(v)] }])
156
- elsif obj.respond_to?(:read) && obj.respond_to?(:rewind)
157
- obj.rewind
158
- content = obj.read.force_encoding("ASCII-8BIT")
159
- encoded_content = Base64.strict_encode64(content)
160
- Google::Protobuf::Value.new(string_value: encoded_content)
93
+ obj.map { |o| object_to_grpc_value(o, arr_field) })
94
+ when Hash
95
+ if field.is_a? Fields
96
+ field.struct(obj).to_grpc_value
97
+ else
98
+ raise ArgumentError,
99
+ "A hash value cannot be set to type #{field}."
100
+ end
161
101
  else
162
- raise ArgumentError,
163
- "A value of type #{obj.class} is not supported."
102
+ if obj.respond_to?(:read) && obj.respond_to?(:rewind)
103
+ obj.rewind
104
+ content = obj.read.force_encoding("ASCII-8BIT")
105
+ encoded_content = Base64.strict_encode64(content)
106
+ Google::Protobuf::Value.new(string_value: encoded_content)
107
+ else
108
+ raise ArgumentError,
109
+ "A value of type #{obj.class} is not supported."
110
+ end
164
111
  end
165
112
  end
166
113
 
167
- def row_to_pairs row_types, row
168
- row_types.zip(row).map do |field, value|
169
- [field.name.to_sym, value_to_raw(value, field.type)]
114
+ def field_for_object obj
115
+ case obj
116
+ when NilClass
117
+ raise ArgumentError, "Cannot determine type for nil values."
118
+ when String
119
+ :STRING
120
+ when Symbol
121
+ :STRING
122
+ when TrueClass
123
+ :BOOL
124
+ when FalseClass
125
+ :BOOL
126
+ when Integer
127
+ :INT64
128
+ when Numeric
129
+ :FLOAT64
130
+ when Time
131
+ :TIMESTAMP
132
+ when DateTime
133
+ :TIMESTAMP
134
+ when Date
135
+ :DATE
136
+ when Array
137
+ if obj.empty?
138
+ raise ArgumentError,
139
+ "Cannot determine type for empty array values."
140
+ end
141
+ non_nil_fields = obj.compact.map { |e| field_for_object e }.compact
142
+ if non_nil_fields.empty?
143
+ raise ArgumentError,
144
+ "Cannot determine type for array of nil values."
145
+ end
146
+ if non_nil_fields.uniq.count > 1
147
+ raise ArgumentError,
148
+ "Cannot determine type for array of different values."
149
+ end
150
+ [non_nil_fields.first]
151
+ when Hash
152
+ raw_type_pairs = obj.map do |key, value|
153
+ [key, field_for_object(value)]
154
+ end
155
+ Fields.new Hash[raw_type_pairs]
156
+ when Data
157
+ obj.fields
158
+ else
159
+ if obj.respond_to?(:read) && obj.respond_to?(:rewind)
160
+ :BYTES
161
+ else
162
+ raise ArgumentError,
163
+ "Cannot determine type for #{obj.class} values."
164
+ end
170
165
  end
171
166
  end
172
167
 
173
- def row_to_raw row_types, row
174
- Hash[row_to_pairs(row_types, row)]
168
+ def grpc_type_for_field field
169
+ return field.to_grpc_type if field.respond_to? :to_grpc_type
170
+
171
+ if Array === field
172
+ Google::Spanner::V1::Type.new(
173
+ code: :ARRAY,
174
+ array_element_type: grpc_type_for_field(field.first)
175
+ )
176
+ else
177
+ Google::Spanner::V1::Type.new(code: field)
178
+ end
175
179
  end
176
180
 
177
- def value_to_raw value, type
181
+ def grpc_value_to_object value, type
178
182
  return nil if value.kind == :null_value
183
+
179
184
  case type.code
180
185
  when :BOOL
181
186
  value.bool_value
@@ -205,13 +210,23 @@ module Google
205
210
  StringIO.new Base64.decode64 value.string_value
206
211
  when :ARRAY
207
212
  value.list_value.values.map do |v|
208
- value_to_raw v, type.array_element_type
213
+ grpc_value_to_object v, type.array_element_type
209
214
  end
210
215
  when :STRUCT
211
216
  Data.from_grpc value.list_value.values, type.struct_type.fields
212
217
  end
213
218
  end
214
219
 
220
+ def row_to_pairs row_types, row
221
+ row_types.zip(row).map do |field, value|
222
+ [field.name.to_sym, grpc_value_to_object(value, field.type)]
223
+ end
224
+ end
225
+
226
+ def row_to_object row_types, row
227
+ Hash[row_to_pairs(row_types, row)]
228
+ end
229
+
215
230
  def number_to_duration number
216
231
  return nil if number.nil?
217
232
 
@@ -247,8 +262,8 @@ module Google
247
262
 
248
263
  def to_key_range range
249
264
  range_opts = {
250
- start_closed: raw_to_value(Array(range.begin)).list_value,
251
- end_closed: raw_to_value(Array(range.end)).list_value }
265
+ start_closed: object_to_grpc_value(Array(range.begin)).list_value,
266
+ end_closed: object_to_grpc_value(Array(range.end)).list_value }
252
267
 
253
268
  if range.respond_to?(:exclude_begin?) && range.exclude_begin?
254
269
  range_opts[:start_open] = range_opts[:start_closed]
@@ -274,7 +289,7 @@ module Google
274
289
 
275
290
  key_list = keys.map do |key|
276
291
  key = [key] unless key.is_a? Array
277
- raw_to_value(key).list_value
292
+ object_to_grpc_value(key).list_value
278
293
  end
279
294
  Google::Spanner::V1::KeySet.new keys: key_list
280
295
  end
@@ -42,7 +42,8 @@ module Google
42
42
  #
43
43
  class Data
44
44
  ##
45
- # Returns the names and values of the data as an array of field objects.
45
+ # Returns the configuration object ({Fields}) of the names and types of
46
+ # the data.
46
47
  #
47
48
  # @return [Array<Array>] An array containing name and value pairs.
48
49
  #
@@ -109,14 +110,15 @@ module Google
109
110
  #
110
111
  def [] key
111
112
  if key.is_a? Integer
112
- return Convert.value_to_raw(@grpc_values[key],
113
- @grpc_fields[key].type)
113
+ return Convert.grpc_value_to_object(@grpc_values[key],
114
+ @grpc_fields[key].type)
114
115
  end
115
116
  name_count = @grpc_fields.find_all { |f| f.name == String(key) }.count
116
117
  return nil if name_count.zero?
117
118
  raise DuplicateNameError if name_count > 1
118
119
  index = @grpc_fields.find_index { |f| f.name == String(key) }
119
- Convert.value_to_raw(@grpc_values[index], @grpc_fields[index].type)
120
+ Convert.grpc_value_to_object(@grpc_values[index],
121
+ @grpc_fields[index].type)
120
122
  end
121
123
 
122
124
  ##
@@ -184,13 +186,44 @@ module Google
184
186
  "#<#{self.class.name} #{self}>"
185
187
  end
186
188
 
189
+ ##
190
+ # @private
191
+ def to_grpc_value
192
+ if @grpc_values.nil?
193
+ return Google::Protobuf::Value.new null_value: :NULL_VALUE
194
+ end
195
+
196
+ Google::Protobuf::Value.new(
197
+ list_value: Google::Protobuf::ListValue.new(
198
+ values: @grpc_values
199
+ )
200
+ )
201
+ end
202
+
203
+ ##
204
+ # @private
205
+ def to_grpc_type
206
+ Google::Spanner::V1::Type.new(
207
+ code: :STRUCT,
208
+ struct_type: Google::Spanner::V1::StructType.new(
209
+ fields: @grpc_fields
210
+ )
211
+ )
212
+ end
213
+
214
+ ##
215
+ # @private
216
+ def to_grpc_value_and_type
217
+ [to_grpc_value, to_grpc_type]
218
+ end
219
+
187
220
  ##
188
221
  # @private Creates a new Data instance from
189
222
  # Spanner values and fields.
190
223
  def self.from_grpc grpc_values, grpc_fields
191
224
  new.tap do |d|
192
- d.instance_variable_set :@grpc_values, grpc_values
193
- d.instance_variable_set :@grpc_fields, grpc_fields
225
+ d.instance_variable_set :@grpc_values, Array(grpc_values)
226
+ d.instance_variable_set :@grpc_fields, Array(grpc_fields)
194
227
  end
195
228
  end
196
229
  end
@@ -36,25 +36,91 @@ module Google
36
36
  # results = db.execute "SELECT * FROM users"
37
37
  #
38
38
  # results.fields.pairs.each do |name, type|
39
- # puts "Column #{name} is type {type}"
39
+ # puts "Column #{name} is type #{type}"
40
40
  # end
41
41
  #
42
42
  class Fields
43
43
  ##
44
- # @private
44
+ # Creates {Fields} object from types. See {Client#fields}.
45
+ #
46
+ # This object can be used to create {Data} objects by providing values
47
+ # that match the field types. See {Fields#struct}.
48
+ #
49
+ # See [Data Types - Constructing a
50
+ # STRUCT](https://cloud.google.com/spanner/docs/data-types#constructing-a-struct).
51
+ #
52
+ # @param [Array, Hash] types Accepts an array or hash types.
53
+ #
54
+ # Arrays can contain just the type value, or a sub-array of the
55
+ # field's name and type value. Hash keys must contain the field name
56
+ # as a `Symbol` or `String`, or the field position as an `Integer`.
57
+ # Hash values must contain the type value. If a Hash is used the
58
+ # fields will be created using the same order as the Hash keys.
59
+ #
60
+ # Supported type values incude:
61
+ #
62
+ # * `:BOOL`
63
+ # * `:BYTES`
64
+ # * `:DATE`
65
+ # * `:FLOAT64`
66
+ # * `:INT64`
67
+ # * `:STRING`
68
+ # * `:TIMESTAMP`
69
+ # * `Array` - Lists are specified by providing the type code in an
70
+ # array. For example, an array of integers are specified as
71
+ # `[:INT64]`.
72
+ # * {Fields} - Nested Structs are specified by providing a Fields
73
+ # object.
74
+ #
75
+ # @return [Fields] The fields of the given types.
76
+ #
77
+ # @example Create a STRUCT value with named fields using Fields object:
78
+ # require "google/cloud/spanner"
79
+ #
80
+ # named_type = Google::Cloud::Spanner::Fields.new(
81
+ # { id: :INT64, name: :STRING, active: :BOOL }
82
+ # )
83
+ # named_data = named_type.struct(
84
+ # { id: 42, name: nil, active: false }
85
+ # )
86
+ #
87
+ # @example Create a STRUCT value with anonymous field names:
88
+ # require "google/cloud/spanner"
89
+ #
90
+ # anon_type = Google::Cloud::Spanner::Fields.new(
91
+ # [:INT64, :STRING, :BOOL]
92
+ # )
93
+ # anon_data = anon_type.struct [42, nil, false]
94
+ #
95
+ # @example Create a STRUCT value with duplicate field names:
96
+ # require "google/cloud/spanner"
97
+ #
98
+ # dup_type = Google::Cloud::Spanner::Fields.new(
99
+ # [[:x, :INT64], [:x, :STRING], [:x, :BOOL]]
100
+ # )
101
+ # dup_data = dup_type.struct [42, nil, false]
102
+ #
45
103
  def initialize types
46
- @fields = []
47
- if types.is_a? Array
48
- types.each do |type|
49
- @fields << field(type)
50
- end
51
- elsif types.is_a? Hash
52
- types.each do |type|
53
- @fields << field(type)
54
- end
55
- else
104
+ types = types.to_a if types.is_a? Hash
105
+
106
+ unless types.is_a? Array
56
107
  raise ArgumentError, "can only accept Array or Hash"
57
108
  end
109
+
110
+ sorted_types, unsorted_types = types.partition do |type|
111
+ type.is_a?(Array) && type.count == 2 && type.first.is_a?(Integer)
112
+ end
113
+
114
+ verify_sorted_types! sorted_types, types.count
115
+
116
+ @grpc_fields = Array.new(types.count) do |index|
117
+ sorted_type = sorted_types.assoc index
118
+ if sorted_type
119
+ to_grpc_field sorted_type.last
120
+ else
121
+ to_grpc_field unsorted_types.shift
122
+ end
123
+ end
58
124
  end
59
125
 
60
126
  ##
@@ -66,7 +132,7 @@ module Google
66
132
  # @return [Array<Symbol>] An array containing the types of the data.
67
133
  #
68
134
  def types
69
- @fields.map(&:type).map do |type|
135
+ @grpc_fields.map(&:type).map do |type|
70
136
  if type.code == :ARRAY
71
137
  if type.array_element_type.code == :STRUCT
72
138
  [Fields.from_grpc(type.array_element_type.struct_type.fields)]
@@ -90,7 +156,7 @@ module Google
90
156
  # data.
91
157
  #
92
158
  def keys
93
- @fields.each_with_index.map do |field, index|
159
+ @grpc_fields.map.with_index do |field, index|
94
160
  if field.name.empty?
95
161
  index
96
162
  else
@@ -134,69 +200,157 @@ module Google
134
200
  #
135
201
  def [] key
136
202
  return types[key] if key.is_a? Integer
137
- name_count = @fields.find_all { |f| f.name == String(key) }.count
203
+ name_count = @grpc_fields.find_all { |f| f.name == String(key) }.count
138
204
  return nil if name_count.zero?
139
205
  raise DuplicateNameError if name_count > 1
140
- index = @fields.find_index { |f| f.name == String(key) }
206
+ index = @grpc_fields.find_index { |f| f.name == String(key) }
141
207
  types[index]
142
208
  end
143
209
 
210
+ # rubocop:disable all
211
+
144
212
  ##
145
- # Returns the type codes as an array. Do not use this method if the data
146
- # has more than one member with the same name. (See {#duplicate_names?})
213
+ # Creates a new {Data} object given the data values matching the fields.
214
+ # Can be provided as either an Array of values, or a Hash where the hash
215
+ # keys match the field name or match the index position of the field.
147
216
  #
148
- # @raise [Google::Cloud::Spanner::DuplicateNameError] if the data
149
- # contains duplicate names.
217
+ # For more information, see [Data Types - Constructing a
218
+ # STRUCT](https://cloud.google.com/spanner/docs/data-types#constructing-a-struct).
150
219
  #
151
- # @return [Array<Symbol>] An array containing the type codes.
220
+ # @param [Array, Hash] data Accepts an array or hash data values.
152
221
  #
153
- def to_a
154
- Array.new(keys.count) do |i|
155
- field = self[i]
156
- if field.is_a? Fields
157
- field.to_h
158
- elsif field.is_a? Array
159
- field.map { |f| f.is_a?(Fields) ? f.to_h : f }
160
- else
161
- field
222
+ # Arrays can contain just the data value, nested arrays will be
223
+ # treated as lists of values. Values must be provided in the same
224
+ # order as the fields, and there is no way to associate values to the
225
+ # field names.
226
+ #
227
+ # Hash keys must contain the field name as a `Symbol` or `String`, or
228
+ # the field position as an `Integer`. Hash values must contain the
229
+ # data value. Hash values will be matched to the fields, so they don't
230
+ # need to match the same order as the fields.
231
+ #
232
+ # @return [Data] A new Data object.
233
+ #
234
+ # @example Create a STRUCT value with named fields using Fields object:
235
+ # require "google/cloud/spanner"
236
+ #
237
+ # spanner = Google::Cloud::Spanner.new
238
+ #
239
+ # db = spanner.client "my-instance", "my-database"
240
+ #
241
+ # named_type = db.fields(
242
+ # { id: :INT64, name: :STRING, active: :BOOL }
243
+ # )
244
+ # named_data = named_type.struct(
245
+ # { id: 42, name: nil, active: false }
246
+ # )
247
+ #
248
+ # @example Create a STRUCT value with anonymous field names:
249
+ # require "google/cloud/spanner"
250
+ #
251
+ # spanner = Google::Cloud::Spanner.new
252
+ #
253
+ # db = spanner.client "my-instance", "my-database"
254
+ #
255
+ # anon_type = db.fields [:INT64, :STRING, :BOOL]
256
+ # anon_data = anon_type.struct [42, nil, false]
257
+ #
258
+ # @example Create a STRUCT value with duplicate field names:
259
+ # require "google/cloud/spanner"
260
+ #
261
+ # spanner = Google::Cloud::Spanner.new
262
+ #
263
+ # db = spanner.client "my-instance", "my-database"
264
+ #
265
+ # dup_type = db.fields [[:x, :INT64], [:x, :STRING], [:x, :BOOL]]
266
+ # dup_data = dup_type.struct [42, nil, false]
267
+ #
268
+ def struct data
269
+ # create local copy of types so they are parsed just once.
270
+ cached_types = types
271
+ if data.nil?
272
+ return Data.from_grpc nil, @grpc_fields
273
+ elsif data.is_a? Array
274
+ # Convert data in the order it was recieved
275
+ values = data.map.with_index do |datum, index|
276
+ Convert.object_to_grpc_value_and_type(datum, cached_types[index]).first
162
277
  end
278
+ return Data.from_grpc values, @grpc_fields
279
+ elsif data.is_a? Hash
280
+ # Pull values from hash in order of the fields,
281
+ # we can't always trust the Hash to be in order.
282
+ values = @grpc_fields.map.with_index do |field, index|
283
+ if data.key? index
284
+ Convert.object_to_grpc_value_and_type(data[index],
285
+ cached_types[index]).first
286
+ elsif !field.name.to_s.empty?
287
+ if data.key? field.name.to_s
288
+ Convert.object_to_grpc_value_and_type(data[field.name.to_s],
289
+ cached_types[index]).first
290
+ elsif data.key? field.name.to_s.to_sym
291
+ Convert.object_to_grpc_value_and_type(data[field.name.to_s.to_sym],
292
+ cached_types[index]).first
293
+ else
294
+ raise "data value for field #{field.name} missing"
295
+ end
296
+ else
297
+ raise "data value for field #{index} missing"
298
+ end
299
+ end
300
+ return Data.from_grpc values, @grpc_fields
163
301
  end
302
+ raise ArgumentError, "can only accept Array or Hash"
303
+ end
304
+ alias data struct
305
+ alias new struct
306
+
307
+ # rubocop:enable all
308
+
309
+ ##
310
+ # Returns the type codes as an array. Do not use this method if the data
311
+ # has more than one member with the same name. (See {#duplicate_names?})
312
+ #
313
+ # @return [Array<Symbol|Array<Symbol>|Fields|Array<Fields>>] An array
314
+ # containing the type codes.
315
+ #
316
+ def to_a
317
+ types
164
318
  end
165
319
 
166
320
  ##
167
321
  # Returns the names or indexes and corresponding type codes as a hash.
168
322
  #
169
- # @return [Hash<(String,Integer)=>Symbol>] A hash containing the names
170
- # or indexes and corresponding types.
323
+ # @raise [Google::Cloud::Spanner::DuplicateNameError] if the data
324
+ # contains duplicate names.
325
+ #
326
+ # @return [Hash<(Symbol|Integer) =>
327
+ # (Symbol|Array<Symbol>|Fields|Array<Fields>)] A hash containing the
328
+ # names or indexes and corresponding types.
171
329
  #
172
330
  def to_h
173
331
  raise DuplicateNameError if duplicate_names?
174
- hashified_pairs = pairs.map do |key, value|
175
- if value.is_a? Fields
176
- [key, value.to_h]
177
- elsif value.is_a? Array
178
- [key, value.map { |v| v.is_a?(Fields) ? v.to_h : v }]
179
- else
180
- [key, value]
181
- end
182
- end
183
- Hash[hashified_pairs]
332
+ Hash[pairs]
184
333
  end
185
334
 
186
335
  # @private
187
- def data data
188
- # TODO: match order of types
189
- data = data.values if data.is_a?(Hash)
190
- values = data.map { |datum| Convert.raw_to_value datum }
191
- Data.from_grpc values, @fields
336
+ def count
337
+ @grpc_fields.count
192
338
  end
193
- alias new data
339
+ alias size count
194
340
 
195
341
  # @private
196
342
  def == other
197
343
  return false unless other.is_a? Fields
198
344
  pairs == other.pairs
199
345
  end
346
+ alias eql? ==
347
+
348
+ # @private
349
+ def hash
350
+ # The Protobuf object looks to maintain consistent hash values
351
+ # for objects with the same configuration.
352
+ to_grpc_type.hash
353
+ end
200
354
 
201
355
  # @private
202
356
  def to_s
@@ -215,40 +369,78 @@ module Google
215
369
  "#<#{self.class.name} #{self}>"
216
370
  end
217
371
 
372
+ ##
373
+ # @private
374
+ def to_grpc_type
375
+ Google::Spanner::V1::Type.new(
376
+ code: :STRUCT,
377
+ struct_type: Google::Spanner::V1::StructType.new(
378
+ fields: @grpc_fields
379
+ )
380
+ )
381
+ end
382
+
218
383
  ##
219
384
  # @private Creates a new Fields instance from a
220
385
  # Google::Spanner::V1::Metadata::Row_type::Fields.
221
386
  def self.from_grpc fields
222
387
  new([]).tap do |f|
223
- f.instance_variable_set :@fields, fields
388
+ f.instance_variable_set :@grpc_fields, Array(fields)
224
389
  end
225
390
  end
226
391
 
227
392
  protected
228
393
 
229
- def field pair
394
+ # rubocop:disable all
395
+
396
+ def verify_sorted_types! sorted_types, total_count
397
+ sorted_unique_positions = sorted_types.map(&:first).uniq.sort
398
+ return if sorted_unique_positions.empty?
399
+
400
+ if sorted_unique_positions.first < 0
401
+ raise ArgumentError, "cannot specify position less than 0"
402
+ end
403
+ if sorted_unique_positions.last >= total_count
404
+ raise ArgumentError, "cannot specify position more than field count"
405
+ end
406
+ if sorted_types.count != sorted_unique_positions.count
407
+ raise ArgumentError, "cannot specify position more than once"
408
+ end
409
+ end
410
+
411
+ def to_grpc_field pair
230
412
  if pair.is_a?(Array)
231
- unless pair.count == 2
232
- raise ArgumentError, "can only accept pairs of name and type"
233
- end
234
- if pair.first.nil? || pair.first.is_a?(Integer)
235
- Google::Spanner::V1::StructType::Field.new(
236
- type: Google::Spanner::V1::Type.new(code: pair.last)
237
- )
413
+ if pair.count == 2
414
+ if pair.first.is_a?(Integer)
415
+ Google::Spanner::V1::StructType::Field.new(
416
+ type: Convert.grpc_type_for_field(pair.last)
417
+ )
418
+ else
419
+ Google::Spanner::V1::StructType::Field.new(
420
+ name: String(pair.first),
421
+ type: Convert.grpc_type_for_field(pair.last)
422
+ )
423
+ end
238
424
  else
239
425
  Google::Spanner::V1::StructType::Field.new(
240
- name: String(pair.first),
241
- type: Google::Spanner::V1::Type.new(code: pair.last)
426
+ type: Google::Spanner::V1::Type.new(
427
+ code: :ARRAY,
428
+ array_element_type: Convert.grpc_type_for_field(pair.last)
429
+ )
242
430
  )
243
431
  end
244
432
  else
433
+ # TODO: Handle Fields object
434
+ # TODO: Handle Hash by creating Fields object
245
435
  unless pair.is_a?(Symbol)
246
436
  raise ArgumentError, "type must be a symbol"
247
437
  end
248
438
  Google::Spanner::V1::StructType::Field.new(
249
- type: Google::Spanner::V1::Type.new(code: pair)
439
+ type: Convert.grpc_type_for_field(pair)
250
440
  )
251
441
  end
442
+
443
+ # rubocop:enable all
252
444
  end
253
445
  end
254
446
  end