├── .gitignore ├── Gemfile ├── LICENSE.markdown ├── README.markdown ├── Rakefile ├── failing_tests.txt ├── lib ├── simple_record.rb └── simple_record │ ├── active_sdb.rb │ ├── attributes.rb │ ├── callbacks.rb │ ├── encryptor.rb │ ├── errors.rb │ ├── json.rb │ ├── logging.rb │ ├── password.rb │ ├── results_array.rb │ ├── sharding.rb │ ├── stats.rb │ ├── translations.rb │ └── validations.rb ├── rails └── init.rb ├── simple_record.gemspec └── test ├── models ├── model_with_enc.rb ├── my_base_model.rb ├── my_child_model.rb ├── my_model.rb ├── my_sharded_model.rb ├── my_simple_model.rb ├── my_translation.rb └── validated_model.rb ├── setup_test_config.bash ├── test_base.rb ├── test_conversions.rb ├── test_dirty.rb ├── test_encodings.rb ├── test_global_options.rb ├── test_helpers.rb ├── test_json.rb ├── test_lobs.rb ├── test_marshalled.rb ├── test_pagination.rb ├── test_rails3.rb ├── test_results_array.rb ├── test_shards.rb ├── test_simple_record.rb ├── test_temp.rb ├── test_translations.rb ├── test_usage.rb └── test_validations.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | pkg 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Declare your gem's dependencies in forms.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | group :development do 9 | gem 'jeweler', :git => 'git://github.com/technicalpickles/jeweler.git' 10 | gem 'pry' 11 | end 12 | group :test do 13 | gem 'test-unit' 14 | gem 'activemodel' 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE.markdown: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Travis Reeder, Appoxy LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # SimpleRecord - ActiveRecord for SimpleDB 2 | 3 | An ActiveRecord interface for SimpleDB. Can be used as a drop in replacement for ActiveRecord in rails. 4 | 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 | 7 | ## Discussion Group 8 | 9 | 10 | 11 | ## Getting Started 12 | 13 | 1. Install 14 | 15 | gem install simple_record 16 | 17 | 2. Create a model 18 | 19 | require 'simple_record' 20 | 21 | class MyModel < SimpleRecord::Base 22 | has_strings :name 23 | has_ints :age 24 | end 25 | 26 | More about ModelAttributes below. 27 | 28 | 3. Setup environment 29 | 30 | AWS_ACCESS_KEY_ID='XXXX' 31 | AWS_SECRET_ACCESS_KEY='YYYY' 32 | SimpleRecord.establish_connection(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) 33 | 34 | 4. Go to town 35 | 36 | # Store a model object to SimpleDB 37 | mm = MyModel.new 38 | mm.name = "Travis" 39 | mm.age = 32 40 | mm.save 41 | id = mm.id 42 | 43 | # Get an object from SimpleDB 44 | mm2 = MyModel.find(id) 45 | puts 'got=' + mm2.name + ' and he/she is ' + mm.age.to_s + ' years old' 46 | # Or more advanced queries? mms = MyModel?.find(:all, ["age=?", 32], :order=>"name", :limit=>10) 47 | 48 | That's literally all you need to do to get started. No database install, no other setup required. 49 | 50 | ## Attributes and modifiers for models 51 | 52 | NOTE: All objects will automatically have :id, :created, :updated attributes. 53 | 54 | ### has_strings 55 | 56 | Add string attributes. 57 | 58 | class MyModel < SimpleRecord::Base 59 | has_strings :name 60 | end 61 | 62 | ### has_ints, has_dates and has_booleans 63 | 64 | This is required because SimpleDB only has strings so SimpleRecord needs to know how to convert, pad, offset, etc. 65 | 66 | class MyModel < SimpleRecord::Base 67 | has_strings :name 68 | has_ints :age, :height 69 | has_dates :birthday 70 | has_booleans :is_nerd 71 | end 72 | 73 | ### Multi-value attributes 74 | 75 | SimpleDB supports having multiple values for the same attribute so to use this feature, simply set an attribute to an 76 | array of values. 77 | 78 | ### belongs_to 79 | 80 | Creates a many-to-one relationship. Can only have one per belongs_to call. 81 | 82 | class MyModel < SimpleRecord::Base 83 | belongs_to :school 84 | has_strings :name 85 | has_ints :age, :height 86 | has_dates :birthday 87 | has_booleans :is_nerd 88 | end 89 | 90 | Which requires another class called 'School' or you can specify the class explicitly with: 91 | 92 | belongs_to :school, :class_name => "Institution" 93 | 94 | ### set_table_name or set_domain_name 95 | 96 | If you want to use a custom domain for a model object, you can specify it with set_table_name (or set_domain_name). 97 | 98 | class SomeModel < SimpleRecord::Base 99 | set_table_name :different_model 100 | end 101 | 102 | 103 | ## Querying 104 | 105 | Querying is similar to ActiveRecord for the most part. 106 | 107 | To find all objects that match conditions returned in an Array: 108 | 109 | Company.find(:all, :conditions => ["created > ?", 10.days.ago], :order=>"name", :limit=>50) 110 | 111 | To find a single object: 112 | 113 | Company.find(:first, :conditions => ["name = ? AND division = ? AND created > ?", "Appoxy", "West", 10.days.ago ]) 114 | 115 | To count objects: 116 | 117 | Company.find(:count, :conditions => ["name = ? AND division = ? AND created > ?", "Appoxy", "West", 10.days.ago ]) 118 | 119 | You can also the dynamic method style, for instance the line below is the same as the Company.find(:first....) line above: 120 | 121 | Company.find_by_name_and_division("Appoxy", "West") 122 | 123 | To find all: 124 | 125 | Company.find_all_by_name_and_division("Appoxy", "West") 126 | 127 | Consistent read: 128 | 129 | Company.find(:all, :conditions => ["created > ?", 10.days.ago], :order=>"name", :limit=>50, :consistent_read=>true) 130 | 131 | There are so many different combinations of the above for querying that I can't put them all here, 132 | but this should get you started. 133 | 134 | You can get more ideas from here: http://api.rubyonrails.org/classes/ActiveRecord/Base.html. Not everything is supported 135 | but a lot is. 136 | 137 | ### Pagination 138 | 139 | SimpleRecord has paging built in and acts much like will_paginate: 140 | 141 | MyModel.paginate(:page=>2, :per_page=>30 [, the other normal query options like in find()]) 142 | 143 | That will return results 30 to 59. 144 | 145 | ## Configuration 146 | 147 | ### Domain Prefix 148 | 149 | To set a global prefix across all your models, use: 150 | 151 | SimpleRecord::Base.set_domain_prefix("myprefix_") 152 | 153 | ### Connection Modes 154 | 155 | There are 3 different connection modes: 156 | 157 | * per_request (default) - opens and closes a new connection to simpledb for every simpledb request. Not the best performance, but it's safe and can handle many concurrent requests at the same time (unlike single mode). 158 | * single - one connection across the entire application, not recommended unless the app is used by a single person. 159 | * per_thread - a connection is used for every thread in the application. This is good, but the catch is that you have to ensure to close the connection. 160 | 161 | You set the mode when you call establish_connection: 162 | 163 | SimpleRecord.establish_connection(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, :connection_mode=>:per_thread) 164 | 165 | We recommend per_thread with explicitly closing the connection after each Rails request (not to be mistaken for a SimpleDB request) or pool for rails apps. 166 | 167 | For rails, be sure to add this to your Application controller if using per_thread mode: 168 | 169 | after_filter :close_sdb_connection 170 | 171 | def close_sdb_connection 172 | SimpleRecord.close_connection 173 | end 174 | 175 | ### LOB and any other S3 Storage 176 | 177 | :s3_bucket=>... 178 | 179 | * :old (default) will use the existing lob location of "#{aws_access_key}_lobs", but any new features will use the :new bucket. 180 | * :new will use the new and recommended s3 bucket location of "simple_record_#{aws_access_key}". 181 | * Any string value will use that value as the bucket name. 182 | 183 | 184 | NOTE: All projects should set this as we may make this default in a future major version (v3?). Existing projects should use 185 | :s3_bucket=>: 186 | 187 | ### Created and Updated At Columns 188 | 189 | The default uses the columns "created" and "updated" which unfortunately are not the same as ActiveRecord, which 190 | uses created_at and updated_at. Although you can use created_at and updated_at methods, you may still want the columns in 191 | SimpleDB to be created_at and updated_at. 192 | 193 | :created_col=>"created", :updated_col=>"updated" 194 | 195 | NOTE: All projects should set these as we may make the "_at" postfix default in a future major version (v3?). Existing 196 | projects should set :created_col=>"created", :updated_col=>"updated" (default) so if it changes in a future version, 197 | there won't be any issues. 198 | 199 | ## SimpleRecord on Rails 200 | 201 | You don't really have to do anything except have your models extends SimpleRecord::Base instead of ActiveRecord::Base, but here are some tips you can use. 202 | 203 | ### Change Connection Mode 204 | 205 | Use per_thread connection mode and close the connection after each request. 206 | 207 | after_filter :close_sdb_connection 208 | 209 | def close_sdb_connection 210 | SimpleRecord.close_connection 211 | end 212 | 213 | ### Disable ActiveRecord so you don't have to setup another database 214 | 215 | #### Rails 2 216 | 217 | This is most helpful on windows so Rails doesn't need sqlite or mysql gems/drivers installed which are painful to install on windows. 218 | In environment.rb, add 'config.frameworks -= [ :active_record ]', so it should look something like: 219 | 220 | Rails::Initializer.run do |config| 221 | config.frameworks -= [ :active_record ] 222 | .... 223 | end 224 | 225 | #### Rails 3 226 | 227 | At the top of application.rb, comment out `require 'rails/all` and add the following: 228 | 229 | #require 'rails/all' 230 | %w( 231 | action_controller 232 | action_mailer 233 | active_resource 234 | rails/test_unit 235 | ).each do |framework| 236 | begin 237 | require "#{framework}/railtie" 238 | rescue LoadError 239 | end 240 | end 241 | 242 | This is the same as rails/all minus active_record. 243 | 244 | ## Large Objects (LOBS) 245 | 246 | Typical databases support BLOB's and/or CLOB's, but SimpleDB has a 1024 character per attribute maximum so larger 247 | values should be stored in S3. Fortunately SimpleRecord takes care of this for you by defining has_clobs for a large 248 | string value. There is no support for blobs yet. 249 | 250 | has_clobs :my_clob 251 | 252 | These clob values will be stored in s3 under a bucket named "#{aws_access_key}_lobs" 253 | OR "simple_record_#{aws_access_key}/lobs" if you set :s3_bucket=>:new in establish_connection (RECOMMENDED). 254 | 255 | If you have more than one clob on an object and if it makes sense for performance reasons, you can set a configuration option on the class to store all clobs 256 | as one item on s3 which means it will do a single put to s3 and a single get for all the clobs on the object. 257 | This would generally be good for somewhat small clob values or when you know you will always be accessing 258 | all the clobs on the object. 259 | 260 | sr_config :single_clob=>true 261 | 262 | Setting this will automatically use :s3_bucket=>:new as well. 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 | 273 | ## Tips and Tricks and Things to Know 274 | 275 | ### Automagic Stuff 276 | 277 | 278 | #### Automatic common fields 279 | 280 | Every object will automatically get the following attributes so you don't need to define them: 281 | 282 | * id - UUID string 283 | * created - set when first save 284 | * updated - set every time you save/update 285 | 286 | 287 | #### belongs_to foreign keys/IDs are accessible without touching the database 288 | 289 | If you had the following in your model: 290 | 291 | belongs_to :something 292 | 293 | Then in addition to being able to access the something object with: 294 | 295 | o.something 296 | 297 | or setting it with: 298 | 299 | o.something = someo 300 | 301 | You can also access the ID for something directly with: 302 | 303 | o.something_id 304 | 305 | or 306 | 307 | o.something_id = x 308 | 309 | Accessing the id can prevent a database call so if you only need the ID, then you 310 | should use this. 311 | 312 | ## Batch Save 313 | 314 | To do a batch save using SimpleDB's batch saving feature to improve performance, simply create your objects, add them to an array, then call: 315 | 316 | MyClass.batch_save(object_list) 317 | 318 | ## Batch Delete 319 | 320 | To do a batch delete using SimpleDB's batch delete feature to improve performance, simply create your objects, add them to an array, then call: 321 | 322 | MyClass.batch_delete(object_list or list_of_ids) 323 | 324 | ## Operations across a Query 325 | 326 | MyClass.delete_all(find_options) 327 | MyClass.destroy_all(find_options) 328 | 329 | find_options can include anything you'd add after a find(:all, find_options) including :conditions, :limit, etc. 330 | 331 | ## Caching 332 | 333 | You can use any cache that supports the ActiveSupport::Cache::Store interface. 334 | 335 | SimpleRecord::Base.cache_store = my_cache_store 336 | 337 | If you want a simple in memory cache store that supports max cache size and expiration, try: . 338 | You can also use memcached or http://www.quetzall.com/cloudcache. 339 | 340 | ## Encryption 341 | 342 | SimpleRecord has built in support for encrypting attributes with AES-256 encryption and one way hashing using SHA-512 (good for passwords). And it's easy to use. 343 | 344 | Here is an example of a model with an encrypted attribute and a hashed attribute. 345 | 346 | class ModelWithEnc < SimpleRecord::Base 347 | has_strings :name, 348 | {:name=>:ssn, :encrypted=>"simple_record_test_key"}, 349 | {:name=>:password, :hashed=>true} 350 | end 351 | 352 | The :encrypted option takes a key that you specify. The attribute can only be decrypted with the exact same key. 353 | 354 | The :hashed option is simply true/false. 355 | 356 | Encryption is generally transparent to you, SimpleRecord will store the encrypted value in the database and unencrypt it when you use it. 357 | 358 | Hashing is not quite as transparent as it cannot be converted back to it's original value, but you can do easy comparisons with it, for instance: 359 | 360 | ob2.password == "mypassword" 361 | 362 | This will actually be compared by hashing "mypassword" first. 363 | 364 | ## Sharding 365 | 366 | Sharding allows you to partition your data for a single class across multiple domains allowing increased write throughput, 367 | faster queries and more space (multiply your 10GB per domain limit). And it's very easy to implement with SimpleRecord. 368 | 369 | shard :shards=>:my_shards_function, :map=>:my_mapping_function 370 | 371 | The :shards function should return a list of shard names, for example: ['CA', 'FL', 'HI', ...] or [1,2,3,4,...] 372 | 373 | The :map function should return which shard name the object should be stored to. 374 | 375 | When executing a find() operation, you can explicitly specify the shard(s) you'd like to find on. This is 376 | particularly useful if you know in advance which shard the data will be in. 377 | 378 | MyShardedClass.find(:all, :conditions=>....., :shard=>["CA", "FL"]) 379 | 380 | You can see some [example classes here](https://github.com/appoxy/simple_record/blob/master/test/my_sharded_model.rb). 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 | 394 | ## Kudos 395 | 396 | Special thanks to Garrett Cox for creating Activerecord2sdb which SimpleRecord is based on: 397 | http://activrecord2sdb.rubyforge.org/ 398 | 399 | ## License 400 | 401 | SimpleRecord is released under the MIT License. 402 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | require 'rubygems' 4 | require './lib/simple_record.rb' 5 | require 'rake/testtask' 6 | 7 | Rake::TestTask.new do |t| 8 | t.libs << "test" 9 | t.test_files = FileList['test/test*.rb'] 10 | t.verbose = true 11 | end 12 | 13 | begin 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |gemspec| 16 | gemspec.name = "simple_record" 17 | gemspec.summary = "ActiveRecord like interface for Amazon SimpleDB. By http://www.appoxy.com" 18 | gemspec.email = "travis@appoxy.com" 19 | gemspec.homepage = "http://github.com/appoxy/simple_record/" 20 | gemspec.description = "ActiveRecord like interface for Amazon SimpleDB. Store, query, shard, etc. By http://www.appoxy.com" 21 | gemspec.authors = ["Travis Reeder", "Chad Arimura", "RightScale"] 22 | gemspec.files = FileList['lib/**/*.rb'] 23 | gemspec.add_dependency 'aws' 24 | gemspec.add_dependency 'concur' 25 | end 26 | Jeweler::GemcutterTasks.new 27 | rescue LoadError 28 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 29 | end 30 | 31 | # vim: syntax=Ruby 32 | -------------------------------------------------------------------------------- /failing_tests.txt: -------------------------------------------------------------------------------- 1 | Finished tests in 565.677035s, 0.1202 tests/s, 4.1030 assertions/s. 2 | 3 | 1) Failure: 4 | test_clobs(TestLobs) [/home/ubuntu/src/simple_record/test/test_lobs.rb:45]: 5 | puts is 1, should be 3. 6 | 7 | 2) Failure: 8 | test_single_clob(TestLobs) [/home/ubuntu/src/simple_record/test/test_lobs.rb:87]: 9 | puts is 1, should be 0. 10 | 11 | 68 tests, 2321 assertions, 2 failures, 0 errors, 0 skips 12 | -------------------------------------------------------------------------------- /lib/simple_record.rb: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # require 'simple_record' 3 | # 4 | # class MyModel < SimpleRecord::Base 5 | # 6 | # has_attributes :name, :age 7 | # are_ints :age 8 | # 9 | # end 10 | # 11 | # AWS_ACCESS_KEY_ID='XXXXX' 12 | # AWS_SECRET_ACCESS_KEY='YYYYY' 13 | # SimpleRecord.establish_connection(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) 14 | # 15 | ## Save an object 16 | # mm = MyModel.new 17 | # mm.name = "Travis" 18 | # mm.age = 32 19 | # mm.save 20 | # id = mm.id 21 | # # Get the object back 22 | # mm2 = MyModel.select(id) 23 | # puts 'got=' + mm2.name + ' and he/she is ' + mm.age.to_s + ' years old' 24 | # 25 | # Forked off old ActiveRecord2sdb library. 26 | 27 | require 'aws' 28 | require 'base64' 29 | require 'active_support' 30 | if ActiveSupport::VERSION::MAJOR >= 3 31 | # had to do this due to some bug: https://github.com/rails/rails/issues/12876 32 | # fix: https://github.com/railsmachine/shadow_puppet/pull/19/files 33 | require 'active_support/deprecation' 34 | require 'active_support/core_ext' 35 | end 36 | begin 37 | # comment out line below to test rails2 validations 38 | require 'active_model' 39 | rescue LoadError => ex 40 | puts "ActiveModel not available, falling back." 41 | end 42 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/validations") 43 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/attributes") 44 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/active_sdb") 45 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/callbacks") 46 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/encryptor") 47 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/errors") 48 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/json") 49 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/logging") 50 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/password") 51 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/results_array") 52 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/stats") 53 | require File.expand_path(File.dirname(__FILE__) + "/simple_record/translations") 54 | require_relative 'simple_record/sharding' 55 | 56 | 57 | module SimpleRecord 58 | 59 | @@options = {} 60 | @@stats = SimpleRecord::Stats.new 61 | @@logging = false 62 | @@s3 = nil 63 | @@auto_close_s3 = false 64 | @@logger = Logger.new(STDOUT) 65 | @@logger.level = Logger::INFO 66 | 67 | class << self; 68 | attr_accessor :aws_access_key, :aws_secret_key 69 | 70 | # Deprecated 71 | def enable_logging 72 | @@logging = true 73 | @@logger.level = Logger::DEBUG 74 | end 75 | 76 | # Deprecated 77 | def disable_logging 78 | @@logging = false 79 | end 80 | 81 | # Deprecated 82 | def logging? 83 | @@logging 84 | end 85 | 86 | def logger 87 | @@logger 88 | end 89 | 90 | # This can be used to log queries and what not to a file. 91 | # Params: 92 | # :select=>{:filename=>"file_to_write_to", :format=>"csv"} 93 | def log_usage(types={}) 94 | @usage_logging_options = {} unless @usage_logging_options 95 | return if types.nil? 96 | types.each_pair do |type, options| 97 | options[:lines_between_flushes] = 100 unless options[:lines_between_flushes] 98 | @usage_logging_options[type] = options 99 | end 100 | #puts 'SimpleRecord.usage_logging_options=' + SimpleRecord.usage_logging_options.inspect 101 | end 102 | 103 | def close_usage_log(type) 104 | return unless @usage_logging_options[type] 105 | @usage_logging_options[type][:file].close if @usage_logging_options[type][:file] 106 | # unless we remove it, it will keep trying to log these events 107 | # and will fail because the file is closed. 108 | @usage_logging_options.delete(type) 109 | end 110 | 111 | def usage_logging_options 112 | @usage_logging_options 113 | end 114 | 115 | def stats 116 | @@stats 117 | end 118 | 119 | 120 | # Create a new handle to an Sdb account. All handles share the same per process or per thread 121 | # HTTP connection to Amazon Sdb. Each handle is for a specific account. 122 | # The +params+ are passed through as-is to Aws::SdbInterface.new 123 | # Params: 124 | # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default) 125 | # :port => 443 # Amazon service port: 80(default) or 443 126 | # :protocol => 'https' # Amazon service protocol: 'http'(default) or 'https' 127 | # :signature_version => '0' # The signature version : '0' or '1'(default) 128 | # :connection_mode => :default # options are 129 | # :default (will use best known safe (as in won't need explicit close) option, may change in the future) 130 | # :per_request (opens and closes a connection on every request to SDB) 131 | # :single (one thread across entire app) 132 | # :per_thread (one connection per thread) 133 | # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET) 134 | # :logger => Logger Object # Logger instance: logs to STDOUT if omitted 135 | def establish_connection(aws_access_key=nil, aws_secret_key=nil, options={}) 136 | @aws_access_key = aws_access_key 137 | @aws_secret_key = aws_secret_key 138 | @@options.merge!(options) 139 | #puts 'SimpleRecord.establish_connection with options: ' + @@options.inspect 140 | SimpleRecord::ActiveSdb.establish_connection(aws_access_key, aws_secret_key, @@options) 141 | if options[:connection_mode] == :per_thread 142 | @@auto_close_s3 = true 143 | # todo: should we init this only when needed? 144 | end 145 | s3_ops = {:connection_mode=>options[:connection_mode] || :default} 146 | @@s3 = Aws::S3.new(SimpleRecord.aws_access_key, SimpleRecord.aws_secret_key, s3_ops) 147 | 148 | if options[:created_col] 149 | SimpleRecord::Base.has_dates options[:created_col] 150 | end 151 | if options[:updated_col] 152 | SimpleRecord::Base.has_dates options[:updated_col] 153 | end 154 | 155 | 156 | end 157 | 158 | # Call this to close the connection to SimpleDB. 159 | # If you're using this in Rails with per_thread connection mode, you should do this in 160 | # an after_filter for each request. 161 | def close_connection() 162 | SimpleRecord::ActiveSdb.close_connection 163 | @@s3.close_connection if @@auto_close_s3 164 | end 165 | 166 | # If you'd like to specify the s3 connection to use for LOBs, you can pass it in here. 167 | # We recommend that this connection matches the type of connection you're using for SimpleDB, 168 | # at least if you're using per_thread connection mode. 169 | def s3=(s3) 170 | @@s3 = s3 171 | end 172 | 173 | def s3 174 | @@s3 175 | end 176 | 177 | def options 178 | @@options 179 | end 180 | 181 | end 182 | 183 | class Base < SimpleRecord::ActiveSdb::Base 184 | 185 | 186 | # puts 'Is ActiveModel defined? ' + defined?(ActiveModel).inspect 187 | 188 | 189 | if defined?(ActiveModel) 190 | @@active_model = true 191 | extend ActiveModel::Naming 192 | include ActiveModel::Conversion 193 | include ActiveModel::Validations 194 | extend ActiveModel::Callbacks # for ActiveRecord like callbacks 195 | include ActiveModel::Validations::Callbacks 196 | define_model_callbacks :save, :create, :update, :destroy 197 | include SimpleRecord::Callbacks3 198 | alias_method :am_valid?, :valid? 199 | else 200 | @@active_model = false 201 | attr_accessor :errors 202 | include SimpleRecord::Callbacks 203 | end 204 | include SimpleRecord::Validations 205 | extend SimpleRecord::Validations::ClassMethods 206 | 207 | include SimpleRecord::Translations 208 | # include SimpleRecord::Attributes 209 | extend SimpleRecord::Attributes::ClassMethods 210 | include SimpleRecord::Attributes 211 | extend SimpleRecord::Sharding::ClassMethods 212 | include SimpleRecord::Sharding 213 | include SimpleRecord::Json 214 | include SimpleRecord::Logging 215 | extend SimpleRecord::Logging::ClassMethods 216 | 217 | def self.extended(base) 218 | 219 | end 220 | 221 | def initialize(attrs={}) 222 | # todo: Need to deal with objects passed in. iterate through belongs_to perhaps and if in attrs, set the objects id rather than the object itself 223 | 224 | initialize_base(attrs) 225 | 226 | # Convert attributes to sdb values 227 | attrs.each_pair do |name, value| 228 | set(name, value, true) 229 | end 230 | end 231 | 232 | def initialize_base(attrs={}) 233 | 234 | #we have to handle the virtuals. 235 | handle_virtuals(attrs) 236 | 237 | clear_errors 238 | 239 | @dirty = {} 240 | 241 | @attributes = {} # sdb values 242 | @attributes_rb = {} # ruby values 243 | @lobs = {} 244 | @new_record = true 245 | 246 | end 247 | 248 | def initialize_from_db(attrs={}) 249 | initialize_base(attrs) 250 | attrs.each_pair do |k, v| 251 | @attributes[k.to_s] = v 252 | end 253 | end 254 | 255 | 256 | def self.inherited(base) 257 | # puts 'SimpleRecord::Base is inherited by ' + base.inspect 258 | Callbacks.setup_callbacks(base) 259 | 260 | # base.has_strings :id 261 | base.has_dates :created, :updated 262 | base.before_create :set_created, :set_updated 263 | base.before_update :set_updated 264 | 265 | end 266 | 267 | 268 | def persisted? 269 | !@new_record && !destroyed? 270 | end 271 | 272 | def destroyed? 273 | @deleted 274 | end 275 | 276 | def defined_attributes_local 277 | # todo: store this somewhere so it doesn't keep going through this 278 | ret = self.class.defined_attributes 279 | ret.merge!(self.class.superclass.defined_attributes) if self.class.superclass.respond_to?(:defined_attributes) 280 | end 281 | 282 | 283 | class << self; 284 | attr_accessor :domain_prefix 285 | end 286 | 287 | #@domain_name_for_class = nil 288 | 289 | @@cache_store = nil 290 | # Set the cache to use 291 | def self.cache_store=(cache) 292 | @@cache_store = cache 293 | end 294 | 295 | def self.cache_store 296 | return @@cache_store 297 | end 298 | 299 | # If you want a domain prefix for all your models, set it here. 300 | def self.set_domain_prefix(prefix) 301 | #puts 'set_domain_prefix=' + prefix 302 | self.domain_prefix = prefix 303 | end 304 | 305 | # Same as set_table_name 306 | def self.set_table_name(table_name) 307 | set_domain_name table_name 308 | end 309 | 310 | # Sets the domain name for this class 311 | def self.set_domain_name(table_name) 312 | super 313 | end 314 | 315 | 316 | def domain 317 | self.class.domain 318 | end 319 | 320 | def self.domain 321 | unless @domain 322 | # This strips off the module if there is one. 323 | n2 = name.split('::').last || name 324 | # puts 'n2=' + n2 325 | if n2.respond_to?(:tableize) 326 | @domain = n2.tableize 327 | else 328 | @domain = n2.downcase 329 | end 330 | set_domain_name @domain 331 | end 332 | domain_name_for_class = (SimpleRecord::Base.domain_prefix || "") + @domain.to_s 333 | domain_name_for_class 334 | end 335 | 336 | def has_id_on_end(name_s) 337 | name_s = name_s.to_s 338 | name_s.length > 3 && name_s[-3..-1] == "_id" 339 | end 340 | 341 | def get_att_meta(name) 342 | name_s = name.to_s 343 | att_meta = defined_attributes_local[name.to_sym] 344 | if att_meta.nil? && has_id_on_end(name_s) 345 | att_meta = defined_attributes_local[name_s[0..-4].to_sym] 346 | end 347 | return att_meta 348 | end 349 | 350 | def sdb_att_name(name) 351 | att_meta = get_att_meta(name) 352 | if att_meta.type == :belongs_to && !has_id_on_end(name.to_s) 353 | return "#{name}_id" 354 | end 355 | name.to_s 356 | end 357 | 358 | def strip_array(arg) 359 | if arg.is_a? Array 360 | if arg.length==1 361 | ret = arg[0] 362 | else 363 | ret = arg 364 | end 365 | else 366 | ret = arg 367 | end 368 | return ret 369 | end 370 | 371 | 372 | def make_dirty(arg, value) 373 | sdb_att_name = sdb_att_name(arg) 374 | arg = arg.to_s 375 | 376 | # puts "Marking #{arg} dirty with #{value}" if SimpleRecord.logging? 377 | if @dirty.include?(sdb_att_name) 378 | old = @dirty[sdb_att_name] 379 | # puts "#{sdb_att_name} was already dirty #{old}" 380 | @dirty.delete(sdb_att_name) if value == old 381 | else 382 | old = get_attribute(arg) 383 | # puts "dirtifying #{sdb_att_name} old=#{old.inspect} to new=#{value.inspect}" if SimpleRecord.logging? 384 | @dirty[sdb_att_name] = old if value != old 385 | end 386 | end 387 | 388 | def clear_errors 389 | if defined?(ActiveModel) 390 | @errors = ActiveModel::Errors.new(self) 391 | else 392 | @errors=SimpleRecord_errors.new 393 | end 394 | end 395 | 396 | def []=(attribute, values) 397 | make_dirty(attribute, values) 398 | super 399 | end 400 | 401 | def [](attribute) 402 | super 403 | end 404 | 405 | 406 | def set_created 407 | set(SimpleRecord.options[:created_col] || :created, Time.now) 408 | end 409 | 410 | def set_updated 411 | set(SimpleRecord.options[:updated_col] || :updated, Time.now) 412 | end 413 | 414 | # an aliased method since many people use created_at/updated_at naming convention 415 | def created_at 416 | self.created 417 | end 418 | 419 | # an aliased method since many people use created_at/updated_at naming convention 420 | def updated_at 421 | self.updated 422 | end 423 | 424 | 425 | def cache_store 426 | @@cache_store 427 | end 428 | 429 | def domain_ok(ex, options={}) 430 | if (ex.message().index("NoSuchDomain") != nil) 431 | dom = options[:domain] || domain 432 | self.class.create_domain(dom) 433 | return true 434 | end 435 | return false 436 | end 437 | 438 | 439 | def new_record? 440 | # todo: new_record in activesdb should align with how we're defining a new record here, ie: if id is nil 441 | super 442 | end 443 | 444 | 445 | @create_domain_called = false 446 | 447 | # Options: 448 | # - :except => Array of attributes to NOT save 449 | # - :dirty => true - Will only store attributes that were modified. To make it save regardless and have it update the :updated value, include this and set it to false. 450 | # - :domain => Explicitly define domain to use. 451 | # 452 | 453 | def save(options={}) 454 | puts 'SAVING: ' + self.inspect if SimpleRecord.logging? 455 | # todo: Clean out undefined values in @attributes (in case someone set the attributes hash with values that they hadn't defined) 456 | clear_errors 457 | # todo: decide whether this should go before pre_save or after pre_save? pre_save dirties "updated" and perhaps other items due to callbacks 458 | if options[:dirty] 459 | # puts '@dirtyA=' + @dirty.inspect 460 | return true if @dirty.size == 0 # Nothing to save so skip it 461 | end 462 | 463 | ok = pre_save(options) # Validates and sets ID 464 | if ok 465 | if @@active_model 466 | ok = create_or_update(options) 467 | else 468 | ok = do_actual_save(options) 469 | end 470 | end 471 | return ok 472 | end 473 | 474 | def do_actual_save(options) 475 | begin 476 | is_create = new_record? # self[:id].nil? 477 | 478 | dirty = @dirty 479 | # puts 'dirty before=' + @dirty.inspect 480 | if options[:dirty] 481 | # puts '@dirty=' + @dirty.inspect 482 | return true if @dirty.size == 0 # This should probably never happen because after pre_save, created/updated dates are changed 483 | options[:dirty_atts] = @dirty 484 | end 485 | to_delete = get_atts_to_delete 486 | 487 | if self.class.is_sharded? 488 | options[:domain] = sharded_domain 489 | end 490 | return save_super(dirty, is_create, options, to_delete) 491 | rescue Aws::AwsError => ex 492 | raise ex 493 | end 494 | 495 | end 496 | 497 | # if @@active_model 498 | # alias_method :old_save, :save 499 | # 500 | # def save(options={}) 501 | ## puts 'extended save' 502 | # x = create_or_update 503 | ## puts 'save x=' + x.to_s 504 | # x 505 | # end 506 | # end 507 | 508 | def create_or_update(options) #:nodoc: 509 | # puts 'create_or_update' 510 | ret = true 511 | run_callbacks :save do 512 | result = new_record? ? create(options) : update(options) 513 | # puts 'save_callbacks result=' + result.inspect 514 | ret = result 515 | end 516 | ret 517 | end 518 | 519 | def create(options) #:nodoc: 520 | puts '3 create' 521 | ret = true 522 | run_callbacks :create do 523 | x = do_actual_save(options) 524 | # puts 'create old_save result=' + x.to_s 525 | ret = x 526 | end 527 | ret 528 | end 529 | 530 | # 531 | def update(options) #:nodoc: 532 | puts '3 update' 533 | ret = true 534 | run_callbacks :update do 535 | x = do_actual_save(options) 536 | # puts 'update old_save result=' + x.to_s 537 | ret = x 538 | end 539 | ret 540 | end 541 | 542 | 543 | def save!(options={}) 544 | save(options) || raise(RecordNotSaved.new(self)) 545 | end 546 | 547 | # this is a bit wonky, save! should call this, not sure why it's here. 548 | def save_with_validation!(options={}) 549 | save! 550 | end 551 | 552 | def self.create(attributes={}) 553 | # puts "About to create in domain #{domain}" 554 | super 555 | end 556 | 557 | def self.create!(attributes={}) 558 | item = self.new(attributes) 559 | item.save! 560 | item 561 | end 562 | 563 | 564 | def save_super(dirty, is_create, options, to_delete) 565 | SimpleRecord.stats.saves += 1 566 | if save2(options) 567 | self.class.cache_results(self) 568 | delete_niled(to_delete) 569 | save_lobs(dirty) 570 | after_save_cleanup 571 | unless @@active_model 572 | if (is_create ? run_after_create : run_after_update) && run_after_save 573 | # puts 'all good?' 574 | return true 575 | else 576 | return false 577 | end 578 | end 579 | return true 580 | else 581 | return false 582 | end 583 | end 584 | 585 | 586 | def save_lobs(dirty=nil) 587 | # puts 'dirty.inspect=' + dirty.inspect 588 | dirty = @dirty if dirty.nil? 589 | all_clobs = {} 590 | dirty_clobs = {} 591 | defined_attributes_local.each_pair do |k, v| 592 | # collect up the clobs in case it's a single put 593 | if v.type == :clob 594 | val = @lobs[k] 595 | all_clobs[k] = val 596 | if dirty.include?(k.to_s) 597 | dirty_clobs[k] = val 598 | else 599 | # puts 'NOT DIRTY' 600 | end 601 | 602 | end 603 | end 604 | if dirty_clobs.size > 0 605 | if self.class.get_sr_config[:single_clob] 606 | # all clobs in one chunk 607 | # using json for now, could change later 608 | val = all_clobs.to_json 609 | puts 'val=' + val.inspect 610 | put_lob(single_clob_id, val, :s3_bucket=>:new) 611 | else 612 | dirty_clobs.each_pair do |k, val| 613 | put_lob(s3_lob_id(k), val) 614 | end 615 | end 616 | end 617 | end 618 | 619 | def delete_lobs 620 | defined_attributes_local.each_pair do |k, v| 621 | if v.type == :clob 622 | if self.class.get_sr_config[:single_clob] 623 | s3_bucket(false, :s3_bucket=>:new).delete_key(single_clob_id) 624 | SimpleRecord.stats.s3_deletes += 1 625 | return 626 | else 627 | s3_bucket.delete_key(s3_lob_id(k)) 628 | SimpleRecord.stats.s3_deletes += 1 629 | end 630 | end 631 | end 632 | end 633 | 634 | 635 | def put_lob(k, val, options={}) 636 | begin 637 | s3_bucket(false, options).put(k, val) 638 | rescue Aws::AwsError => ex 639 | if ex.include? /NoSuchBucket/ 640 | s3_bucket(true, options).put(k, val) 641 | else 642 | raise ex 643 | end 644 | end 645 | SimpleRecord.stats.s3_puts += 1 646 | end 647 | 648 | 649 | def is_dirty?(name) 650 | # todo: should change all the dirty stuff to symbols? 651 | # puts '@dirty=' + @dirty.inspect 652 | # puts 'name=' +name.to_s 653 | @dirty.include? name.to_s 654 | end 655 | 656 | def s3 657 | 658 | return SimpleRecord.s3 if SimpleRecord.s3 659 | # todo: should optimize this somehow, like use the same connection_mode as used in SR 660 | # or keep open while looping in ResultsArray. 661 | Aws::S3.new(SimpleRecord.aws_access_key, SimpleRecord.aws_secret_key) 662 | end 663 | 664 | # options: 665 | # :s3_bucket => :old/:new/"#{any_bucket_name}". :new if want to use new bucket. Defaults to :old for backwards compatability. 666 | def s3_bucket(create=false, options={}) 667 | s3.bucket(s3_bucket_name(options[:s3_bucket]), create) 668 | end 669 | 670 | def s3_bucket_name(s3_bucket_option=:old) 671 | if s3_bucket_option == :new || SimpleRecord.options[:s3_bucket] == :new 672 | # this is the bucket that will be used going forward for anything related to s3 673 | ret = "simple_record_#{SimpleRecord.aws_access_key}" 674 | elsif !SimpleRecord.options[:s3_bucket].nil? && SimpleRecord.options[:s3_bucket] != :old 675 | ret = SimpleRecord.options[:s3_bucket] 676 | else 677 | ret = SimpleRecord.aws_access_key + "_lobs" 678 | end 679 | ret 680 | end 681 | 682 | def s3_lob_id(name) 683 | # if s3_bucket is not nil and not :old, then we use the new key. 684 | if !SimpleRecord.options[:s3_bucket].nil? && SimpleRecord.options[:s3_bucket] != :old 685 | "lobs/#{self.id}_#{name}" 686 | else 687 | self.id + "_" + name.to_s 688 | end 689 | end 690 | 691 | def single_clob_id 692 | "lobs/#{self.id}_single_clob" 693 | end 694 | 695 | 696 | def self.get_encryption_key() 697 | key = SimpleRecord.options[:encryption_key] 698 | # if key.nil? 699 | # puts 'WARNING: Encrypting attributes with your AWS Access Key. You should use your own :encryption_key so it doesn\'t change' 700 | # key = connection.aws_access_key_id # default to aws access key. NOT recommended in case you start using a new key 701 | # end 702 | return key 703 | end 704 | 705 | def pre_save(options) 706 | 707 | # puts '@@active_model ? ' + @@active_model.inspect 708 | 709 | ok = true 710 | is_create = self[:id].nil? 711 | unless @@active_model 712 | ok = run_before_validation && (is_create ? run_before_validation_on_create : run_before_validation_on_update) 713 | return false unless ok 714 | end 715 | 716 | # validate() 717 | # is_create ? validate_on_create : validate_on_update 718 | if !valid? 719 | puts 'not valid' 720 | return false 721 | end 722 | # 723 | ## puts 'AFTER VALIDATIONS, ERRORS=' + errors.inspect 724 | # if (!errors.nil? && errors.size > 0) 725 | ## puts 'THERE ARE ERRORS, returning false' 726 | # return false 727 | # end 728 | 729 | unless @@active_model 730 | ok = run_after_validation && (is_create ? run_after_validation_on_create : run_after_validation_on_update) 731 | return false unless ok 732 | end 733 | 734 | # Now for callbacks 735 | unless @@active_model 736 | ok = respond_to?(:before_save) ? before_save : true 737 | if ok 738 | # puts 'ok' 739 | if is_create && respond_to?(:before_create) 740 | # puts 'responsds to before_create' 741 | ok = before_create 742 | elsif !is_create && respond_to?(:before_update) 743 | ok = before_update 744 | end 745 | end 746 | if ok 747 | ok = run_before_save && (is_create ? run_before_create : run_before_update) 748 | end 749 | else 750 | 751 | end 752 | prepare_for_update 753 | ok 754 | end 755 | 756 | 757 | def get_atts_to_delete 758 | to_delete = [] 759 | changes.each_pair do |key, v| 760 | if v[1].nil? 761 | to_delete << key 762 | @attributes.delete(key) 763 | end 764 | end 765 | # @attributes.each do |key, value| 766 | ## puts 'key=' + key.inspect + ' value=' + value.inspect 767 | # if value.nil? || (value.is_a?(Array) && value.size == 0) || (value.is_a?(Array) && value.size == 1 && value[0] == nil) 768 | # to_delete << key 769 | # @attributes.delete(key) 770 | # end 771 | # end 772 | return to_delete 773 | end 774 | 775 | # Run pre_save on each object, then runs batch_put_attributes 776 | # Returns 777 | def self.batch_save(objects, options={}) 778 | options[:create_domain] = true if options[:create_domain].nil? 779 | results = [] 780 | to_save = [] 781 | if objects && objects.size > 0 782 | objects.each_slice(25) do |objs| 783 | objs.each do |o| 784 | ok = o.pre_save(options) 785 | raise "Pre save failed on object [" + o.inspect + "]" if !ok 786 | results << ok 787 | next if !ok # todo: this shouldn't be here should it? raises above 788 | o.pre_save2 789 | to_save << Aws::SdbInterface::Item.new(o.id, o.attributes, true) 790 | end 791 | connection.batch_put_attributes(domain, to_save, options) 792 | to_save.clear 793 | end 794 | end 795 | objects.each do |o| 796 | o.save_lobs(nil) 797 | end 798 | results 799 | end 800 | 801 | # Pass in an array of objects 802 | def self.batch_delete(objects, options={}) 803 | if objects 804 | objects.each_slice(25) do |objs| 805 | connection.batch_delete_attributes @domain, objs.collect { |x| x.id } 806 | end 807 | end 808 | end 809 | 810 | # 811 | # Usage: ClassName.delete id 812 | # 813 | def self.delete(id) 814 | connection.delete_attributes(domain, id) 815 | @deleted = true 816 | end 817 | 818 | # Pass in the same OPTIONS you'd pass into a find(:all, OPTIONS) 819 | def self.delete_all(options={}) 820 | # could make this quicker by just getting item_names and deleting attributes rather than creating objects 821 | obs = self.find(:all, options) 822 | i = 0 823 | obs.each do |a| 824 | a.delete 825 | i+=1 826 | end 827 | return i 828 | end 829 | 830 | # Pass in the same OPTIONS you'd pass into a find(:all, OPTIONS) 831 | def self.destroy_all(options={}) 832 | obs = self.find(:all, options) 833 | i = 0 834 | obs.each do |a| 835 | a.destroy 836 | i+=1 837 | end 838 | return i 839 | end 840 | 841 | def delete(options={}) 842 | if self.class.is_sharded? 843 | options[:domain] = sharded_domain 844 | end 845 | super(options) 846 | 847 | # delete lobs now too 848 | delete_lobs 849 | end 850 | 851 | def destroy 852 | if @@active_model 853 | run_callbacks :destroy do 854 | delete 855 | end 856 | else 857 | return run_before_destroy && delete && run_after_destroy 858 | end 859 | end 860 | 861 | def delete_niled(to_delete) 862 | # puts 'to_delete=' + to_delete.inspect 863 | if to_delete.size > 0 864 | # puts 'Deleting attributes=' + to_delete.inspect 865 | SimpleRecord.stats.deletes += 1 866 | delete_attributes to_delete 867 | to_delete.each do |att| 868 | att_meta = get_att_meta(att) 869 | if att_meta.type == :clob 870 | s3_bucket.key(s3_lob_id(att)).delete 871 | end 872 | end 873 | end 874 | end 875 | 876 | def reload 877 | super() 878 | end 879 | 880 | 881 | def update_attributes(atts) 882 | set_attributes(atts) 883 | save 884 | end 885 | 886 | def update_attributes!(atts) 887 | set_attributes(atts) 888 | save! 889 | end 890 | 891 | 892 | def self.quote_regexp(a, re) 893 | a =~ re 894 | #was there a match? 895 | if $& 896 | before=$` 897 | middle=$& 898 | after =$' 899 | 900 | before =~ /'$/ #is there already a quote immediately before the match? 901 | unless $& 902 | return "#{before}'#{middle}'#{quote_regexp(after, re)}" #if not, put quotes around the match 903 | else 904 | return "#{before}#{middle}#{quote_regexp(after, re)}" #if so, assume it is quoted already and move on 905 | end 906 | else 907 | #no match, just return the string 908 | return a 909 | end 910 | end 911 | 912 | @@regex_no_id = /.*Couldn't find.*with ID.*/ 913 | 914 | # 915 | # Usage: 916 | # Find by ID: 917 | # MyModel.find(ID) 918 | # 919 | # Query example: 920 | # MyModel.find(:all, :conditions=>["name = ?", name], :order=>"created desc", :limit=>10) 921 | # 922 | # Extra options: 923 | # :per_token => the number of results to return per next_token, max is 2500. 924 | # :consistent_read => true/false -- as per http://developer.amazonwebservices.com/connect/entry.jspa?externalID=3572 925 | # :retries => maximum number of times to retry this query on an error response. 926 | # :shard => shard name or array of shard names to use on this query. 927 | def self.find(*params) 928 | #puts 'params=' + params.inspect 929 | 930 | q_type = :all 931 | select_attributes=[] 932 | if params.size > 0 933 | q_type = params[0] 934 | end 935 | options = {} 936 | if params.size > 1 937 | options = params[1] 938 | end 939 | conditions = options[:conditions] 940 | if conditions && conditions.is_a?(String) 941 | conditions = [conditions] 942 | options[:conditions] = conditions 943 | end 944 | 945 | if !options[:shard_find] && is_sharded? 946 | # then break off and get results across all shards 947 | return find_sharded(*params) 948 | end 949 | 950 | # Pad and Offset number attributes 951 | params_dup = params.dup 952 | if params.size > 1 953 | options = params[1] 954 | #puts 'options=' + options.inspect 955 | #puts 'after collect=' + options.inspect 956 | convert_condition_params(options) 957 | per_token = options[:per_token] 958 | consistent_read = options[:consistent_read] 959 | if per_token || consistent_read then 960 | op_dup = options.dup 961 | op_dup[:limit] = per_token # simpledb uses Limit as a paging thing, not what is normal 962 | op_dup[:consistent_read] = consistent_read 963 | params_dup[1] = op_dup 964 | end 965 | end 966 | # puts 'params2=' + params.inspect 967 | 968 | ret = q_type == :all ? [] : nil 969 | begin 970 | results=find_with_metadata(*params_dup) 971 | #puts "RESULT=" + results.inspect 972 | write_usage(:select, domain, q_type, options, results) 973 | #puts 'params3=' + params.inspect 974 | SimpleRecord.stats.selects += 1 975 | if q_type == :count 976 | ret = results[:count] 977 | elsif q_type == :first 978 | ret = results[:items].first 979 | # todo: we should store request_id and box_usage with the object maybe? 980 | cache_results(ret) 981 | elsif results[:single_only] 982 | ret = results[:single] 983 | #puts 'results[:single] ' + ret.inspect 984 | cache_results(ret) 985 | else 986 | #puts 'last step items = ' + results.inspect 987 | if results[:items] #.is_a?(Array) 988 | cache_results(results[:items]) 989 | ret = SimpleRecord::ResultsArray.new(self, params, results, next_token) 990 | end 991 | end 992 | rescue Aws::AwsError, SimpleRecord::ActiveSdb::ActiveSdbError => ex 993 | #puts "RESCUED: " + ex.message 994 | if (ex.message().index("NoSuchDomain") != nil) 995 | # this is ok 996 | # elsif (ex.message() =~ @@regex_no_id) This is RecordNotFound now 997 | # ret = nil 998 | else 999 | #puts 're-raising' 1000 | raise ex 1001 | end 1002 | end 1003 | # puts 'single2=' + ret.inspect 1004 | return ret 1005 | end 1006 | 1007 | def self.select(*params) 1008 | return find(*params) 1009 | end 1010 | 1011 | def self.all(*args) 1012 | find(:all, *args) 1013 | end 1014 | 1015 | def self.first(*args) 1016 | find(:first, *args) 1017 | end 1018 | 1019 | def self.count(*args) 1020 | find(:count, *args) 1021 | end 1022 | 1023 | # This gets less and less efficient the higher the page since SimpleDB has no way 1024 | # to start at a specific row. So it will iterate from the first record and pull out the specific pages. 1025 | def self.paginate(options={}) 1026 | # options = args.pop 1027 | # puts 'paginate options=' + options.inspect if SimpleRecord.logging? 1028 | page = options[:page] || 1 1029 | per_page = options[:per_page] || 50 1030 | # total = options[:total_entries].to_i 1031 | options[:page] = page.to_i # makes sure it's to_i 1032 | options[:per_page] = per_page.to_i 1033 | options[:limit] = options[:page] * options[:per_page] 1034 | # puts 'paging options=' + options.inspect 1035 | fr = find(:all, options) 1036 | return fr 1037 | 1038 | end 1039 | 1040 | 1041 | def self.convert_condition_params(options) 1042 | return if options.nil? 1043 | conditions = options[:conditions] 1044 | return if conditions.nil? 1045 | if conditions.size > 1 1046 | # all after first are values 1047 | conditions.collect! { |x| 1048 | Translations.pad_and_offset(x) 1049 | } 1050 | end 1051 | 1052 | end 1053 | 1054 | def self.cache_results(results) 1055 | if !cache_store.nil? && !results.nil? 1056 | if results.is_a?(Array) 1057 | # todo: cache each result 1058 | results.each do |item| 1059 | class_name = item.class.name 1060 | id = item.id 1061 | cache_key = self.cache_key(class_name, id) 1062 | #puts 'caching result at ' + cache_key + ': ' + results.inspect 1063 | cache_store.write(cache_key, item, :expires_in =>30) 1064 | end 1065 | else 1066 | class_name = results.class.name 1067 | id = results.id 1068 | cache_key = self.cache_key(class_name, id) 1069 | #puts 'caching result at ' + cache_key + ': ' + results.inspect 1070 | cache_store.write(cache_key, results, :expires_in =>30) 1071 | end 1072 | end 1073 | end 1074 | 1075 | def self.cache_key(class_name, id) 1076 | return class_name + "/" + id.to_s 1077 | end 1078 | 1079 | @@debug="" 1080 | 1081 | def self.debug 1082 | @@debug 1083 | end 1084 | 1085 | def self.sanitize_sql(*params) 1086 | return ActiveRecord::Base.sanitize_sql(*params) 1087 | end 1088 | 1089 | def self.table_name 1090 | return domain 1091 | end 1092 | 1093 | def changed 1094 | return @dirty.keys 1095 | end 1096 | 1097 | def changed? 1098 | return @dirty.size > 0 1099 | end 1100 | 1101 | def changes 1102 | ret = {} 1103 | #puts 'in CHANGES=' + @dirty.inspect 1104 | @dirty.each_pair { |key, value| ret[key] = [value, get_attribute(key)] } 1105 | return ret 1106 | end 1107 | 1108 | def after_save_cleanup 1109 | @dirty = {} 1110 | end 1111 | 1112 | def hash 1113 | # same as ActiveRecord 1114 | id.hash 1115 | end 1116 | 1117 | 1118 | end 1119 | 1120 | 1121 | class Activerecordtosdb_subrecord_array 1122 | def initialize(subname, referencename, referencevalue) 1123 | @subname =subname.classify 1124 | @referencename =referencename.tableize.singularize + "_id" 1125 | @referencevalue=referencevalue 1126 | end 1127 | 1128 | # Performance optimization if you know the array should be empty 1129 | 1130 | def init_empty 1131 | @records = [] 1132 | end 1133 | 1134 | def load 1135 | if @records.nil? 1136 | @records = find_all 1137 | end 1138 | return @records 1139 | end 1140 | 1141 | def [](key) 1142 | return load[key] 1143 | end 1144 | 1145 | def first 1146 | load[0] 1147 | end 1148 | 1149 | def <<(ob) 1150 | return load << ob 1151 | end 1152 | 1153 | def count 1154 | return load.count 1155 | end 1156 | 1157 | def size 1158 | return count 1159 | end 1160 | 1161 | def each(*params, &block) 1162 | return load.each(*params) { |record| block.call(record) } 1163 | end 1164 | 1165 | def find_all(*params) 1166 | find(:all, *params) 1167 | end 1168 | 1169 | def empty? 1170 | return load.empty? 1171 | end 1172 | 1173 | def build(*params) 1174 | params[0][@referencename]=@referencevalue 1175 | eval(@subname).new(*params) 1176 | end 1177 | 1178 | def create(*params) 1179 | params[0][@referencename]=@referencevalue 1180 | record = eval(@subname).new(*params) 1181 | record.save 1182 | end 1183 | 1184 | def find(*params) 1185 | query=[:first, {}] 1186 | #{:conditions=>"id=>1"} 1187 | if params[0] 1188 | if params[0]==:all 1189 | query[0]=:all 1190 | end 1191 | end 1192 | 1193 | if params[1] 1194 | query[1]=params[1] 1195 | if query[1][:conditions] 1196 | query[1][:conditions]=SimpleRecord::Base.sanitize_sql(query[1][:conditions])+" AND "+ SimpleRecord::Base.sanitize_sql(["#{@referencename} = ?", @referencevalue]) 1197 | #query[1][:conditions]=Activerecordtosdb.sanitize_sql(query[1][:conditions])+" AND id='#{@id}'" 1198 | else 1199 | query[1][:conditions]=["#{@referencename} = ?", @referencevalue] 1200 | #query[1][:conditions]="id='#{@id}'" 1201 | end 1202 | else 1203 | query[1][:conditions]=["#{@referencename} = ?", @referencevalue] 1204 | #query[1][:conditions]="id='#{@id}'" 1205 | end 1206 | 1207 | return eval(@subname).find(*query) 1208 | end 1209 | 1210 | end 1211 | 1212 | # This is simply a place holder so we don't keep doing gets to s3 or simpledb if already checked. 1213 | class RemoteNil 1214 | 1215 | end 1216 | 1217 | 1218 | end 1219 | 1220 | -------------------------------------------------------------------------------- /lib/simple_record/active_sdb.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2008 RightScale Inc 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining 5 | # a copy of this software and associated documentation files (the 6 | # "Software"), to deal in the Software without restriction, including 7 | # without limitation the rights to use, copy, modify, merge, publish, 8 | # distribute, sublicense, and/or sell copies of the Software, and to 9 | # permit persons to whom the Software is furnished to do so, subject to 10 | # the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | # 23 | require 'uuidtools' 24 | 25 | module SimpleRecord 26 | 27 | # = Aws::ActiveSdb -- RightScale SDB interface (alpha release) 28 | # The Aws::ActiveSdb class provides a complete interface to Amazon's Simple 29 | # Database Service. 30 | # 31 | # 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: 32 | # 33 | # require 'right_aws' 34 | # require 'sdb/active_sdb' 35 | # 36 | # Additionally, the ActiveSdb class requires the 'uuidtools' gem; this gem is not normally required by Aws and is not installed as a 37 | # dependency of Aws. 38 | # 39 | # Simple ActiveSdb usage example: 40 | # 41 | # class Client < Aws::ActiveSdb::Base 42 | # end 43 | # 44 | # # connect to SDB 45 | # Aws::ActiveSdb.establish_connection 46 | # 47 | # # create domain 48 | # Client.create_domain 49 | # 50 | # # create initial DB 51 | # Client.create 'name' => 'Bush', 'country' => 'USA', 'gender' => 'male', 'expiration' => '2009', 'post' => 'president' 52 | # Client.create 'name' => 'Putin', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2008', 'post' => 'president' 53 | # Client.create 'name' => 'Medvedev', 'country' => 'Russia', 'gender' => 'male', 'expiration' => '2012', 'post' => 'president' 54 | # Client.create 'name' => 'Mary', 'country' => 'USA', 'gender' => 'female', 'hobby' => ['patchwork', 'bundle jumping'] 55 | # Client.create 'name' => 'Mary', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking'] 56 | # sandy_id = Client.create('name' => 'Sandy', 'country' => 'Russia', 'gender' => 'female', 'hobby' => ['flowers', 'cats', 'cooking']).id 57 | # 58 | # # find all Bushes in USA 59 | # Client.find(:all, :conditions => ["['name'=?] intersection ['country'=?]",'Bush','USA']).each do |client| 60 | # client.reload 61 | # puts client.attributes.inspect 62 | # end 63 | # 64 | # # find all Maries through the world 65 | # Client.find_all_by_name_and_gender('Mary','female').each do |mary| 66 | # mary.reload 67 | # puts "#{mary[:name]}, gender: #{mary[:gender]}, hobbies: #{mary[:hobby].join(',')}" 68 | # end 69 | # 70 | # # find new russian president 71 | # medvedev = Client.find_by_post_and_country_and_expiration('president','Russia','2012') 72 | # if medvedev 73 | # medvedev.reload 74 | # medvedev.save_attributes('age' => '42', 'hobby' => 'Gazprom') 75 | # end 76 | # 77 | # # retire old president 78 | # Client.find_by_name('Putin').delete 79 | # 80 | # # Sandy disappointed in 'cooking' and decided to hide her 'gender' and 'country' () 81 | # sandy = Client.find(sandy_id) 82 | # sandy.reload 83 | # sandy.delete_values('hobby' => ['cooking'] ) 84 | # sandy.delete_attributes('country', 'gender') 85 | # 86 | # # remove domain 87 | # Client.delete_domain 88 | # 89 | class ActiveSdb 90 | 91 | module ActiveSdbConnect 92 | def connection 93 | @connection || raise(ActiveSdbError.new('Connection to SDB is not established')) 94 | end 95 | 96 | # Create a new handle to an Sdb account. All handles share the same per process or per thread 97 | # HTTP connection to Amazon Sdb. Each handle is for a specific account. 98 | # The +params+ are passed through as-is to Aws::SdbInterface.new 99 | # Params: 100 | # { :server => 'sdb.amazonaws.com' # Amazon service host: 'sdb.amazonaws.com'(default) 101 | # :port => 443 # Amazon service port: 80 or 443(default) 102 | # :protocol => 'https' # Amazon service protocol: 'http' or 'https'(default) 103 | # :signature_version => '2' # The signature version : '0', '1' or '2' (default) 104 | # DEPRECATED :multi_thread => true|false # Multi-threaded (connection per each thread): true or false(default) 105 | # :connection_mode => :default # options are :default (will use best known option, may change in the future) 106 | # :per_request (opens and closes a connection on every request to SDB) 107 | # :single (same as old multi_thread=>false) 108 | # :per_thread (same as old multi_thread=>true) 109 | # :pool (uses a connection pool with a maximum number of connections - NOT IMPLEMENTED YET) 110 | # :logger => Logger Object # Logger instance: logs to STDOUT if omitted 111 | # :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') 112 | # :service_endpoint => '/' # Set this to /mdb/request.mgwsi for usage with M/DB 113 | 114 | def establish_connection(aws_access_key_id=nil, aws_secret_access_key=nil, params={}) 115 | @connection = Aws::SdbInterface.new(aws_access_key_id, aws_secret_access_key, params) 116 | end 117 | 118 | def close_connection 119 | @connection.close_connection unless @connection.nil? 120 | end 121 | end 122 | 123 | class ActiveSdbError < RuntimeError 124 | end 125 | 126 | class << self 127 | include ActiveSdbConnect 128 | 129 | # Retreive a list of domains. 130 | # 131 | # put Aws::ActiveSdb.domains #=> ['co-workers','family','friends','clients'] 132 | # 133 | def domains 134 | connection.list_domains[:domains] 135 | end 136 | 137 | # Create new domain. 138 | # Raises no errors if the domain already exists. 139 | # 140 | # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"} 141 | # 142 | def create_domain(domain_name) 143 | connection.create_domain(domain_name) 144 | end 145 | 146 | # Remove domain from SDB. 147 | # Raises no errors if the domain does not exist. 148 | # 149 | # Aws::ActiveSdb.create_domain('alpha') #=> {:request_id=>"6fc652a0-0000-41c6-91f4-3ed390a3d3b2", :box_usage=>"0.0055590001"} 150 | # 151 | def delete_domain(domain_name) 152 | connection.delete_domain(domain_name) 153 | end 154 | end 155 | 156 | class Base 157 | 158 | class << self 159 | include ActiveSdbConnect 160 | 161 | # next_token value returned by last find: is useful to continue finding 162 | attr_accessor :next_token 163 | 164 | # Returns a Aws::SdbInterface object 165 | # 166 | # class A < Aws::ActiveSdb::Base 167 | # end 168 | # 169 | # class B < Aws::ActiveSdb::Base 170 | # end 171 | # 172 | # class C < Aws::ActiveSdb::Base 173 | # end 174 | # 175 | # Aws::ActiveSdb.establish_connection 'key_id_1', 'secret_key_1' 176 | # 177 | # C.establish_connection 'key_id_2', 'secret_key_2' 178 | # 179 | # # A and B uses the default connection, C - uses its own 180 | # puts A.connection #=> # 181 | # puts B.connection #=> # 182 | # puts C.connection #=> # 183 | # 184 | def connection 185 | @connection || ActiveSdb::connection 186 | end 187 | 188 | @domain = nil 189 | 190 | # Current domain name. 191 | # 192 | # # if 'ActiveSupport' is not loaded then class name converted to downcase 193 | # class Client < Aws::ActiveSdb::Base 194 | # end 195 | # puts Client.domain #=> 'client' 196 | # 197 | # # if 'ActiveSupport' is loaded then class name being tableized 198 | # require 'activesupport' 199 | # class Client < Aws::ActiveSdb::Base 200 | # end 201 | # puts Client.domain #=> 'clients' 202 | # 203 | # # Explicit domain name definition 204 | # class Client < Aws::ActiveSdb::Base 205 | # set_domain_name :foreign_clients 206 | # end 207 | # puts Client.domain #=> 'foreign_clients' 208 | # 209 | def domain 210 | unless @domain 211 | if defined? ActiveSupport::CoreExtensions::String::Inflections 212 | @domain = name.tableize 213 | else 214 | @domain = name.downcase 215 | end 216 | end 217 | @domain 218 | end 219 | 220 | # Change the default domain name to user defined. 221 | # 222 | # class Client < Aws::ActiveSdb::Base 223 | # set_domain_name :foreign_clients 224 | # end 225 | # 226 | def set_domain_name(domain) 227 | @domain = domain.to_s 228 | end 229 | 230 | # Create domain at SDB. 231 | # Raises no errors if the domain already exists. 232 | # 233 | # class Client < Aws::ActiveSdb::Base 234 | # end 235 | # Client.create_domain #=> {:request_id=>"6fc652a0-0000-41d5-91f4-3ed390a3d3b2", :box_usage=>"0.0055590278"} 236 | # 237 | def create_domain(dom=nil) 238 | dom = domain if dom.nil? 239 | puts "Creating new SimpleDB Domain: " + dom 240 | connection.create_domain(dom) 241 | end 242 | 243 | # Remove domain from SDB. 244 | # Raises no errors if the domain does not exist. 245 | # 246 | # class Client < Aws::ActiveSdb::Base 247 | # end 248 | # Client.delete_domain #=> {:request_id=>"e14d90d3-0000-4898-9995-0de28cdda270", :box_usage=>"0.0055590278"} 249 | # 250 | def delete_domain(dom=nil) 251 | dom = domain if dom.nil? 252 | puts "!!! DELETING SimpleDB Domain: " + dom 253 | connection.delete_domain(dom) 254 | end 255 | 256 | # 257 | # See select(), original find with QUERY syntax is deprecated so now find and select are synonyms. 258 | # 259 | def find(*args) 260 | options = args.last.is_a?(Hash) ? args.pop : {} 261 | case args.first 262 | when nil then 263 | raise "Invalid parameters passed to find: nil." 264 | when :all then 265 | sql_select(options)[:items] 266 | when :first then 267 | sql_select(options.merge(:limit => 1))[:items].first 268 | when :count then 269 | res = sql_select(options.merge(:count => true))[:count] 270 | res 271 | else 272 | res = select_from_ids(args, options) 273 | return res[:single] if res[:single] 274 | return res[:items] 275 | end 276 | end 277 | 278 | # 279 | # Same as find, but will return SimpleDB metadata like :request_id and :box_usage 280 | # 281 | def find_with_metadata(*args) 282 | options = args.last.is_a?(Hash) ? args.pop : {} 283 | case args.first 284 | when nil then 285 | raise "Invalid parameters passed to find: nil." 286 | when :all then 287 | sql_select(options) 288 | when :first then 289 | sql_select(options.merge(:limit => 1)) 290 | when :count then 291 | res = sql_select(options.merge(:count => true)) 292 | res 293 | else 294 | select_from_ids args, options 295 | end 296 | end 297 | 298 | # Perform a SQL-like select request. 299 | # 300 | # Single record: 301 | # 302 | # Client.select(:first) 303 | # Client.select(:first, :conditions=> [ "name=? AND wife=?", "Jon", "Sandy"]) 304 | # Client.select(:first, :conditions=> { :name=>"Jon", :wife=>"Sandy" }, :select => :girfriends) 305 | # 306 | # Bunch of records: 307 | # 308 | # Client.select(:all) 309 | # Client.select(:all, :limit => 10) 310 | # Client.select(:all, :conditions=> [ "name=? AND 'girlfriend'=?", "Jon", "Judy"]) 311 | # Client.select(:all, :conditions=> { :name=>"Sandy" }, :limit => 3) 312 | # 313 | # Records by ids: 314 | # 315 | # Client.select('1') 316 | # Client.select('1234987b4583475347523948') 317 | # Client.select('1','2','3','4', :conditions=> ["toys=?", "beer"]) 318 | # 319 | # Find helpers: Aws::ActiveSdb::Base.select_by_... and Aws::ActiveSdb::Base.select_all_by_... 320 | # 321 | # Client.select_by_name('Matias Rust') 322 | # Client.select_by_name_and_city('Putin','Moscow') 323 | # Client.select_by_name_and_city_and_post('Medvedev','Moscow','president') 324 | # 325 | # Client.select_all_by_author('G.Bush jr') 326 | # Client.select_all_by_age_and_gender_and_ethnicity('34','male','russian') 327 | # Client.select_all_by_gender_and_country('male', 'Russia', :order => 'name') 328 | # 329 | # Continue listing: 330 | # 331 | # # initial listing 332 | # Client.select(:all, :limit => 10) 333 | # # continue listing 334 | # begin 335 | # Client.select(:all, :limit => 10, :next_token => Client.next_token) 336 | # end while Client.next_token 337 | # 338 | # Sort oder: 339 | # If :order=>'attribute' option is specified then result response (ordered by 'attribute') will contain only items where attribute is defined (is not null). 340 | # 341 | # Client.select(:all) # returns all records 342 | # Client.select(:all, :order => 'gender') # returns all records ordered by gender where gender attribute exists 343 | # Client.select(:all, :order => 'name desc') # returns all records ordered by name in desc order where name attribute exists 344 | # 345 | # see http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?UsingSelect.html 346 | # 347 | def select(*args) 348 | find(*args) 349 | end 350 | 351 | def generate_id # :nodoc: 352 | # mac address creation sometimes throws errors if mac address isn't available 353 | UUIDTools::UUID.random_create().to_s 354 | end 355 | 356 | protected 357 | 358 | def logger 359 | SimpleRecord.logger 360 | end 361 | 362 | # Select 363 | 364 | def select_from_ids(args, options) # :nodoc: 365 | cond = [] 366 | # detect amount of records requested 367 | bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array) 368 | # flatten ids 369 | args = args.to_a.flatten 370 | args.each { |id| cond << "itemName() = #{self.connection.escape(id)}" } 371 | ids_cond = "(#{cond.join(' OR ')})" 372 | # user defined :conditions to string (if it was defined) 373 | options[:conditions] = build_conditions(options[:conditions]) 374 | # join ids condition and user defined conditions 375 | options[:conditions] = options[:conditions].blank? ? ids_cond : "(#{options[:conditions]}) AND #{ids_cond}" 376 | #puts 'options=' + options.inspect 377 | result = sql_select(options) 378 | #puts 'select_from_ids result=' + result.inspect 379 | # if one record was requested then return it 380 | unless bunch_of_records_requested 381 | result[:single_only] = true 382 | record = result[:items].first 383 | # railse if nothing was found 384 | raise SimpleRecord::RecordNotFound.new("Couldn't find #{name} with ID #{args}") unless record || is_sharded? 385 | result[:single] = record 386 | else 387 | # if a bunch of records was requested then return check that we found all of them 388 | # and return as an array 389 | puts 'is_sharded? ' + is_sharded?.to_s 390 | unless is_sharded? || args.size == result[:items].size 391 | # todo: might make sense to return the array but with nil values in the slots where an item wasn't found? 392 | id_list = args.map { |i| "'#{i}'" }.join(',') 393 | raise SimpleRecord::RecordNotFound.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result[:items].size} results, but was looking for #{args.size})") 394 | else 395 | result 396 | end 397 | end 398 | result 399 | end 400 | 401 | def sql_select(options) # :nodoc: 402 | count = options[:count] || false 403 | #puts 'count? ' + count.to_s 404 | @next_token = options[:next_token] 405 | @consistent_read = options[:consistent_read] 406 | select_expression = build_select(options) 407 | logger.debug 'SELECT=' + select_expression 408 | # request items 409 | 410 | ret = {} 411 | if count 412 | # we'll keep going to get full count 413 | total_count = 0 414 | total_box_usage = 0 415 | query_result = self.connection.select(select_expression, options) do |result| 416 | #puts 'result=' + result.inspect 417 | total_count += result[:items][0]["Domain"]["Count"][0].to_i # result.delete(:items)[0]["Domain"]["Count"][0].to_i 418 | total_box_usage += result[:box_usage].to_i 419 | true #continue loop 420 | end 421 | ret[:count] = total_count 422 | ret[:box_usage] = total_box_usage 423 | return ret 424 | else 425 | query_result = self.connection.select(select_expression, options) 426 | @next_token = query_result[:next_token] 427 | end 428 | # puts 'QR=' + query_result.inspect 429 | 430 | #if count 431 | #ret[:count] = query_result.delete(:items)[0]["Domain"]["Count"][0].to_i 432 | #ret.merge!(query_result) 433 | #return ret 434 | #end 435 | 436 | items = query_result.delete(:items).map do |hash| 437 | id, attributes = hash.shift 438 | new_item = self.new() 439 | new_item.initialize_from_db(attributes.merge({'id' => id})) 440 | new_item.mark_as_old 441 | new_item 442 | end 443 | ret[:items] = items 444 | ret.merge!(query_result) 445 | ret 446 | end 447 | 448 | # select_by helpers 449 | def select_all_by_(format_str, args, options) # :nodoc: 450 | fields = format_str.to_s.sub(/^select_(all_)?by_/, '').split('_and_') 451 | conditions = fields.map { |field| "#{field}=?" }.join(' AND ') 452 | options[:conditions] = [conditions, *args] 453 | find(:all, options) 454 | end 455 | 456 | def select_by_(format_str, args, options) # :nodoc: 457 | options[:limit] = 1 458 | select_all_by_(format_str, args, options).first 459 | end 460 | 461 | # Query 462 | 463 | # Returns an array of query attributes. 464 | # Query_expression must be a well formated SDB query string: 465 | # query_attributes("['title' starts-with 'O\\'Reily'] intersection ['year' = '2007']") #=> ["title", "year"] 466 | def query_attributes(query_expression) # :nodoc: 467 | attrs = [] 468 | array = query_expression.scan(/['"](.*?[^\\])['"]/).flatten 469 | until array.empty? do 470 | attrs << array.shift # skip it's value 471 | array.shift # 472 | end 473 | attrs 474 | end 475 | 476 | # Returns an array of [attribute_name, 'asc'|'desc'] 477 | def sort_options(sort_string) 478 | sort_string[/['"]?(\w+)['"]? *(asc|desc)?/i] 479 | [$1, ($2 || 'asc')] 480 | end 481 | 482 | # Perform a query request. 483 | # 484 | # Options 485 | # :query_expression nil | string | array 486 | # :max_number_of_items nil | integer 487 | # :next_token nil | string 488 | # :sort_option nil | string "name desc|asc" 489 | # 490 | def query(options) # :nodoc: 491 | @next_token = options[:next_token] 492 | @consistent_read = options[:consistent_read] 493 | query_expression = build_conditions(options[:query_expression]) 494 | # add sort_options to the query_expression 495 | if options[:sort_option] 496 | sort_by, sort_order = sort_options(options[:sort_option]) 497 | sort_query_expression = "['#{sort_by}' starts-with '']" 498 | sort_by_expression = " sort '#{sort_by}' #{sort_order}" 499 | # make query_expression to be a string (it may be null) 500 | query_expression = query_expression.to_s 501 | # quote from Amazon: 502 | # The sort attribute must be present in at least one of the predicates of the query expression. 503 | if query_expression.blank? 504 | query_expression = sort_query_expression 505 | elsif !query_attributes(query_expression).include?(sort_by) 506 | query_expression += " intersection #{sort_query_expression}" 507 | end 508 | query_expression += sort_by_expression 509 | end 510 | # request items 511 | query_result = self.connection.query(domain, query_expression, options[:max_number_of_items], @next_token, @consistent_read) 512 | @next_token = query_result[:next_token] 513 | items = query_result[:items].map do |name| 514 | new_item = self.new('id' => name) 515 | new_item.mark_as_old 516 | reload_if_exists(record) if options[:auto_load] 517 | new_item 518 | end 519 | items 520 | end 521 | 522 | # reload a record unless it is nil 523 | def reload_if_exists(record) # :nodoc: 524 | record && record.reload 525 | end 526 | 527 | def reload_all_records(*list) # :nodoc: 528 | list.flatten.each { |record| reload_if_exists(record) } 529 | end 530 | 531 | def find_every(options) # :nodoc: 532 | records = query(:query_expression => options[:conditions], 533 | :max_number_of_items => options[:limit], 534 | :next_token => options[:next_token], 535 | :sort_option => options[:sort] || options[:order], 536 | :consistent_read => options[:consistent_read]) 537 | options[:auto_load] ? reload_all_records(records) : records 538 | end 539 | 540 | def find_initial(options) # :nodoc: 541 | options[:limit] = 1 542 | record = find_every(options).first 543 | options[:auto_load] ? reload_all_records(record).first : record 544 | end 545 | 546 | def find_from_ids(args, options) # :nodoc: 547 | cond = [] 548 | # detect amount of records requested 549 | bunch_of_records_requested = args.size > 1 || args.first.is_a?(Array) 550 | # flatten ids 551 | args = args.to_a.flatten 552 | args.each { |id| cond << "'id'=#{self.connection.escape(id)}" } 553 | ids_cond = "[#{cond.join(' OR ')}]" 554 | # user defined :conditions to string (if it was defined) 555 | options[:conditions] = build_conditions(options[:conditions]) 556 | # join ids condition and user defined conditions 557 | options[:conditions] = options[:conditions].blank? ? ids_cond : "#{options[:conditions]} intersection #{ids_cond}" 558 | result = find_every(options) 559 | # if one record was requested then return it 560 | unless bunch_of_records_requested 561 | record = result.first 562 | # railse if nothing was found 563 | raise ActiveSdbError.new("Couldn't find #{name} with ID #{args}") unless record 564 | options[:auto_load] ? reload_all_records(record).first : record 565 | else 566 | # if a bunch of records was requested then return check that we found all of them 567 | # and return as an array 568 | unless args.size == result.size 569 | id_list = args.map { |i| "'#{i}'" }.join(',') 570 | raise ActiveSdbError.new("Couldn't find all #{name} with IDs (#{id_list}) (found #{result.size} results, but was looking for #{args.size})") 571 | else 572 | options[:auto_load] ? reload_all_records(result) : result 573 | end 574 | end 575 | end 576 | 577 | # find_by helpers 578 | def find_all_by_(format_str, args, options) # :nodoc: 579 | fields = format_str.to_s.sub(/^find_(all_)?by_/, '').split('_and_') 580 | conditions = fields.map { |field| "['#{field}'=?]" }.join(' intersection ') 581 | options[:conditions] = [conditions, *args] 582 | find(:all, options) 583 | end 584 | 585 | def find_by_(format_str, args, options) # :nodoc: 586 | options[:limit] = 1 587 | find_all_by_(format_str, args, options).first 588 | end 589 | 590 | # Misc 591 | 592 | def method_missing(method, *args) # :nodoc: 593 | if method.to_s[/^(find_all_by_|find_by_|select_all_by_|select_by_)/] 594 | # get rid of the find ones, only select now 595 | to_send_to = $1 596 | attributes = method.to_s[$1.length..method.to_s.length] 597 | # puts 'attributes=' + attributes 598 | if to_send_to[0...4] == "find" 599 | to_send_to = "select" + to_send_to[4..to_send_to.length] 600 | # puts 'CONVERTED ' + $1 + " to " + to_send_to 601 | end 602 | 603 | options = args.last.is_a?(Hash) ? args.pop : {} 604 | __send__(to_send_to, attributes, args, options) 605 | else 606 | super(method, *args) 607 | end 608 | end 609 | 610 | def build_select(options) # :nodoc: 611 | select = options[:select] || '*' 612 | select = options[:count] ? "count(*)" : select 613 | #puts 'select=' + select.to_s 614 | from = options[:from] || domain 615 | condition_fields = parse_condition_fields(options[:conditions]) 616 | conditions = options[:conditions] ? "#{build_conditions(options[:conditions])}" : '' 617 | order = options[:order] ? " ORDER BY #{options[:order]}" : '' 618 | limit = options[:limit] ? " LIMIT #{options[:limit]}" : '' 619 | # mix sort by argument (it must present in response) 620 | unless order.blank? 621 | sort_by, sort_order = sort_options(options[:order]) 622 | if condition_fields.nil? || !condition_fields.include?(sort_by) 623 | # conditions << (conditions.blank? ? " WHERE " : " AND ") << "(#{sort_by} IS NOT NULL)" 624 | conditions = (conditions.blank? ? "" : "(#{conditions}) AND ") << "(#{sort_by} IS NOT NULL)" 625 | else 626 | # puts 'skipping is not null on sort because already there.' 627 | end 628 | 629 | end 630 | conditions = conditions.blank? ? "" : " WHERE #{conditions}" 631 | # puts 'CONDITIONS=' + conditions 632 | "SELECT #{select} FROM `#{from}`#{conditions}#{order}#{limit}" 633 | end 634 | 635 | def build_conditions(conditions) # :nodoc: 636 | case 637 | when conditions.is_a?(Array) then 638 | connection.query_expression_from_array(conditions) 639 | when conditions.is_a?(Hash) then 640 | connection.query_expression_from_hash(conditions) 641 | else 642 | conditions 643 | end 644 | end 645 | 646 | # This will currently return and's, or's and betweens. Doesn't hurt anything, but could remove. 647 | def parse_condition_fields(conditions) 648 | return nil unless conditions && conditions.present? && conditions.is_a?(Array) 649 | rx = /\b(\w*)[\s|>=|<=|!=|=|>|<|like|between]/ 650 | fields = conditions[0].scan(rx) 651 | # puts 'condition_fields = ' + fields.inspect 652 | fields.flatten 653 | end 654 | 655 | end 656 | 657 | public 658 | 659 | # instance attributes 660 | attr_accessor :attributes 661 | 662 | # item name 663 | attr_accessor :id 664 | 665 | # Create new Item instance. 666 | # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }. 667 | # 668 | # item = Client.new('name' => 'Jon', 'toys' => ['girls', 'beer', 'pub']) 669 | # puts item.inspect #=> #["Jon"], "toys"=>["girls", "beer", "pub"]}> 670 | # item.save #=> {"name"=>["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]} 671 | # puts item.inspect #=> #["Jon"], "id"=>"c03edb7e-e45c-11dc-bede-001bfc466dd7", "toys"=>["girls", "beer", "pub"]}> 672 | # 673 | def initialize(attrs={}) 674 | @attributes = uniq_values(attrs) 675 | @new_record = true 676 | end 677 | 678 | # This is to separate initialization from user vs coming from db (ie: find()) 679 | def initialize_from_db(attrs={}) 680 | initialize(attrs) 681 | end 682 | 683 | # Create and save new Item instance. 684 | # +Attributes+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }. 685 | # 686 | # item = Client.create('name' => 'Cat', 'toys' => ['Jons socks', 'mice', 'clew']) 687 | # puts item.inspect #=> #["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "mice", "clew"]}> 688 | # 689 | def self.create(attributes={}) 690 | item = self.new(attributes) 691 | item.save 692 | item 693 | end 694 | 695 | # Returns an item id. Same as: item['id'] or item.attributes['id'] 696 | def id 697 | @attributes['id'] 698 | end 699 | 700 | # Sets an item id. 701 | def id=(id) 702 | @attributes['id'] = id.to_s 703 | end 704 | 705 | # Returns a hash of all the attributes. 706 | # 707 | # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]} 708 | # 709 | def attributes 710 | @attributes.dup 711 | end 712 | 713 | # Allows one to set all the attributes at once by passing in a hash with keys matching the attribute names. 714 | # if '+id+' attribute is not set in new attributes has then it being derived from old attributes. 715 | # 716 | # puts item.attributes.inspect #=> {"name"=>["Cat"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["Jons socks", "clew", "mice"]} 717 | # # set new attributes ('id' is missed) 718 | # item.attributes = { 'name'=>'Dog', 'toys'=>['bones','cats'] } 719 | # puts item.attributes.inspect #=> {"name"=>["Dog"], "id"=>"2937601a-e45d-11dc-a75f-001bfc466dd7", "toys"=>["bones", "cats"]} 720 | # # set new attributes ('id' is set) 721 | # item.attributes = { 'id' => 'blah-blah', 'name'=>'Birds', 'toys'=>['seeds','dogs tail'] } 722 | # puts item.attributes.inspect #=> {"name"=>["Birds"], "id"=>"blah-blah", "toys"=>["seeds", "dogs tail"]} 723 | # 724 | def attributes=(attrs) 725 | old_id = @attributes['id'] 726 | @attributes = uniq_values(attrs) 727 | @attributes['id'] = old_id if @attributes['id'].blank? && !old_id.blank? 728 | self.attributes 729 | end 730 | 731 | def connection 732 | self.class.connection 733 | end 734 | 735 | # Item domain name. 736 | def domain 737 | self.class.domain 738 | end 739 | 740 | # Returns the values of the attribute identified by +attribute+. 741 | # 742 | # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"] 743 | # 744 | def [](attribute) 745 | @attributes[attribute.to_s] 746 | end 747 | 748 | # Updates the attribute identified by +attribute+ with the specified +values+. 749 | # 750 | # puts item['Cat'].inspect #=> ["Jons socks", "clew", "mice"] 751 | # item['Cat'] = ["Whiskas", "chicken"] 752 | # puts item['Cat'].inspect #=> ["Whiskas", "chicken"] 753 | # 754 | def []=(attribute, values) 755 | attribute = attribute.to_s 756 | @attributes[attribute] = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values] 757 | 758 | end 759 | 760 | # Reload attributes from SDB. Replaces in-memory attributes. 761 | # 762 | # item = Client.find_by_name('Cat') #=> #"2937601a-e45d-11dc-a75f-001bfc466dd7"}, @new_record=false> 763 | # item.reload #=> #"2937601a-e45d-11dc-a75f-001bfc466dd7", "name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]}, @new_record=false> 764 | # 765 | def reload 766 | raise_on_id_absence 767 | old_id = id 768 | attrs = connection.get_attributes(domain, id)[:attributes] 769 | @attributes = {} 770 | unless attrs.blank? 771 | attrs.each { |attribute, values| @attributes[attribute] = values } 772 | @attributes['id'] = old_id 773 | end 774 | mark_as_old 775 | @attributes 776 | end 777 | 778 | # Reload a set of attributes from SDB. Adds the loaded list to in-memory data. 779 | # +attrs_list+ is an array or comma separated list of attributes names. 780 | # Returns a hash of loaded attributes. 781 | # 782 | # This is not the best method to get a bunch of attributes because 783 | # a web service call is being performed for every attribute. 784 | # 785 | # item = Client.find_by_name('Cat') 786 | # item.reload_attributes('toys', 'name') #=> {"name"=>["Cat"], "toys"=>["Jons socks", "clew", "mice"]} 787 | # 788 | def reload_attributes(*attrs_list) 789 | raise_on_id_absence 790 | attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s } 791 | attrs_list.delete('id') 792 | result = {} 793 | attrs_list.flatten.uniq.each do |attribute| 794 | attribute = attribute.to_s 795 | values = connection.get_attributes(domain, id, attribute)[:attributes][attribute] 796 | unless values.blank? 797 | @attributes[attribute] = result[attribute] = values 798 | else 799 | @attributes.delete(attribute) 800 | end 801 | end 802 | mark_as_old 803 | result 804 | end 805 | 806 | # Stores in-memory attributes to SDB. 807 | # Adds the attributes values to already stored at SDB. 808 | # Returns a hash of stored attributes. 809 | # 810 | # sandy = Client.new(:name => 'Sandy') #=> #["Sandy"]}, @new_record=true> 811 | # sandy['toys'] = 'boys' 812 | # sandy.put 813 | # sandy['toys'] = 'patchwork' 814 | # sandy.put 815 | # sandy['toys'] = 'kids' 816 | # sandy.put 817 | # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]} 818 | # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]} 819 | # 820 | # compare to +save+ method 821 | def put 822 | @attributes = uniq_values(@attributes) 823 | prepare_for_update 824 | attrs = @attributes.dup 825 | attrs.delete('id') 826 | connection.put_attributes(domain, id, attrs) unless attrs.blank? 827 | connection.put_attributes(domain, id, {'id' => id}, :replace) 828 | mark_as_old 829 | @attributes 830 | end 831 | 832 | # Stores specified attributes. 833 | # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }. 834 | # Returns a hash of saved attributes. 835 | # 836 | # see to +put+ method 837 | def put_attributes(attrs) 838 | attrs = uniq_values(attrs) 839 | prepare_for_update 840 | # if 'id' is present in attrs hash: 841 | # replace internal 'id' attribute and remove it from the attributes to be sent 842 | @attributes['id'] = attrs['id'] unless attrs['id'].blank? 843 | attrs.delete('id') 844 | # add new values to all attributes from list 845 | connection.put_attributes(domain, id, attrs) unless attrs.blank? 846 | connection.put_attributes(domain, id, {'id' => id}, :replace) 847 | attrs.each do |attribute, values| 848 | @attributes[attribute] ||= [] 849 | @attributes[attribute] += values 850 | @attributes[attribute].uniq! 851 | end 852 | mark_as_old 853 | attributes 854 | end 855 | 856 | # Store in-memory attributes to SDB. 857 | # Replaces the attributes values already stored at SDB by in-memory data. 858 | # Returns a hash of stored attributes. 859 | # 860 | # sandy = Client.new(:name => 'Sandy') #=> #["Sandy"]}, @new_record=true> 861 | # sandy['toys'] = 'boys' 862 | # sandy.save 863 | # sandy['toys'] = 'patchwork' 864 | # sandy.save 865 | # sandy['toys'] = 'kids' 866 | # sandy.save 867 | # puts sandy.attributes.inspect #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]} 868 | # sandy.reload #=> {"name"=>["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["kids"]} 869 | # 870 | # Options: 871 | # - :except => Array of attributes to NOT save 872 | # 873 | # compare to +put+ method 874 | def save2(options={}) 875 | options[:create_domain] = true if options[:create_domain].nil? 876 | pre_save2 877 | atts_to_save = @attributes.dup 878 | #puts 'atts_to_save=' + atts_to_save.inspect 879 | #options = params.first.is_a?(Hash) ? params.pop : {} 880 | if options[:except] 881 | options[:except].each do |e| 882 | atts_to_save.delete(e).inspect 883 | end 884 | end 885 | if options[:dirty] # Only used in simple_record right now 886 | # only save if the attribute is dirty 887 | dirty_atts = options[:dirty_atts] 888 | atts_to_save.delete_if { |key, value| !dirty_atts.has_key?(key) } 889 | end 890 | dom = options[:domain] || domain 891 | #puts 'atts_to_save2=' + atts_to_save.inspect 892 | connection.put_attributes(dom, id, atts_to_save, :replace, options) 893 | apres_save2 894 | @attributes 895 | end 896 | 897 | def pre_save2 898 | @attributes = uniq_values(@attributes) 899 | prepare_for_update 900 | end 901 | 902 | def apres_save2 903 | mark_as_old 904 | end 905 | 906 | # Replaces the attributes at SDB by the given values. 907 | # +Attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }. 908 | # The other in-memory attributes are not being saved. 909 | # Returns a hash of stored attributes. 910 | # 911 | # see +save+ method 912 | def save_attributes(attrs) 913 | prepare_for_update 914 | attrs = uniq_values(attrs) 915 | # if 'id' is present in attrs hash then replace internal 'id' attribute 916 | unless attrs['id'].blank? 917 | @attributes['id'] = attrs['id'] 918 | else 919 | attrs['id'] = id 920 | end 921 | connection.put_attributes(domain, id, attrs, :replace) unless attrs.blank? 922 | attrs.each { |attribute, values| attrs[attribute] = values } 923 | mark_as_old 924 | attrs 925 | end 926 | 927 | # Remove specified values from corresponding attributes. 928 | # +attrs+ is a hash: { attribute1 => values1, ..., attributeN => valuesN }. 929 | # 930 | # sandy = Client.find_by_name 'Sandy' 931 | # sandy.reload 932 | # puts sandy.inspect #=> #["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"]}> 933 | # puts sandy.delete_values('toys' => 'patchwork') #=> { 'toys' => ['patchwork'] } 934 | # puts sandy.inspect #=> #["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids"]}> 935 | # 936 | def delete_values(attrs) 937 | raise_on_id_absence 938 | attrs = uniq_values(attrs) 939 | attrs.delete('id') 940 | unless attrs.blank? 941 | connection.delete_attributes(domain, id, attrs) 942 | attrs.each do |attribute, values| 943 | # remove the values from the attribute 944 | if @attributes[attribute] 945 | @attributes[attribute] -= values 946 | else 947 | # if the attribute is unknown remove it from a resulting list of fixed attributes 948 | attrs.delete(attribute) 949 | end 950 | end 951 | end 952 | attrs 953 | end 954 | 955 | # Removes specified attributes from the item. 956 | # +attrs_list+ is an array or comma separated list of attributes names. 957 | # Returns the list of deleted attributes. 958 | # 959 | # sandy = Client.find_by_name 'Sandy' 960 | # sandy.reload 961 | # puts sandy.inspect #=> #["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}> 962 | # puts sandy.delete_attributes('toys') #=> ['toys'] 963 | # puts sandy.inspect #=> #["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7"}> 964 | # 965 | def delete_attributes(*attrs_list) 966 | raise_on_id_absence 967 | attrs_list = attrs_list.flatten.map { |attribute| attribute.to_s } 968 | attrs_list.delete('id') 969 | unless attrs_list.blank? 970 | connection.delete_attributes(domain, id, attrs_list) 971 | attrs_list.each { |attribute| @attributes.delete(attribute) } 972 | end 973 | attrs_list 974 | end 975 | 976 | # Delete the Item entirely from SDB. 977 | # 978 | # sandy = Client.find_by_name 'Sandy' 979 | # sandy.reload 980 | # sandy.inspect #=> #["Sandy"], "id"=>"b2832ce2-e461-11dc-b13c-001bfc466dd7", "toys"=>["boys", "kids", "patchwork"}> 981 | # puts sandy.delete 982 | # sandy.reload 983 | # puts sandy.inspect #=> # 984 | # 985 | def delete(options={}) 986 | raise_on_id_absence 987 | connection.delete_attributes(options[:domain] || domain, id) 988 | end 989 | 990 | # Returns true if this object hasn�t been saved yet. 991 | def new_record? 992 | @new_record 993 | end 994 | 995 | def mark_as_old # :nodoc: 996 | @new_record = false 997 | end 998 | 999 | private 1000 | 1001 | def raise_on_id_absence 1002 | raise ActiveSdbError.new('Unknown record id') unless id 1003 | end 1004 | 1005 | def prepare_for_update 1006 | @attributes['id'] = self.class.generate_id if @attributes['id'].blank? 1007 | end 1008 | 1009 | def uniq_values(attributes=nil) # :nodoc: 1010 | attrs = {} 1011 | attributes.each do |attribute, values| 1012 | attribute = attribute.to_s 1013 | newval = attribute == 'id' ? values.to_s : values.is_a?(Array) ? values.uniq : [values] 1014 | attrs[attribute] = newval 1015 | if newval.blank? 1016 | # puts "VALUE IS BLANK " + attribute.to_s + " val=" + values.inspect 1017 | attrs.delete(attribute) 1018 | end 1019 | end 1020 | attrs 1021 | end 1022 | 1023 | end 1024 | end 1025 | end 1026 | -------------------------------------------------------------------------------- /lib/simple_record/attributes.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | module Attributes 3 | # For all things related to defining attributes. 4 | 5 | 6 | def self.included(base) 7 | #puts 'Callbacks included in ' + base.inspect 8 | =begin 9 | instance_eval <<-endofeval 10 | 11 | def self.defined_attributes 12 | #puts 'class defined_attributes' 13 | @attributes ||= {} 14 | @attributes 15 | endendofeval 16 | endofeval 17 | =end 18 | 19 | end 20 | 21 | 22 | module ClassMethods 23 | 24 | 25 | # Add configuration to this particular class. 26 | # :single_clob=> true/false. If true will store all clobs as a single object in s3. Default is false. 27 | def sr_config(options={}) 28 | get_sr_config 29 | @sr_config.merge!(options) 30 | end 31 | 32 | def get_sr_config 33 | @sr_config ||= {} 34 | end 35 | 36 | def defined_attributes 37 | @attributes ||= {} 38 | @attributes 39 | end 40 | 41 | def has_attributes(*args) 42 | has_attributes2(args) 43 | end 44 | 45 | def has_attributes2(args, options_for_all={}) 46 | # puts 'args=' + args.inspect 47 | # puts 'options_for_all = ' + options_for_all.inspect 48 | args.each do |arg| 49 | arg_options = {} 50 | if arg.is_a?(Hash) 51 | # then attribute may have extra options 52 | arg_options = arg 53 | arg = arg_options[:name].to_sym 54 | else 55 | arg = arg.to_sym 56 | end 57 | type = options_for_all[:type] || :string 58 | attr = Attribute.new(type, arg_options) 59 | defined_attributes[arg] = attr if defined_attributes[arg].nil? 60 | 61 | # define reader method 62 | arg_s = arg.to_s # to get rid of all the to_s calls 63 | send(:define_method, arg) do 64 | ret = get_attribute(arg) 65 | return ret 66 | end 67 | 68 | # define writer method 69 | send(:define_method, arg_s+"=") do |value| 70 | set(arg, value) 71 | end 72 | 73 | define_dirty_methods(arg_s) 74 | end 75 | end 76 | 77 | def define_dirty_methods(arg_s) 78 | # Now for dirty methods: http://api.rubyonrails.org/classes/ActiveRecord/Dirty.html 79 | # define changed? method 80 | send(:define_method, arg_s + "_changed?") do 81 | @dirty.has_key?(sdb_att_name(arg_s)) 82 | end 83 | 84 | # define change method 85 | send(:define_method, arg_s + "_change") do 86 | old_val = @dirty[sdb_att_name(arg_s)] 87 | [old_val, get_attribute(arg_s)] 88 | end 89 | 90 | # define was method 91 | send(:define_method, arg_s + "_was") do 92 | old_val = @dirty[sdb_att_name(arg_s)] 93 | old_val 94 | end 95 | end 96 | 97 | def has_strings(*args) 98 | has_attributes(*args) 99 | end 100 | 101 | def has_ints(*args) 102 | has_attributes(*args) 103 | are_ints(*args) 104 | end 105 | 106 | def has_floats(*args) 107 | has_attributes(*args) 108 | are_floats(*args) 109 | end 110 | 111 | def has_dates(*args) 112 | has_attributes(*args) 113 | are_dates(*args) 114 | end 115 | 116 | def has_booleans(*args) 117 | has_attributes(*args) 118 | are_booleans(*args) 119 | end 120 | 121 | def are_ints(*args) 122 | # puts 'calling are_ints: ' + args.inspect 123 | args.each do |arg| 124 | defined_attributes[arg.to_sym].type = :int 125 | end 126 | end 127 | 128 | def are_floats(*args) 129 | # puts 'calling are_ints: ' + args.inspect 130 | args.each do |arg| 131 | defined_attributes[arg.to_sym].type = :float 132 | end 133 | end 134 | 135 | def are_dates(*args) 136 | args.each do |arg| 137 | defined_attributes[arg.to_sym].type = :date 138 | end 139 | end 140 | 141 | def are_booleans(*args) 142 | args.each do |arg| 143 | defined_attributes[arg.to_sym].type = :boolean 144 | end 145 | end 146 | 147 | def has_clobs(*args) 148 | has_attributes2(args, :type=>:clob) 149 | 150 | end 151 | 152 | def virtuals 153 | @virtuals ||= [] 154 | @virtuals 155 | end 156 | 157 | def has_virtuals(*args) 158 | virtuals.concat(args) 159 | args.each do |arg| 160 | #we just create the accessor functions here, the actual instance variable is created during initialize 161 | attr_accessor(arg) 162 | end 163 | end 164 | 165 | # One belongs_to association per call. Call multiple times if there are more than one. 166 | # 167 | # This method will also create an {association)_id method that will return the ID of the foreign object 168 | # without actually materializing it. 169 | # 170 | # options: 171 | # :class_name=>"User" - to change the default class to use 172 | def belongs_to(association_id, options = {}) 173 | arg = association_id 174 | arg_s = arg.to_s 175 | arg_id = arg.to_s + '_id' 176 | attribute = Attribute.new(:belongs_to, options) 177 | defined_attributes[arg] = attribute 178 | 179 | # todo: should also handle foreign_key http://74.125.95.132/search?q=cache:KqLkxuXiBBQJ:wiki.rubyonrails.org/rails/show/belongs_to+rails+belongs_to&hl=en&ct=clnk&cd=1&gl=us 180 | # puts "arg_id=#{arg}_id" 181 | # puts "is defined? " + eval("(defined? #{arg}_id)").to_s 182 | # puts 'atts=' + @attributes.inspect 183 | 184 | # Define reader method 185 | send(:define_method, arg) do 186 | return get_attribute(arg) 187 | end 188 | 189 | 190 | # Define writer method 191 | send(:define_method, arg.to_s + "=") do |value| 192 | set(arg, value) 193 | end 194 | 195 | 196 | # Define ID reader method for reading the associated objects id without getting the entire object 197 | send(:define_method, arg_id) do 198 | get_attribute_sdb(arg_s) 199 | end 200 | 201 | # Define writer method for setting the _id directly without the associated object 202 | send(:define_method, arg_id + "=") do |value| 203 | # rb_att_name = arg_s # n2 = name.to_s[0, name.length-3] 204 | set(arg_id, value) 205 | # if value.nil? 206 | # self[arg_id] = nil unless self[arg_id].nil? # if it went from something to nil, then we have to remember and remove attribute on save 207 | # else 208 | # self[arg_id] = value 209 | # end 210 | end 211 | 212 | send(:define_method, "create_"+arg.to_s) do |*params| 213 | newsubrecord=eval(arg.to_s.classify).new(*params) 214 | newsubrecord.save 215 | arg_id = arg.to_s + '_id' 216 | self[arg_id]=newsubrecord.id 217 | end 218 | 219 | define_dirty_methods(arg_s) 220 | end 221 | 222 | # allows foreign key through class_name 223 | # i.e. options[:class_name] = 'User' 224 | def has_many(association_id, options = {}) 225 | 226 | send(:define_method, association_id) do 227 | return eval(%{Activerecordtosdb_subrecord_array.new('#{options[:class_name] ? options[:class_name] : association_id}', '#{options[:class_name] ? association_id.to_s : self.class.name}', id)}) 228 | end 229 | end 230 | 231 | def has_many(association_id, options = {}) 232 | send(:define_method, association_id) do 233 | return eval(%{Activerecordtosdb_subrecord_array.new('#{options[:class_name] ? options[:class_name] : association_id}', '#{options[:class_name] ? association_id.to_s : self.class.name}', id)}).first 234 | end 235 | end 236 | 237 | 238 | end 239 | 240 | def handle_virtuals(attrs) 241 | #puts 'handle_virtuals' 242 | self.class.virtuals.each do |virtual| 243 | # puts 'virtual=' + virtual.inspect 244 | #we first copy the information for the virtual to an instance variable of the same name 245 | send("#{virtual}=", attrs[virtual]) 246 | #eval("@#{virtual}=attrs['#{virtual}']") 247 | #and then remove the parameter before it is passed to initialize, so that it is NOT sent to SimpleDB 248 | attrs.delete(virtual) 249 | #eval("attrs.delete('#{virtual}')") 250 | end 251 | end 252 | 253 | 254 | def set(name, value, dirtify=true) 255 | # puts "SET #{name}=#{value.inspect}" if SimpleRecord.logging? 256 | # puts "self=" + self.inspect 257 | attname = name.to_s # default attname 258 | name = name.to_sym 259 | att_meta = get_att_meta(name) 260 | store_rb_val = false 261 | if att_meta.nil? 262 | # check if it ends with id and see if att_meta is there 263 | ends_with = name.to_s[-3, 3] 264 | if ends_with == "_id" 265 | # puts 'ends with id' 266 | n2 = name.to_s[0, name.length-3] 267 | # puts 'n2=' + n2 268 | att_meta = defined_attributes_local[n2.to_sym] 269 | # puts 'defined_attributes_local=' + defined_attributes_local.inspect 270 | attname = name.to_s 271 | attvalue = value 272 | name = n2.to_sym 273 | end 274 | return if att_meta.nil? 275 | else 276 | if att_meta.type == :belongs_to 277 | ends_with = name.to_s[-3, 3] 278 | if ends_with == "_id" 279 | att_name = name.to_s 280 | attvalue = value 281 | else 282 | attname = name.to_s + '_id' 283 | attvalue = value.nil? ? nil : value.id 284 | store_rb_val = true 285 | end 286 | elsif att_meta.type == :clob 287 | make_dirty(name, value) if dirtify 288 | @lobs[name] = value 289 | return 290 | else 291 | attname = name.to_s 292 | attvalue = att_meta.init_value(value) 293 | # attvalue = value 294 | #puts 'converted ' + value.inspect + ' to ' + attvalue.inspect 295 | end 296 | end 297 | attvalue = strip_array(attvalue) 298 | make_dirty(name, attvalue) if dirtify 299 | # puts "ARG=#{attname.to_s} setting to #{attvalue}" 300 | sdb_val = ruby_to_sdb(name, attvalue) 301 | # puts "sdb_val=" + sdb_val.to_s 302 | @attributes[attname] = sdb_val 303 | # attvalue = wrap_if_required(name, attvalue, sdb_val) 304 | # puts 'attvalue2=' + attvalue.to_s 305 | 306 | if store_rb_val 307 | @attributes_rb[name.to_s] = value 308 | else 309 | @attributes_rb.delete(name.to_s) 310 | end 311 | 312 | end 313 | 314 | 315 | def set_attribute_sdb(name, val) 316 | @attributes[sdb_att_name(name)] = val 317 | end 318 | 319 | 320 | def get_attribute_sdb(name) 321 | name = name.to_sym 322 | ret = strip_array(@attributes[sdb_att_name(name)]) 323 | return ret 324 | end 325 | 326 | # Since SimpleDB supports multiple attributes per value, the values are an array. 327 | # This method will return the value unwrapped if it's the only, otherwise it will return the array. 328 | def get_attribute(name) 329 | # puts "get_attribute #{name}" 330 | # Check if this arg is already converted 331 | name_s = name.to_s 332 | name = name.to_sym 333 | att_meta = get_att_meta(name) 334 | # puts "att_meta for #{name}: " + att_meta.inspect 335 | if att_meta && att_meta.type == :clob 336 | ret = @lobs[name] 337 | # puts 'get_attribute clob ' + ret.inspect 338 | if ret 339 | if ret.is_a? RemoteNil 340 | return nil 341 | else 342 | return ret 343 | end 344 | end 345 | # get it from s3 346 | unless new_record? 347 | if self.class.get_sr_config[:single_clob] 348 | begin 349 | single_clob = s3_bucket(false, :s3_bucket=>:new).get(single_clob_id) 350 | single_clob = JSON.parse(single_clob) 351 | # puts "single_clob=" + single_clob.inspect 352 | single_clob.each_pair do |name2, val| 353 | @lobs[name2.to_sym] = val 354 | end 355 | ret = @lobs[name] 356 | SimpleRecord.stats.s3_gets += 1 357 | rescue Aws::AwsError => ex 358 | if ex.include?(/NoSuchKey/) || ex.include?(/NoSuchBucket/) 359 | ret = nil 360 | else 361 | raise ex 362 | end 363 | end 364 | else 365 | begin 366 | ret = s3_bucket.get(s3_lob_id(name)) 367 | # puts 'got from s3 ' + ret.inspect 368 | SimpleRecord.stats.s3_gets += 1 369 | rescue Aws::AwsError => ex 370 | if ex.include?(/NoSuchKey/) || ex.include?(/NoSuchBucket/) 371 | ret = nil 372 | else 373 | raise ex 374 | end 375 | end 376 | end 377 | 378 | if ret.nil? 379 | ret = RemoteNil.new 380 | end 381 | end 382 | @lobs[name] = ret 383 | return nil if ret.is_a? RemoteNil 384 | return ret 385 | else 386 | @attributes_rb = {} unless @attributes_rb # was getting errors after upgrade. 387 | ret = @attributes_rb[name_s] # instance_variable_get(instance_var) 388 | return ret unless ret.nil? 389 | return nil if ret.is_a? RemoteNil 390 | ret = get_attribute_sdb(name) 391 | # p ret 392 | ret = sdb_to_ruby(name, ret) 393 | # p ret 394 | @attributes_rb[name_s] = ret 395 | return ret 396 | end 397 | 398 | end 399 | 400 | 401 | private 402 | def set_attributes(atts) 403 | atts.each_pair do |k, v| 404 | set(k, v) 405 | end 406 | end 407 | 408 | 409 | # Holds information about an attribute 410 | class Attribute 411 | attr_accessor :type, :options 412 | 413 | def initialize(type, options=nil) 414 | @type = type 415 | @options = options 416 | end 417 | 418 | def init_value(value) 419 | return value if value.nil? 420 | ret = value 421 | case self.type 422 | when :int 423 | if value.is_a? Array 424 | ret = value.collect { |x| x.to_i } 425 | else 426 | ret = value.to_i 427 | end 428 | end 429 | ret 430 | end 431 | 432 | end 433 | 434 | end 435 | end 436 | -------------------------------------------------------------------------------- /lib/simple_record/callbacks.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | 3 | # For Rails3 support 4 | module Callbacks3 5 | 6 | # def destroy #:nodoc: 7 | # _run_destroy_callbacks { super } 8 | # end 9 | # 10 | # private 11 | # 12 | # def create_or_update #:nodoc: 13 | # puts '3 create_or_update' 14 | # _run_save_callbacks { super } 15 | # end 16 | # 17 | # def create #:nodoc: 18 | # puts '3 create' 19 | # _run_create_callbacks { super } 20 | # end 21 | # 22 | # def update(*) #:nodoc: 23 | # puts '3 update' 24 | # _run_update_callbacks { super } 25 | # end 26 | end 27 | 28 | module Callbacks 29 | #this bit of code creates a "run_blank" function for everything value in the @@callbacks array. 30 | #this function can then be inserted in the appropriate place in the save, new, destroy, etc overrides 31 | #basically, this is how we recreate the callback functions 32 | @@callbacks=["before_validation", "before_validation_on_create", "before_validation_on_update", 33 | "after_validation", "after_validation_on_create", "after_validation_on_update", 34 | "before_save", "before_create", "before_update", "before_destroy", 35 | "after_create", "after_update", "after_save", 36 | "after_destroy"] 37 | 38 | def self.included(base) 39 | #puts 'Callbacks included in ' + base.inspect 40 | 41 | # puts "setup callbacks #{base.inspect}" 42 | base.instance_eval <<-endofeval 43 | 44 | def callbacks 45 | @callbacks ||= {} 46 | @callbacks 47 | end 48 | 49 | 50 | endofeval 51 | 52 | @@callbacks.each do |callback| 53 | base.class_eval <<-endofeval 54 | 55 | def run_#{callback} 56 | # puts 'CLASS CALLBACKS for ' + self.inspect + ' = ' + self.class.callbacks.inspect 57 | return true if self.class.callbacks.nil? 58 | cnames = self.class.callbacks['#{callback}'] 59 | cnames = [] if cnames.nil? 60 | # cnames += super.class.callbacks['#{callback}'] unless super.class.callbacks.nil? 61 | # puts 'cnames for #{callback} = ' + cnames.inspect 62 | return true if cnames.nil? 63 | cnames.each { |name| 64 | #puts 'run_ #{name}' 65 | if eval(name) == false # nil should be an ok return, only looking for false 66 | return false 67 | end 68 | } 69 | # super.run_#{callback} 70 | return true 71 | end 72 | 73 | endofeval 74 | 75 | #this bit of code creates a "run_blank" function for everything value in the @@callbacks array. 76 | #this function can then be inserted in the appropriate place in the save, new, destroy, etc overrides 77 | #basically, this is how we recreate the callback functions 78 | base.instance_eval <<-endofeval 79 | 80 | # puts 'defining callback=' + callback + ' for ' + self.inspect 81 | #we first have to make an initialized array for each of the callbacks, to prevent problems if they are not called 82 | 83 | def #{callback}(*args) 84 | # puts 'callback called in ' + self.inspect + ' with ' + args.inspect 85 | 86 | #make_dirty(arg_s, value) 87 | #self[arg.to_s]=value 88 | #puts 'value in callback #{callback}=' + value.to_s 89 | args.each do |arg| 90 | cnames = callbacks['#{callback}'] 91 | #puts '\tcnames1=' + cnames.inspect + ' for class ' + self.inspect 92 | cnames = [] if cnames.nil? 93 | cnames << arg.to_s if cnames.index(arg.to_s).nil? 94 | #puts '\tcnames2=' + cnames.inspect 95 | callbacks['#{callback}'] = cnames 96 | end 97 | end 98 | 99 | endofeval 100 | end 101 | end 102 | 103 | def before_destroy() 104 | end 105 | 106 | def after_destroy() 107 | end 108 | 109 | 110 | def self.setup_callbacks(base) 111 | 112 | end 113 | 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/simple_record/encryptor.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | # Thanks to http://github.com/shuber/encryptor 4 | module SimpleRecord 5 | module Encryptor 6 | # The default options to use when calling the encrypt and decrypt methods 7 | # 8 | # Defaults to { :algorithm => 'aes-256-cbc' } 9 | # 10 | # Run 'openssl list-cipher-commands' in your terminal to view a list all cipher algorithms that are supported on your platform 11 | class << self; attr_accessor :default_options; end 12 | self.default_options = { :algorithm => 'aes-256-cbc' } 13 | 14 | # Encrypts a :value with a specified :key 15 | # 16 | # Optionally accepts :iv and :algorithm options 17 | # 18 | # Example 19 | # 20 | # encrypted_value = Huberry::Encryptor.encrypt(:value => 'some string to encrypt', :key => 'some secret key') 21 | def self.encrypt(options) 22 | crypt :encrypt, options 23 | end 24 | 25 | # Decrypts a :value with a specified :key 26 | # 27 | # Optionally accepts :iv and :algorithm options 28 | # 29 | # Example 30 | # 31 | # decrypted_value = Huberry::Encryptor.decrypt(:value => 'some encrypted string', :key => 'some secret key') 32 | def self.decrypt(options) 33 | crypt :decrypt, options 34 | end 35 | 36 | protected 37 | 38 | def self.crypt(cipher_method, options = {}) 39 | options = default_options.merge(options) 40 | cipher = OpenSSL::Cipher::Cipher.new(options[:algorithm]) 41 | cipher.send(cipher_method) 42 | secret_key = Digest::SHA512.hexdigest(options[:key]) 43 | if options[:iv] 44 | cipher.key = secret_key 45 | cipher.iv = options[:iv] 46 | else 47 | cipher.pkcs5_keyivgen(secret_key) 48 | end 49 | result = cipher.update(options[:value]) 50 | result << cipher.final 51 | end 52 | end 53 | end 54 | 55 | -------------------------------------------------------------------------------- /lib/simple_record/errors.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | 3 | class SimpleRecordError < StandardError 4 | 5 | end 6 | 7 | 8 | class RecordNotSaved < SimpleRecordError 9 | attr_accessor :record 10 | 11 | def initialize(record=nil) 12 | @record = record 13 | super("Validation failed: #{@record.errors.full_messages.join(", ")}") 14 | end 15 | end 16 | 17 | class RecordNotFound < SimpleRecordError 18 | 19 | end 20 | 21 | class Error 22 | attr_accessor :base, :attribute, :type, :message, :options 23 | 24 | def initialize(base, attribute, message, options = {}) 25 | self.base = base 26 | self.attribute = attribute 27 | self.message = message 28 | end 29 | 30 | def message 31 | # When type is a string, it means that we do not have to do a lookup, because 32 | # the user already sent the "final" message. 33 | generate_message 34 | end 35 | 36 | def full_message 37 | attribute.to_s == 'base' ? message : generate_full_message() 38 | end 39 | 40 | alias :to_s :message 41 | 42 | def generate_message(options = {}) 43 | @message 44 | end 45 | 46 | def generate_full_message(options = {}) 47 | "#{attribute.to_s} #{message}" 48 | end 49 | end 50 | 51 | class SimpleRecord_errors 52 | attr_reader :errors 53 | 54 | def initialize(*params) 55 | super(*params) 56 | @errors={} 57 | end 58 | 59 | def add_to_base(msg) 60 | add(:base, msg) 61 | end 62 | 63 | def add(attribute, message, options = {}) 64 | # options param note used; just for drop in compatibility with ActiveRecord 65 | error, message = message, nil if message.is_a?(Error) 66 | @errors[attribute.to_s] ||= [] 67 | @errors[attribute.to_s] << (error || Error.new(@base, attribute, message, options)) 68 | end 69 | 70 | def length 71 | return @errors.length 72 | end 73 | 74 | alias count length 75 | alias size length 76 | 77 | def full_messages 78 | @errors.values.inject([]) do |full_messages, errors| 79 | full_messages + errors.map { |error| error.full_message } 80 | end 81 | end 82 | 83 | def clear 84 | @errors.clear 85 | end 86 | 87 | def empty? 88 | @errors.empty? 89 | end 90 | 91 | def on(attribute) 92 | attribute = attribute.to_s 93 | return nil unless @errors.has_key?(attribute) 94 | errors = @errors[attribute].map(&:to_s) 95 | errors.size == 1 ? errors.first : errors 96 | end 97 | 98 | alias :[] :on 99 | 100 | def on_base 101 | on(:base) 102 | end 103 | end 104 | 105 | end 106 | 107 | -------------------------------------------------------------------------------- /lib/simple_record/json.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | module Json 3 | 4 | def self.included(base) 5 | base.extend ClassMethods 6 | end 7 | 8 | module ClassMethods 9 | 10 | def json_create(object) 11 | obj = new 12 | for key, value in object 13 | next if key == 'json_class' 14 | if key == 'id' 15 | obj.id = value 16 | next 17 | end 18 | obj.set key, value 19 | end 20 | obj 21 | end 22 | 23 | def from_json(json_string) 24 | return JSON.parse(json_string) 25 | end 26 | 27 | end 28 | 29 | def as_json(options={}) 30 | puts 'SimpleRecord as_json called with options: ' + options.inspect 31 | result = { 32 | 'id' => self.id 33 | } 34 | result['json_class'] = self.class.name unless options && options[:exclude_json_class] 35 | defined_attributes_local.each_pair do |name, val| 36 | # puts name.to_s + "=" + val.inspect 37 | if val.type == :belongs_to 38 | result[name.to_s + "_id"] = get_attribute_sdb(name) 39 | else 40 | result[name] = get_attribute(name) 41 | end 42 | # puts 'result[name]=' + result[name].inspect 43 | end 44 | # ret = result.as_json(options) 45 | # puts 'ret=' + ret.inspect 46 | # return ret 47 | result 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/simple_record/logging.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | 3 | require 'csv' 4 | 5 | module Logging 6 | 7 | 8 | module ClassMethods 9 | def write_usage(type, domain, q_type, params, results) 10 | #puts 'params=' + params.inspect 11 | #puts 'logging_options=' + SimpleRecord.usage_logging_options.inspect 12 | if SimpleRecord.usage_logging_options 13 | type_options = SimpleRecord.usage_logging_options[type] 14 | if type_options 15 | file = type_options[:file] 16 | if file.nil? 17 | file = File.open(type_options[:filename], File.exists?(type_options[:filename]) ? "a" : "w") 18 | puts file.path 19 | type_options[:file] = file 20 | end 21 | conditions = params[:conditions][0] if params[:conditions] 22 | line = usage_line(type_options[:format], [type, domain, q_type, conditions, params[:order]], results[:request_id], results[:box_usage]) 23 | file.puts line 24 | type_options[:lines] = type_options[:lines] ? type_options[:lines] + 1 : 1 25 | #puts 'lines=' + type_options[:lines].to_s 26 | if type_options[:lines] % type_options[:lines_between_flushes] == 0 27 | #puts "flushing to file..." 28 | file.flush 29 | # sleep 20 30 | end 31 | # puts 'line=' + line 32 | end 33 | end 34 | end 35 | 36 | def usage_line(format, query_data, request_id, box_usage) 37 | if format == :csv 38 | line_data = [] 39 | line_data << Time.now.iso8601 40 | query_data.each do |r| 41 | line_data << r.to_s 42 | end 43 | line_data << request_id 44 | line_data << box_usage 45 | return CSV.generate_line(line_data) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | 52 | -------------------------------------------------------------------------------- /lib/simple_record/password.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha2' 2 | 3 | # Thanks to: http://www.zacharyfox.com/blog/ruby-on-rails/password-hashing 4 | module SimpleRecord 5 | # This module contains functions for hashing and storing passwords 6 | module Password 7 | 8 | # Generates a new salt and rehashes the password 9 | def Password.create_hash(password) 10 | salt = self.salt 11 | hash = self.hash(password, salt) 12 | self.store(hash, salt) 13 | end 14 | 15 | # Checks the password against the stored password 16 | def Password.check(password, store) 17 | hash = self.get_hash(store) 18 | salt = self.get_salt(store) 19 | if self.hash(password, salt) == hash 20 | true 21 | else 22 | false 23 | end 24 | end 25 | 26 | protected 27 | 28 | # Generates a psuedo-random 64 character string 29 | 30 | def Password.salt 31 | salt = '' 32 | 64.times { salt << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr } 33 | salt 34 | end 35 | 36 | # Generates a 128 character hash 37 | def Password.hash(password, salt) 38 | Digest::SHA512.hexdigest("#{password}:#{salt}") 39 | end 40 | 41 | # Mixes the hash and salt together for storage 42 | def Password.store(hash, salt) 43 | hash + salt 44 | end 45 | 46 | # Gets the hash from a stored password 47 | def Password.get_hash(store) 48 | store[0..127] 49 | end 50 | 51 | # Gets the salt from a stored password 52 | def Password.get_salt(store) 53 | store[128..192] 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /lib/simple_record/results_array.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | 3 | # 4 | # We need to make this behave as if the full set were loaded into the array. 5 | class ResultsArray 6 | include Enumerable 7 | 8 | attr_reader :next_token, :clz, :params, :items, :index, :box_usage, :request_id 9 | 10 | 11 | def initialize(clz=nil, params=[], results=nil, next_token=nil) 12 | @clz = clz 13 | #puts 'class=' + clz.inspect 14 | @params = params 15 | if @params.size <= 1 16 | options = {} 17 | @params[1] = options 18 | end 19 | @items = results[:items] 20 | @currentset_items = results[:items] 21 | @next_token = next_token 22 | # puts 'bu=' + results[:box_usage] 23 | @box_usage = results[:box_usage].to_f 24 | @request_id = results[:request_id] 25 | @options = @params[1] 26 | if @options[:page] 27 | load_to(@options[:per_page] * @options[:page]) 28 | @start_at = @options[:per_page] * (@options[:page] - 1) 29 | end 30 | @index = 0 31 | # puts 'RESULTS_ARRAY=' + self.inspect 32 | end 33 | 34 | def << (val) 35 | @items << val 36 | end 37 | 38 | def [](*i) 39 | # puts 'i.inspect=' + i.inspect 40 | # puts i.size.to_s 41 | # i.each do |x| 42 | # puts 'x=' + x.inspect + " -- " + x.class.name 43 | # end 44 | if i.size == 1 45 | # either fixnum or range 46 | x = i[0] 47 | if x.is_a?(Fixnum) 48 | load_to(x) 49 | else 50 | # range 51 | end_val = x.exclude_end? ? x.end-1 : x.end 52 | load_to(end_val) 53 | end 54 | elsif i.size == 2 55 | # two fixnums 56 | end_val = i[0] + i[1] 57 | load_to(end_val) 58 | end 59 | @items[*i] 60 | end 61 | 62 | # Will load items from SimpleDB up to i. 63 | def load_to(i) 64 | return if @items.size >= i 65 | while @items.size < i && !@next_token.nil? 66 | load_next_token_set 67 | end 68 | end 69 | 70 | def first 71 | @items[0] 72 | end 73 | 74 | def last 75 | @items[@items.length-1] 76 | end 77 | 78 | def empty? 79 | @items.empty? 80 | end 81 | 82 | def include?(obj) 83 | @items.include?(obj) 84 | end 85 | 86 | def size 87 | # if @options[:per_page] 88 | # return @items.size - @start_at 89 | # end 90 | if @next_token.nil? 91 | return @items.size 92 | end 93 | return @count if @count 94 | # puts '@params=' + @params.inspect 95 | params_for_count = @params.dup 96 | params_for_count[0] = :count 97 | params_for_count[1] = params_for_count[1].dup # for deep clone 98 | params_for_count[1].delete(:limit) 99 | params_for_count[1].delete(:per_token) 100 | params_for_count[1][:called_by] = :results_array 101 | 102 | # puts '@params2=' + @params.inspect 103 | # puts 'params_for_count=' + params_for_count.inspect 104 | @count = clz.find(*params_for_count) 105 | # puts '@count=' + @count.to_s 106 | @count 107 | end 108 | 109 | def length 110 | return size 111 | end 112 | 113 | def each(&blk) 114 | each2((@start_at || 0), &blk) 115 | end 116 | 117 | def each2(i, &blk) 118 | options = @params[1] 119 | # puts 'options=' + options.inspect 120 | limit = options[:limit] 121 | # puts 'limit=' + limit.inspect 122 | 123 | if i > @items.size 124 | i = @items.size 125 | end 126 | range = i..@items.size 127 | # puts 'range=' + range.inspect 128 | @items[range].each do |v| 129 | # puts "i=" + i.to_s 130 | yield v 131 | i += 1 132 | @index += 1 133 | if !limit.nil? && i >= limit 134 | return 135 | end 136 | end 137 | return if @clz.nil? 138 | 139 | # no more items, but is there a next token? 140 | unless @next_token.nil? 141 | #puts 'finding more items...' 142 | #puts 'params in block=' + params.inspect 143 | #puts "i from results_array = " + @i.to_s 144 | 145 | load_next_token_set 146 | each2(i, &blk) 147 | end 148 | end 149 | 150 | # for will_paginate support 151 | def total_pages 152 | #puts 'total_pages' 153 | # puts @params[1][:per_page].to_s 154 | return 1 if @params[1][:per_page].nil? 155 | ret = (size / @params[1][:per_page].to_f).ceil 156 | #puts 'ret=' + ret.to_s 157 | ret 158 | end 159 | 160 | def current_page 161 | return query_options[:page] || 1 162 | end 163 | 164 | def query_options 165 | return @options 166 | end 167 | 168 | def total_entries 169 | return size 170 | end 171 | 172 | # Helper method that is true when someone tries to fetch a page with a 173 | # larger number than the last page. Can be used in combination with flashes 174 | # and redirecting. 175 | def out_of_bounds? 176 | current_page > total_pages 177 | end 178 | 179 | # Current offset of the paginated collection. If we're on the first page, 180 | # it is always 0. If we're on the 2nd page and there are 30 entries per page, 181 | # the offset is 30. This property is useful if you want to render ordinals 182 | # side by side with records in the view: simply start with offset + 1. 183 | def offset 184 | (current_page - 1) * per_page 185 | end 186 | 187 | # current_page - 1 or nil if there is no previous page 188 | def previous_page 189 | current_page > 1 ? (current_page - 1) : nil 190 | end 191 | 192 | # current_page + 1 or nil if there is no next page 193 | def next_page 194 | current_page < total_pages ? (current_page + 1) : nil 195 | end 196 | 197 | def load_next_token_set 198 | options = @params[1] 199 | options[:next_token] = @next_token 200 | options[:called_by] = :results_array 201 | res = @clz.find(*@params) 202 | @currentset_items = res.items # get the real items array from the ResultsArray 203 | @currentset_items.each do |item| 204 | @items << item 205 | end 206 | @next_token = res.next_token 207 | end 208 | 209 | def delete(item) 210 | @items.delete(item) 211 | end 212 | 213 | def delete_at(index) 214 | @items.delete_at(index) 215 | end 216 | 217 | 218 | # A couple json serialization methods copied from active_support 219 | def as_json(options = nil) #:nodoc: 220 | # use encoder as a proxy to call as_json on all elements, to protect from circular references 221 | encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) 222 | map { |v| encoder.as_json(v) } 223 | end 224 | 225 | def encode_json(encoder) #:nodoc: 226 | # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly 227 | "[#{map { |v| v.encode_json(encoder) } * ','}]" 228 | end 229 | 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/simple_record/sharding.rb: -------------------------------------------------------------------------------- 1 | require 'concur' 2 | 3 | module SimpleRecord 4 | 5 | module Sharding 6 | 7 | def self.included(base) 8 | # base.extend ClassMethods 9 | end 10 | 11 | module ClassMethods 12 | 13 | def shard(options=nil) 14 | @sharding_options = options 15 | end 16 | 17 | def sharding_options 18 | @sharding_options 19 | end 20 | 21 | def is_sharded? 22 | @sharding_options 23 | end 24 | 25 | def find_sharded(*params) 26 | puts 'find_sharded ' + params.inspect 27 | 28 | options = params.size > 1 ? params[1] : {} 29 | 30 | if options[:shard] # User specified shard. 31 | shard = options[:shard] 32 | domains = shard.is_a?(Array) ? (shard.collect { |x| prefix_shard_name(x) }) : [prefix_shard_name(shard)] 33 | else 34 | domains = sharded_domains 35 | end 36 | # puts "sharded_domains=" + domains.inspect 37 | 38 | single = false 39 | by_ids = false 40 | case params.first 41 | when nil then 42 | raise "Invalid parameters passed to find: nil." 43 | when :all, :first, :count 44 | # nada 45 | else # single id 46 | by_ids = true 47 | unless params.first.is_a?(Array) 48 | single = true 49 | end 50 | end 51 | puts 'single? ' + single.inspect 52 | puts 'by_ids? ' + by_ids.inspect 53 | 54 | # todo: should have a global executor 55 | executor = options[:concurrent] ? Concur::Executor.new_multi_threaded_executor : Concur::Executor.new_single_threaded_executor 56 | results = nil 57 | if by_ids 58 | results = [] 59 | else 60 | results = ShardedResults.new(params) 61 | end 62 | futures = [] 63 | domains.each do |d| 64 | p2 = params.dup 65 | op2 = options.dup 66 | op2[:from] = d 67 | op2[:shard_find] = true 68 | p2[1] = op2 69 | 70 | futures << executor.execute do 71 | puts 'executing=' + p2.inspect 72 | # todo: catch RecordNotFound errors and throw later if there really isn't any record found. 73 | rs = find(*p2) 74 | puts 'rs=' + rs.inspect 75 | rs 76 | end 77 | end 78 | futures.each do |f| 79 | puts 'getting future ' + f.inspect 80 | if params.first == :first || single 81 | puts 'f.get=' + f.get.inspect 82 | return f.get if f.get 83 | elsif by_ids 84 | results << f.get if f.get 85 | else 86 | results.add_results f.get 87 | end 88 | end 89 | executor.shutdown 90 | # puts 'results=' + results.inspect 91 | if params.first == :first || single 92 | # Then we found nothing by this point so return nil 93 | return nil 94 | elsif params.first == :count 95 | return results.sum_count 96 | end 97 | results 98 | 99 | end 100 | 101 | def shards 102 | send(sharding_options[:shards]) 103 | end 104 | 105 | def prefix_shard_name(s) 106 | "#{domain}_#{s}" 107 | end 108 | 109 | 110 | def sharded_domains 111 | sharded_domains = [] 112 | shard_names = shards 113 | shard_names.each do |s| 114 | sharded_domains << prefix_shard_name(s) 115 | end 116 | sharded_domains 117 | end 118 | end 119 | 120 | def sharded_domain 121 | # puts 'getting sharded_domain' 122 | options = self.class.sharding_options 123 | # val = self.send(options[:on]) 124 | # puts "val=" + val.inspect 125 | # shards = options[:shards] # is user passed in static array of shards 126 | # if options[:shards].is_a?(Symbol) 127 | # shards = self.send(shards) 128 | # end 129 | sharded_domain = "#{domain}_#{self.send(options[:map])}" 130 | # puts "sharded_domain=" + sharded_domain.inspect 131 | sharded_domain 132 | end 133 | 134 | class ShardedResults 135 | include Enumerable 136 | 137 | def initialize(params) 138 | @params = params 139 | @options = params.size > 1 ? params[1] : {} 140 | @results_arrays = [] 141 | end 142 | 143 | def add_results(rs) 144 | # puts 'adding results=' + rs.inspect 145 | @results_arrays << rs 146 | end 147 | 148 | # only used for count queries 149 | def sum_count 150 | x = 0 151 | @results_arrays.each do |rs| 152 | x += rs if rs 153 | end 154 | x 155 | end 156 | 157 | def <<(val) 158 | raise "Not supported." 159 | end 160 | 161 | def element_at(index) 162 | @results_arrays.each do |rs| 163 | if rs.size > index 164 | return rs[index] 165 | end 166 | index -= rs.size 167 | end 168 | end 169 | 170 | def [](*i) 171 | if i.size == 1 172 | # puts '[] i=' + i.to_s 173 | index = i[0] 174 | return element_at(index) 175 | else 176 | offset = i[0] 177 | rows = i[1] 178 | ret = [] 179 | x = offset 180 | while x < (offset+rows) 181 | ret << element_at(x) 182 | x+=1 183 | end 184 | ret 185 | end 186 | end 187 | 188 | def first 189 | @results_arrays.first.first 190 | end 191 | 192 | def last 193 | @results_arrays.last.last 194 | end 195 | 196 | def empty? 197 | @results_arrays.each do |rs| 198 | return false if !rs.empty? 199 | end 200 | true 201 | end 202 | 203 | def include?(obj) 204 | @results_arrays.each do |rs| 205 | x = rs.include?(obj) 206 | return true if x 207 | end 208 | false 209 | end 210 | 211 | def size 212 | return @size if @size 213 | s = 0 214 | @results_arrays.each do |rs| 215 | # puts 'rs=' + rs.inspect 216 | # puts 'rs.size=' + rs.size.inspect 217 | s += rs.size 218 | end 219 | @size = s 220 | s 221 | end 222 | 223 | def length 224 | return size 225 | end 226 | 227 | def each(&blk) 228 | i = 0 229 | @results_arrays.each do |rs| 230 | rs.each(&blk) 231 | i+=1 232 | end 233 | end 234 | 235 | # for will_paginate support 236 | def total_pages 237 | # puts 'total_pages' 238 | # puts @params[1][:per_page].to_s 239 | return 1 if @params[1][:per_page].nil? 240 | ret = (size / @params[1][:per_page].to_f).ceil 241 | #puts 'ret=' + ret.to_s 242 | ret 243 | end 244 | 245 | def current_page 246 | return query_options[:page] || 1 247 | end 248 | 249 | def query_options 250 | return @options 251 | end 252 | 253 | def total_entries 254 | return size 255 | end 256 | 257 | # Helper method that is true when someone tries to fetch a page with a 258 | # larger number than the last page. Can be used in combination with flashes 259 | # and redirecting. 260 | def out_of_bounds? 261 | current_page > total_pages 262 | end 263 | 264 | # Current offset of the paginated collection. If we're on the first page, 265 | # it is always 0. If we're on the 2nd page and there are 30 entries per page, 266 | # the offset is 30. This property is useful if you want to render ordinals 267 | # side by side with records in the view: simply start with offset + 1. 268 | def offset 269 | (current_page - 1) * per_page 270 | end 271 | 272 | # current_page - 1 or nil if there is no previous page 273 | def previous_page 274 | current_page > 1 ? (current_page - 1) : nil 275 | end 276 | 277 | # current_page + 1 or nil if there is no next page 278 | def next_page 279 | current_page < total_pages ? (current_page + 1) : nil 280 | end 281 | 282 | 283 | def delete(item) 284 | raise "Not supported" 285 | end 286 | 287 | def delete_at(index) 288 | raise "Not supported" 289 | end 290 | 291 | end 292 | 293 | # Some hashing algorithms 294 | module Hashing 295 | def self.sdbm_hash(str, len=str.length) 296 | # puts 'sdbm_hash ' + str.inspect 297 | hash = 0 298 | len.times { |i| 299 | c = str[i] 300 | # puts "c=" + c.class.name + "--" + c.inspect + " -- " + c.ord.inspect 301 | c = c.ord 302 | hash = c + (hash << 6) + (hash << 16) - hash 303 | } 304 | # puts "hash=" + hash.inspect 305 | return hash 306 | end 307 | end 308 | end 309 | 310 | end 311 | -------------------------------------------------------------------------------- /lib/simple_record/stats.rb: -------------------------------------------------------------------------------- 1 | module SimpleRecord 2 | class Stats 3 | attr_accessor :selects, :saves, :deletes, :s3_puts, :s3_gets, :s3_deletes 4 | 5 | def initialize 6 | @selects = 0 7 | @saves = 0 8 | @deletes = 0 9 | @s3_puts = 0 10 | @s3_gets = 0 11 | @s3_deletes = 0 12 | end 13 | 14 | def clear 15 | self.selects = 0 16 | self.saves = 0 17 | self.deletes = 0 18 | self.s3_puts = 0 19 | self.s3_gets = 0 20 | self.s3_deletes = 0 21 | end 22 | end 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/simple_record/translations.rb: -------------------------------------------------------------------------------- 1 | # This module defines all the methods that perform data translations for storage and retrieval. 2 | module SimpleRecord 3 | module Translations 4 | 5 | @@offset = 9223372036854775808 6 | @@padding = 20 7 | @@date_format = "%Y-%m-%dT%H:%M:%S"; 8 | 9 | def ruby_to_string_val(att_meta, value) 10 | if att_meta.type == :int 11 | ret = Translations.pad_and_offset(value, att_meta) 12 | elsif att_meta.type == :date 13 | ret = Translations.pad_and_offset(value, att_meta) 14 | else 15 | ret = value.to_s 16 | end 17 | ret 18 | end 19 | 20 | # Time to second precision 21 | 22 | def ruby_to_sdb(name, value) 23 | return nil if value.nil? 24 | name = name.to_s 25 | # puts "Converting #{name} to sdb value=#{value}" 26 | # puts "atts_local=" + defined_attributes_local.inspect 27 | 28 | att_meta = get_att_meta(name) 29 | 30 | if value.is_a? Array 31 | ret = value.collect { |x| ruby_to_string_val(att_meta, x) } 32 | else 33 | ret = ruby_to_string_val(att_meta, value) 34 | end 35 | 36 | unless value.blank? 37 | if att_meta.options 38 | if att_meta.options[:encrypted] 39 | # puts "ENCRYPTING #{name} value #{value}" 40 | ret = Translations.encrypt(ret, att_meta.options[:encrypted]) 41 | # puts 'encrypted value=' + ret.to_s 42 | end 43 | if att_meta.options[:hashed] 44 | # puts "hashing #{name}" 45 | ret = Translations.pass_hash(ret) 46 | # puts "hashed value=" + ret.inspect 47 | end 48 | end 49 | end 50 | 51 | return ret 52 | 53 | end 54 | 55 | 56 | # Convert value from SimpleDB String version to real ruby value. 57 | def sdb_to_ruby(name, value) 58 | # puts 'sdb_to_ruby arg=' + name.inspect + ' - ' + name.class.name + ' - value=' + value.to_s 59 | return nil if value.nil? 60 | att_meta = get_att_meta(name) 61 | 62 | if att_meta.options 63 | if att_meta.options[:encrypted] 64 | value = Translations.decrypt(value, att_meta.options[:encrypted]) 65 | end 66 | if att_meta.options[:hashed] 67 | return PasswordHashed.new(value) 68 | end 69 | end 70 | 71 | 72 | if !has_id_on_end(name) && att_meta.type == :belongs_to 73 | class_name = att_meta.options[:class_name] || name.to_s[0...1].capitalize + name.to_s[1...name.to_s.length] 74 | # Camelize classnames with underscores (ie my_model.rb --> MyModel) 75 | class_name = class_name.camelize 76 | # puts "attr=" + @attributes[arg_id].inspect 77 | # puts 'val=' + @attributes[arg_id][0].inspect unless @attributes[arg_id].nil? 78 | ret = nil 79 | arg_id = name.to_s + '_id' 80 | arg_id_val = send("#{arg_id}") 81 | if arg_id_val 82 | if !cache_store.nil? 83 | # arg_id_val = @attributes[arg_id][0] 84 | cache_key = self.class.cache_key(class_name, arg_id_val) 85 | # puts 'cache_key=' + cache_key 86 | ret = cache_store.read(cache_key) 87 | # puts 'belongs_to incache=' + ret.inspect 88 | end 89 | if ret.nil? 90 | to_eval = "#{class_name}.find('#{arg_id_val}')" 91 | # puts 'to eval=' + to_eval 92 | begin 93 | ret = eval(to_eval) # (defined? #{arg}_id) 94 | rescue SimpleRecord::ActiveSdb::ActiveSdbError => ex 95 | if ex.message.include? "Couldn't find" 96 | ret = RemoteNil.new 97 | else 98 | raise ex 99 | end 100 | end 101 | 102 | end 103 | end 104 | value = ret 105 | else 106 | if value.is_a? Array 107 | value = value.collect { |x| string_val_to_ruby(att_meta, x) } 108 | else 109 | value = string_val_to_ruby(att_meta, value) 110 | end 111 | end 112 | value 113 | end 114 | 115 | def string_val_to_ruby(att_meta, value) 116 | if att_meta.type == :int 117 | value = Translations.un_offset_int(value) 118 | elsif att_meta.type == :date 119 | value = to_date(value) 120 | elsif att_meta.type == :boolean 121 | value = to_bool(value) 122 | elsif att_meta.type == :float 123 | value = Float(value) 124 | end 125 | value 126 | end 127 | 128 | 129 | def self.pad_and_offset(x, att_meta=nil) # Change name to something more appropriate like ruby_to_sdb 130 | # todo: add Float, etc 131 | # puts 'padding=' + x.class.name + " -- " + x.inspect 132 | if x.kind_of? Integer 133 | x += @@offset 134 | x_str = x.to_s 135 | # pad 136 | x_str = '0' + x_str while x_str.size < 20 137 | return x_str 138 | elsif x.respond_to?(:iso8601) 139 | # puts x.class.name + ' responds to iso8601' 140 | # 141 | # There is an issue here where Time.iso8601 on an incomparable value to DateTime.iso8601. 142 | # Amazon suggests: 2008-02-10T16:52:01.000-05:00 143 | # "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 144 | # 145 | if x.is_a? DateTime 146 | x_str = x.getutc.strftime(@@date_format) 147 | elsif x.is_a? Time 148 | x_str = x.getutc.strftime(@@date_format) 149 | elsif x.is_a? Date 150 | x_str = x.strftime(@@date_format) 151 | 152 | end 153 | return x_str 154 | elsif x.is_a? Float 155 | from_float(x) 156 | else 157 | return x 158 | end 159 | end 160 | 161 | # This conversion to a string is based on: http://tools.ietf.org/html/draft-wood-ldapext-float-00 162 | # Java code sample is here: http://code.google.com/p/typica/source/browse/trunk/java/com/xerox/amazonws/simpledb/DataUtils.java 163 | def self.from_float(x) 164 | return x 165 | # if x == 0.0 166 | # return "3 000 0.0000000000000000" 167 | # end 168 | end 169 | 170 | 171 | def wrap_if_required(arg, value, sdb_val) 172 | return nil if value.nil? 173 | 174 | att_meta = defined_attributes_local[arg.to_sym] 175 | if att_meta && att_meta.options 176 | if att_meta.options[:hashed] 177 | # puts 'wrapping ' + arg_s 178 | return PasswordHashed.new(sdb_val) 179 | end 180 | end 181 | value 182 | end 183 | 184 | def to_date(x) 185 | if x.is_a?(String) 186 | DateTime.parse(x) 187 | else 188 | x 189 | end 190 | end 191 | 192 | def to_bool(x) 193 | if x.is_a?(String) 194 | x == "true" || x == "1" 195 | else 196 | x 197 | end 198 | end 199 | 200 | def self.un_offset_int(x) 201 | if x.is_a?(String) 202 | x2 = x.to_i 203 | # puts 'to_i=' + x2.to_s 204 | x2 -= @@offset 205 | # puts 'after subtracting offset='+ x2.to_s 206 | x2 207 | else 208 | x 209 | end 210 | end 211 | 212 | def unpad(i, attributes) 213 | if !attributes[i].nil? 214 | # puts 'before=' + self[i].inspect 215 | attributes[i].collect! { |x| 216 | un_offset_int(x) 217 | 218 | } 219 | end 220 | end 221 | 222 | def unpad_self 223 | defined_attributes_local.each_pair do |name, att_meta| 224 | if att_meta.type == :int 225 | unpad(name, @attributes) 226 | end 227 | end 228 | end 229 | 230 | 231 | def self.encrypt(value, key=nil) 232 | key = key || get_encryption_key() 233 | raise SimpleRecordError, "Encryption key must be defined on the attribute." if key.nil? 234 | encrypted_value = SimpleRecord::Encryptor.encrypt(:value => value, :key => key) 235 | encoded_value = Base64.encode64(encrypted_value) 236 | encoded_value 237 | end 238 | 239 | 240 | def self.decrypt(value, key=nil) 241 | # puts "decrypt orig value #{value} " 242 | unencoded_value = Base64.decode64(value) 243 | raise SimpleRecordError, "Encryption key must be defined on the attribute." if key.nil? 244 | key = key || get_encryption_key() 245 | # puts "decrypting #{unencoded_value} " 246 | decrypted_value = SimpleRecord::Encryptor.decrypt(:value => unencoded_value, :key => key) 247 | # "decrypted #{unencoded_value} to #{decrypted_value}" 248 | decrypted_value 249 | end 250 | 251 | 252 | def pad_and_offset_ints_to_sdb() 253 | 254 | # defined_attributes_local.each_pair do |name, att_meta| 255 | # if att_meta.type == :int && !self[name.to_s].nil? 256 | # arr = @attributes[name.to_s] 257 | # arr.collect!{ |x| self.class.pad_and_offset(x) } 258 | # @attributes[name.to_s] = arr 259 | # end 260 | # end 261 | end 262 | 263 | def convert_dates_to_sdb() 264 | 265 | # defined_attributes_local.each_pair do |name, att_meta| 266 | # puts 'int encoding: ' + i.to_s 267 | 268 | # end 269 | end 270 | 271 | def self.pass_hash(value) 272 | hashed = Password::create_hash(value) 273 | encoded_value = Base64.encode64(hashed) 274 | encoded_value 275 | end 276 | 277 | def self.pass_hash_check(value, value_to_compare) 278 | unencoded_value = Base64.decode64(value) 279 | return Password::check(value_to_compare, unencoded_value) 280 | end 281 | 282 | end 283 | 284 | 285 | class PasswordHashed 286 | 287 | def initialize(value) 288 | @value = value 289 | end 290 | 291 | def hashed_value 292 | @value 293 | end 294 | 295 | # This allows you to compare an unhashed string to the hashed one. 296 | def ==(val) 297 | if val.is_a?(PasswordHashed) 298 | return val.hashed_value == self.hashed_value 299 | end 300 | return Translations.pass_hash_check(@value, val) 301 | end 302 | 303 | def to_s 304 | @value 305 | end 306 | end 307 | 308 | end 309 | -------------------------------------------------------------------------------- /lib/simple_record/validations.rb: -------------------------------------------------------------------------------- 1 | # This is actually still used to continue support for this. 2 | # ActiveModel does not work the same way so need to continue using this, will change name. 3 | 4 | module SimpleRecord 5 | module Validations 6 | 7 | # if defined?(:valid?) # from ActiveModel 8 | # alias_method :am_valid?, :valid? 9 | # end 10 | 11 | def self.included(base) 12 | # puts 'Validations included ' + base.inspect 13 | # if defined?(ActiveModel) 14 | # base.class_eval do 15 | # alias_method :am_valid?, :valid? 16 | # end 17 | # end 18 | end 19 | 20 | module ClassMethods 21 | 22 | def uniques 23 | @uniques ||= {} 24 | @uniques 25 | end 26 | 27 | # only supporting single attr name right now 28 | def validates_uniqueness_of(attr) 29 | uniques[attr] = true 30 | end 31 | 32 | end 33 | 34 | def valid? 35 | # puts 'in rails2 valid?' 36 | errors.clear 37 | 38 | if respond_to?(:am_valid?) 39 | # And now ActiveModel validations too 40 | am_valid? 41 | end 42 | 43 | # run_callbacks(:validate) 44 | validate 45 | validate_uniques 46 | 47 | if new_record? 48 | # run_callbacks(:validate_on_create) 49 | validate_on_create 50 | else 51 | # run_callbacks(:validate_on_update) 52 | validate_on_update 53 | end 54 | 55 | 56 | errors.empty? 57 | end 58 | 59 | def invalid? 60 | !valid? 61 | end 62 | 63 | 64 | def read_attribute_for_validation(key) 65 | @attributes[key.to_s] 66 | end 67 | 68 | def validate_uniques 69 | puts 'uniques=' + self.class.uniques.inspect 70 | self.class.uniques.each_pair do |k, v| 71 | val = self.send(k) 72 | puts 'val=' + val.inspect 73 | if val 74 | conditions = new_record? ? ["#{k}=?", val] : ["#{k}=? AND id != ?", val, self.id] 75 | 76 | ret = self.class.find(:first, :conditions=>conditions) 77 | puts 'ret=' + ret.inspect 78 | if ret 79 | errors.add(k, "must be unique.") 80 | end 81 | end 82 | end 83 | end 84 | 85 | def validate 86 | true 87 | end 88 | 89 | def validate_on_create 90 | true 91 | end 92 | 93 | def validate_on_update 94 | true 95 | end 96 | 97 | 98 | end 99 | end 100 | 101 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | # This will help setup for easier setup in Rails apps. 2 | 3 | puts 'SimpleRecord rails/init.rb...' 4 | SimpleRecord.options[:connection_mode] = :per_thread 5 | 6 | ::ApplicationController.class_eval do 7 | def close_sdb_connection 8 | puts "Closing sdb connection." 9 | SimpleRecord.close_connection 10 | end 11 | end 12 | ::ApplicationController.send :after_filter, :close_sdb_connection 13 | -------------------------------------------------------------------------------- /simple_record.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "simple_record" 8 | s.version = "4.0.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Travis Reeder", "Chad Arimura", "RightScale"] 12 | s.date = "2012-01-12" 13 | s.description = "ActiveRecord like interface for Amazon SimpleDB. Store, query, shard, etc. By http://www.appoxy.com" 14 | s.email = "travis@appoxy.com" 15 | s.extra_rdoc_files = [ 16 | "LICENSE.markdown", 17 | "README.markdown" 18 | ] 19 | s.files = [ 20 | "lib/simple_record.rb", 21 | "lib/simple_record/active_sdb.rb", 22 | "lib/simple_record/attributes.rb", 23 | "lib/simple_record/callbacks.rb", 24 | "lib/simple_record/encryptor.rb", 25 | "lib/simple_record/errors.rb", 26 | "lib/simple_record/json.rb", 27 | "lib/simple_record/logging.rb", 28 | "lib/simple_record/password.rb", 29 | "lib/simple_record/results_array.rb", 30 | "lib/simple_record/sharding.rb", 31 | "lib/simple_record/stats.rb", 32 | "lib/simple_record/translations.rb", 33 | "lib/simple_record/validations.rb" 34 | ] 35 | s.homepage = "http://github.com/appoxy/simple_record/" 36 | s.require_paths = ["lib"] 37 | s.rubygems_version = "1.8.11" 38 | s.summary = "ActiveRecord like interface for Amazon SimpleDB. By http://www.appoxy.com" 39 | 40 | s.add_runtime_dependency(%q, [">= 0"]) 41 | s.add_runtime_dependency(%q, [">= 0"]) 42 | s.add_runtime_dependency(%q, [">= 0"]) 43 | s.add_development_dependency(%q, [">= 0"]) 44 | s.add_runtime_dependency(%q, [">= 0"]) 45 | s.add_runtime_dependency(%q, [">= 0"]) 46 | 47 | end 48 | -------------------------------------------------------------------------------- /test/models/model_with_enc.rb: -------------------------------------------------------------------------------- 1 | 2 | class ModelWithEnc < SimpleRecord::Base 3 | has_strings :name, 4 | {:name=>:ssn, :encrypted=>"simple_record_test_key"}, 5 | {:name=>:password, :hashed=>true} 6 | end 7 | -------------------------------------------------------------------------------- /test/models/my_base_model.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../lib/simple_record") 2 | 3 | class MyBaseModel < SimpleRecord::Base 4 | 5 | has_strings :base_string 6 | 7 | has_virtuals :v1 8 | 9 | 10 | end 11 | -------------------------------------------------------------------------------- /test/models/my_child_model.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../lib/simple_record") 2 | require_relative 'my_model' 3 | 4 | class MyChildModel < SimpleRecord::Base 5 | belongs_to :my_model 6 | belongs_to :x, :class_name=>"MyModel" 7 | has_attributes :name, :child_attr 8 | 9 | end 10 | 11 | 12 | =begin 13 | 14 | 15 | puts 'word' 16 | 17 | mm = MyModel.new 18 | puts 'word2' 19 | 20 | mcm = MyChildModel.new 21 | 22 | puts 'mcm instance methods=' + MyChildModel.instance_methods(true).inspect 23 | #puts 'mcm=' + mcm.instance_methods(false) 24 | puts 'mcm class vars = ' + mcm.class.class_variables.inspect 25 | puts mcm.class == MyChildModel 26 | puts 'saved? ' + mm.save.to_s 27 | puts mm.errors.inspect 28 | 29 | puts "mm attributes=" + MyModel.defined_attributes.inspect 30 | puts "mcm attributes=" + MyChildModel.defined_attributes.inspect 31 | 32 | mcm2 = MyChildModel.new 33 | puts "mcm2 attributes=" + MyChildModel.defined_attributes.inspect 34 | 35 | =end 36 | -------------------------------------------------------------------------------- /test/models/my_model.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../lib/simple_record") 2 | require_relative 'my_base_model' 3 | require_relative 'my_sharded_model' 4 | require 'active_model' 5 | 6 | class MyModel < MyBaseModel 7 | 8 | has_strings :name, :nickname, :s1, :s2 9 | has_ints :age, :save_count 10 | has_booleans :cool 11 | has_dates :birthday, :date1, :date2, :date3 12 | 13 | # validates_presence_of :name 14 | 15 | 16 | # validate :validate 17 | # before_create :validate_on_create 18 | # before_update :validate_on_update 19 | 20 | validates_uniqueness_of :name 21 | 22 | belongs_to :my_sharded_model 23 | 24 | has_clobs :clob1, :clob2 25 | 26 | attr_accessor :attr_before_save, :attr_after_save, :attr_before_create, :attr_after_create, :attr_after_update 27 | 28 | #callbacks 29 | before_create :set_nickname 30 | after_create :after_create 31 | 32 | before_save :before_save 33 | 34 | after_save :after_save 35 | after_update :after_update 36 | 37 | def set_nickname 38 | @attr_before_create = true 39 | self.nickname = name if self.nickname.blank? 40 | end 41 | 42 | def before_save 43 | @attr_before_save = true 44 | end 45 | 46 | def after_create 47 | @attr_after_create = true 48 | end 49 | 50 | def after_save 51 | @attr_after_save = true 52 | bump_save_count 53 | end 54 | def after_update 55 | @attr_after_update = true 56 | end 57 | 58 | def bump_save_count 59 | if save_count.nil? 60 | self.save_count = 1 61 | else 62 | self.save_count += 1 63 | end 64 | end 65 | 66 | def validate 67 | errors.add("name", "can't be empty.") if name.blank? 68 | end 69 | 70 | def validate_on_create 71 | errors.add("save_count", "should be zero.") if !save_count.blank? && save_count > 0 72 | end 73 | 74 | def validate_on_update 75 | end 76 | 77 | def atts 78 | @@attributes 79 | end 80 | 81 | 82 | end 83 | 84 | 85 | class SingleClobClass < SimpleRecord::Base 86 | 87 | sr_config :single_clob=>true 88 | 89 | has_strings :name 90 | 91 | has_clobs :clob1, :clob2 92 | end 93 | -------------------------------------------------------------------------------- /test/models/my_sharded_model.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../lib/simple_record") 2 | 3 | class MyShardedModel < SimpleRecord::Base 4 | 5 | shard :shards=>:my_shards, :map=>:my_mapping_function 6 | 7 | has_strings :name 8 | 9 | def self.num_shards 10 | 10 11 | end 12 | 13 | def self.my_shards 14 | Array(0...self.num_shards) 15 | end 16 | 17 | def my_mapping_function 18 | shard_num = SimpleRecord::Sharding::Hashing.sdbm_hash(self.id) % self.class.num_shards 19 | shard_num 20 | end 21 | 22 | def self.shard_for_find(id) 23 | shard_num = SimpleRecord::Sharding::Hashing.sdbm_hash(id) % self.num_shards 24 | end 25 | 26 | end 27 | 28 | 29 | class MyShardedByFieldModel < SimpleRecord::Base 30 | 31 | shard :shards=>:my_shards, :map=>:my_mapping_function 32 | 33 | has_strings :name, :state 34 | 35 | def self.my_shards 36 | ['AL', 'CA', 'FL', 'NY'] 37 | end 38 | 39 | def my_mapping_function 40 | state 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/models/my_simple_model.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../lib/simple_record") 2 | require_relative 'my_base_model' 3 | require_relative 'my_sharded_model' 4 | 5 | class MySimpleModel < SimpleRecord::Base 6 | 7 | has_strings :name, :nickname, :s1, :s2 8 | has_ints :age, :save_count 9 | has_booleans :cool 10 | has_dates :birthday, :date1, :date2, :date3 11 | 12 | 13 | end 14 | -------------------------------------------------------------------------------- /test/models/my_translation.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + "/../../lib/simple_record") 2 | 3 | class MyTranslation < SimpleRecord::Base 4 | 5 | has_strings :name, :stage_name 6 | has_ints :age 7 | has_booleans :singer 8 | has_dates :birthday 9 | has_floats :weight, :height 10 | 11 | end 12 | -------------------------------------------------------------------------------- /test/models/validated_model.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../lib/simple_record" 2 | require 'active_model' 3 | 4 | class ValidatedModel < MyBaseModel 5 | 6 | has_strings :name 7 | 8 | validates_presence_of :name 9 | validates_uniqueness_of :name 10 | 11 | 12 | end 13 | -------------------------------------------------------------------------------- /test/setup_test_config.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CONFIG="~/.test_configs/simple_record.yml" 3 | CONFIG_DIR=`dirname $CONFIG` 4 | CONFIG_EXP=`eval "echo $CONFIG"` 5 | if [ ! -d $CONFIG_DIR ]; then 6 | mkdir -p `eval "echo $CONFIG_DIR"` 7 | fi 8 | echo "amazon:" > $CONFIG_EXP 9 | echo -n " access_key: " >> $CONFIG_EXP 10 | echo $AMAZON_ACCESS_KEY_ID >> $CONFIG_EXP 11 | echo -n " secret_key: " >> $CONFIG_EXP 12 | echo $AMAZON_SECRET_ACCESS_KEY >> $CONFIG_EXP 13 | chmod og-rwx $CONFIG_EXP 14 | -------------------------------------------------------------------------------- /test/test_base.rb: -------------------------------------------------------------------------------- 1 | gem 'test-unit' 2 | require 'test/unit' 3 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 4 | require File.join(File.dirname(__FILE__), "./test_helpers") 5 | require "yaml" 6 | require 'aws' 7 | require_relative 'models/my_model' 8 | require_relative 'models/my_child_model' 9 | #require 'active_support' 10 | 11 | class TestBase < Test::Unit::TestCase 12 | 13 | 14 | def setup 15 | reset_connection() 16 | 17 | end 18 | 19 | def teardown 20 | SimpleRecord.close_connection 21 | end 22 | 23 | def delete_all(clz) 24 | obs = clz.find(:all,:consistent_read=>true) 25 | obs.each do |o| 26 | o.delete 27 | end 28 | end 29 | 30 | def reset_connection(options={}) 31 | @config = YAML::load(File.open(File.expand_path("~/.test_configs/simple_record.yml"))) 32 | 33 | SimpleRecord.enable_logging 34 | 35 | SimpleRecord::Base.set_domain_prefix("simplerecord_tests_") 36 | SimpleRecord.establish_connection(@config['amazon']['access_key'], @config['amazon']['secret_key'], 37 | {:connection_mode=>:per_thread}.merge(options)) 38 | 39 | 40 | # Establish AWS connection directly 41 | @@sdb = Aws::SdbInterface.new(@config['amazon']['access_key'], @config['amazon']['secret_key'], 42 | {:connection_mode => :per_thread}.merge(options)) 43 | 44 | end 45 | 46 | 47 | # Use to populate db 48 | def create_my_models(count) 49 | batch = [] 50 | count.times do |i| 51 | mm = MyModel.new(:name=>"model_#{i}") 52 | mm.age = i 53 | batch << mm 54 | end 55 | MyModel.batch_save batch 56 | end 57 | 58 | 59 | end 60 | -------------------------------------------------------------------------------- /test/test_conversions.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require 'aws' 3 | 4 | require_relative "test_base" 5 | require_relative "../lib/simple_record" 6 | require_relative 'models/my_model' 7 | require_relative 'models/my_child_model' 8 | 9 | class ConversionsTest < TestBase 10 | 11 | def test_ints 12 | x = 0 13 | assert_equal "09223372036854775808", SimpleRecord::Translations.pad_and_offset(x) 14 | 15 | x = 1 16 | assert_equal "09223372036854775809", SimpleRecord::Translations.pad_and_offset(x) 17 | 18 | x = "09223372036854775838" 19 | assert_equal 30, SimpleRecord::Translations.un_offset_int(x) 20 | end 21 | 22 | def test_float 23 | assert_equal 0.0, SimpleRecord::Translations.pad_and_offset("0.0".to_f) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/test_dirty.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.expand_path(File.dirname(__FILE__) + "/../lib/simple_record") 3 | require "yaml" 4 | require 'aws' 5 | require_relative 'models/my_model' 6 | require_relative 'models/my_child_model' 7 | require 'active_support' 8 | require 'test_base' 9 | 10 | 11 | class Person < SimpleRecord::Base 12 | has_strings :name, :i_as_s 13 | has_ints :age 14 | end 15 | class DirtyTest < TestBase 16 | 17 | def setup 18 | super 19 | 20 | Person.create_domain 21 | @person = Person.new(:name => 'old', :age => 70) 22 | @person.save 23 | 24 | assert !@person.changed? 25 | assert !@person.name_changed? 26 | end 27 | 28 | def teardown 29 | Person.delete_domain 30 | SimpleRecord.close_connection 31 | end 32 | 33 | def test_same_value_are_not_dirty 34 | @person.name = "old" 35 | 36 | assert !@person.changed? 37 | assert !@person.name_changed? 38 | 39 | @person.age = 70 40 | assert !@person.changed? 41 | assert !@person.age_changed? 42 | end 43 | 44 | def test_reverted_changes_are_not_dirty 45 | @person.name = "new" 46 | assert @person.changed? 47 | assert @person.name_changed? 48 | 49 | @person.name = "old" 50 | assert !@person.changed? 51 | assert !@person.name_changed? 52 | 53 | @person.age = 15 54 | assert @person.changed? 55 | assert @person.age_changed? 56 | 57 | @person.age = 70 58 | assert !@person.changed? 59 | assert !@person.age_changed? 60 | end 61 | 62 | def test_storing_int_as_string 63 | @person.i_as_s = 5 64 | assert @person.changed? 65 | assert @person.i_as_s_changed? 66 | @person.save 67 | 68 | sleep 2 69 | 70 | @person.i_as_s = 5 71 | # Maybe this should fail? What do we expect this behavior to be? 72 | # assert !@person.changed? 73 | # assert !@person.i_as_s_changed? 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/test_encodings.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.expand_path(File.dirname(__FILE__) + "/../lib/simple_record") 3 | require "yaml" 4 | require 'aws' 5 | require_relative 'models/my_simple_model' 6 | require 'active_support' 7 | require 'test_base' 8 | 9 | 10 | class TestEncodings < TestBase 11 | 12 | def test_aaa_setup_delete_domain 13 | MySimpleModel.delete_domain 14 | MySimpleModel.create_domain 15 | end 16 | def test_ascii_http_post 17 | name = "joe" + ("X" * 1000) # pad the field to help get the URL over the 2000 length limit so AWS uses a POST 18 | nickname = "blow" + ("X" * 1000) # pad the field to help get the URL over the 2000 length limit so AWS uses a POST 19 | mm = MySimpleModel.create :name=>name, :nickname=>nickname 20 | assert mm.save 21 | assert_equal mm.name, name 22 | assert_equal mm.nickname, nickname 23 | mm2 = MySimpleModel.find(mm.id,:consistent_read=>true) 24 | assert_equal mm2.name, name 25 | assert_equal mm2.nickname, nickname 26 | assert mm2.delete 27 | end 28 | 29 | def test_utf8_http_post 30 | name = "jos\u00E9" + ("X" * 1000) # pad the field to help get the URL over the 2000 length limit so AWS uses a POST 31 | nickname = "??" + ("X" * 1000) # pad the field to help get the URL over the 2000 length limit so AWS uses a POST 32 | mm = MySimpleModel.create :name=>name, :nickname=>nickname 33 | assert mm.save 34 | assert_equal mm.name, name 35 | assert_equal mm.nickname, nickname 36 | mm2 = MySimpleModel.find(mm.id,:consistent_read=>true) 37 | assert_equal mm2.name, name 38 | assert_equal mm2.nickname, nickname 39 | assert mm2.delete 40 | end 41 | def test_zzz_cleanup_delete_domain 42 | MySimpleModel.delete_domain 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/test_global_options.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require_relative "../lib/simple_record" 3 | require "yaml" 4 | require 'aws' 5 | require_relative 'models/my_model' 6 | require_relative 'models/my_child_model' 7 | require 'active_support/core_ext' 8 | require_relative 'test_base' 9 | 10 | 11 | class Person < SimpleRecord::Base 12 | has_strings :name, :i_as_s 13 | has_ints :age 14 | end 15 | class TestGlobalOptions < TestBase 16 | 17 | def setup 18 | super 19 | end 20 | 21 | def test_domain_prefix 22 | 23 | SimpleRecord::Base.set_domain_prefix("someprefix_") 24 | 25 | p = Person.create(:name=>"my prefix name") 26 | 27 | sleep 1 28 | 29 | sdb_atts = @@sdb.select("select * from someprefix_people") 30 | 31 | @@sdb.delete_domain("someprefix_people") # doing it here so it's done before assertions might fail 32 | 33 | assert_equal sdb_atts[:items].size, 1 34 | 35 | end 36 | 37 | def test_created_col_and_updated_col 38 | reset_connection(:created_col=>"created_at", :updated_col=>"updated_at") 39 | 40 | p = Person.create(:name=>"my prefix name") 41 | sleep 1 42 | 43 | sdb_atts = @@sdb.select("select * from simplerecord_tests_people") 44 | 45 | @@sdb.delete_domain("simplerecord_tests_people") 46 | 47 | items = sdb_atts[:items][0] 48 | first = nil 49 | items.each_pair do |k, v| 50 | first = v 51 | break 52 | end 53 | 54 | assert_nil first["created"] 55 | assert_not_nil first["created_at"] 56 | 57 | # put this back to normal so it doesn't interfere with other tests 58 | reset_connection(:created_col=>"created", :updated_col=>"updated") 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/test_helpers.rb: -------------------------------------------------------------------------------- 1 | class TestHelpers 2 | 3 | end 4 | -------------------------------------------------------------------------------- /test/test_json.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 3 | require File.join(File.dirname(__FILE__), "./test_helpers") 4 | require File.join(File.dirname(__FILE__), "./test_base") 5 | require "yaml" 6 | require 'aws' 7 | require_relative 'models/my_model' 8 | require_relative 'models/my_child_model' 9 | require_relative 'models/model_with_enc' 10 | require 'active_support/core_ext' 11 | 12 | # Tests for SimpleRecord 13 | # 14 | 15 | class TestJson < TestBase 16 | 17 | def test_prep 18 | MyModel.delete_domain 19 | end 20 | 21 | def test_json 22 | mm = MyModel.new 23 | 24 | mm.name = "whatever" 25 | mm.age = "1" 26 | 27 | 28 | jsoned = mm.to_json 29 | puts 'jsoned=' + jsoned 30 | unjsoned = JSON.parse jsoned 31 | puts 'unjsoned=' + unjsoned.inspect 32 | assert_equal unjsoned.name, "whatever" 33 | 34 | mm.save 35 | 36 | puts 'no trying an array' 37 | 38 | data = {} 39 | models = [] 40 | data[:models] = models 41 | 42 | models << mm 43 | 44 | jsoned = models.to_json 45 | puts 'jsoned=' + jsoned 46 | unjsoned = JSON.parse jsoned 47 | puts 'unjsoned=' + unjsoned.inspect 48 | assert_equal unjsoned.size, models.size 49 | assert_equal unjsoned[0].name, mm.name 50 | assert_equal unjsoned[0].age, mm.age 51 | assert unjsoned[0].created.present? 52 | assert unjsoned[0].id.present? 53 | assert_equal unjsoned[0].id, mm.id 54 | 55 | puts 'array good' 56 | 57 | t = Tester.new 58 | t2 = Tester.new 59 | t2.x1 = "i'm number 2" 60 | t.x1 = 1 61 | t.x2 = t2 62 | jsoned = t.to_json 63 | 64 | puts 'jsoned=' + jsoned 65 | 66 | puts 'non simplerecord object good' 67 | 68 | mcm = MyChildModel.new 69 | mcm.name = "child" 70 | mcm.my_model = mm 71 | jsoned = mcm.to_json 72 | puts 'jsoned=' + jsoned 73 | unjsoned = JSON.parse jsoned 74 | puts 'unjsoned=' + unjsoned.inspect 75 | assert_equal mcm.my_model.id, unjsoned.my_model.id 76 | 77 | end 78 | 79 | def test_cleanup 80 | MyModel.delete_domain 81 | end 82 | 83 | end 84 | 85 | class Tester 86 | 87 | attr_accessor :x1, :x2 88 | 89 | end 90 | -------------------------------------------------------------------------------- /test/test_lobs.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require_relative "../lib/simple_record" 3 | require_relative "test_helpers" 4 | require_relative "test_base" 5 | require "yaml" 6 | require 'aws' 7 | require_relative 'models/my_model' 8 | require_relative 'models/my_child_model' 9 | require_relative 'models/model_with_enc' 10 | 11 | # Tests for SimpleRecord 12 | # 13 | 14 | class TestLobs < TestBase 15 | ['puts','gets','deletes'].each do |stat| 16 | eval "def assert_#{stat}(x) 17 | assert_stat('#{stat}',x) 18 | end" 19 | end 20 | 21 | def assert_stat(stat, x) 22 | assert eval("SimpleRecord.stats.s3_#{stat} == x"), "#{stat} is #{eval("SimpleRecord.stats.s3_#{stat}")}, should be #{x}." 23 | end 24 | 25 | def test_prep 26 | MyModel.delete_domain 27 | end 28 | 29 | def test_clobs 30 | mm = MyModel.new 31 | 32 | assert mm.clob1.nil? 33 | 34 | mm.name = "whatever" 35 | mm.age = "1" 36 | mm.clob1 = "0" * 2000 37 | assert_puts(0) 38 | mm.save 39 | 40 | assert_puts(1) 41 | 42 | mm.clob1 = "1" * 2000 43 | mm.clob2 = "2" * 2000 44 | mm.save 45 | assert_puts(3) 46 | 47 | mm2 = MyModel.find(mm.id,:consistent_read=>true) 48 | assert mm.id == mm2.id 49 | assert mm.clob1 == mm2.clob1 50 | assert_puts(3) 51 | assert_gets(1) 52 | mm2.clob1 # make sure it doesn't do another get 53 | assert_gets(1) 54 | 55 | assert mm.clob2 == mm2.clob2 56 | assert_gets(2) 57 | 58 | mm2.save 59 | 60 | # shouldn't save twice if not dirty 61 | assert_puts(3) 62 | 63 | mm2.delete 64 | 65 | assert_deletes(2) 66 | 67 | e = assert_raise(Aws::AwsError) do 68 | sclob = SimpleRecord.s3.bucket(mm2.s3_bucket_name2).get(mm2.s3_lob_id("clob1")) 69 | end 70 | assert_match(/NoSuchKey/, e.message) 71 | e = assert_raise(Aws::AwsError) do 72 | sclob = SimpleRecord.s3.bucket(mm2.s3_bucket_name2).get(mm2.s3_lob_id("clob2")) 73 | end 74 | assert_match(/NoSuchKey/, e.message) 75 | 76 | 77 | end 78 | 79 | def test_single_clob 80 | mm = SingleClobClass.new 81 | 82 | assert mm.clob1.nil? 83 | 84 | mm.name = "whatever" 85 | mm.clob1 = "0" * 2000 86 | mm.clob2 = "2" * 2000 87 | assert_puts(0) 88 | mm.save 89 | 90 | assert_puts(1) 91 | 92 | mm2 = SingleClobClass.find(mm.id,:consistent_read=>true) 93 | assert mm.id == mm2.id 94 | assert_equal mm.clob1, mm2.clob1 95 | assert_puts(1) 96 | assert_gets(1) 97 | mm2.clob1 # make sure it doesn't do another get 98 | assert_gets(1) 99 | 100 | assert mm.clob2 == mm2.clob2 101 | assert_gets(1) 102 | 103 | mm2.save 104 | 105 | # shouldn't save twice if not dirty 106 | assert_puts(1) 107 | 108 | mm2.delete 109 | 110 | assert_deletes(1) 111 | 112 | e = assert_raise(Aws::AwsError) do 113 | sclob = SimpleRecord.s3.bucket(mm2.s3_bucket_name2).get(mm2.single_clob_id) 114 | end 115 | assert_match(/NoSuchKey/, e.message) 116 | 117 | end 118 | 119 | def test_cleanup 120 | MyModel.delete_domain 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/test_marshalled.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.expand_path(File.dirname(__FILE__) + "/../lib/simple_record") 3 | require "yaml" 4 | require 'aws' 5 | require_relative 'models/my_model' 6 | require_relative 'models/my_child_model' 7 | require 'active_support' 8 | require 'test_base' 9 | 10 | 11 | class Person < SimpleRecord::Base 12 | has_strings :name, :i_as_s 13 | has_ints :age, :i2 14 | end 15 | class MarshalTest < TestBase 16 | 17 | def setup 18 | super 19 | 20 | Person.create_domain 21 | @person = Person.new(:name => 'old', :age => 70) 22 | @person.save 23 | 24 | assert !@person.changed? 25 | assert !@person.name_changed? 26 | end 27 | 28 | def teardown 29 | Person.delete_domain 30 | SimpleRecord.close_connection 31 | end 32 | 33 | def test_string_on_initialize 34 | p = Person.new(:name=>"Travis", :age=>5, :i2=>"6") 35 | assert p.name == "Travis" 36 | assert p.age == 5 37 | assert p.i2 == 6, "i2 == #{p.i2}" 38 | 39 | 40 | end 41 | 42 | end 43 | 44 | -------------------------------------------------------------------------------- /test/test_pagination.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 3 | require File.join(File.dirname(__FILE__), "./test_helpers") 4 | require File.join(File.dirname(__FILE__), "./test_base") 5 | require "yaml" 6 | require 'aws' 7 | require_relative 'models/my_model' 8 | require_relative 'models/my_child_model' 9 | require_relative 'models/model_with_enc' 10 | require 'active_support' 11 | 12 | 13 | # Pagination is intended to be just like will_paginate. 14 | class TestPagination < TestBase 15 | 16 | def setup 17 | super 18 | MyModel.delete_domain 19 | MyModel.create_domain 20 | end 21 | 22 | def teardown 23 | MyModel.delete_domain 24 | super 25 | end 26 | def test_paginate 27 | create_my_models(20) 28 | 29 | i = 20 30 | (1..3).each do |page| 31 | models = MyModel.paginate :page=>page, :per_page=>5, :order=>"age desc", :consistent_read => true 32 | assert models.count == 5, "models.count=#{models.count}" 33 | assert models.size == 20, "models.size=#{models.size}" 34 | models.each do |m| 35 | i -= 1 36 | assert m.age == i 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/test_rails3.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'active_model' 3 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 4 | require File.join(File.dirname(__FILE__), "./test_helpers") 5 | require File.join(File.dirname(__FILE__), "./test_base") 6 | require "yaml" 7 | require 'aws' 8 | require_relative 'models/my_model' 9 | require_relative 'models/my_child_model' 10 | require_relative 'models/model_with_enc' 11 | 12 | # To test things related to rails 3 like ActiveModel usage. 13 | class TestRails3 < TestBase 14 | 15 | def test_active_model_defined 16 | 17 | my_model = MyModel.new 18 | 19 | assert (defined?(MyModel.model_name)) 20 | 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /test/test_results_array.rb: -------------------------------------------------------------------------------- 1 | # These ones take longer to run 2 | 3 | require 'test/unit' 4 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 5 | require File.join(File.dirname(__FILE__), "./test_helpers") 6 | require File.join(File.dirname(__FILE__), "./test_base") 7 | require "yaml" 8 | require 'aws' 9 | require_relative 'models/my_model' 10 | require_relative 'models/my_child_model' 11 | require 'active_support' 12 | 13 | # Tests for SimpleRecord 14 | # 15 | 16 | class TestResultsArray < TestBase 17 | 18 | 19 | # ensures that it uses next token and what not 20 | def test_big_result 21 | MyModel.delete_domain 22 | MyModel.create_domain 23 | SimpleRecord.stats.clear 24 | num_made = 110 25 | num_made.times do |i| 26 | mm = MyModel.create(:name=>"Travis big_result #{i}", :age=>i, :cool=>true) 27 | end 28 | assert SimpleRecord.stats.saves == num_made, "SimpleRecord.stats.saves should be #{num_made}, is #{SimpleRecord.stats.saves}" 29 | SimpleRecord.stats.clear # have to clear them again, as each save above created a select (in pre/post actions) 30 | rs = MyModel.find(:all,:consistent_read=>true) # should get 100 at a time 31 | assert rs.size == num_made, "rs.size should be #{num_made}, is #{rs.size}" 32 | i = 0 33 | rs.each do |x| 34 | i+=1 35 | end 36 | assert SimpleRecord.stats.selects == 3, "SimpleRecord.stats.selects should be 3, is #{SimpleRecord.stats.selects}" # one for count. 37 | assert i == num_made, "num_made should be #{i}, is #{num_made}" 38 | # running through all the results twice to ensure it works properly after lazy loading complete. 39 | SimpleRecord.stats.clear 40 | i = 0 41 | rs.each do |x| 42 | #puts 'x=' + x.id 43 | i+=1 44 | end 45 | assert SimpleRecord.stats.selects == 0, "SimpleRecord.stats.selects should be 0, is #{SimpleRecord.stats.selects}" # one for count. 46 | assert i == num_made, "num_made should be #{i}, is #{num_made}" 47 | end 48 | 49 | def test_limit 50 | SimpleRecord.stats.clear 51 | rs = MyModel.find(:all, :per_token=>2500,:consistent_read=>true) 52 | assert rs.size == 110, "rs.size should be 110, is #{rs.size}" 53 | assert SimpleRecord.stats.selects == 1, "SimpleRecord.stats.selects is #{SimpleRecord.stats.selects}" 54 | 55 | end 56 | 57 | 58 | end 59 | -------------------------------------------------------------------------------- /test/test_shards.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 3 | require File.join(File.dirname(__FILE__), "./test_helpers") 4 | require File.join(File.dirname(__FILE__), "./test_base") 5 | require "yaml" 6 | require 'aws' 7 | require_relative 'models/my_sharded_model' 8 | 9 | # Tests for SimpleRecord 10 | # 11 | class TestShards < TestBase 12 | 13 | def setup 14 | super 15 | # delete_all MyShardedModel 16 | # delete_all MyShardedByFieldModel 17 | end 18 | 19 | def teardown 20 | super 21 | 22 | end 23 | 24 | # We'll want to shard based on ID's, user decides how many shards and some mapping function will 25 | # be used to select the shard. 26 | def test_id_sharding 27 | 28 | # test_id_sharding start 29 | ob_count = 1000 30 | 31 | mm = MyShardedModel.new(:name=>"single") 32 | mm.save 33 | # finding by id 34 | mm2 = MyShardedModel.find(mm.id,:consistent_read=>true) 35 | assert_equal mm.id, mm2.id 36 | # deleting 37 | mm2.delete 38 | mm3 = MyShardedModel.find(mm.id,:consistent_read=>true) 39 | assert_nil mm3 40 | 41 | saved = [] 42 | ob_count.times do |i| 43 | mm = MyShardedModel.new(:name=>"name #{i}") 44 | mm.save 45 | saved << mm 46 | end 47 | 48 | # todo: assert that we're actually sharding 49 | 50 | # finding them all sequentially 51 | start_time = Time.now 52 | found = [] 53 | rs = MyShardedModel.find(:all, :per_token=>2500,:consistent_read=>true) 54 | rs.each do |m| 55 | found << m 56 | end 57 | duration = Time.now.to_f - start_time.to_f 58 | # Find sequential duration 59 | saved.each do |so| 60 | assert(found.find { |m1| m1.id == so.id }) 61 | end 62 | 63 | 64 | # Now let's try concurrently 65 | start_time = Time.now 66 | found = [] 67 | rs = MyShardedModel.find(:all, :concurrent=>true, :per_token=>2500,:consistent_read=>true) 68 | rs.each do |m| 69 | found << m 70 | end 71 | concurrent_duration = Time.now.to_f - start_time.to_f 72 | saved.each do |so| 73 | assert(found.find { |m1| m1.id == so.id }) 74 | end 75 | 76 | assert concurrent_duration < duration 77 | 78 | # deleting all of them 79 | found.each do |fo| 80 | fo.delete 81 | end 82 | 83 | # Now ensure that all are deleted 84 | rs = MyShardedModel.find(:all,:consistent_read=>true) 85 | assert rs.size == 0 86 | 87 | # Testing belongs_to sharding 88 | 89 | end 90 | 91 | 92 | def test_field_sharding 93 | 94 | states = MyShardedByFieldModel.shards 95 | 96 | mm = MyShardedByFieldModel.new(:name=>"single", :state=>"CA") 97 | mm.save 98 | mm2 = MyShardedByFieldModel.find(mm.id,:consistent_read=>true) 99 | assert_equal mm.id, mm2.id 100 | mm2.delete 101 | mm3 = MyShardedByFieldModel.find(mm.id,:consistent_read=>true) 102 | assert_nil mm3 103 | 104 | # saving 20 now 105 | saved = [] 106 | 20.times do |i| 107 | mm = MyShardedByFieldModel.new(:name=>"name #{i}", :state=>states[i % states.size]) 108 | mm.save 109 | saved << mm 110 | end 111 | 112 | # todo: assert that we're actually sharding 113 | 114 | # finding them all 115 | found = [] 116 | rs = MyShardedByFieldModel.find(:all,:consistent_read=>true) 117 | rs.each do |m| 118 | found << m 119 | end 120 | saved.each do |so| 121 | assert(found.find { |m1| m1.id == so.id }) 122 | end 123 | 124 | rs = MyShardedByFieldModel.find(:all,:consistent_read=>true) 125 | rs.each do |m| 126 | found << m 127 | end 128 | saved.each do |so| 129 | assert(found.find { |m1| m1.id == so.id }) 130 | end 131 | 132 | # Try to find on a specific known shard 133 | selects = SimpleRecord.stats.selects 134 | cali_models = MyShardedByFieldModel.find(:all, :shard => "CA",:consistent_read=>true) 135 | assert_equal(5, cali_models.size) 136 | assert_equal(selects + 1, SimpleRecord.stats.selects) 137 | 138 | # deleting all of them 139 | found.each do |fo| 140 | fo.delete 141 | end 142 | 143 | # Now ensure that all are deleted 144 | rs = MyShardedByFieldModel.find(:all,:consistent_read=>true) 145 | assert_equal rs.size, 0 146 | end 147 | 148 | def test_time_sharding 149 | 150 | end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /test/test_simple_record.rb: -------------------------------------------------------------------------------- 1 | gem 'test-unit' 2 | require 'test/unit' 3 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 4 | require File.join(File.dirname(__FILE__), "./test_helpers") 5 | require_relative "test_base" 6 | require "yaml" 7 | require 'aws' 8 | require_relative 'models/my_model' 9 | require_relative 'models/my_child_model' 10 | require_relative 'models/model_with_enc' 11 | require_relative 'models/my_simple_model' 12 | 13 | # Tests for SimpleRecord 14 | # 15 | 16 | class TestSimpleRecord < TestBase 17 | def test_aaa_first_at_bat 18 | MyModel.delete_domain 19 | MyChildModel.delete_domain 20 | ModelWithEnc.delete_domain 21 | MyModel.create_domain 22 | MyChildModel.create_domain 23 | ModelWithEnc.create_domain 24 | end 25 | 26 | def test_save_get 27 | mm = MyModel.new 28 | mm.name = "Travis" 29 | mm.age = 32 30 | mm.cool = true 31 | mm.save 32 | 33 | assert !mm.created.nil? 34 | assert !mm.updated.nil? 35 | assert !mm.id.nil? 36 | assert_equal mm.age, 32 37 | assert_equal mm.cool, true 38 | assert_equal mm.name, "Travis" 39 | 40 | id = mm.id 41 | # Get the object back 42 | mm2 = MyModel.find(id,:consistent_read=>true) 43 | assert_equal mm2.id, mm.id 44 | assert_equal mm2.age, mm.age 45 | assert_equal mm2.cool, mm.cool 46 | assert_equal mm2.age, 32 47 | assert_equal mm2.cool, true 48 | assert_equal mm2.name, "Travis" 49 | assert mm2.created.is_a? DateTime 50 | 51 | # test nilification 52 | mm2.age = nil 53 | mm2.save 54 | sleep(2) # not sure why this might work... not respecting consistent_read? 55 | mm3 = MyModel.find(id,:consistent_read=>true) 56 | assert mm2.age.nil?, "doh, age should be nil, but it's " + mm2.age.inspect 57 | end 58 | 59 | def test_custom_id 60 | custom_id = "id-travis" 61 | mm = MyModel.new 62 | mm.id = custom_id 63 | mm.name = "Marvin" 64 | mm.age = 32 65 | mm.cool = true 66 | mm.save 67 | mm2 = MyModel.find(custom_id,:consistent_read=>true) 68 | assert_equal mm2.id, mm.id 69 | end 70 | 71 | def test_updates 72 | mm = MyModel.new 73 | mm.name = "Angela" 74 | mm.age = 32 75 | mm.cool = true 76 | mm.s1 = "Initial value" 77 | mm.save 78 | id = mm.id 79 | 80 | mm = MyModel.find(id, :consistent_read=>true) 81 | mm.name = "Angela2" 82 | mm.age = 10 83 | mm.cool = false 84 | mm.s1 = "" # test blank string 85 | 86 | mm.save 87 | 88 | assert_equal mm.s1, "" 89 | 90 | mm = MyModel.find(id, :consistent_read=>true) 91 | assert_equal mm.name, "Angela2" 92 | assert_equal mm.age, 10 93 | assert_equal mm.cool, false 94 | assert_equal mm.s1, "" 95 | 96 | end 97 | 98 | def test_funky_values 99 | mm = MyModel.new(:name=>"Funky") 100 | mm.s1 = "other/2009-11-10/04/84.eml" # reported here: http://groups.google.com/group/simple-record/browse_thread/thread/3659e82491d03a2c?hl=en 101 | assert mm.save 102 | assert_equal mm.errors.size, 0 103 | 104 | mm2 = MyModel.find(mm.id,:consistent_read=>true) 105 | 106 | end 107 | 108 | 109 | def test_create 110 | mm = MyModel.create(:name=>"Craven", :age=>32, :cool=>true) 111 | assert !mm.id.nil? 112 | end 113 | 114 | def test_bad_query 115 | assert_raise Aws::AwsError do 116 | mm2 = MyModel.find(:all, :conditions=>["name =4?", "1"],:consistent_read=>true) 117 | end 118 | end 119 | 120 | def test_batch_save 121 | items = [] 122 | mm = MyModel.new 123 | mm.name = "Beavis" 124 | mm.age = 32 125 | mm.cool = true 126 | items << mm 127 | mm = MyModel.new 128 | mm.name = "Butthead" 129 | mm.age = 44 130 | mm.cool = false 131 | items << mm 132 | MyModel.batch_save(items) 133 | items.each do |item| 134 | new_item = MyModel.find(item.id,:consistent_read=>true) 135 | assert_equal item.id, new_item.id 136 | assert_equal item.name, new_item.name 137 | assert_equal item.cool, new_item.cool 138 | end 139 | end 140 | 141 | # Testing getting the association ID without materializing the obejct 142 | def test_get_belongs_to_id 143 | mm = MyModel.new 144 | mm.name = "Parent" 145 | mm.age = 55 146 | mm.cool = true 147 | mm.save 148 | sleep(1) #needed because child.my_model below does not have :consistent_read set 149 | 150 | child = MyChildModel.new 151 | child.name = "Child" 152 | child.my_model = mm 153 | assert_equal child.my_model_id, mm.id 154 | child.save 155 | 156 | child = MyChildModel.find(child.id,:consistent_read=>true) 157 | assert !child.my_model_id.nil? 158 | assert !child.my_model.nil? 159 | assert_equal child.my_model_id, mm.id 160 | end 161 | 162 | def test_callbacks 163 | 164 | 165 | mm = MyModel.new 166 | assert !mm.save 167 | assert_equal mm.errors.count, 1 # name is required 168 | 169 | # test queued callback before_create 170 | mm.name = "Oresund" 171 | assert mm.save 172 | # now nickname should be set on before_create 173 | assert_equal mm.nickname, mm.name 174 | 175 | mm2 = MyModel.find(mm.id,:consistent_read=>true) 176 | assert_equal mm2.nickname, mm.nickname 177 | assert_equal mm2.name, mm.name 178 | 179 | 180 | end 181 | 182 | def test_dirty 183 | mm = MyModel.new 184 | mm.name = "Persephone" 185 | mm.age = 32 186 | mm.cool = true 187 | mm.save 188 | id = mm.id 189 | # Get the object back 190 | mm2 = MyModel.find(id,:consistent_read=>true) 191 | assert_equal mm2.id, mm.id 192 | assert_equal mm2.age, mm.age 193 | assert_equal mm2.cool, mm.cool 194 | 195 | mm2.name = "Persephone 2" 196 | mm2.save(:dirty=>true) 197 | 198 | # todo: how do we assert this? perhaps change a value directly in sdb and see that it doesn't get overwritten. 199 | # or check stats and ensure only 1 attribute was put 200 | 201 | # Test to ensure that if an item is not dirty, sdb doesn't get hit 202 | SimpleRecord.stats.clear 203 | mm2.save(:dirty=>true) 204 | assert_equal 0, SimpleRecord.stats.saves 205 | 206 | sleep(1) #needed because mmc.my_model below does not have :consistent_read set 207 | mmc = MyChildModel.new 208 | mmc.my_model = mm 209 | mmc.x = mm 210 | mmc.save 211 | 212 | 213 | mmc2 = MyChildModel.find(mmc.id,:consistent_read=>true) 214 | assert_equal mmc2.my_model_id, mmc.my_model_id 215 | mmc2.my_model = nil 216 | mmc2.x = nil 217 | SimpleRecord.stats.clear 218 | assert mmc2.save(:dirty=>true) 219 | assert_equal SimpleRecord.stats.saves, 1 220 | assert_equal SimpleRecord.stats.deletes, 1 221 | assert_equal mmc2.id, mmc.id 222 | assert_equal mmc2.my_model_id, nil 223 | assert_equal mmc2.my_model, nil 224 | 225 | mmc3 = MyChildModel.find(mmc.id,:consistent_read=>true) 226 | assert_equal mmc3.my_model_id, nil 227 | assert_equal mmc3.my_model, nil 228 | 229 | mm3 = MyModel.new(:name=>"test") 230 | assert mm3.save 231 | sleep(1) #needed because mmc3.my_model below does not have :consistent_read set 232 | 233 | mmc3.my_model = mm3 234 | assert mmc3.my_model_changed? 235 | assert mmc3.save(:dirty=>true) 236 | assert_equal mmc3.my_model_id, mm3.id 237 | assert_equal mmc3.my_model.id, mm3.id 238 | 239 | mmc3 = MyChildModel.find(mmc3.id,:consistent_read=>true) 240 | assert_equal mmc3.my_model_id, mm3.id 241 | assert_equal mmc3.my_model.id, mm3.id 242 | 243 | mmc3 = MyChildModel.find(mmc3.id,:consistent_read=>true) 244 | mmc3.my_model_id = mm2.id 245 | assert_equal mmc3.my_model_id, mm2.id 246 | assert mmc3.changed? 247 | assert mmc3.my_model_changed? 248 | assert_equal mmc3.my_model.id, mm2.id 249 | 250 | end 251 | 252 | # http://api.rubyonrails.org/classes/ActiveRecord/Dirty.html#M002136 253 | def test_changed 254 | mm = MySimpleModel.new 255 | mm.name = "Horace" 256 | mm.age = 32 257 | mm.cool = true 258 | mm.save 259 | 260 | assert !mm.changed? 261 | assert_equal mm.changed.size, 0 262 | assert_equal mm.changes.size, 0 263 | assert !mm.name_changed? 264 | 265 | mm.name = "Jim" 266 | assert mm.changed? 267 | assert_equal mm.changed.size, 1 268 | assert_equal mm.changed[0], "name" 269 | 270 | assert_equal mm.changes.size, 1 271 | assert_equal mm.changes["name"][0], "Horace" 272 | assert_equal mm.changes["name"][1], "Jim" 273 | 274 | assert mm.name_changed? 275 | assert_equal mm.name_was, "Horace" 276 | assert_equal mm.name_change[0], "Horace" 277 | assert_equal mm.name_change[1], "Jim" 278 | 279 | end 280 | 281 | def test_count 282 | 283 | SimpleRecord.stats.clear 284 | 285 | count = MyModel.find(:count,:consistent_read=>true) # select 1 286 | assert count > 0 287 | 288 | mms = MyModel.find(:all,:consistent_read=>true) # select 2 289 | assert mms.size > 0 # still select 2 290 | assert_equal mms.size, count 291 | assert_equal 2, SimpleRecord.stats.selects 292 | 293 | sleep 2 294 | count = MyModel.find(:count, :conditions=>["name=?", "Beavis"],:consistent_read=>true) 295 | assert count > 0 296 | 297 | mms = MyModel.find(:all, :conditions=>["name=?", "Beavis"],:consistent_read=>true) 298 | assert mms.size > 0 299 | assert_equal mms.size, count 300 | 301 | end 302 | 303 | def test_attributes_correct 304 | # child should contain child class attributes + parent class attributes 305 | 306 | #MyModel.defined_attributes.each do |a| 307 | # 308 | #end 309 | #MyChildModel.defined_attributes.inspect 310 | 311 | end 312 | 313 | def test_results_array 314 | mms = MyModel.find(:all,:consistent_read=>true) # select 2 315 | assert !mms.first.nil? 316 | assert !mms.last.nil? 317 | assert !mms.empty? 318 | assert mms.include?(mms[0]) 319 | 320 | assert_equal mms[2, 2].size, 2 321 | assert_equal mms[2..5].size, 4 322 | assert_equal mms[2...5].size, 3 323 | 324 | end 325 | 326 | def test_random_index 327 | create_my_models(120) 328 | mms = MyModel.find(:all,:consistent_read=>true) 329 | o = mms[85] 330 | assert !o.nil? 331 | o = mms[111] 332 | assert !o.nil? 333 | end 334 | 335 | def test_objects_in_constructor 336 | mm = MyModel.new(:name=>"model1") 337 | mm.save 338 | # my_model should be treated differently since it's a belong_to 339 | mcm = MyChildModel.new(:name=>"johnny", :my_model=>mm) 340 | mcm.save 341 | sleep(1) #needed because mcm.my_model below does not have :consistent_read set 342 | 343 | assert mcm.my_model != nil 344 | 345 | mcm = MyChildModel.find(mcm.id,:consistent_read=>true) 346 | assert mcm.my_model != nil 347 | 348 | end 349 | 350 | 351 | def test_nil_attr_deletion 352 | mm = MyModel.new 353 | mm.name = "Chad" 354 | mm.age = 30 355 | mm.cool = false 356 | mm.save 357 | 358 | # Should have 1 age attribute 359 | sdb_atts = @@sdb.get_attributes('simplerecord_tests_my_models', mm.id, 'age',true) # consistent_read 360 | assert_equal sdb_atts[:attributes].size, 1 361 | 362 | mm.age = nil 363 | mm.save 364 | 365 | # Should be NIL 366 | assert_equal mm.age, nil 367 | 368 | sleep 1 #doesn't seem to be respecting consistent_read below 369 | # Should have NO age attributes 370 | assert_equal @@sdb.get_attributes('simplerecord_tests_my_models', mm.id, 'age',true)[:attributes].size, 0 371 | end 372 | 373 | def test_null 374 | MyModel.delete_domain 375 | MyModel.create_domain 376 | 377 | mm = MyModel.new(:name=>"birthay is null") 378 | mm.save 379 | mm2 = MyModel.new(:name=>"birthday is not null") 380 | mm2.birthday = Time.now 381 | mm2.save 382 | mms = MyModel.find(:all, :conditions=>["birthday is null"],:consistent_read=>true) 383 | mms.each do |m| 384 | m.inspect 385 | end 386 | assert_equal 1, mms.size 387 | assert_equal mms[0].id, mm.id 388 | mms = MyModel.find(:all, :conditions=>["birthday is not null"],:consistent_read=>true) 389 | mms.each do |m| 390 | m.inspect 391 | end 392 | assert_equal 1, mms.size 393 | assert_equal mms[0].id, mm2.id 394 | end 395 | 396 | # Test to add support for IN 397 | def test_in_clause 398 | # mms = MyModel.find(:all,:consistent_read=>true) 399 | 400 | # mms2 = MyModel.find(:all, :conditions=>["id in ?"],:consistent_read=>true) 401 | 402 | end 403 | 404 | def test_base_attributes 405 | mm = MyModel.new() 406 | mm.name = "test name tba" 407 | mm.base_string = "in base class" 408 | mm.save_with_validation! 409 | 410 | mm2 = MyModel.find(mm.id,:consistent_read=>true) 411 | assert_equal mm2.base_string, mm.base_string 412 | assert_equal mm2.name, mm.name 413 | assert_equal mm2.id, mm.id 414 | mm2.name += " 2" 415 | 416 | mm2.base_string = "changed base string" 417 | mm2.save_with_validation! 418 | 419 | mm3 = MyModel.find(mm2.id,:consistent_read=>true) 420 | assert_equal mm3.base_string, mm2.base_string 421 | end 422 | 423 | def test_dates 424 | mm = MyModel.new() 425 | mm.name = "test name td" 426 | mm.date1 = Date.today 427 | mm.date2 = Time.now 428 | mm.date3 = DateTime.now 429 | mm.save 430 | 431 | mm = MyModel.find(:first, :conditions=>["date1 >= ?", 1.days.ago.to_date],:consistent_read=>true) 432 | assert mm.is_a? MyModel 433 | 434 | mm = MyModel.find(:first, :conditions=>["date2 >= ?", 1.minutes.ago],:consistent_read=>true) 435 | assert mm.is_a? MyModel 436 | 437 | mm = MyModel.find(:first, :conditions=>["date3 >= ?", 1.minutes.ago],:consistent_read=>true) 438 | assert mm.is_a? MyModel 439 | 440 | end 441 | 442 | def test_attr_encrypted 443 | require_relative 'models/model_with_enc' 444 | ssn = "123456789" 445 | password = "my_password" 446 | 447 | ob = ModelWithEnc.new 448 | ob.name = "my name" 449 | ob.ssn = ssn 450 | ob.password = password 451 | assert_equal ssn, ob.ssn 452 | assert password != ob.password # we know this doesn't work right 453 | assert_equal ob.password, password 454 | ob.save 455 | 456 | # try also with constructor, just to be safe 457 | ob = ModelWithEnc.create(:ssn=>ssn, :name=>"my name", :password=>password) 458 | assert_equal ssn, ob.ssn 459 | assert password != ob.password # we know this doesn't work right 460 | assert_equal ob.password, password 461 | assert_equal ssn, ob.ssn 462 | assert_equal ob.password, password 463 | 464 | ob2 = ModelWithEnc.find(ob.id,:consistent_read=>true) 465 | assert_equal ob2.name, ob.name 466 | assert_equal ob2.ssn, ob.ssn 467 | assert_equal ob2.ssn, ssn 468 | assert_equal ob2.password, password 469 | assert ob2.attributes["password"] != password 470 | assert_equal ob2.password, ob.password 471 | end 472 | 473 | def test_non_persistent_attributes 474 | mm = MyModel.new({:some_np_att=>"word"}) 475 | mm = MyModel.new({"some_other_np_att"=>"up"}) 476 | 477 | end 478 | def test_atts_using_strings_and_symbols 479 | mm = MyModel.new({:name=>"mynamex1",:age=>32}) 480 | mm2 = MyModel.new({"name"=>"mynamex2","age"=>32}) 481 | assert_equal(mm.age, mm2.age) 482 | 483 | mm.save 484 | mm2.save 485 | 486 | mm = MyModel.find(mm.id,:consistent_read=>true) 487 | mm2 = MyModel.find(mm2.id,:consistent_read=>true) 488 | assert_equal(mm.age, mm2.age) 489 | end 490 | 491 | def test_constructor_using_belongs_to_ids 492 | mm = MyModel.new({:name=>"myname tcubti"}) 493 | mm.save 494 | sleep(1) #needed because mm2.my_model below does not have :consistent_read set 495 | 496 | mm2 = MyChildModel.new({"name"=>"myname tcubti 2", :my_model_id=>mm.id}) 497 | assert_equal mm.id, mm2.my_model_id, "#{mm.id} != #{mm2.my_model_id}" 498 | mm3 = mm2.my_model 499 | assert_equal mm.name, mm3.name 500 | 501 | mm3 = MyChildModel.create(:my_model_id=>mm.id, :name=>"myname tcubti 3") 502 | 503 | mm4 = MyChildModel.find(mm3.id,:consistent_read=>true) 504 | assert_equal mm4.my_model_id, mm.id 505 | assert !mm4.my_model.nil? 506 | 507 | end 508 | 509 | def test_update_attributes 510 | mm = MyModel.new({:name=>"myname tua"}) 511 | mm.save 512 | 513 | now = Time.now 514 | mm.update_attributes(:name=>"name2", :age=>21, "date2"=>now) 515 | assert_equal mm.name, "name2" 516 | assert_equal mm.age, 21 517 | 518 | mm = MyModel.find(mm.id,:consistent_read=>true) 519 | assert_equal mm.name, "name2" 520 | assert_equal mm.age, 21 521 | end 522 | 523 | def test_explicit_class_name 524 | mm = MyModel.new({:name=>"myname tecn"}) 525 | mm.save 526 | 527 | mm2 = MyChildModel.new({"name"=>"myname tecn 2"}) 528 | mm2.x = mm 529 | assert_equal mm2.x.id, mm.id 530 | mm2.save 531 | 532 | sleep 1 #sometimes consistent_read isn't good enough. Why? Dunno. 533 | mm3 = MyChildModel.find(mm2.id,:consistent_read=>true) 534 | assert_equal mm3.x.id, mm.id 535 | end 536 | 537 | def test_storage_format 538 | 539 | mm = MyModel.new({:name=>"myname tsf"}) 540 | mm.date1 = Time.now 541 | mm.date2 = DateTime.now 542 | mm.save 543 | 544 | raw = @@sdb.get_attributes(MyModel.domain, mm.id, nil, true) 545 | puts raw.inspect #observation interferes with this in some way 546 | assert_equal raw[:attributes]["updated"][0].size, "2010-01-06T16:04:23".size 547 | assert_equal raw[:attributes]["date1"][0].size, "2010-01-06T16:04:23".size 548 | assert_equal raw[:attributes]["date2"][0].size, "2010-01-06T16:04:23".size 549 | 550 | end 551 | 552 | def test_empty_initialize 553 | mm = MyModel.new 554 | 555 | mme = ModelWithEnc.new 556 | mme = ModelWithEnc.new(:ssn=>"", :password=>"") # this caused encryptor errors 557 | mme = ModelWithEnc.new(:ssn=>nil, :password=>nil) 558 | end 559 | 560 | def test_string_ints 561 | mm = MyModel.new 562 | mm.name = "whenever" 563 | mm.age = "1" 564 | 565 | mm2 = MyModel.new 566 | mm2.name = "whenever2" 567 | mm2.age = 1 568 | params = {:name=>"scooby", :age=>"123"} 569 | mm3 = MyModel.new(params) 570 | 571 | assert_equal mm.age, 1 572 | assert_equal mm2.age, 1 573 | assert_equal mm3.age, 123 574 | 575 | mm.save! 576 | mm2.save! 577 | mm3.save! 578 | 579 | assert_equal mm.age, 1 580 | assert_equal mm2.age, 1 581 | assert_equal mm3.age, 123 582 | 583 | mmf1 = MyModel.find(mm.id,:consistent_read=>true) 584 | mmf2 = MyModel.find(mm2.id,:consistent_read=>true) 585 | mmf3 = MyModel.find(mm3.id,:consistent_read=>true) 586 | 587 | assert_equal mmf1.age, 1 588 | assert_equal mmf2.age, 1 589 | assert_equal mmf3.age, 123 590 | 591 | mmf1.update_attributes({:age=>"456"}) 592 | 593 | assert_equal mmf1.age, 456 594 | end 595 | 596 | def test_box_usage 597 | mm = MyModel.new 598 | mm.name = "however" 599 | mm.age = "1" 600 | mm.save 601 | 602 | mms = MyModel.all 603 | 604 | assert mms.box_usage && mms.box_usage > 0 605 | assert mms.request_id 606 | end 607 | 608 | def test_multi_value_attributes 609 | 610 | val = ['a', 'b', 'c'] 611 | val2 = [1, 2, 3] 612 | 613 | mm = MyModel.new 614 | mm.name = val 615 | mm.age = val2 616 | assert_equal val, mm.name 617 | assert_equal val2, mm.age 618 | mm.save 619 | 620 | mm = MyModel.find(mm.id,:consistent_read=>true) 621 | # Values are not returned in order 622 | assert_equal val, mm.name.sort 623 | assert_equal val2, mm.age.sort 624 | end 625 | 626 | def test_zzz_last_batter_up 627 | MyModel.delete_domain 628 | MyChildModel.delete_domain 629 | ModelWithEnc.delete_domain 630 | end 631 | 632 | end 633 | -------------------------------------------------------------------------------- /test/test_temp.rb: -------------------------------------------------------------------------------- 1 | gem 'test-unit' 2 | require 'test/unit' 3 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 4 | require File.join(File.dirname(__FILE__), "./test_helpers") 5 | require_relative "test_base" 6 | require "yaml" 7 | require 'aws' 8 | require_relative 'models/my_model' 9 | require_relative 'models/my_child_model' 10 | require_relative 'models/model_with_enc' 11 | require_relative 'models/my_simple_model' 12 | 13 | # Tests for SimpleRecord 14 | # 15 | 16 | class TestSimpleRecord < TestBase 17 | 18 | def test_virtuals 19 | model = MyBaseModel.new(:v1=>'abc', :base_string=>'what') 20 | assert model.v1 == 'abc', "model.v1=" + model.v1.inspect 21 | 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /test/test_translations.rb: -------------------------------------------------------------------------------- 1 | gem 'test-unit' 2 | require 'test/unit' 3 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 4 | require File.join(File.dirname(__FILE__), "./test_helpers") 5 | require_relative "test_base" 6 | require "yaml" 7 | require 'aws' 8 | require_relative 'models/my_translation' 9 | 10 | # Tests for simple_record/translations.rb 11 | # 12 | 13 | class TestTranslations < TestBase 14 | 15 | def test_aaa1 # run first 16 | MyTranslation.delete_domain 17 | MyTranslation.create_domain 18 | end 19 | 20 | def test_first_validations 21 | mt = MyTranslation.new() 22 | mt.name = "Marvin" 23 | mt.stage_name = "Kelly" 24 | mt.age = 29 25 | mt.singer = true 26 | mt.birthday = Date.new(1990,03,15) 27 | mt.weight = 70 28 | mt.height = 150 29 | mt.save 30 | 31 | mt2 = MyTranslation.find(mt.id, :consistent_read => true) 32 | assert_kind_of String, mt2.name 33 | assert_kind_of String, mt2.stage_name 34 | assert_kind_of Integer, mt2.age 35 | assert_kind_of Date, mt2.birthday 36 | assert_kind_of Float, mt2.weight 37 | assert_kind_of Float, mt2.height 38 | # sadly, there is no bool type in Ruby 39 | assert (mt.singer.is_a?(TrueClass) || mt.singer.is_a?(FalseClass)) 40 | assert_equal mt.name, mt2.name 41 | assert_equal mt.stage_name, mt2.stage_name 42 | assert_equal mt.age, mt2.age 43 | assert_equal mt.singer, mt2.singer 44 | assert_equal mt.birthday, mt2.birthday 45 | assert_equal mt.weight, mt2.weight 46 | assert_equal mt.height, mt2.height 47 | end 48 | 49 | def test_zzz9 # run last 50 | MyTranslation.delete_domain 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/test_usage.rb: -------------------------------------------------------------------------------- 1 | # These ones take longer to run 2 | 3 | require 'test/unit' 4 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 5 | require File.join(File.dirname(__FILE__), "./test_helpers") 6 | require File.join(File.dirname(__FILE__), "./test_base") 7 | require "yaml" 8 | require 'aws' 9 | require_relative 'models/my_model' 10 | require_relative 'models/my_child_model' 11 | require 'active_support' 12 | 13 | # Tests for SimpleRecord 14 | # 15 | 16 | class TestUsage < TestBase 17 | 18 | def test_aaa_first_at_bat 19 | MyModel.delete_domain 20 | MyModel.create_domain 21 | end 22 | 23 | # ensures that it uses next token and what not 24 | def test_select_usage_logging 25 | 26 | SimpleRecord.log_usage(:select=>{:filename=>"/tmp/selects.csv", :format=>:csv, :lines_between_flushes=>2}) 27 | 28 | num_made = 10 29 | num_made.times do |i| 30 | mm = MyModel.create(:name=>"Gravis", :age=>i, :cool=>true) 31 | end 32 | 33 | mms = MyModel.find(:all, :conditions=>["name=?", "Gravis"],:consistent_read=>true) 34 | mms = MyModel.find(:all, :conditions=>["name=?", "Gravis"], :order=>"name desc",:consistent_read=>true) 35 | mms = MyModel.find(:all, :conditions=>["name=? and age>?", "Gravis", 3], :order=>"name desc",:consistent_read=>true) 36 | 37 | SimpleRecord.close_usage_log(:select) 38 | end 39 | 40 | def test_zzz_last_at_bat 41 | MyModel.delete_domain 42 | end 43 | 44 | end 45 | 46 | -------------------------------------------------------------------------------- /test/test_validations.rb: -------------------------------------------------------------------------------- 1 | gem 'test-unit' 2 | require 'test/unit' 3 | require File.join(File.dirname(__FILE__), "/../lib/simple_record") 4 | require File.join(File.dirname(__FILE__), "./test_helpers") 5 | require_relative "test_base" 6 | require "yaml" 7 | require 'aws' 8 | require_relative 'models/my_model' 9 | require_relative 'models/my_child_model' 10 | require_relative 'models/model_with_enc' 11 | require_relative 'models/validated_model' 12 | 13 | # Tests for SimpleRecord 14 | # 15 | 16 | class TestValidations < TestBase 17 | 18 | def test_aaa1 # run first 19 | MyModel.delete_domain 20 | ValidatedModel.delete_domain 21 | MyModel.create_domain 22 | ValidatedModel.create_domain 23 | end 24 | 25 | def test_first_validations 26 | mm = MyModel.new() 27 | assert mm.invalid?, "mm is valid. invalid? returned #{mm.invalid?}" 28 | assert_equal 1, mm.errors.size 29 | assert !mm.attr_before_create 30 | assert !mm.valid? 31 | assert mm.save == false, mm.errors.inspect 32 | assert !mm.attr_before_create # should not get called if not valid 33 | assert !mm.attr_after_save 34 | assert !mm.attr_after_create 35 | mm.name = "abcd" 36 | assert mm.valid?, mm.errors.inspect 37 | assert_equal 0, mm.errors.size 38 | 39 | mm.save_count = 2 40 | assert mm.invalid? 41 | 42 | mm.save_count = nil 43 | assert mm.valid? 44 | assert mm.save, mm.errors.inspect 45 | 46 | assert mm.attr_before_create 47 | assert mm.attr_after_save 48 | assert mm.attr_after_create 49 | assert !mm.attr_after_update 50 | 51 | assert mm.valid?, mm.errors.inspect 52 | assert_equal 1, mm.save_count 53 | 54 | mm.name = "abc123" 55 | assert mm.save 56 | 57 | assert mm.attr_after_update 58 | end 59 | 60 | def test_more_validations 61 | 62 | name = 'abcd' 63 | 64 | model = ValidatedModel.new 65 | assert !model.valid? 66 | assert !model.save 67 | model.name = name 68 | assert model.valid? 69 | assert model.save 70 | sleep 1 71 | 72 | model2 = ValidatedModel.new 73 | model2.name = name 74 | assert !model.valid? 75 | assert !model.save 76 | assert model.errors.size > 0 77 | end 78 | 79 | def test_zzz9 # run last 80 | MyModel.delete_domain 81 | ValidatedModel.delete_domain 82 | end 83 | end 84 | --------------------------------------------------------------------------------