simple_record 2.1.3 → 2.1.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  An ActiveRecord interface for SimpleDB. Can be used as a drop in replacement for ActiveRecord in rails.
4
4
 
5
- Brought to you by: [![Appoxy](http://www.simpledeployr.com/images/global/appoxy-small.png)](http://www.appoxy.com)
5
+ Brought to you by: [![Appoxy](https://lh5.googleusercontent.com/_-J9DSaseOX8/TX2Bq564w-I/AAAAAAAAxYU/xjeReyoxa8o/s800/appoxy-small%20%282%29.png)](http://www.appoxy.com)
6
6
 
7
7
  ## Discussion Group
8
8
 
@@ -261,6 +261,15 @@ all the clobs on the object.
261
261
 
262
262
  Setting this will automatically use :s3_bucket=>:new as well.
263
263
 
264
+ ## JSON Support
265
+
266
+ You can easily marshal and unmarshal SimpleRecord objects and results by calling `to_json` on them. The default
267
+ serialization will include a `json_class` value which will be used when deserializing to find the class. If you're using
268
+ the results in an API though, you may not want to include json_class because the receiving end may not have that class
269
+ around, so you can pass in `:exclude_json_class` option to to_json, eg:
270
+
271
+ my_ob.to_json(:exclude_json_class=>true)
272
+
264
273
  ## Tips and Tricks and Things to Know
265
274
 
266
275
  ### Automagic Stuff
@@ -366,10 +375,22 @@ The :map function should return which shard name the object should be stored to.
366
375
  When executing a find() operation, you can explicitly specify the shard(s) you'd like to find on. This is
367
376
  particularly useful if you know in advance which shard the data will be in.
368
377
 
369
- MyClass.find(:all, :conditions=>....., :shard=>["CA", "FL"])
378
+ MyShardedClass.find(:all, :conditions=>....., :shard=>["CA", "FL"])
370
379
 
371
380
  You can see some [example classes here](https://github.com/appoxy/simple_record/blob/master/test/my_sharded_model.rb).
372
381
 
382
+ ## Concurrency
383
+
384
+ **Subject to change**
385
+
386
+ This was brought on as a way to query across shards in parallel. Not being able to find a good generic concurrency library,
387
+ I ended up rolling my own called [concur](https://github.com/appoxy/concur).
388
+
389
+ MyShardedClass.find(:all, :concurrent=>true)
390
+
391
+ We may enable a global [Executor](https://github.com/appoxy/concur/blob/master/lib/executor.rb) so you can have a fixed
392
+ thread pool across your app, but for now, it will fire up a thread per shard.
393
+
373
394
  ## Kudos
374
395
 
375
396
  Special thanks to Garrett Cox for creating Activerecord2sdb which SimpleRecord is based on:
data/lib/simple_record.rb CHANGED
@@ -51,20 +51,20 @@ require_relative 'simple_record/sharding'
51
51
 
52
52
  module SimpleRecord
53
53
 
54
- @@options = {}
55
- @@stats = SimpleRecord::Stats.new
56
- @@logging = false
57
- @@s3 = nil
54
+ @@options = {}
55
+ @@stats = SimpleRecord::Stats.new
56
+ @@logging = false
57
+ @@s3 = nil
58
58
  @@auto_close_s3 = false
59
- @@logger = Logger.new(STDOUT)
60
- @@logger.level = Logger::INFO
59
+ @@logger = Logger.new(STDOUT)
60
+ @@logger.level = Logger::INFO
61
61
 
62
62
  class << self;
63
63
  attr_accessor :aws_access_key, :aws_secret_key
64
64
 
65
65
  # Deprecated
66
66
  def enable_logging
67
- @@logging = true
67
+ @@logging = true
68
68
  @@logger.level = Logger::DEBUG
69
69
  end
70
70
 
@@ -135,7 +135,7 @@ module SimpleRecord
135
135
  # todo: should we init this only when needed?
136
136
  end
137
137
  s3_ops = {:connection_mode=>options[:connection_mode] || :default}
138
- @@s3 = Aws::S3.new(SimpleRecord.aws_access_key, SimpleRecord.aws_secret_key, s3_ops)
138
+ @@s3 = Aws::S3.new(SimpleRecord.aws_access_key, SimpleRecord.aws_secret_key, s3_ops)
139
139
 
140
140
  if options[:created_col]
141
141
  SimpleRecord::Base.has_dates options[:created_col]
@@ -230,12 +230,12 @@ module SimpleRecord
230
230
 
231
231
  clear_errors
232
232
 
233
- @dirty = {}
233
+ @dirty = {}
234
234
 
235
- @attributes = {} # sdb values
235
+ @attributes = {} # sdb values
236
236
  @attributes_rb = {} # ruby values
237
- @lobs = {}
238
- @new_record = true
237
+ @lobs = {}
238
+ @new_record = true
239
239
 
240
240
  end
241
241
 
@@ -333,7 +333,7 @@ module SimpleRecord
333
333
  end
334
334
 
335
335
  def get_att_meta(name)
336
- name_s = name.to_s
336
+ name_s = name.to_s
337
337
  att_meta = defined_attributes_local[name.to_sym]
338
338
  if att_meta.nil? && has_id_on_end(name_s)
339
339
  att_meta = defined_attributes_local[name_s[0..-4].to_sym]
@@ -365,7 +365,7 @@ module SimpleRecord
365
365
 
366
366
  def make_dirty(arg, value)
367
367
  sdb_att_name = sdb_att_name(arg)
368
- arg = arg.to_s
368
+ arg = arg.to_s
369
369
 
370
370
  # puts "Marking #{arg} dirty with #{value}" if SimpleRecord.logging?
371
371
  if @dirty.include?(sdb_att_name)
@@ -469,7 +469,7 @@ module SimpleRecord
469
469
  begin
470
470
  is_create = new_record? # self[:id].nil?
471
471
 
472
- dirty = @dirty
472
+ dirty = @dirty
473
473
  # puts 'dirty before=' + @dirty.inspect
474
474
  if options[:dirty]
475
475
  # puts '@dirty=' + @dirty.inspect
@@ -505,7 +505,7 @@ module SimpleRecord
505
505
  _run_save_callbacks do
506
506
  result = new_record? ? create(options) : update(options)
507
507
  # puts 'save_callbacks result=' + result.inspect
508
- ret = result
508
+ ret = result
509
509
  end
510
510
  ret
511
511
  end
@@ -514,7 +514,7 @@ module SimpleRecord
514
514
  puts '3 create'
515
515
  ret = true
516
516
  _run_create_callbacks do
517
- x = do_actual_save(options)
517
+ x = do_actual_save(options)
518
518
  # puts 'create old_save result=' + x.to_s
519
519
  ret = x
520
520
  end
@@ -526,7 +526,7 @@ module SimpleRecord
526
526
  puts '3 update'
527
527
  ret = true
528
528
  _run_update_callbacks do
529
- x = do_actual_save(options)
529
+ x = do_actual_save(options)
530
530
  # puts 'update old_save result=' + x.to_s
531
531
  ret = x
532
532
  end
@@ -580,12 +580,12 @@ module SimpleRecord
580
580
  def save_lobs(dirty=nil)
581
581
  # puts 'dirty.inspect=' + dirty.inspect
582
582
  dirty = @dirty if dirty.nil?
583
- all_clobs = {}
583
+ all_clobs = {}
584
584
  dirty_clobs = {}
585
585
  defined_attributes_local.each_pair do |k, v|
586
586
  # collect up the clobs in case it's a single put
587
587
  if v.type == :clob
588
- val = @lobs[k]
588
+ val = @lobs[k]
589
589
  all_clobs[k] = val
590
590
  if dirty.include?(k.to_s)
591
591
  dirty_clobs[k] = val
@@ -700,7 +700,7 @@ module SimpleRecord
700
700
 
701
701
  # puts '@@active_model ? ' + @@active_model.inspect
702
702
 
703
- ok = true
703
+ ok = true
704
704
  is_create = self[:id].nil?
705
705
  unless @@active_model
706
706
  ok = run_before_validation && (is_create ? run_before_validation_on_create : run_before_validation_on_update)
@@ -810,10 +810,10 @@ module SimpleRecord
810
810
  end
811
811
 
812
812
  # Pass in the same OPTIONS you'd pass into a find(:all, OPTIONS)
813
- def self.delete_all(options)
813
+ def self.delete_all(options={})
814
814
  # could make this quicker by just getting item_names and deleting attributes rather than creating objects
815
815
  obs = self.find(:all, options)
816
- i = 0
816
+ i = 0
817
817
  obs.each do |a|
818
818
  a.delete
819
819
  i+=1
@@ -822,9 +822,9 @@ module SimpleRecord
822
822
  end
823
823
 
824
824
  # Pass in the same OPTIONS you'd pass into a find(:all, OPTIONS)
825
- def self.destroy_all(options)
825
+ def self.destroy_all(options={})
826
826
  obs = self.find(:all, options)
827
- i = 0
827
+ i = 0
828
828
  obs.each do |a|
829
829
  a.destroy
830
830
  i+=1
@@ -921,7 +921,7 @@ module SimpleRecord
921
921
  def self.find(*params)
922
922
  #puts 'params=' + params.inspect
923
923
 
924
- q_type = :all
924
+ q_type = :all
925
925
  select_attributes=[]
926
926
  if params.size > 0
927
927
  q_type = params[0]
@@ -932,7 +932,7 @@ module SimpleRecord
932
932
  end
933
933
  conditions = options[:conditions]
934
934
  if conditions && conditions.is_a?(String)
935
- conditions = [conditions]
935
+ conditions = [conditions]
936
936
  options[:conditions] = conditions
937
937
  end
938
938
 
@@ -948,13 +948,13 @@ module SimpleRecord
948
948
  #puts 'options=' + options.inspect
949
949
  #puts 'after collect=' + options.inspect
950
950
  convert_condition_params(options)
951
- per_token = options[:per_token]
951
+ per_token = options[:per_token]
952
952
  consistent_read = options[:consistent_read]
953
953
  if per_token || consistent_read then
954
- op_dup = options.dup
955
- op_dup[:limit] = per_token # simpledb uses Limit as a paging thing, not what is normal
954
+ op_dup = options.dup
955
+ op_dup[:limit] = per_token # simpledb uses Limit as a paging thing, not what is normal
956
956
  op_dup[:consistent_read] = consistent_read
957
- params_dup[1] = op_dup
957
+ params_dup[1] = op_dup
958
958
  end
959
959
  end
960
960
  # puts 'params2=' + params.inspect
@@ -962,7 +962,7 @@ module SimpleRecord
962
962
  ret = q_type == :all ? [] : nil
963
963
  begin
964
964
  results=find_with_metadata(*params_dup)
965
- # puts "RESULT=" + results.inspect
965
+ puts "RESULT=" + results.inspect
966
966
  write_usage(:select, domain, q_type, options, results)
967
967
  #puts 'params3=' + params.inspect
968
968
  SimpleRecord.stats.selects += 1
@@ -972,22 +972,25 @@ module SimpleRecord
972
972
  ret = results[:items].first
973
973
  # todo: we should store request_id and box_usage with the object maybe?
974
974
  cache_results(ret)
975
- elsif results[:single]
975
+ elsif results[:single_only]
976
976
  ret = results[:single]
977
+ puts 'results[:single] ' + ret.inspect
977
978
  cache_results(ret)
978
979
  else
980
+ puts 'last step items = ' + results.inspect
979
981
  if results[:items] #.is_a?(Array)
980
982
  cache_results(results[:items])
981
983
  ret = SimpleRecord::ResultsArray.new(self, params, results, next_token)
982
984
  end
983
985
  end
984
986
  rescue Aws::AwsError, SimpleRecord::ActiveSdb::ActiveSdbError => ex
985
- # puts "RESCUED: " + ex.message
987
+ puts "RESCUED: " + ex.message
986
988
  if (ex.message().index("NoSuchDomain") != nil)
987
989
  # this is ok
988
- elsif (ex.message() =~ @@regex_no_id)
989
- ret = nil
990
+ # elsif (ex.message() =~ @@regex_no_id) This is RecordNotFound now
991
+ # ret = nil
990
992
  else
993
+ puts 're-raising'
991
994
  raise ex
992
995
  end
993
996
  end
@@ -1016,14 +1019,14 @@ module SimpleRecord
1016
1019
  def self.paginate(options={})
1017
1020
  # options = args.pop
1018
1021
  # puts 'paginate options=' + options.inspect if SimpleRecord.logging?
1019
- page = options[:page] || 1
1020
- per_page = options[:per_page] || 50
1022
+ page = options[:page] || 1
1023
+ per_page = options[:per_page] || 50
1021
1024
  # total = options[:total_entries].to_i
1022
- options[:page] = page.to_i # makes sure it's to_i
1025
+ options[:page] = page.to_i # makes sure it's to_i
1023
1026
  options[:per_page] = per_page.to_i
1024
- options[:limit] = options[:page] * options[:per_page]
1027
+ options[:limit] = options[:page] * options[:per_page]
1025
1028
  # puts 'paging options=' + options.inspect
1026
- fr = find(:all, options)
1029
+ fr = find(:all, options)
1027
1030
  return fr
1028
1031
 
1029
1032
  end
@@ -1048,15 +1051,15 @@ module SimpleRecord
1048
1051
  # todo: cache each result
1049
1052
  results.each do |item|
1050
1053
  class_name = item.class.name
1051
- id = item.id
1052
- cache_key = self.cache_key(class_name, id)
1054
+ id = item.id
1055
+ cache_key = self.cache_key(class_name, id)
1053
1056
  #puts 'caching result at ' + cache_key + ': ' + results.inspect
1054
1057
  cache_store.write(cache_key, item, :expires_in =>30)
1055
1058
  end
1056
1059
  else
1057
1060
  class_name = results.class.name
1058
- id = results.id
1059
- cache_key = self.cache_key(class_name, id)
1061
+ id = results.id
1062
+ cache_key = self.cache_key(class_name, id)
1060
1063
  #puts 'caching result at ' + cache_key + ': ' + results.inspect
1061
1064
  cache_store.write(cache_key, results, :expires_in =>30)
1062
1065
  end
@@ -1111,7 +1114,7 @@ module SimpleRecord
1111
1114
 
1112
1115
  class Activerecordtosdb_subrecord_array
1113
1116
  def initialize(subname, referencename, referencevalue)
1114
- @subname =subname.classify
1117
+ @subname =subname.classify
1115
1118
  @referencename =referencename.tableize.singularize + "_id"
1116
1119
  @referencevalue=referencevalue
1117
1120
  end
@@ -1168,7 +1171,7 @@ module SimpleRecord
1168
1171
 
1169
1172
  def create(*params)
1170
1173
  params[0][@referencename]=@referencevalue
1171
- record = eval(@subname).new(*params)
1174
+ record = eval(@subname).new(*params)
1172
1175
  record.save
1173
1176
  end
1174
1177
 
@@ -22,989 +22,992 @@
22
22
  #
23
23
 
24
24
  begin
25
- require 'uuidtools'
25
+ require 'uuidtools'
26
26
  rescue LoadError => e
27
- STDERR.puts("SimpleRecord requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
28
- exit
27
+ STDERR.puts("SimpleRecord requires the uuidtools gem. Run \'gem install uuidtools\' and try again.")
28
+ exit
29
29
  end
30
30
 
31
31
  module SimpleRecord
32
32
 
33
- # = Aws::ActiveSdb -- RightScale SDB interface (alpha release)
34
- # The Aws::ActiveSdb class provides a complete interface to Amazon's Simple
35
- # Database Service.
36
- #
37
- # 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:
38
- #
39
- # require 'right_aws'
40
- # require 'sdb/active_sdb'
41
- #
42
- # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by Aws and is not installed as a
43
- # dependency of Aws.
44
- #
45
- # Simple ActiveSdb usage example:
46
- #
47
- # class Client < Aws::ActiveSdb::Base
48
- # end
49
- #
50
- # # connect to SDB
51
- # Aws::ActiveSdb.establish_connection
52
- #
53
- # # create domain
54
- # Client.create_domain
55
- #
56
- # # create initial DB
57
- # Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president'
58
- # Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president'
59
- # Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president'
60
- # Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping']
61
- # Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']
62
- # sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id
63
- #
64
- # # find all Bushes in USA
65
- # Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client|
66
- # client.reload
67
- # puts client.attributes.inspect
68
- # end
69
- #
70
- # # find all Maries through the world
71
- # Client.find_all_by_name_and_gender('Mary','female').each do |mary|
72
- # mary.reload
73
- # puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}"
74
- # end
75
- #
76
- # # find new russian president
77
- # medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012')
78
- # if medvedev
79
- # medvedev.reload
80
- # medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom')
81
- # end
82
- #
83
- # # retire old president
84
- # Client.find_by_name('Putin').delete
85
- #
86
- # # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' ()
87
- # sandy = Client.find(sandy_id)
88
- # sandy.reload
89
- # sandy.delete_values('hobby' => ['cooking'] )
90
- # sandy.delete_attributes('country', 'gender')
91
- #
92
- # # remove domain
93
- # Client.delete_domain
94
- #
95
- class ActiveSdb
96
-
97
- module ActiveSdbConnect
98
- def connection
99
- @connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
100
- end
33
+ # = Aws::ActiveSdb -- RightScale SDB interface (alpha release)
34
+ # The Aws::ActiveSdb class provides a complete interface to Amazon's Simple
35
+ # Database Service.
36
+ #
37
+ # 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:
38
+ #
39
+ # require 'right_aws'
40
+ # require 'sdb/active_sdb'
41
+ #
42
+ # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by Aws and is not installed as a
43
+ # dependency of Aws.
44
+ #
45
+ # Simple ActiveSdb usage example:
46
+ #
47
+ # class Client < Aws::ActiveSdb::Base
48
+ # end
49
+ #
50
+ # # connect to SDB
51
+ # Aws::ActiveSdb.establish_connection
52
+ #
53
+ # # create domain
54
+ # Client.create_domain
55
+ #
56
+ # # create initial DB
57
+ # Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president'
58
+ # Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president'
59
+ # Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president'
60
+ # Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping']
61
+ # Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']
62
+ # sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id
63
+ #
64
+ # # find all Bushes in USA
65
+ # Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client|
66
+ # client.reload
67
+ # puts client.attributes.inspect
68
+ # end
69
+ #
70
+ # # find all Maries through the world
71
+ # Client.find_all_by_name_and_gender('Mary','female').each do |mary|
72
+ # mary.reload
73
+ # puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}"
74
+ # end
75
+ #
76
+ # # find new russian president
77
+ # medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012')
78
+ # if medvedev
79
+ # medvedev.reload
80
+ # medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom')
81
+ # end
82
+ #
83
+ # # retire old president
84
+ # Client.find_by_name('Putin').delete
85
+ #
86
+ # # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' ()
87
+ # sandy = Client.find(sandy_id)
88
+ # sandy.reload
89
+ # sandy.delete_values('hobby' => ['cooking'] )
90
+ # sandy.delete_attributes('country', 'gender')
91
+ #
92
+ # # remove domain
93
+ # Client.delete_domain
94
+ #
95
+ class ActiveSdb
96
+
97
+ module ActiveSdbConnect
98
+ def connection
99
+ @connection || raise(ActiveSdbError.new('Connection to SDB is not established'))
100
+ end
101
+
102
+ # Create a new handle to an Sdb account. All handles share the same per process or per thread
103
+ # HTTP connection to Amazon Sdb. Each handle is for a specific account.
104
+ # The +params+ are passed through as-is to Aws::SdbInterface.new
105
+ # Params:
106
+ # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
107
+ # :port => 443 # Amazon service port: 80 or 443(default)
108
+ # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
109
+ # :signature_version => '2' # The signature version : '0', '1' or '2' (default)
110
+ # DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
111
+ # :connection_mode => :default # options are :default (will use best known option, may change in the future)
112
+ # :per_request (opens and closes a connection on every request to SDB)
113
+ # :single (same as old multi_thread=>false)
114
+ # :per_thread (same as old multi_thread=>true)
115
+ # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
116
+ # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
117
+ # :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')
118
+ # :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB
119
+
120
+ def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
121
+ @connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
122
+ end
123
+
124
+ def close_connection
125
+ @connection.close_connection unless @connection.nil?
126
+ end
127
+ end
101
128
 
102
- # Create a new handle to an Sdb account. All handles share the same per process or per thread
103
- # HTTP connection to Amazon Sdb. Each handle is for a specific account.
104
- # The +params+ are passed through as-is to Aws::SdbInterface.new
105
- # Params:
106
- # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default)
107
- # :port => 443 # Amazon service port: 80 or 443(default)
108
- # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default)
109
- # :signature_version => '2' # The signature version : '0', '1' or '2' (default)
110
- # DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default)
111
- # :connection_mode => :default # options are :default (will use best known option, may change in the future)
112
- # :per_request (opens and closes a connection on every request to SDB)
113
- # :single (same as old multi_thread=>false)
114
- # :per_thread (same as old multi_thread=>true)
115
- # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET)
116
- # :logger => Logger Object # Logger instance: logs to STDOUT if omitted
117
- # :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')
118
- # :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB
119
-
120
- def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={})
121
- @connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params)
122
- end
129
+ class ActiveSdbError < RuntimeError
130
+ end
123
131
 
124
- def close_connection
125
- @connection.close_connection unless @connection.nil?
126
- end
127
- end
132
+ class << self
133
+ include ActiveSdbConnect
134
+
135
+ # Retreive a list of domains.
136
+ #
137
+ # put Aws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
138
+ #
139
+ def domains
140
+ connection.list_domains[:domains]
141
+ end
142
+
143
+ # Create new domain.
144
+ # Raises no errors if the domain already exists.
145
+ #
146
+ # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
147
+ #
148
+ def create_domain(domain_name)
149
+ connection.create_domain(domain_name)
150
+ end
151
+
152
+ # Remove domain from SDB.
153
+ # Raises no errors if the domain does not exist.
154
+ #
155
+ # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
156
+ #
157
+ def delete_domain(domain_name)
158
+ connection.delete_domain(domain_name)
159
+ end
160
+ end
128
161
 
129
- class ActiveSdbError < RuntimeError
162
+ class Base
163
+
164
+ class << self
165
+ include ActiveSdbConnect
166
+
167
+ # next_token value returned by last find: is useful to continue finding
168
+ attr_accessor :next_token
169
+
170
+ # Returns a Aws::SdbInterface object
171
+ #
172
+ # class A < Aws::ActiveSdb::Base
173
+ # end
174
+ #
175
+ # class B < Aws::ActiveSdb::Base
176
+ # end
177
+ #
178
+ # class C < Aws::ActiveSdb::Base
179
+ # end
180
+ #
181
+ # Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
182
+ #
183
+ # C.establish_connection 'key_id_2', 'secret_key_2'
184
+ #
185
+ # # A and B uses the default connection, C - uses its own
186
+ # puts A.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
187
+ # puts B.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
188
+ # puts C.connection #=> #<Aws::SdbInterface:0xb76d6ca0>
189
+ #
190
+ def connection
191
+ @connection || ActiveSdb::connection
130
192
  end
131
193
 
132
- class << self
133
- include ActiveSdbConnect
134
-
135
- # Retreive a list of domains.
136
- #
137
- # put Aws::ActiveSdb.domains #=> ['co-workers','family','friends','clients']
138
- #
139
- def domains
140
- connection.list_domains[:domains]
141
- end
142
-
143
- # Create new domain.
144
- # Raises no errors if the domain already exists.
145
- #
146
- # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
147
- #
148
- def create_domain(domain_name)
149
- connection.create_domain(domain_name)
150
- end
151
-
152
- # Remove domain from SDB.
153
- # Raises no errors if the domain does not exist.
154
- #
155
- # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"}
156
- #
157
- def delete_domain(domain_name)
158
- connection.delete_domain(domain_name)
159
- end
194
+ @domain = nil
195
+
196
+ # Current domain name.
197
+ #
198
+ # # if 'ActiveSupport' is not loaded then class name converted to downcase
199
+ # class Client < Aws::ActiveSdb::Base
200
+ # end
201
+ # puts Client.domain #=> 'client'
202
+ #
203
+ # # if 'ActiveSupport' is loaded then class name being tableized
204
+ # require 'activesupport'
205
+ # class Client < Aws::ActiveSdb::Base
206
+ # end
207
+ # puts Client.domain #=> 'clients'
208
+ #
209
+ # # Explicit domain name definition
210
+ # class Client < Aws::ActiveSdb::Base
211
+ # set_domain_name :foreign_clients
212
+ # end
213
+ # puts Client.domain #=> 'foreign_clients'
214
+ #
215
+ def domain
216
+ unless @domain
217
+ if defined? ActiveSupport::CoreExtensions::String::Inflections
218
+ @domain = name.tableize
219
+ else
220
+ @domain = name.downcase
221
+ end
222
+ end
223
+ @domain
160
224
  end
161
225
 
162
- class Base
163
-
164
- class << self
165
- include ActiveSdbConnect
166
-
167
- # next_token value returned by last find: is useful to continue finding
168
- attr_accessor :next_token
169
-
170
- # Returns a Aws::SdbInterface object
171
- #
172
- # class A < Aws::ActiveSdb::Base
173
- # end
174
- #
175
- # class B < Aws::ActiveSdb::Base
176
- # end
177
- #
178
- # class C < Aws::ActiveSdb::Base
179
- # end
180
- #
181
- # Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1'
182
- #
183
- # C.establish_connection 'key_id_2', 'secret_key_2'
184
- #
185
- # # A and B uses the default connection, C - uses its own
186
- # puts A.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
187
- # puts B.connection #=> #<Aws::SdbInterface:0xb76d6d7c>
188
- # puts C.connection #=> #<Aws::SdbInterface:0xb76d6ca0>
189
- #
190
- def connection
191
- @connection || ActiveSdb::connection
192
- end
193
-
194
- @domain = nil
195
-
196
- # Current domain name.
197
- #
198
- # # if 'ActiveSupport' is not loaded then class name converted to downcase
199
- # class Client < Aws::ActiveSdb::Base
200
- # end
201
- # puts Client.domain #=> 'client'
202
- #
203
- # # if 'ActiveSupport' is loaded then class name being tableized
204
- # require 'activesupport'
205
- # class Client < Aws::ActiveSdb::Base
206
- # end
207
- # puts Client.domain #=> 'clients'
208
- #
209
- # # Explicit domain name definition
210
- # class Client < Aws::ActiveSdb::Base
211
- # set_domain_name :foreign_clients
212
- # end
213
- # puts Client.domain #=> 'foreign_clients'
214
- #
215
- def domain
216
- unless @domain
217
- if defined? ActiveSupport::CoreExtensions::String::Inflections
218
- @domain = name.tableize
219
- else
220
- @domain = name.downcase
221
- end
222
- end
223
- @domain
224
- end
225
-
226
- # Change the default domain name to user defined.
227
- #
228
- # class Client < Aws::ActiveSdb::Base
229
- # set_domain_name :foreign_clients
230
- # end
231
- #
232
- def set_domain_name(domain)
233
- @domain = domain.to_s
234
- end
235
-
236
- # Create domain at SDB.
237
- # Raises no errors if the domain already exists.
238
- #
239
- # class Client < Aws::ActiveSdb::Base
240
- # end
241
- # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
242
- #
243
- def create_domain(dom=nil)
244
- dom = domain if dom.nil?
245
- puts "Creating new SimpleDB Domain: " + dom
246
- connection.create_domain(dom)
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(dom=nil)
257
- dom = domain if dom.nil?
258
- puts "!!! DELETING SimpleDB Domain: " + dom
259
- connection.delete_domain(dom)
260
- end
261
-
262
- #
263
- # See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
264
- #
265
- def find(*args)
266
- options = args.last.is_a?(Hash) ? args.pop : {}
267
- case args.first
268
- when nil then
269
- raise "Invalid parameters passed to find: nil."
270
- when :all then
271
- sql_select(options)[:items]
272
- when :first then
273
- sql_select(options.merge(:limit => 1))[:items].first
274
- when :count then
275
- res = sql_select(options.merge(:count => true))[:count]
276
- res
277
- else
278
- res = select_from_ids(args, options)
279
- return res[:single] if res[:single]
280
- return res[:items]
281
- end
282
- end
283
-
284
- #
285
- # Same as find, but will return SimpleDB metadata like :request_id and :box_usage
286
- #
287
- def find_with_metadata(*args)
288
- options = args.last.is_a?(Hash) ? args.pop : {}
289
- case args.first
290
- when nil then
291
- raise "Invalid parameters passed to find: nil."
292
- when :all then
293
- sql_select(options)
294
- when :first then
295
- sql_select(options.merge(:limit => 1))
296
- when :count then
297
- res = sql_select(options.merge(:count => true))
298
- res
299
- else
300
- select_from_ids args, options
301
- end
302
- end
303
-
304
- # Perform a SQL-like select request.
305
- #
306
- # Single record:
307
- #
308
- # Client.select(:first)
309
- # Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
310
- # Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
311
- #
312
- # Bunch of records:
313
- #
314
- # Client.select(:all)
315
- # Client.select(:all, :limit => 10)
316
- # Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
317
- # Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
318
- #
319
- # Records by ids:
320
- #
321
- # Client.select('1')
322
- # Client.select('1234987b4583475347523948')
323
- # Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
324
- #
325
- # Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_...
326
- #
327
- # Client.select_by_name('Matias Rust')
328
- # Client.select_by_name_and_city('Putin','Moscow')
329
- # Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
330
- #
331
- # Client.select_all_by_author('G.Bush jr')
332
- # Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
333
- # Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
334
- #
335
- # Continue listing:
336
- #
337
- # # initial listing
338
- # Client.select(:all, :limit => 10)
339
- # # continue listing
340
- # begin
341
- # Client.select(:all, :limit => 10, :next_token => Client.next_token)
342
- # end while Client.next_token
343
- #
344
- # Sort oder:
345
- # If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
346
- #
347
- # Client.select(:all) # returns all records
348
- # Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
349
- # Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
350
- #
351
- # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
352
- #
353
- def select(*args)
354
- find(*args)
355
- end
356
-
357
- def generate_id # :nodoc:
358
- UUIDTools::UUID.timestamp_create().to_s
359
- end
360
-
361
- protected
362
-
363
- def logger
364
- SimpleRecord.logger
365
- end
366
- # Select
367
-
368
- def select_from_ids(args, options) # :nodoc:
369
- cond = []
370
- # detect amount of records requested
371
- bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
372
- # flatten ids
373
- args = args.to_a.flatten
374
- args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" }
375
- ids_cond = "(#{cond.join(' OR ')})"
376
- # user defined :conditions to string (if it was defined)
377
- options[:conditions] = build_conditions(options[:conditions])
378
- # join ids condition and user defined conditions
379
- options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
380
- #puts 'options=' + options.inspect
381
- result = sql_select(options)
382
- #puts 'select_from_ids result=' + result.inspect
383
- # if one record was requested then return it
384
- unless bunch_of_records_requested
385
- record = result[:items].first
386
- # railse if nothing was found
387
- raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
388
- result[:single] = record
389
- else
390
- # if a bunch of records was requested then return check that we found all of them
391
- # and return as an array
392
- unless args.size == result[:items].size
393
- # todo: might make sense to return the array but with nil values in the slots where an item wasn't found?
394
- id_list = args.map { |i| "'#{i}'" }.join(',')
395
- raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})")
396
- else
397
- result
398
- end
399
- end
400
- result
401
- end
402
-
403
- def sql_select(options) # :nodoc:
404
- count = options[:count] || false
405
- #puts 'count? ' + count.to_s
406
- @next_token = options[:next_token]
407
- @consistent_read = options[:consistent_read]
408
- select_expression = build_select(options)
409
- logger.debug 'SELECT=' + select_expression
410
- # request items
411
- query_result = self.connection.select(select_expression, options)
412
- # puts 'QR=' + query_result.inspect
413
- @next_token = query_result[:next_token]
414
- ret = {}
415
- if count
416
- ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i
417
- ret.merge!(query_result)
418
- return ret
419
- end
420
-
421
- items = query_result.delete(:items).map do |hash|
422
- id, attributes = hash.shift
423
- new_item = self.new()
424
- new_item.initialize_from_db(attributes.merge({'id' => id}))
425
- new_item.mark_as_old
426
- new_item
427
- end
428
- ret[:items] = items
429
- ret.merge!(query_result)
430
- ret
431
- end
432
-
433
- # select_by helpers
434
- def select_all_by_(format_str, args, options) # :nodoc:
435
- fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_')
436
- conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
437
- options[:conditions] = [conditions, *args]
438
- find(:all, options)
439
- end
440
-
441
- def select_by_(format_str, args, options) # :nodoc:
442
- options[:limit] = 1
443
- select_all_by_(format_str, args, options).first
444
- end
445
-
446
- # Query
447
-
448
- # Returns an array of query attributes.
449
- # Query_expression must be a well formated SDB query string:
450
- # query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
451
- def query_attributes(query_expression) # :nodoc:
452
- attrs = []
453
- array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
454
- until array.empty? do
455
- attrs << array.shift # skip it's value
456
- array.shift #
457
- end
458
- attrs
459
- end
460
-
461
- # Returns an array of [attribute_name, 'asc'|'desc']
462
- def sort_options(sort_string)
463
- sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
464
- [$1, ($2 || 'asc')]
465
- end
466
-
467
- # Perform a query request.
468
- #
469
- # Options
470
- # :query_expression nil | string | array
471
- # :max_number_of_items nil | integer
472
- # :next_token nil | string
473
- # :sort_option nil | string "name desc|asc"
474
- #
475
- def query(options) # :nodoc:
476
- @next_token = options[:next_token]
477
- @consistent_read = options[:consistent_read]
478
- query_expression = build_conditions(options[:query_expression])
479
- # add sort_options to the query_expression
480
- if options[:sort_option]
481
- sort_by, sort_order = sort_options(options[:sort_option])
482
- sort_query_expression = "['#{sort_by}' starts-with '']"
483
- sort_by_expression = " sort '#{sort_by}' #{sort_order}"
484
- # make query_expression to be a string (it may be null)
485
- query_expression = query_expression.to_s
486
- # quote from Amazon:
487
- # The sort attribute must be present in at least one of the predicates of the query expression.
488
- if query_expression.blank?
489
- query_expression = sort_query_expression
490
- elsif !query_attributes(query_expression).include?(sort_by)
491
- query_expression += " intersection #{sort_query_expression}"
492
- end
493
- query_expression += sort_by_expression
494
- end
495
- # request items
496
- query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token, @consistent_read)
497
- @next_token = query_result[:next_token]
498
- items = query_result[:items].map do |name|
499
- new_item = self.new('id' => name)
500
- new_item.mark_as_old
501
- reload_if_exists(record) if options[:auto_load]
502
- new_item
503
- end
504
- items
505
- end
506
-
507
- # reload a record unless it is nil
508
- def reload_if_exists(record) # :nodoc:
509
- record && record.reload
510
- end
511
-
512
- def reload_all_records(*list) # :nodoc:
513
- list.flatten.each { |record| reload_if_exists(record) }
514
- end
515
-
516
- def find_every(options) # :nodoc:
517
- records = query(:query_expression => options[:conditions],
518
- :max_number_of_items => options[:limit],
519
- :next_token => options[:next_token],
520
- :sort_option => options[:sort] || options[:order],
521
- :consistent_read => options[:consistent_read])
522
- options[:auto_load] ? reload_all_records(records) : records
523
- end
524
-
525
- def find_initial(options) # :nodoc:
526
- options[:limit] = 1
527
- record = find_every(options).first
528
- options[:auto_load] ? reload_all_records(record).first : record
529
- end
530
-
531
- def find_from_ids(args, options) # :nodoc:
532
- cond = []
533
- # detect amount of records requested
534
- bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
535
- # flatten ids
536
- args = args.to_a.flatten
537
- args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
538
- ids_cond = "[#{cond.join(' OR ')}]"
539
- # user defined :conditions to string (if it was defined)
540
- options[:conditions] = build_conditions(options[:conditions])
541
- # join ids condition and user defined conditions
542
- options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
543
- result = find_every(options)
544
- # if one record was requested then return it
545
- unless bunch_of_records_requested
546
- record = result.first
547
- # railse if nothing was found
548
- raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
549
- options[:auto_load] ? reload_all_records(record).first : record
550
- else
551
- # if a bunch of records was requested then return check that we found all of them
552
- # and return as an array
553
- unless args.size == result.size
554
- id_list = args.map { |i| "'#{i}'" }.join(',')
555
- raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
556
- else
557
- options[:auto_load] ? reload_all_records(result) : result
558
- end
559
- end
560
- end
561
-
562
- # find_by helpers
563
- def find_all_by_(format_str, args, options) # :nodoc:
564
- fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_')
565
- conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
566
- options[:conditions] = [conditions, *args]
567
- find(:all, options)
568
- end
569
-
570
- def find_by_(format_str, args, options) # :nodoc:
571
- options[:limit] = 1
572
- find_all_by_(format_str, args, options).first
573
- end
574
-
575
- # Misc
576
-
577
- def method_missing(method, *args) # :nodoc:
578
- if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
579
- # get rid of the find ones, only select now
580
- to_send_to = $1
581
- attributes = method.to_s[$1.length..method.to_s.length]
582
- # puts 'attributes=' + attributes
583
- if to_send_to[0...4] == "find"
584
- to_send_to = "select" + to_send_to[4..to_send_to.length]
585
- # puts 'CONVERTED ' + $1 + " to " + to_send_to
586
- end
587
-
588
- options = args.last.is_a?(Hash) ? args.pop : {}
589
- __send__(to_send_to, attributes, args, options)
590
- else
591
- super(method, *args)
592
- end
593
- end
594
-
595
- def build_select(options) # :nodoc:
596
- select = options[:select] || '*'
597
- select = options[:count] ? "count(*)" : select
598
- #puts 'select=' + select.to_s
599
- from = options[:from] || domain
600
- condition_fields = parse_condition_fields(options[:conditions])
601
- conditions = options[:conditions] ? "#{build_conditions(options[:conditions])}" : ''
602
- order = options[:order] ? " ORDER BY #{options[:order]}" : ''
603
- limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
604
- # mix sort by argument (it must present in response)
605
- unless order.blank?
606
- sort_by, sort_order = sort_options(options[:order])
607
- if condition_fields.nil? || !condition_fields.include?(sort_by)
608
- # conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
609
- conditions = (conditions.blank? ? "" : "(#{conditions}) AND ") << "(#{sort_by} IS NOT NULL)"
610
- else
611
- # puts 'skipping is not null on sort because already there.'
612
- end
613
-
614
- end
615
- conditions = conditions.blank? ? "" : " WHERE #{conditions}"
616
- # puts 'CONDITIONS=' + conditions
617
- "SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}"
618
- end
619
-
620
- def build_conditions(conditions) # :nodoc:
621
- case
622
- when conditions.is_a?(Array) then
623
- connection.query_expression_from_array(conditions)
624
- when conditions.is_a?(Hash) then
625
- connection.query_expression_from_hash(conditions)
626
- else
627
- conditions
628
- end
629
- end
630
-
631
- # This will currently return and's, or's and betweens. Doesn't hurt anything, but could remove.
632
- def parse_condition_fields(conditions)
633
- return nil unless conditions && conditions.present?
634
- rx = /\b(\w*)[\s|>=|<=|!=|=|>|<|like|between]/
635
- fields = conditions[0].scan(rx)
636
- # puts 'condition_fields = ' + fields.inspect
637
- fields.flatten
638
- end
226
+ # Change the default domain name to user defined.
227
+ #
228
+ # class Client < Aws::ActiveSdb::Base
229
+ # set_domain_name :foreign_clients
230
+ # end
231
+ #
232
+ def set_domain_name(domain)
233
+ @domain = domain.to_s
234
+ end
639
235
 
640
- end
236
+ # Create domain at SDB.
237
+ # Raises no errors if the domain already exists.
238
+ #
239
+ # class Client < Aws::ActiveSdb::Base
240
+ # end
241
+ # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"}
242
+ #
243
+ def create_domain(dom=nil)
244
+ dom = domain if dom.nil?
245
+ puts "Creating new SimpleDB Domain: " + dom
246
+ connection.create_domain(dom)
247
+ end
641
248
 
642
- public
643
-
644
- # instance attributes
645
- attr_accessor :attributes
646
-
647
- # item name
648
- attr_accessor :id
649
-
650
- # Create new Item instance.
651
- # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
652
- #
653
- # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
654
- # puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
655
- # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
656
- # puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
657
- #
658
- def initialize(attrs={})
659
- @attributes = uniq_values(attrs)
660
- @new_record = true
661
- end
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(dom=nil)
257
+ dom = domain if dom.nil?
258
+ puts "!!! DELETING SimpleDB Domain: " + dom
259
+ connection.delete_domain(dom)
260
+ end
662
261
 
663
- # This is to separate initialization from user vs coming from db (ie: find())
664
- def initialize_from_db(attrs={})
665
- initialize(attrs)
666
- end
262
+ #
263
+ # See select(), original find with QUERY syntax is deprecated so now find and select are synonyms.
264
+ #
265
+ def find(*args)
266
+ options = args.last.is_a?(Hash) ? args.pop : {}
267
+ case args.first
268
+ when nil then
269
+ raise "Invalid parameters passed to find: nil."
270
+ when :all then
271
+ sql_select(options)[:items]
272
+ when :first then
273
+ sql_select(options.merge(:limit => 1))[:items].first
274
+ when :count then
275
+ res = sql_select(options.merge(:count => true))[:count]
276
+ res
277
+ else
278
+ res = select_from_ids(args, options)
279
+ return res[:single] if res[:single]
280
+ return res[:items]
281
+ end
282
+ end
667
283
 
668
- # Create and save new Item instance.
669
- # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
670
- #
671
- # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
672
- # puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
673
- #
674
- def self.create(attributes={})
675
- item = self.new(attributes)
676
- item.save
677
- item
678
- end
284
+ #
285
+ # Same as find, but will return SimpleDB metadata like :request_id and :box_usage
286
+ #
287
+ def find_with_metadata(*args)
288
+ options = args.last.is_a?(Hash) ? args.pop : {}
289
+ case args.first
290
+ when nil then
291
+ raise "Invalid parameters passed to find: nil."
292
+ when :all then
293
+ sql_select(options)
294
+ when :first then
295
+ sql_select(options.merge(:limit => 1))
296
+ when :count then
297
+ res = sql_select(options.merge(:count => true))
298
+ res
299
+ else
300
+ select_from_ids args, options
301
+ end
302
+ end
679
303
 
680
- # Returns an item id. Same as: item['id'] or item.attributes['id']
681
- def id
682
- @attributes['id']
683
- end
304
+ # Perform a SQL-like select request.
305
+ #
306
+ # Single record:
307
+ #
308
+ # Client.select(:first)
309
+ # Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"])
310
+ # Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends)
311
+ #
312
+ # Bunch of records:
313
+ #
314
+ # Client.select(:all)
315
+ # Client.select(:all, :limit => 10)
316
+ # Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"])
317
+ # Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3)
318
+ #
319
+ # Records by ids:
320
+ #
321
+ # Client.select('1')
322
+ # Client.select('1234987b4583475347523948')
323
+ # Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"])
324
+ #
325
+ # Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_...
326
+ #
327
+ # Client.select_by_name('Matias Rust')
328
+ # Client.select_by_name_and_city('Putin','Moscow')
329
+ # Client.select_by_name_and_city_and_post('Medvedev','Moscow','president')
330
+ #
331
+ # Client.select_all_by_author('G.Bush jr')
332
+ # Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian')
333
+ # Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name')
334
+ #
335
+ # Continue listing:
336
+ #
337
+ # # initial listing
338
+ # Client.select(:all, :limit => 10)
339
+ # # continue listing
340
+ # begin
341
+ # Client.select(:all, :limit => 10, :next_token => Client.next_token)
342
+ # end while Client.next_token
343
+ #
344
+ # Sort oder:
345
+ # If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null).
346
+ #
347
+ # Client.select(:all) # returns all records
348
+ # Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists
349
+ # Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists
350
+ #
351
+ # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html
352
+ #
353
+ def select(*args)
354
+ find(*args)
355
+ end
684
356
 
685
- # Sets an item id.
686
- def id=(id)
687
- @attributes['id'] = id.to_s
688
- end
357
+ def generate_id # :nodoc:
358
+ UUIDTools::UUID.timestamp_create().to_s
359
+ end
689
360
 
690
- # Returns a hash of all the attributes.
691
- #
692
- # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
693
- #
694
- def attributes
695
- @attributes.dup
696
- end
361
+ protected
697
362
 
698
- # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
699
- # if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
700
- #
701
- # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
702
- # # set new attributes ('id' is missed)
703
- # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
704
- # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
705
- # # set new attributes ('id' is set)
706
- # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
707
- # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
708
- #
709
- def attributes=(attrs)
710
- old_id = @attributes['id']
711
- @attributes = uniq_values(attrs)
712
- @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
713
- self.attributes
714
- end
363
+ def logger
364
+ SimpleRecord.logger
365
+ end
715
366
 
716
- def connection
717
- self.class.connection
718
- end
367
+ # Select
368
+
369
+ def select_from_ids(args, options) # :nodoc:
370
+ cond = []
371
+ # detect amount of records requested
372
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
373
+ # flatten ids
374
+ args = args.to_a.flatten
375
+ args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" }
376
+ ids_cond = "(#{cond.join(' OR ')})"
377
+ # user defined :conditions to string (if it was defined)
378
+ options[:conditions] = build_conditions(options[:conditions])
379
+ # join ids condition and user defined conditions
380
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}"
381
+ #puts 'options=' + options.inspect
382
+ result = sql_select(options)
383
+ puts 'select_from_ids result=' + result.inspect
384
+ # if one record was requested then return it
385
+ unless bunch_of_records_requested
386
+ result[:single_only] = true
387
+ record = result[:items].first
388
+ # railse if nothing was found
389
+ raise SimpleRecord::RecordNotFound.new("Couldn't find #{name} with ID #{args}") unless record || is_sharded?
390
+ result[:single] = record
391
+ else
392
+ # if a bunch of records was requested then return check that we found all of them
393
+ # and return as an array
394
+ puts 'is_sharded? ' + is_sharded?.to_s
395
+ unless is_sharded? || args.size == result[:items].size
396
+ # todo: might make sense to return the array but with nil values in the slots where an item wasn't found?
397
+ id_list = args.map { |i| "'#{i}'" }.join(',')
398
+ raise SimpleRecord::RecordNotFound.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})")
399
+ else
400
+ result
401
+ end
402
+ end
403
+ result
404
+ end
719
405
 
720
- # Item domain name.
721
- def domain
722
- self.class.domain
723
- end
406
+ def sql_select(options) # :nodoc:
407
+ count = options[:count] || false
408
+ #puts 'count? ' + count.to_s
409
+ @next_token = options[:next_token]
410
+ @consistent_read = options[:consistent_read]
411
+ select_expression = build_select(options)
412
+ logger.debug 'SELECT=' + select_expression
413
+ # request items
414
+ query_result = self.connection.select(select_expression, options)
415
+ # puts 'QR=' + query_result.inspect
416
+ @next_token = query_result[:next_token]
417
+ ret = {}
418
+ if count
419
+ ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i
420
+ ret.merge!(query_result)
421
+ return ret
422
+ end
423
+
424
+ items = query_result.delete(:items).map do |hash|
425
+ id, attributes = hash.shift
426
+ new_item = self.new()
427
+ new_item.initialize_from_db(attributes.merge({'id' => id}))
428
+ new_item.mark_as_old
429
+ new_item
430
+ end
431
+ ret[:items] = items
432
+ ret.merge!(query_result)
433
+ ret
434
+ end
724
435
 
725
- # Returns the values of the attribute identified by +attribute+.
726
- #
727
- # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
728
- #
729
- def [](attribute)
730
- @attributes[attribute.to_s]
731
- end
436
+ # select_by helpers
437
+ def select_all_by_(format_str, args, options) # :nodoc:
438
+ fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_')
439
+ conditions = fields.map { |field| "#{field}=?" }.join(' AND ')
440
+ options[:conditions] = [conditions, *args]
441
+ find(:all, options)
442
+ end
732
443
 
733
- # Updates the attribute identified by +attribute+ with the specified +values+.
734
- #
735
- # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
736
- # item['Cat'] = ["Whiskas", "chicken"]
737
- # puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
738
- #
739
- def []=(attribute, values)
740
- attribute = attribute.to_s
741
- @attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
444
+ def select_by_(format_str, args, options) # :nodoc:
445
+ options[:limit] = 1
446
+ select_all_by_(format_str, args, options).first
447
+ end
742
448
 
743
- end
449
+ # Query
450
+
451
+ # Returns an array of query attributes.
452
+ # Query_expression must be a well formated SDB query string:
453
+ # query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"]
454
+ def query_attributes(query_expression) # :nodoc:
455
+ attrs = []
456
+ array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten
457
+ until array.empty? do
458
+ attrs << array.shift # skip it's value
459
+ array.shift #
460
+ end
461
+ attrs
462
+ end
744
463
 
745
- # Reload attributes from SDB. Replaces in-memory attributes.
746
- #
747
- # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
748
- # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
749
- #
750
- def reload
751
- raise_on_id_absence
752
- old_id = id
753
- attrs = connection.get_attributes(domain, id)[:attributes]
754
- @attributes = {}
755
- unless attrs.blank?
756
- attrs.each { |attribute, values| @attributes[attribute] = values }
757
- @attributes['id'] = old_id
758
- end
759
- mark_as_old
760
- @attributes
761
- end
464
+ # Returns an array of [attribute_name, 'asc'|'desc']
465
+ def sort_options(sort_string)
466
+ sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i]
467
+ [$1, ($2 || 'asc')]
468
+ end
762
469
 
763
- # Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
764
- # +attrs_list+ is an array or comma separated list of attributes names.
765
- # Returns a hash of loaded attributes.
766
- #
767
- # This is not the best method to get a bunch of attributes because
768
- # a web service call is being performed for every attribute.
769
- #
770
- # item = Client.find_by_name('Cat')
771
- # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
772
- #
773
- def reload_attributes(*attrs_list)
774
- raise_on_id_absence
775
- attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
776
- attrs_list.delete('id')
777
- result = {}
778
- attrs_list.flatten.uniq.each do |attribute|
779
- attribute = attribute.to_s
780
- values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
781
- unless values.blank?
782
- @attributes[attribute] = result[attribute] = values
783
- else
784
- @attributes.delete(attribute)
785
- end
786
- end
787
- mark_as_old
788
- result
789
- end
470
+ # Perform a query request.
471
+ #
472
+ # Options
473
+ # :query_expression nil | string | array
474
+ # :max_number_of_items nil | integer
475
+ # :next_token nil | string
476
+ # :sort_option nil | string "name desc|asc"
477
+ #
478
+ def query(options) # :nodoc:
479
+ @next_token = options[:next_token]
480
+ @consistent_read = options[:consistent_read]
481
+ query_expression = build_conditions(options[:query_expression])
482
+ # add sort_options to the query_expression
483
+ if options[:sort_option]
484
+ sort_by, sort_order = sort_options(options[:sort_option])
485
+ sort_query_expression = "['#{sort_by}' starts-with '']"
486
+ sort_by_expression = " sort '#{sort_by}' #{sort_order}"
487
+ # make query_expression to be a string (it may be null)
488
+ query_expression = query_expression.to_s
489
+ # quote from Amazon:
490
+ # The sort attribute must be present in at least one of the predicates of the query expression.
491
+ if query_expression.blank?
492
+ query_expression = sort_query_expression
493
+ elsif !query_attributes(query_expression).include?(sort_by)
494
+ query_expression += " intersection #{sort_query_expression}"
495
+ end
496
+ query_expression += sort_by_expression
497
+ end
498
+ # request items
499
+ query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token, @consistent_read)
500
+ @next_token = query_result[:next_token]
501
+ items = query_result[:items].map do |name|
502
+ new_item = self.new('id' => name)
503
+ new_item.mark_as_old
504
+ reload_if_exists(record) if options[:auto_load]
505
+ new_item
506
+ end
507
+ items
508
+ end
790
509
 
791
- # Stores in-memory attributes to SDB.
792
- # Adds the attributes values to already stored at SDB.
793
- # Returns a hash of stored attributes.
794
- #
795
- # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
796
- # sandy['toys'] = 'boys'
797
- # sandy.put
798
- # sandy['toys'] = 'patchwork'
799
- # sandy.put
800
- # sandy['toys'] = 'kids'
801
- # sandy.put
802
- # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
803
- # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
804
- #
805
- # compare to +save+ method
806
- def put
807
- @attributes = uniq_values(@attributes)
808
- prepare_for_update
809
- attrs = @attributes.dup
810
- attrs.delete('id')
811
- connection.put_attributes(domain, id, attrs) unless attrs.blank?
812
- connection.put_attributes(domain, id, {'id' => id}, :replace)
813
- mark_as_old
814
- @attributes
815
- end
510
+ # reload a record unless it is nil
511
+ def reload_if_exists(record) # :nodoc:
512
+ record && record.reload
513
+ end
816
514
 
817
- # Stores specified attributes.
818
- # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
819
- # Returns a hash of saved attributes.
820
- #
821
- # see to +put+ method
822
- def put_attributes(attrs)
823
- attrs = uniq_values(attrs)
824
- prepare_for_update
825
- # if 'id' is present in attrs hash:
826
- # replace internal 'id' attribute and remove it from the attributes to be sent
827
- @attributes['id'] = attrs['id'] unless attrs['id'].blank?
828
- attrs.delete('id')
829
- # add new values to all attributes from list
830
- connection.put_attributes(domain, id, attrs) unless attrs.blank?
831
- connection.put_attributes(domain, id, {'id' => id}, :replace)
832
- attrs.each do |attribute, values|
833
- @attributes[attribute] ||= []
834
- @attributes[attribute] += values
835
- @attributes[attribute].uniq!
836
- end
837
- mark_as_old
838
- attributes
839
- end
515
+ def reload_all_records(*list) # :nodoc:
516
+ list.flatten.each { |record| reload_if_exists(record) }
517
+ end
840
518
 
841
- # Store in-memory attributes to SDB.
842
- # Replaces the attributes values already stored at SDB by in-memory data.
843
- # Returns a hash of stored attributes.
844
- #
845
- # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
846
- # sandy['toys'] = 'boys'
847
- # sandy.save
848
- # sandy['toys'] = 'patchwork'
849
- # sandy.save
850
- # sandy['toys'] = 'kids'
851
- # sandy.save
852
- # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
853
- # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
854
- #
855
- # Options:
856
- # - :except => Array of attributes to NOT save
857
- #
858
- # compare to +put+ method
859
- def save2(options={})
860
- options[:create_domain] = true if options[:create_domain].nil?
861
- pre_save2
862
- atts_to_save = @attributes.dup
863
- #puts 'atts_to_save=' + atts_to_save.inspect
864
- #options = params.first.is_a?(Hash) ? params.pop : {}
865
- if options[:except]
866
- options[:except].each do |e|
867
- atts_to_save.delete(e).inspect
868
- end
869
- end
870
- if options[:dirty] # Only used in simple_record right now
871
- # only save if the attribute is dirty
872
- dirty_atts = options[:dirty_atts]
873
- atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) }
874
- end
875
- dom = options[:domain] || domain
876
- #puts 'atts_to_save2=' + atts_to_save.inspect
877
- connection.put_attributes(dom, id, atts_to_save, :replace, options)
878
- apres_save2
879
- @attributes
880
- end
519
+ def find_every(options) # :nodoc:
520
+ records = query(:query_expression => options[:conditions],
521
+ :max_number_of_items => options[:limit],
522
+ :next_token => options[:next_token],
523
+ :sort_option => options[:sort] || options[:order],
524
+ :consistent_read => options[:consistent_read])
525
+ options[:auto_load] ? reload_all_records(records) : records
526
+ end
881
527
 
882
- def pre_save2
883
- @attributes = uniq_values(@attributes)
884
- prepare_for_update
885
- end
528
+ def find_initial(options) # :nodoc:
529
+ options[:limit] = 1
530
+ record = find_every(options).first
531
+ options[:auto_load] ? reload_all_records(record).first : record
532
+ end
886
533
 
887
- def apres_save2
888
- mark_as_old
889
- end
534
+ def find_from_ids(args, options) # :nodoc:
535
+ cond = []
536
+ # detect amount of records requested
537
+ bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array)
538
+ # flatten ids
539
+ args = args.to_a.flatten
540
+ args.each { |id| cond << "'id'=#{self.connection.escape(id)}" }
541
+ ids_cond = "[#{cond.join(' OR ')}]"
542
+ # user defined :conditions to string (if it was defined)
543
+ options[:conditions] = build_conditions(options[:conditions])
544
+ # join ids condition and user defined conditions
545
+ options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}"
546
+ result = find_every(options)
547
+ # if one record was requested then return it
548
+ unless bunch_of_records_requested
549
+ record = result.first
550
+ # railse if nothing was found
551
+ raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record
552
+ options[:auto_load] ? reload_all_records(record).first : record
553
+ else
554
+ # if a bunch of records was requested then return check that we found all of them
555
+ # and return as an array
556
+ unless args.size == result.size
557
+ id_list = args.map { |i| "'#{i}'" }.join(',')
558
+ raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})")
559
+ else
560
+ options[:auto_load] ? reload_all_records(result) : result
561
+ end
562
+ end
563
+ end
890
564
 
891
- # Replaces the attributes at SDB by the given values.
892
- # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
893
- # The other in-memory attributes are not being saved.
894
- # Returns a hash of stored attributes.
895
- #
896
- # see +save+ method
897
- def save_attributes(attrs)
898
- prepare_for_update
899
- attrs = uniq_values(attrs)
900
- # if 'id' is present in attrs hash then replace internal 'id' attribute
901
- unless attrs['id'].blank?
902
- @attributes['id'] = attrs['id']
903
- else
904
- attrs['id'] = id
905
- end
906
- connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
907
- attrs.each { |attribute, values| attrs[attribute] = values }
908
- mark_as_old
909
- attrs
910
- end
565
+ # find_by helpers
566
+ def find_all_by_(format_str, args, options) # :nodoc:
567
+ fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_')
568
+ conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ')
569
+ options[:conditions] = [conditions, *args]
570
+ find(:all, options)
571
+ end
911
572
 
912
- # Remove specified values from corresponding attributes.
913
- # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
914
- #
915
- # sandy = Client.find_by_name 'Sandy'
916
- # sandy.reload
917
- # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
918
- # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
919
- # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
920
- #
921
- def delete_values(attrs)
922
- raise_on_id_absence
923
- attrs = uniq_values(attrs)
924
- attrs.delete('id')
925
- unless attrs.blank?
926
- connection.delete_attributes(domain, id, attrs)
927
- attrs.each do |attribute, values|
928
- # remove the values from the attribute
929
- if @attributes[attribute]
930
- @attributes[attribute] -= values
931
- else
932
- # if the attribute is unknown remove it from a resulting list of fixed attributes
933
- attrs.delete(attribute)
934
- end
935
- end
936
- end
937
- attrs
938
- end
573
+ def find_by_(format_str, args, options) # :nodoc:
574
+ options[:limit] = 1
575
+ find_all_by_(format_str, args, options).first
576
+ end
939
577
 
940
- # Removes specified attributes from the item.
941
- # +attrs_list+ is an array or comma separated list of attributes names.
942
- # Returns the list of deleted attributes.
943
- #
944
- # sandy = Client.find_by_name 'Sandy'
945
- # sandy.reload
946
- # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
947
- # puts sandy.delete_attributes('toys') #=> ['toys']
948
- # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
949
- #
950
- def delete_attributes(*attrs_list)
951
- raise_on_id_absence
952
- attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
953
- attrs_list.delete('id')
954
- unless attrs_list.blank?
955
- connection.delete_attributes(domain, id, attrs_list)
956
- attrs_list.each { |attribute| @attributes.delete(attribute) }
957
- end
958
- attrs_list
959
- end
578
+ # Misc
960
579
 
961
- # Delete the Item entirely from SDB.
962
- #
963
- # sandy = Client.find_by_name 'Sandy'
964
- # sandy.reload
965
- # sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
966
- # puts sandy.delete
967
- # sandy.reload
968
- # puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
969
- #
970
- def delete(options={})
971
- raise_on_id_absence
972
- connection.delete_attributes(options[:domain] || domain, id)
580
+ def method_missing(method, *args) # :nodoc:
581
+ if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/]
582
+ # get rid of the find ones, only select now
583
+ to_send_to = $1
584
+ attributes = method.to_s[$1.length..method.to_s.length]
585
+ # puts 'attributes=' + attributes
586
+ if to_send_to[0...4] == "find"
587
+ to_send_to = "select" + to_send_to[4..to_send_to.length]
588
+ # puts 'CONVERTED ' + $1 + " to " + to_send_to
973
589
  end
974
590
 
975
- # Returns true if this object hasn�t been saved yet.
976
- def new_record?
977
- @new_record
978
- end
591
+ options = args.last.is_a?(Hash) ? args.pop : {}
592
+ __send__(to_send_to, attributes, args, options)
593
+ else
594
+ super(method, *args)
595
+ end
596
+ end
979
597
 
980
- def mark_as_old # :nodoc:
981
- @new_record = false
598
+ def build_select(options) # :nodoc:
599
+ select = options[:select] || '*'
600
+ select = options[:count] ? "count(*)" : select
601
+ #puts 'select=' + select.to_s
602
+ from = options[:from] || domain
603
+ condition_fields = parse_condition_fields(options[:conditions])
604
+ conditions = options[:conditions] ? "#{build_conditions(options[:conditions])}" : ''
605
+ order = options[:order] ? " ORDER BY #{options[:order]}" : ''
606
+ limit = options[:limit] ? " LIMIT #{options[:limit]}" : ''
607
+ # mix sort by argument (it must present in response)
608
+ unless order.blank?
609
+ sort_by, sort_order = sort_options(options[:order])
610
+ if condition_fields.nil? || !condition_fields.include?(sort_by)
611
+ # conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)"
612
+ conditions = (conditions.blank? ? "" : "(#{conditions}) AND ") << "(#{sort_by} IS NOT NULL)"
613
+ else
614
+ # puts 'skipping is not null on sort because already there.'
982
615
  end
983
616
 
984
- private
617
+ end
618
+ conditions = conditions.blank? ? "" : " WHERE #{conditions}"
619
+ # puts 'CONDITIONS=' + conditions
620
+ "SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}"
621
+ end
985
622
 
986
- def raise_on_id_absence
987
- raise ActiveSdbError.new('Unknown record id') unless id
988
- end
623
+ def build_conditions(conditions) # :nodoc:
624
+ case
625
+ when conditions.is_a?(Array) then
626
+ connection.query_expression_from_array(conditions)
627
+ when conditions.is_a?(Hash) then
628
+ connection.query_expression_from_hash(conditions)
629
+ else
630
+ conditions
631
+ end
632
+ end
989
633
 
990
- def prepare_for_update
991
- @attributes['id'] = self.class.generate_id if @attributes['id'].blank?
992
- end
634
+ # This will currently return and's, or's and betweens. Doesn't hurt anything, but could remove.
635
+ def parse_condition_fields(conditions)
636
+ return nil unless conditions && conditions.present?
637
+ rx = /\b(\w*)[\s|>=|<=|!=|=|>|<|like|between]/
638
+ fields = conditions[0].scan(rx)
639
+ # puts 'condition_fields = ' + fields.inspect
640
+ fields.flatten
641
+ end
993
642
 
994
- def uniq_values(attributes=nil) # :nodoc:
995
- attrs = {}
996
- attributes.each do |attribute, values|
997
- attribute = attribute.to_s
998
- newval = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
999
- attrs[attribute] = newval
1000
- if newval.blank?
643
+ end
644
+
645
+ public
646
+
647
+ # instance attributes
648
+ attr_accessor :attributes
649
+
650
+ # item name
651
+ attr_accessor :id
652
+
653
+ # Create new Item instance.
654
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
655
+ #
656
+ # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub'])
657
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=true, @attributes={"name"=>["Jon"], "toys"=>["girls", "beer", "pub"]}>
658
+ # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}
659
+ # puts item.inspect #=> #<Client:0xb77a2698 @new_record=false, @attributes={"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}>
660
+ #
661
+ def initialize(attrs={})
662
+ @attributes = uniq_values(attrs)
663
+ @new_record = true
664
+ end
665
+
666
+ # This is to separate initialization from user vs coming from db (ie: find())
667
+ def initialize_from_db(attrs={})
668
+ initialize(attrs)
669
+ end
670
+
671
+ # Create and save new Item instance.
672
+ # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
673
+ #
674
+ # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew'])
675
+ # puts item.inspect #=> #<Client:0xb77a0a78 @new_record=false, @attributes={"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}>
676
+ #
677
+ def self.create(attributes={})
678
+ item = self.new(attributes)
679
+ item.save
680
+ item
681
+ end
682
+
683
+ # Returns an item id. Same as: item['id'] or item.attributes['id']
684
+ def id
685
+ @attributes['id']
686
+ end
687
+
688
+ # Sets an item id.
689
+ def id=(id)
690
+ @attributes['id'] = id.to_s
691
+ end
692
+
693
+ # Returns a hash of all the attributes.
694
+ #
695
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
696
+ #
697
+ def attributes
698
+ @attributes.dup
699
+ end
700
+
701
+ # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names.
702
+ # if '+id+' attribute is not set in new attributes has then it being derived from old attributes.
703
+ #
704
+ # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]}
705
+ # # set new attributes ('id' is missed)
706
+ # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] }
707
+ # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]}
708
+ # # set new attributes ('id' is set)
709
+ # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] }
710
+ # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]}
711
+ #
712
+ def attributes=(attrs)
713
+ old_id = @attributes['id']
714
+ @attributes = uniq_values(attrs)
715
+ @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank?
716
+ self.attributes
717
+ end
718
+
719
+ def connection
720
+ self.class.connection
721
+ end
722
+
723
+ # Item domain name.
724
+ def domain
725
+ self.class.domain
726
+ end
727
+
728
+ # Returns the values of the attribute identified by +attribute+.
729
+ #
730
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
731
+ #
732
+ def [](attribute)
733
+ @attributes[attribute.to_s]
734
+ end
735
+
736
+ # Updates the attribute identified by +attribute+ with the specified +values+.
737
+ #
738
+ # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"]
739
+ # item['Cat'] = ["Whiskas", "chicken"]
740
+ # puts item['Cat'].inspect #=> ["Whiskas", "chicken"]
741
+ #
742
+ def []=(attribute, values)
743
+ attribute = attribute.to_s
744
+ @attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
745
+
746
+ end
747
+
748
+ # Reload attributes from SDB. Replaces in-memory attributes.
749
+ #
750
+ # item = Client.find_by_name('Cat') #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false>
751
+ # item.reload #=> #<Client:0xb77d0d40 @attributes={"id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false>
752
+ #
753
+ def reload
754
+ raise_on_id_absence
755
+ old_id = id
756
+ attrs = connection.get_attributes(domain, id)[:attributes]
757
+ @attributes = {}
758
+ unless attrs.blank?
759
+ attrs.each { |attribute, values| @attributes[attribute] = values }
760
+ @attributes['id'] = old_id
761
+ end
762
+ mark_as_old
763
+ @attributes
764
+ end
765
+
766
+ # Reload a set of attributes from SDB. Adds the loaded list to in-memory data.
767
+ # +attrs_list+ is an array or comma separated list of attributes names.
768
+ # Returns a hash of loaded attributes.
769
+ #
770
+ # This is not the best method to get a bunch of attributes because
771
+ # a web service call is being performed for every attribute.
772
+ #
773
+ # item = Client.find_by_name('Cat')
774
+ # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}
775
+ #
776
+ def reload_attributes(*attrs_list)
777
+ raise_on_id_absence
778
+ attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
779
+ attrs_list.delete('id')
780
+ result = {}
781
+ attrs_list.flatten.uniq.each do |attribute|
782
+ attribute = attribute.to_s
783
+ values = connection.get_attributes(domain, id, attribute)[:attributes][attribute]
784
+ unless values.blank?
785
+ @attributes[attribute] = result[attribute] = values
786
+ else
787
+ @attributes.delete(attribute)
788
+ end
789
+ end
790
+ mark_as_old
791
+ result
792
+ end
793
+
794
+ # Stores in-memory attributes to SDB.
795
+ # Adds the attributes values to already stored at SDB.
796
+ # Returns a hash of stored attributes.
797
+ #
798
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
799
+ # sandy['toys'] = 'boys'
800
+ # sandy.put
801
+ # sandy['toys'] = 'patchwork'
802
+ # sandy.put
803
+ # sandy['toys'] = 'kids'
804
+ # sandy.put
805
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
806
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}
807
+ #
808
+ # compare to +save+ method
809
+ def put
810
+ @attributes = uniq_values(@attributes)
811
+ prepare_for_update
812
+ attrs = @attributes.dup
813
+ attrs.delete('id')
814
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
815
+ connection.put_attributes(domain, id, {'id' => id}, :replace)
816
+ mark_as_old
817
+ @attributes
818
+ end
819
+
820
+ # Stores specified attributes.
821
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
822
+ # Returns a hash of saved attributes.
823
+ #
824
+ # see to +put+ method
825
+ def put_attributes(attrs)
826
+ attrs = uniq_values(attrs)
827
+ prepare_for_update
828
+ # if 'id' is present in attrs hash:
829
+ # replace internal 'id' attribute and remove it from the attributes to be sent
830
+ @attributes['id'] = attrs['id'] unless attrs['id'].blank?
831
+ attrs.delete('id')
832
+ # add new values to all attributes from list
833
+ connection.put_attributes(domain, id, attrs) unless attrs.blank?
834
+ connection.put_attributes(domain, id, {'id' => id}, :replace)
835
+ attrs.each do |attribute, values|
836
+ @attributes[attribute] ||= []
837
+ @attributes[attribute] += values
838
+ @attributes[attribute].uniq!
839
+ end
840
+ mark_as_old
841
+ attributes
842
+ end
843
+
844
+ # Store in-memory attributes to SDB.
845
+ # Replaces the attributes values already stored at SDB by in-memory data.
846
+ # Returns a hash of stored attributes.
847
+ #
848
+ # sandy = Client.new(:name => 'Sandy') #=> #<Client:0xb775a7a8 @attributes={"name"=>["Sandy"]}, @new_record=true>
849
+ # sandy['toys'] = 'boys'
850
+ # sandy.save
851
+ # sandy['toys'] = 'patchwork'
852
+ # sandy.save
853
+ # sandy['toys'] = 'kids'
854
+ # sandy.save
855
+ # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
856
+ # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]}
857
+ #
858
+ # Options:
859
+ # - :except => Array of attributes to NOT save
860
+ #
861
+ # compare to +put+ method
862
+ def save2(options={})
863
+ options[:create_domain] = true if options[:create_domain].nil?
864
+ pre_save2
865
+ atts_to_save = @attributes.dup
866
+ #puts 'atts_to_save=' + atts_to_save.inspect
867
+ #options = params.first.is_a?(Hash) ? params.pop : {}
868
+ if options[:except]
869
+ options[:except].each do |e|
870
+ atts_to_save.delete(e).inspect
871
+ end
872
+ end
873
+ if options[:dirty] # Only used in simple_record right now
874
+ # only save if the attribute is dirty
875
+ dirty_atts = options[:dirty_atts]
876
+ atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) }
877
+ end
878
+ dom = options[:domain] || domain
879
+ #puts 'atts_to_save2=' + atts_to_save.inspect
880
+ connection.put_attributes(dom, id, atts_to_save, :replace, options)
881
+ apres_save2
882
+ @attributes
883
+ end
884
+
885
+ def pre_save2
886
+ @attributes = uniq_values(@attributes)
887
+ prepare_for_update
888
+ end
889
+
890
+ def apres_save2
891
+ mark_as_old
892
+ end
893
+
894
+ # Replaces the attributes at SDB by the given values.
895
+ # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
896
+ # The other in-memory attributes are not being saved.
897
+ # Returns a hash of stored attributes.
898
+ #
899
+ # see +save+ method
900
+ def save_attributes(attrs)
901
+ prepare_for_update
902
+ attrs = uniq_values(attrs)
903
+ # if 'id' is present in attrs hash then replace internal 'id' attribute
904
+ unless attrs['id'].blank?
905
+ @attributes['id'] = attrs['id']
906
+ else
907
+ attrs['id'] = id
908
+ end
909
+ connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank?
910
+ attrs.each { |attribute, values| attrs[attribute] = values }
911
+ mark_as_old
912
+ attrs
913
+ end
914
+
915
+ # Remove specified values from corresponding attributes.
916
+ # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }.
917
+ #
918
+ # sandy = Client.find_by_name 'Sandy'
919
+ # sandy.reload
920
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}>
921
+ # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] }
922
+ # puts sandy.inspect #=> #<Client:0xb77b48fc @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}>
923
+ #
924
+ def delete_values(attrs)
925
+ raise_on_id_absence
926
+ attrs = uniq_values(attrs)
927
+ attrs.delete('id')
928
+ unless attrs.blank?
929
+ connection.delete_attributes(domain, id, attrs)
930
+ attrs.each do |attribute, values|
931
+ # remove the values from the attribute
932
+ if @attributes[attribute]
933
+ @attributes[attribute] -= values
934
+ else
935
+ # if the attribute is unknown remove it from a resulting list of fixed attributes
936
+ attrs.delete(attribute)
937
+ end
938
+ end
939
+ end
940
+ attrs
941
+ end
942
+
943
+ # Removes specified attributes from the item.
944
+ # +attrs_list+ is an array or comma separated list of attributes names.
945
+ # Returns the list of deleted attributes.
946
+ #
947
+ # sandy = Client.find_by_name 'Sandy'
948
+ # sandy.reload
949
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
950
+ # puts sandy.delete_attributes('toys') #=> ['toys']
951
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}>
952
+ #
953
+ def delete_attributes(*attrs_list)
954
+ raise_on_id_absence
955
+ attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s }
956
+ attrs_list.delete('id')
957
+ unless attrs_list.blank?
958
+ connection.delete_attributes(domain, id, attrs_list)
959
+ attrs_list.each { |attribute| @attributes.delete(attribute) }
960
+ end
961
+ attrs_list
962
+ end
963
+
964
+ # Delete the Item entirely from SDB.
965
+ #
966
+ # sandy = Client.find_by_name 'Sandy'
967
+ # sandy.reload
968
+ # sandy.inspect #=> #<Client:0xb7761d28 @new_record=false, @attributes={"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}>
969
+ # puts sandy.delete
970
+ # sandy.reload
971
+ # puts sandy.inspect #=> #<Client:0xb7761d28 @attributes={}, @new_record=false>
972
+ #
973
+ def delete(options={})
974
+ raise_on_id_absence
975
+ connection.delete_attributes(options[:domain] || domain, id)
976
+ end
977
+
978
+ # Returns true if this object hasn�t been saved yet.
979
+ def new_record?
980
+ @new_record
981
+ end
982
+
983
+ def mark_as_old # :nodoc:
984
+ @new_record = false
985
+ end
986
+
987
+ private
988
+
989
+ def raise_on_id_absence
990
+ raise ActiveSdbError.new('Unknown record id') unless id
991
+ end
992
+
993
+ def prepare_for_update
994
+ @attributes['id'] = self.class.generate_id if @attributes['id'].blank?
995
+ end
996
+
997
+ def uniq_values(attributes=nil) # :nodoc:
998
+ attrs = {}
999
+ attributes.each do |attribute, values|
1000
+ attribute = attribute.to_s
1001
+ newval = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values]
1002
+ attrs[attribute] = newval
1003
+ if newval.blank?
1001
1004
  # puts "VALUE IS BLANK " + attribute.to_s + " val=" + values.inspect
1002
- attrs.delete(attribute)
1003
- end
1004
- end
1005
- attrs
1006
- end
1007
-
1005
+ attrs.delete(attribute)
1006
+ end
1008
1007
  end
1008
+ attrs
1009
+ end
1010
+
1009
1011
  end
1012
+ end
1010
1013
  end