google-cloud-spanner 1.4.0 → 1.5.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.
@@ -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