simple_record 1.4.0 → 1.4.2

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/lib/simple_record.rb CHANGED
@@ -25,14 +25,15 @@
25
25
  # Forked off old ActiveRecord2sdb library.
26
26
 
27
27
  require 'aws'
28
- require 'sdb/active_sdb'
29
28
  require 'base64'
30
29
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/attributes")
30
+ require File.expand_path(File.dirname(__FILE__) + "/simple_record/active_sdb")
31
31
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/callbacks")
32
32
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/encryptor")
33
33
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/exceptions")
34
34
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/errors")
35
35
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/json")
36
+ require File.expand_path(File.dirname(__FILE__) + "/simple_record/logging")
36
37
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/password")
37
38
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/results_array")
38
39
  require File.expand_path(File.dirname(__FILE__) + "/simple_record/stats")
@@ -44,66 +45,89 @@ module SimpleRecord
44
45
  @@options = {}
45
46
  @@stats = SimpleRecord::Stats.new
46
47
  @@logging = false
48
+ @@usage_logging_options = {}
47
49
 
48
50
  class << self;
49
51
  attr_accessor :aws_access_key, :aws_secret_key
50
- end
51
52
 
52
- def self.enable_logging
53
- @@logging = true
54
- end
53
+ def enable_logging
54
+ @@logging = true
55
+ end
55
56
 
56
- def self.disable_logging
57
- @@logging = false
58
- end
57
+ def disable_logging
58
+ @@logging = false
59
+ end
59
60
 
60
- def self.logging?
61
- @@logging
62
- end
61
+ def logging?
62
+ @@logging
63
+ end
63
64
 
64
- def self.stats
65
- @@stats
66
- end
65
+ # This can be used to log queries and what not to a file.
66
+ # Params:
67
+ # :select=>{:filename=>"file_to_write_to", :format=>"csv"}
68
+ def log_usage(types={})
69
+ return if types.nil?
70
+ types.each_pair do |type, options|
71
+ @@usage_logging_options[type] = options
72
+ end
73
+ end
67
74
 
68
- # Create a new handle to an Sdb account. All handles share the same per process or per thread
69
- # HTTP connection to Amazon Sdb. Each handle is for a specific account.
70
- # The +params+ are passed through as-is to Aws::SdbInterface.new
71
- # Params:
72
- # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
73
- # :port => 443 # Amazon service port: 80(default) or 443
74
- # :protocol => 'https' # Amazon service protocol: 'http'(default) or 'https'
75
- # :signature_version => '0' # The signature version : '0' or '1'(default)
76
- # :connection_mode => :default # options are
77
- # :default (will use best known safe (as in won't need explicit close) option, may change in the future)
78
- # :per_request (opens and closes a connection on every request to SDB)
79
- # :single (one thread across entire app)
80
- # :per_thread (one connection per thread)
81
- # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
82
- # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
83
- def self.establish_connection(aws_access_key=nil, aws_secret_key=nil, params={})
84
- @aws_access_key = aws_access_key
85
- @aws_secret_key = aws_secret_key
86
- @@options.merge!(params)
87
- #puts 'SimpleRecord.establish_connection with options: ' + @@options.inspect
88
- Aws::ActiveSdb.establish_connection(aws_access_key, aws_secret_key, @@options)
89
- end
75
+ def close_usage_log(type)
76
+ @@usage_logging_options[type][:file].close if @@usage_logging_options[type][:file]
77
+ end
90
78
 
91
- def self.close_connection()
92
- Aws::ActiveSdb.close_connection
93
- end
79
+ def usage_logging_options
80
+ @@usage_logging_options
81
+ end
94
82
 
95
- def self.options
96
- @@options
97
- end
83
+ def stats
84
+ @@stats
85
+ end
86
+
87
+
88
+ # Create a new handle to an Sdb account. All handles share the same per process or per thread
89
+ # HTTP connection to Amazon Sdb. Each handle is for a specific account.
90
+ # The +params+ are passed through as-is to Aws::SdbInterface.new
91
+ # Params:
92
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
93
+ # :port => 443 # Amazon service port: 80(default) or 443
94
+ # :protocol => 'https' # Amazon service protocol: 'http'(default) or 'https'
95
+ # :signature_version => '0' # The signature version : '0' or '1'(default)
96
+ # :connection_mode => :default # options are
97
+ # :default (will use best known safe (as in won't need explicit close) option, may change in the future)
98
+ # :per_request (opens and closes a connection on every request to SDB)
99
+ # :single (one thread across entire app)
100
+ # :per_thread (one connection per thread)
101
+ # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
102
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
103
+ def establish_connection(aws_access_key=nil, aws_secret_key=nil, params={})
104
+ @aws_access_key = aws_access_key
105
+ @aws_secret_key = aws_secret_key
106
+ @@options.merge!(params)
107
+ #puts 'SimpleRecord.establish_connection with options: ' + @@options.inspect
108
+ SimpleRecord::ActiveSdb.establish_connection(aws_access_key, aws_secret_key, @@options)
109
+ end
98
110
 
111
+ def close_connection()
112
+ SimpleRecord::ActiveSdb.close_connection
113
+ end
114
+
115
+ def options
116
+ @@options
117
+ end
99
118
 
100
- class Base < Aws::ActiveSdb::Base
119
+ end
120
+
121
+ class Base < SimpleRecord::ActiveSdb::Base
101
122
 
102
123
  include SimpleRecord::Translations
103
124
  # include SimpleRecord::Attributes
104
125
  extend SimpleRecord::Attributes
105
126
  include SimpleRecord::Callbacks
106
127
  include SimpleRecord::Json
128
+ include SimpleRecord::Logging
129
+ extend SimpleRecord::Logging::ClassMethods
130
+
107
131
 
108
132
  def self.extended(base)
109
133
 
@@ -831,7 +855,8 @@ module SimpleRecord
831
855
  results = q_type == :all ? [] : nil
832
856
  begin
833
857
  results=find_with_metadata(*params)
834
- # puts "RESULT=" + results.inspect
858
+ puts "RESULT=" + results.inspect
859
+ write_usage(:select, domain, q_type, options, results)
835
860
  #puts 'params3=' + params.inspect
836
861
  SimpleRecord.stats.selects += 1
837
862
  if q_type == :count
@@ -850,7 +875,7 @@ module SimpleRecord
850
875
  results = SimpleRecord::ResultsArray.new(self, params, results, next_token)
851
876
  end
852
877
  end
853
- rescue Aws::AwsError, Aws::ActiveSdb::ActiveSdbError
878
+ rescue Aws::AwsError, SimpleRecord::ActiveSdb::ActiveSdbError
854
879
  # puts "RESCUED: " + $!.message
855
880
  if ($!.message().index("NoSuchDomain") != nil)
856
881
  # this is ok
@@ -0,0 +1,985 @@
1
+ # Moved this into SimpleRecord because it's too tightly tied together and ActiveSDB should go away.
2
+
3
+ #
4
+ # Copyright (c) 2008 RightScale Inc
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ begin
27
+ require 'uuidtools'
28
+ rescue LoadError => e
29
+ STDERR.puts("SimpleRecord requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
30
+ exit
31
+ end
32
+
33
+ module SimpleRecord
34
+
35
+ # = Aws::ActiveSdb -- RightScale SDB interface (alpha release)
36
+ # The Aws::ActiveSdb class provides a complete interface to Amazon's Simple
37
+ # Database Service.
38
+ #
39
+ # ActiveSdb is in alpha and does not load by default with the rest of Aws. You must use an additional require statement to load the ActiveSdb class. For example:
40
+ #
41
+ # require 'right_aws'
42
+ # require 'sdb/active_sdb'
43
+ #
44
+ # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by Aws and is not installed as a
45
+ # dependency of Aws.
46
+ #
47
+ # Simple ActiveSdb usage example:
48
+ #
49
+ # class Client < Aws::ActiveSdb::Base
50
+ # end
51
+ #
52
+ # # connect to SDB
53
+ # Aws::ActiveSdb.establish_connection
54
+ #
55
+ # # create domain
56
+ # Client.create_domain
57
+ #
58
+ # # create initial DB
59
+ # Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president'
60
+ # Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president'
61
+ # Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president'
62
+ # Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping']
63
+ # Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']
64
+ # sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id
65
+ #
66
+ # # find all Bushes in USA
67
+ # Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client|
68
+ # client.reload
69
+ # puts client.attributes.inspect
70
+ # end
71
+ #
72
+ # # find all Maries through the world
73
+ # Client.find_all_by_name_and_gender('Mary','female').each do |mary|
74
+ # mary.reload
75
+ # puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}"
76
+ # end
77
+ #
78
+ # # find new russian president
79
+ # medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012')
80
+ # if medvedev
81
+ # medvedev.reload
82
+ # medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom')
83
+ # end
84
+ #
85
+ # # retire old president
86
+ # Client.find_by_name('Putin').delete
87
+ #
88
+ # # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' ()
89
+ # sandy = Client.find(sandy_id)
90
+ # sandy.reload
91
+ # sandy.delete_values('hobby' => ['cooking'] )
92
+ # sandy.delete_attributes('country', 'gender')
93
+ #
94
+ # # remove domain
95
+ # Client.delete_domain
96
+ #
97
+ class ActiveSdb
98
+
99
+ module ActiveSdbConnect
100
+ def connection
101
+ @connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
102
+ end
103
+
104
+ # Create a new handle to an Sdb account. All handles share the same per process or per thread
105
+ # HTTP connection to Amazon Sdb. Each handle is for a specific account.
106
+ # The +params+ are passed through as-is to Aws::SdbInterface.new
107
+ # Params:
108
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
109
+ # :port => 443 # Amazon service port: 80 or 443(default)
110
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
111
+ # :signature_version => '2' # The signature version : '0', '1' or '2' (default)
112
+ # DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
113
+ # :connection_mode => :default # options are :default (will use best known option, may change in the future)
114
+ # :per_request (opens and closes a connection on every request to SDB)
115
+ # :single (same as old multi_thread=>false)
116
+ # :per_thread (same as old multi_thread=>true)
117
+ # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
118
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
119
+ # :nil_representation => 'mynil'} # interpret Ruby nil as this string value; i.e. use this string in SDB to represent Ruby nils (default is the string 'nil')
120
+ # :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB
121
+
122
+ def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
123
+ @connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
124
+ end
125
+
126
+ def close_connection
127
+ @connection.close_connection unless @connection.nil?
128
+ end
129
+ end
130
+
131
+ class ActiveSdbError < RuntimeError
132
+ end
133
+
134
+ class << self
135
+ include ActiveSdbConnect
136
+
137
+ # Retreive a list of domains.
138
+ #
139
+ # put Aws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
140
+ #
141
+ def domains
142
+ connection.list_domains[:domains]
143
+ end
144
+
145
+ # Create new domain.
146
+ # Raises no errors if the domain already exists.
147
+ #
148
+ # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
149
+ #
150
+ def create_domain(domain_name)
151
+ connection.create_domain(domain_name)
152
+ end
153
+
154
+ # Remove domain from SDB.
155
+ # Raises no errors if the domain does not exist.
156
+ #
157
+ # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
158
+ #
159
+ def delete_domain(domain_name)
160
+ connection.delete_domain(domain_name)
161
+ end
162
+ end
163
+
164
+ class Base
165
+
166
+ class << self
167
+ include ActiveSdbConnect
168
+
169
+ # next_token value returned by last find: is useful to continue finding
170
+ attr_accessor :next_token
171
+
172
+ # Returns a Aws::SdbInterface object
173
+ #
174
+ # class A < Aws::ActiveSdb::Base
175
+ # end
176
+ #
177
+ # class B < Aws::ActiveSdb::Base
178
+ # end
179
+ #
180
+ # class C < Aws::ActiveSdb::Base
181
+ # end
182
+ #
183
+ # Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
184
+ #
185
+ # C.establish_connection 'key_id_2', 'secret_key_2'
186
+ #
187
+ # # A and B uses the default connection, C - uses its own
188
+ # puts A.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
189
+ # puts B.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
190
+ # puts C.connection #=> #<Aws::SdbInterface:0xb76d6ca0>
191
+ #
192
+ def connection
193
+ @connection || ActiveSdb::connection
194
+ end
195
+
196
+ @domain = nil
197
+
198
+ # Current domain name.
199
+ #
200
+ # # if 'ActiveSupport' is not loaded then class name converted to downcase
201
+ # class Client < Aws::ActiveSdb::Base
202
+ # end
203
+ # puts Client.domain #=> 'client'
204
+ #
205
+ # # if 'ActiveSupport' is loaded then class name being tableized
206
+ # require 'activesupport'
207
+ # class Client < Aws::ActiveSdb::Base
208
+ # end
209
+ # puts Client.domain #=> 'clients'
210
+ #
211
+ # # Explicit domain name definition
212
+ # class Client < Aws::ActiveSdb::Base
213
+ # set_domain_name :foreign_clients
214
+ # end
215
+ # puts Client.domain #=> 'foreign_clients'
216
+ #
217
+ def domain
218
+ unless @domain
219
+ if defined? ActiveSupport::CoreExtensions::String::Inflections
220
+ @domain = name.tableize
221
+ else
222
+ @domain = name.downcase
223
+ end
224
+ end
225
+ @domain
226
+ end
227
+
228
+ # Change the default domain name to user defined.
229
+ #
230
+ # class Client < Aws::ActiveSdb::Base
231
+ # set_domain_name :foreign_clients
232
+ # end
233
+ #
234
+ def set_domain_name(domain)
235
+ @domain = domain.to_s
236
+ end
237
+
238
+ # Create domain at SDB.
239
+ # Raises no errors if the domain already exists.
240
+ #
241
+ # class Client < Aws::ActiveSdb::Base
242
+ # end
243
+ # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
244
+ #
245
+ def create_domain
246
+ connection.create_domain(domain)
247
+ end
248
+
249
+ # Remove domain from SDB.
250
+ # Raises no errors if the domain does not exist.
251
+ #
252
+ # class Client < Aws::ActiveSdb::Base
253
+ # end
254
+ # Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"}
255
+ #
256
+ def delete_domain
257
+ connection.delete_domain(domain)
258
+ end
259
+
260
+ #
261
+ # See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
262
+ #
263
+ def find(*args)
264
+ options = args.last.is_a?(Hash) ? args.pop : {}
265
+ case args.first
266
+ when nil then
267
+ raise "Invalid parameters passed to find: nil."
268
+ when :all then
269
+ sql_select(options)[:items]
270
+ when :first then
271
+ sql_select(options.merge(:limit => 1))[:items].first
272
+ when :count then
273
+ res = sql_select(options.merge(:count => true))[:count]
274
+ res
275
+ else
276
+ res = select_from_ids(args, options)
277
+ return res[:single] if res[:single]
278
+ return res[:items]
279
+ end
280
+ end
281
+
282
+ #
283
+ # Same as find, but will return SimpleDB metadata like :request_id and :box_usage
284
+ #
285
+ def find_with_metadata(*args)
286
+ options = args.last.is_a?(Hash) ? args.pop : {}
287
+ case args.first
288
+ when nil then
289
+ raise "Invalid parameters passed to find: nil."
290
+ when :all then
291
+ sql_select(options)
292
+ when :first then
293
+ sql_select(options.merge(:limit => 1))
294
+ when :count then
295
+ res = sql_select(options.merge(:count => true))
296
+ res
297
+ else
298
+ select_from_ids args, options
299
+ end
300
+ end
301
+
302
+ # Perform a SQL-like select request.
303
+ #
304
+ # Single record:
305
+ #
306
+ # Client.select(:first)
307
+ # Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
308
+ # Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
309
+ #
310
+ # Bunch of records:
311
+ #
312
+ # Client.select(:all)
313
+ # Client.select(:all, :limit => 10)
314
+ # Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
315
+ # Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
316
+ #
317
+ # Records by ids:
318
+ #
319
+ # Client.select('1')
320
+ # Client.select('1234987b4583475347523948')
321
+ # Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
322
+ #
323
+ # Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_...
324
+ #
325
+ # Client.select_by_name('Matias Rust')
326
+ # Client.select_by_name_and_city('Putin','Moscow')
327
+ # Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
328
+ #
329
+ # Client.select_all_by_author('G.Bush jr')
330
+ # Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
331
+ # Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
332
+ #
333
+ # Continue listing:
334
+ #
335
+ # # initial listing
336
+ # Client.select(:all, :limit => 10)
337
+ # # continue listing
338
+ # begin
339
+ # Client.select(:all, :limit => 10, :next_token => Client.next_token)
340
+ # end while Client.next_token
341
+ #
342
+ # Sort oder:
343
+ # If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
344
+ #
345
+ # Client.select(:all) # returns all records
346
+ # Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
347
+ # Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
348
+ #
349
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
350
+ #
351
+ def select(*args)
352
+ find(*args)
353
+ end
354
+
355
+ def generate_id # :nodoc:
356
+ UUIDTools::UUID.timestamp_create().to_s
357
+ end
358
+
359
+ protected
360
+
361
+ # Select
362
+
363
+ def select_from_ids(args, options) # :nodoc:
364
+ cond = []
365
+ # detect amount of records requested
366
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
367
+ # flatten ids
368
+ args = args.to_a.flatten
369
+ args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" }
370
+ ids_cond = "(#{cond.join(' OR ')})"
371
+ # user defined :conditions to string (if it was defined)
372
+ options[:conditions] = build_conditions(options[:conditions])
373
+ # join ids condition and user defined conditions
374
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
375
+ puts 'options=' + options.inspect
376
+ result = sql_select(options)
377
+ puts 'select_from_ids result=' + result.inspect
378
+ # if one record was requested then return it
379
+ unless bunch_of_records_requested
380
+ record = result[:items].first
381
+ # railse if nothing was found
382
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
383
+ result[:single] = record
384
+ else
385
+ # if a bunch of records was requested then return check that we found all of them
386
+ # and return as an array
387
+ unless args.size == result[:items].size
388
+ # todo: might make sense to return the array but with nil values in the slots where an item wasn't found?
389
+ id_list = args.map{|i| "'#{i}'"}.join(',')
390
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})")
391
+ else
392
+ result
393
+ end
394
+ end
395
+ result
396
+ end
397
+
398
+ def sql_select(options) # :nodoc:
399
+ count = options[:count] || false
400
+ #puts 'count? ' + count.to_s
401
+ @next_token = options[:next_token]
402
+ select_expression = build_select(options)
403
+ # request items
404
+ query_result = self.connection.select(select_expression, @next_token)
405
+ # puts 'QR=' + query_result.inspect
406
+ ret = {}
407
+ if count
408
+ ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i
409
+ ret.merge!(query_result)
410
+ return ret
411
+ end
412
+ @next_token = query_result[:next_token]
413
+ items = query_result.delete(:items).map do |hash|
414
+ id, attributes = hash.shift
415
+ new_item = self.new( )
416
+ new_item.initialize_from_db(attributes.merge({ 'id' => id }))
417
+ new_item.mark_as_old
418
+ new_item
419
+ end
420
+ ret[:items] = items
421
+ ret.merge!(query_result)
422
+ ret
423
+ end
424
+
425
+ # select_by helpers
426
+ def select_all_by_(format_str, args, options) # :nodoc:
427
+ fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_')
428
+ conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
429
+ options[:conditions] = [conditions, *args]
430
+ find(:all, options)
431
+ end
432
+
433
+ def select_by_(format_str, args, options) # :nodoc:
434
+ options[:limit] = 1
435
+ select_all_by_(format_str, args, options).first
436
+ end
437
+
438
+ # Query
439
+
440
+ # Returns an array of query attributes.
441
+ # Query_expression must be a well formated SDB query string:
442
+ # query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
443
+ def query_attributes(query_expression) # :nodoc:
444
+ attrs = []
445
+ array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
446
+ until array.empty? do
447
+ attrs << array.shift # skip it's value
448
+ array.shift #
449
+ end
450
+ attrs
451
+ end
452
+
453
+ # Returns an array of [attribute_name, 'asc'|'desc']
454
+ def sort_options(sort_string)
455
+ sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
456
+ [$1, ($2 || 'asc')]
457
+ end
458
+
459
+ # Perform a query request.
460
+ #
461
+ # Options
462
+ # :query_expression nil | string | array
463
+ # :max_number_of_items nil | integer
464
+ # :next_token nil | string
465
+ # :sort_option nil | string "name desc|asc"
466
+ #
467
+ def query(options) # :nodoc:
468
+ @next_token = options[:next_token]
469
+ query_expression = build_conditions(options[:query_expression])
470
+ # add sort_options to the query_expression
471
+ if options[:sort_option]
472
+ sort_by, sort_order = sort_options(options[:sort_option])
473
+ sort_query_expression = "['#{sort_by}' starts-with '']"
474
+ sort_by_expression = " sort '#{sort_by}' #{sort_order}"
475
+ # make query_expression to be a string (it may be null)
476
+ query_expression = query_expression.to_s
477
+ # quote from Amazon:
478
+ # The sort attribute must be present in at least one of the predicates of the query expression.
479
+ if query_expression.blank?
480
+ query_expression = sort_query_expression
481
+ elsif !query_attributes(query_expression).include?(sort_by)
482
+ query_expression += " intersection #{sort_query_expression}"
483
+ end
484
+ query_expression += sort_by_expression
485
+ end
486
+ # request items
487
+ query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token)
488
+ @next_token = query_result[:next_token]
489
+ items = query_result[:items].map do |name|
490
+ new_item = self.new('id' => name)
491
+ new_item.mark_as_old
492
+ reload_if_exists(record) if options[:auto_load]
493
+ new_item
494
+ end
495
+ items
496
+ end
497
+
498
+ # reload a record unless it is nil
499
+ def reload_if_exists(record) # :nodoc:
500
+ record && record.reload
501
+ end
502
+
503
+ def reload_all_records(*list) # :nodoc:
504
+ list.flatten.each { |record| reload_if_exists(record) }
505
+ end
506
+
507
+ def find_every(options) # :nodoc:
508
+ records = query( :query_expression => options[:conditions],
509
+ :max_number_of_items => options[:limit],
510
+ :next_token => options[:next_token],
511
+ :sort_option => options[:sort] || options[:order] )
512
+ options[:auto_load] ? reload_all_records(records) : records
513
+ end
514
+
515
+ def find_initial(options) # :nodoc:
516
+ options[:limit] = 1
517
+ record = find_every(options).first
518
+ options[:auto_load] ? reload_all_records(record).first : record
519
+ end
520
+
521
+ def find_from_ids(args, options) # :nodoc:
522
+ cond = []
523
+ # detect amount of records requested
524
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
525
+ # flatten ids
526
+ args = args.to_a.flatten
527
+ args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
528
+ ids_cond = "[#{cond.join(' OR ')}]"
529
+ # user defined :conditions to string (if it was defined)
530
+ options[:conditions] = build_conditions(options[:conditions])
531
+ # join ids condition and user defined conditions
532
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
533
+ result = find_every(options)
534
+ # if one record was requested then return it
535
+ unless bunch_of_records_requested
536
+ record = result.first
537
+ # railse if nothing was found
538
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
539
+ options[:auto_load] ? reload_all_records(record).first : record
540
+ else
541
+ # if a bunch of records was requested then return check that we found all of them
542
+ # and return as an array
543
+ unless args.size == result.size
544
+ id_list = args.map{|i| "'#{i}'"}.join(',')
545
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
546
+ else
547
+ options[:auto_load] ? reload_all_records(result) : result
548
+ end
549
+ end
550
+ end
551
+
552
+ # find_by helpers
553
+ def find_all_by_(format_str, args, options) # :nodoc:
554
+ fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_')
555
+ conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
556
+ options[:conditions] = [conditions, *args]
557
+ find(:all, options)
558
+ end
559
+
560
+ def find_by_(format_str, args, options) # :nodoc:
561
+ options[:limit] = 1
562
+ find_all_by_(format_str, args, options).first
563
+ end
564
+
565
+ # Misc
566
+
567
+ def method_missing(method, *args) # :nodoc:
568
+ if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
569
+ # get rid of the find ones, only select now
570
+ to_send_to = $1
571
+ attributes = method.to_s[$1.length..method.to_s.length]
572
+ # puts 'attributes=' + attributes
573
+ if to_send_to[0...4] == "find"
574
+ to_send_to = "select" + to_send_to[4..to_send_to.length]
575
+ # puts 'CONVERTED ' + $1 + " to " + to_send_to
576
+ end
577
+
578
+ options = args.last.is_a?(Hash) ? args.pop : {}
579
+ __send__(to_send_to, attributes, args, options)
580
+ else
581
+ super(method, *args)
582
+ end
583
+ end
584
+
585
+ def build_select(options) # :nodoc:
586
+ select = options[:select] || '*'
587
+ select = options[:count] ? "count(*)" : select
588
+ #puts 'select=' + select.to_s
589
+ from = options[:from] || domain
590
+ conditions = options[:conditions] ? " WHERE #{build_conditions(options[:conditions])}" : ''
591
+ order = options[:order] ? " ORDER BY #{options[:order]}" : ''
592
+ limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
593
+ # mix sort by argument (it must present in response)
594
+ unless order.blank?
595
+ sort_by, sort_order = sort_options(options[:order])
596
+ conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
597
+ end
598
+ "SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}"
599
+ end
600
+
601
+ def build_conditions(conditions) # :nodoc:
602
+ case
603
+ when conditions.is_a?(Array) then
604
+ connection.query_expression_from_array(conditions)
605
+ when conditions.is_a?(Hash) then
606
+ connection.query_expression_from_hash(conditions)
607
+ else
608
+ conditions
609
+ end
610
+ end
611
+
612
+ end
613
+
614
+ public
615
+
616
+ # instance attributes
617
+ attr_accessor :attributes
618
+
619
+ # item name
620
+ attr_accessor :id
621
+
622
+ # Create new Item instance.
623
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
624
+ #
625
+ # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
626
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
627
+ # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
628
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
629
+ #
630
+ def initialize(attrs={})
631
+ @attributes = uniq_values(attrs)
632
+ @new_record = true
633
+ end
634
+
635
+ # This is to separate initialization from user vs coming from db (ie: find())
636
+ def initialize_from_db(attrs={})
637
+ initialize(attrs)
638
+ end
639
+
640
+ # Create and save new Item instance.
641
+ # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
642
+ #
643
+ # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
644
+ # puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
645
+ #
646
+ def self.create(attributes={})
647
+ item = self.new(attributes)
648
+ item.save
649
+ item
650
+ end
651
+
652
+ # Returns an item id. Same as: item['id'] or item.attributes['id']
653
+ def id
654
+ @attributes['id']
655
+ end
656
+
657
+ # Sets an item id.
658
+ def id=(id)
659
+ @attributes['id'] = id.to_s
660
+ end
661
+
662
+ # Returns a hash of all the attributes.
663
+ #
664
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
665
+ #
666
+ def attributes
667
+ @attributes.dup
668
+ end
669
+
670
+ # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
671
+ # if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
672
+ #
673
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
674
+ # # set new attributes ('id' is missed)
675
+ # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
676
+ # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
677
+ # # set new attributes ('id' is set)
678
+ # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
679
+ # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
680
+ #
681
+ def attributes=(attrs)
682
+ old_id = @attributes['id']
683
+ @attributes = uniq_values(attrs)
684
+ @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
685
+ self.attributes
686
+ end
687
+
688
+ def connection
689
+ self.class.connection
690
+ end
691
+
692
+ # Item domain name.
693
+ def domain
694
+ self.class.domain
695
+ end
696
+
697
+ # Returns the values of the attribute identified by +attribute+.
698
+ #
699
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
700
+ #
701
+ def [](attribute)
702
+ @attributes[attribute.to_s]
703
+ end
704
+
705
+ # Updates the attribute identified by +attribute+ with the specified +values+.
706
+ #
707
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
708
+ # item['Cat'] = ["Whiskas", "chicken"]
709
+ # puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
710
+ #
711
+ def []=(attribute, values)
712
+ attribute = attribute.to_s
713
+ @attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
714
+
715
+ end
716
+
717
+ # Reload attributes from SDB. Replaces in-memory attributes.
718
+ #
719
+ # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
720
+ # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
721
+ #
722
+ def reload
723
+ raise_on_id_absence
724
+ old_id = id
725
+ attrs = connection.get_attributes(domain, id)[:attributes]
726
+ @attributes = {}
727
+ unless attrs.blank?
728
+ attrs.each { |attribute, values| @attributes[attribute] = values }
729
+ @attributes['id'] = old_id
730
+ end
731
+ mark_as_old
732
+ @attributes
733
+ end
734
+
735
+ # Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
736
+ # +attrs_list+ is an array or comma separated list of attributes names.
737
+ # Returns a hash of loaded attributes.
738
+ #
739
+ # This is not the best method to get a bunch of attributes because
740
+ # a web service call is being performed for every attribute.
741
+ #
742
+ # item = Client.find_by_name('Cat')
743
+ # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
744
+ #
745
+ def reload_attributes(*attrs_list)
746
+ raise_on_id_absence
747
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
748
+ attrs_list.delete('id')
749
+ result = {}
750
+ attrs_list.flatten.uniq.each do |attribute|
751
+ attribute = attribute.to_s
752
+ values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
753
+ unless values.blank?
754
+ @attributes[attribute] = result[attribute] = values
755
+ else
756
+ @attributes.delete(attribute)
757
+ end
758
+ end
759
+ mark_as_old
760
+ result
761
+ end
762
+
763
+ # Stores in-memory attributes to SDB.
764
+ # Adds the attributes values to already stored at SDB.
765
+ # Returns a hash of stored attributes.
766
+ #
767
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
768
+ # sandy['toys'] = 'boys'
769
+ # sandy.put
770
+ # sandy['toys'] = 'patchwork'
771
+ # sandy.put
772
+ # sandy['toys'] = 'kids'
773
+ # sandy.put
774
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
775
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
776
+ #
777
+ # compare to +save+ method
778
+ def put
779
+ @attributes = uniq_values(@attributes)
780
+ prepare_for_update
781
+ attrs = @attributes.dup
782
+ attrs.delete('id')
783
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
784
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
785
+ mark_as_old
786
+ @attributes
787
+ end
788
+
789
+ # Stores specified attributes.
790
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
791
+ # Returns a hash of saved attributes.
792
+ #
793
+ # see to +put+ method
794
+ def put_attributes(attrs)
795
+ attrs = uniq_values(attrs)
796
+ prepare_for_update
797
+ # if 'id' is present in attrs hash:
798
+ # replace internal 'id' attribute and remove it from the attributes to be sent
799
+ @attributes['id'] = attrs['id'] unless attrs['id'].blank?
800
+ attrs.delete('id')
801
+ # add new values to all attributes from list
802
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
803
+ connection.put_attributes(domain, id, { 'id' => id }, :replace)
804
+ attrs.each do |attribute, values|
805
+ @attributes[attribute] ||= []
806
+ @attributes[attribute] += values
807
+ @attributes[attribute].uniq!
808
+ end
809
+ mark_as_old
810
+ attributes
811
+ end
812
+
813
+ # Store in-memory attributes to SDB.
814
+ # Replaces the attributes values already stored at SDB by in-memory data.
815
+ # Returns a hash of stored attributes.
816
+ #
817
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
818
+ # sandy['toys'] = 'boys'
819
+ # sandy.save
820
+ # sandy['toys'] = 'patchwork'
821
+ # sandy.save
822
+ # sandy['toys'] = 'kids'
823
+ # sandy.save
824
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
825
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
826
+ #
827
+ # Options:
828
+ # - :except => Array of attributes to NOT save
829
+ #
830
+ # compare to +put+ method
831
+ def save(options={})
832
+ pre_save2
833
+ atts_to_save = @attributes.dup
834
+ #puts 'atts_to_save=' + atts_to_save.inspect
835
+ #options = params.first.is_a?(Hash) ? params.pop : {}
836
+ if options[:except]
837
+ options[:except].each do |e|
838
+ atts_to_save.delete(e).inspect
839
+ end
840
+ end
841
+ if options[:dirty] # Only used in simple_record right now
842
+ # only save if the attribute is dirty
843
+ dirty_atts = options[:dirty_atts]
844
+ atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) }
845
+ end
846
+ #puts 'atts_to_save2=' + atts_to_save.inspect
847
+ connection.put_attributes(domain, id, atts_to_save, :replace)
848
+ apres_save2
849
+ @attributes
850
+ end
851
+
852
+ def pre_save2
853
+ @attributes = uniq_values(@attributes)
854
+ prepare_for_update
855
+ end
856
+
857
+ def apres_save2
858
+ mark_as_old
859
+ end
860
+
861
+ # Replaces the attributes at SDB by the given values.
862
+ # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
863
+ # The other in-memory attributes are not being saved.
864
+ # Returns a hash of stored attributes.
865
+ #
866
+ # see +save+ method
867
+ def save_attributes(attrs)
868
+ prepare_for_update
869
+ attrs = uniq_values(attrs)
870
+ # if 'id' is present in attrs hash then replace internal 'id' attribute
871
+ unless attrs['id'].blank?
872
+ @attributes['id'] = attrs['id']
873
+ else
874
+ attrs['id'] = id
875
+ end
876
+ connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
877
+ attrs.each { |attribute, values| attrs[attribute] = values }
878
+ mark_as_old
879
+ attrs
880
+ end
881
+
882
+ # Remove specified values from corresponding attributes.
883
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
884
+ #
885
+ # sandy = Client.find_by_name 'Sandy'
886
+ # sandy.reload
887
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
888
+ # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
889
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
890
+ #
891
+ def delete_values(attrs)
892
+ raise_on_id_absence
893
+ attrs = uniq_values(attrs)
894
+ attrs.delete('id')
895
+ unless attrs.blank?
896
+ connection.delete_attributes(domain, id, attrs)
897
+ attrs.each do |attribute, values|
898
+ # remove the values from the attribute
899
+ if @attributes[attribute]
900
+ @attributes[attribute] -= values
901
+ else
902
+ # if the attribute is unknown remove it from a resulting list of fixed attributes
903
+ attrs.delete(attribute)
904
+ end
905
+ end
906
+ end
907
+ attrs
908
+ end
909
+
910
+ # Removes specified attributes from the item.
911
+ # +attrs_list+ is an array or comma separated list of attributes names.
912
+ # Returns the list of deleted attributes.
913
+ #
914
+ # sandy = Client.find_by_name 'Sandy'
915
+ # sandy.reload
916
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
917
+ # puts sandy.delete_attributes('toys') #=> ['toys']
918
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
919
+ #
920
+ def delete_attributes(*attrs_list)
921
+ raise_on_id_absence
922
+ attrs_list = attrs_list.flatten.map{ |attribute| attribute.to_s }
923
+ attrs_list.delete('id')
924
+ unless attrs_list.blank?
925
+ connection.delete_attributes(domain, id, attrs_list)
926
+ attrs_list.each { |attribute| @attributes.delete(attribute) }
927
+ end
928
+ attrs_list
929
+ end
930
+
931
+ # Delete the Item entirely from SDB.
932
+ #
933
+ # sandy = Client.find_by_name 'Sandy'
934
+ # sandy.reload
935
+ # sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
936
+ # puts sandy.delete
937
+ # sandy.reload
938
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
939
+ #
940
+ def delete
941
+ raise_on_id_absence
942
+ connection.delete_attributes(domain, id)
943
+ end
944
+
945
+ # Item ID
946
+ def to_s
947
+ @id
948
+ end
949
+
950
+ # Returns true if this object hasn�t been saved yet.
951
+ def new_record?
952
+ @new_record
953
+ end
954
+
955
+ def mark_as_old # :nodoc:
956
+ @new_record = false
957
+ end
958
+
959
+ private
960
+
961
+ def raise_on_id_absence
962
+ raise ActiveSdbError.new('Unknown record id') unless id
963
+ end
964
+
965
+ def prepare_for_update
966
+ @attributes['id'] = self.class.generate_id if @attributes['id'].blank?
967
+ end
968
+
969
+ def uniq_values(attributes=nil) # :nodoc:
970
+ attrs = {}
971
+ attributes.each do |attribute, values|
972
+ attribute = attribute.to_s
973
+ newval = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
974
+ attrs[attribute] = newval
975
+ if newval.blank?
976
+ # puts "VALUE IS BLANK " + attribute.to_s + " val=" + values.inspect
977
+ attrs.delete(attribute)
978
+ end
979
+ end
980
+ attrs
981
+ end
982
+
983
+ end
984
+ end
985
+ end