diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..91600595 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci diff --git a/.dev/vagrant/minion b/.dev/vagrant/minion new file mode 100755 index 00000000..8e3cbc27 --- /dev/null +++ b/.dev/vagrant/minion @@ -0,0 +1,9 @@ +# Masterless Minion Configuration File +master: localhost +id: development +file_client: local + +# Where your salt state exists +file_roots: + base: + - /vagrant/.dev/vagrant/salt diff --git a/.dev/vagrant/salt/apt/init.sls b/.dev/vagrant/salt/apt/init.sls new file mode 100755 index 00000000..9355c0c2 --- /dev/null +++ b/.dev/vagrant/salt/apt/init.sls @@ -0,0 +1,13 @@ +apt-pkgs: + pkg.latest: + - pkgs: + - daemontools + - git-core + - openjdk-8-jre-headless + - tmux + - vim + +# JAVA_HOME +/home/vagrant/.bashrc: + file.append: + - text: export JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64" diff --git a/.dev/vagrant/salt/dynamodb/init.sls b/.dev/vagrant/salt/dynamodb/init.sls new file mode 100644 index 00000000..a0767416 --- /dev/null +++ b/.dev/vagrant/salt/dynamodb/init.sls @@ -0,0 +1,17 @@ +/opt/install/aws/dynamodb.tar.gz: + file.managed: + - source: https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_2017-02-16.tar.gz + - source_hash: sha256=d79732d7cd6e4b66fbf4bb7a7fc06cb75abbbe1bbbfb3d677a24815a1465a0b2 + - makedirs: True + +/vagrant/spec/DynamoDBLocal-latest: + file.directory: + - name: /vagrant/spec/DynamoDBLocal-latest + - user: vagrant + - group: vagrant + +dynamodb.install: + cmd.wait: + - name: cd /vagrant/spec/DynamoDBLocal-latest && tar xfz /opt/install/aws/dynamodb.tar.gz + - watch: + - file: /opt/install/aws/dynamodb.tar.gz diff --git a/.dev/vagrant/salt/rvm/.gemrc b/.dev/vagrant/salt/rvm/.gemrc new file mode 100755 index 00000000..6153a6e0 --- /dev/null +++ b/.dev/vagrant/salt/rvm/.gemrc @@ -0,0 +1 @@ +gem: --no-ri --no-rdoc diff --git a/.dev/vagrant/salt/rvm/init.sls b/.dev/vagrant/salt/rvm/init.sls new file mode 100644 index 00000000..17cca912 --- /dev/null +++ b/.dev/vagrant/salt/rvm/init.sls @@ -0,0 +1,80 @@ +# https://docs.saltstack.com/en/latest/ref/states/all/salt.states.rvm.html +rvm-deps: + pkg.installed: + - pkgs: + - bash + - coreutils + - gzip + - bzip2 + - gawk + - sed + - curl + - git-core + - subversion + - gnupg2 + +mri-deps: + pkg.installed: + - pkgs: + - build-essential + - openssl + - libreadline6 + - libreadline6-dev + - curl + - git-core + - zlib1g + - zlib1g-dev + - libssl-dev + - libyaml-dev + - libsqlite3-0 + - libsqlite3-dev + - sqlite3 + - libxml2-dev + - libxslt1-dev + - autoconf + - libc6-dev + - libncurses5-dev + - automake + - libtool + - bison + - subversion + - ruby + +gpg-trust: + cmd.run: + - cwd: /home/vagrant + - name: gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 + - runas: vagrant + +ruby-{{ pillar['ruby']['version'] }}: + rvm.installed: + - name: {{ pillar['ruby']['version'] }} + - default: True + - user: vagrant + - require: + - pkg: rvm-deps + - pkg: mri-deps + +# Disable Documentation Installation +/home/vagrant/.gemrc: + file.managed: + - user: vagrant + - group: vagrant + - name: /home/vagrant/.gemrc + - source: salt://rvm/.gemrc + - makedirs: True + +# Bundler +bundler.install: + gem.installed: + - user: vagrant + - name: bundler + - ruby: ruby-{{ pillar['ruby']['version'] }} + - rdoc: false + - ri: false + +bundle: + cmd.run: + - cwd: /vagrant + - name: bundle install + - runas: vagrant diff --git a/.dev/vagrant/salt/top.sls b/.dev/vagrant/salt/top.sls new file mode 100755 index 00000000..c9ad5fdc --- /dev/null +++ b/.dev/vagrant/salt/top.sls @@ -0,0 +1,5 @@ +base: + 'development': + - apt + - dynamodb + - rvm diff --git a/.gitignore b/.gitignore index 63309684..30ed578a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,12 +8,13 @@ rdoc # yardoc generated .yardoc +/_yardoc/ # bundler -.bundle +/.bundle/ # jeweler generated -pkg +/pkg/ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: # @@ -52,9 +53,22 @@ pkg .rvmrc # For RubyMine: -.idea +/.idea/ # For Ctags .gemtags .tags .tags_sorted_by_file + +Gemfile.lock +/doc/ +/spec/reports/ +/tmp/ +/spec/DynamoDBLocal-latest/ +/vendor/ + +# For vagrant +.vagrant + +# For Appraisals +gemfiles/*.gemfile.lock diff --git a/.rspec b/.rspec index 4e1e0d2f..8c18f1ab 100644 --- a/.rspec +++ b/.rspec @@ -1 +1,2 @@ +--format documentation --color diff --git a/.travis.yml b/.travis.yml index b5cdd365..c096bc95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,61 @@ +sudo: required + language: ruby rvm: - - ruby-1.9.3-p551 - - ruby-2.2.2 - - jruby-1.7.11 - - jruby-head - - rbx-2 -script: bundle exec rake unattended_spec + - ruby-2.0.0-p648 + - ruby-2.1.10 + - ruby-2.2.7 + - ruby-2.3.4 + - ruby-2.4.1 + - jruby-9.1.9.0 +gemfile: +gemfile: + - gemfiles/rails_4_0.gemfile + - gemfiles/rails_4_1.gemfile + - gemfiles/rails_4_2.gemfile + - gemfiles/rails_5_0.gemfile + - gemfiles/rails_5_1.gemfile + - gemfiles/rails_5_2.gemfile +matrix: + exclude: + - rvm: ruby-2.0.0-p648 + gemfile: gemfiles/rails_5_0.gemfile + - rvm: ruby-2.0.0-p648 + gemfile: gemfiles/rails_5_1.gemfile + - rvm: ruby-2.0.0-p648 + gemfile: gemfiles/rails_5_2.gemfile + - rvm: ruby-2.1.10 + gemfile: gemfiles/rails_5_0.gemfile + - rvm: ruby-2.1.10 + gemfile: gemfiles/rails_5_1.gemfile + - rvm: ruby-2.1.10 + gemfile: gemfiles/rails_5_2.gemfile + - rvm: ruby-2.4.1 + gemfile: gemfiles/rails_4_0.gemfile + - rvm: ruby-2.4.1 + gemfile: gemfiles/rails_4_1.gemfile + +### BUILD LIFECYCLE STEPS ### + +before_install: + # Debugging: Print out the current docker-compose version. + - docker-compose --version + + # If one of your containers does not build for + # whatever reason it's best to report that now before your tests start + # otherwise it can be really tricky to debug why tests are failing sometimes. + - docker ps + +after_install: + - gem install bundler -v 1.15.4 + - bundle install + +before_script: + # Start Docker Compose as a daemon + - docker-compose up -d + +script: + - bundle exec rake spec + +after_script: + - docker-compose down diff --git a/Appraisals b/Appraisals new file mode 100644 index 00000000..491bd4a6 --- /dev/null +++ b/Appraisals @@ -0,0 +1,26 @@ +appraise "rails-4-0" do + gem "rails", "~> 4.0.0" + gem "nokogiri", "~> 1.6.8" # can be removed once we drop support for Ruby 2.0.0 +end + +appraise "rails-4-1" do + gem "rails", "~> 4.1.0" + gem "nokogiri", "~> 1.6.8" # can be removed once we drop support for Ruby 2.0.0 +end + +appraise "rails-4-2" do + gem "rails", "~> 4.2.0" + gem "nokogiri", "~> 1.6.8" # can be removed once we drop support for Ruby 2.0.0 +end + +appraise "rails-5-0" do + gem "rails", "~> 5.0.0" +end + +appraise "rails-5-1" do + gem "rails", "~> 5.1.0" +end + +appraise "rails-5-2" do + gem "rails", "~> 5.2.0" +end diff --git a/CHANGELOG.md b/CHANGELOG.md index ced2d60c..1c187983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,223 @@ -# 2.0.0 +# 3.0.0 * Added hash support * Changed the way booleans are stored (like true/false) * Added fix to remove empty strings in Hashes, Arrays, Sets -* Added destroy! and delete_table -* Added secondary index support + +# HEAD + +## Breaking + +* N/A + +## Improvements + +* N/A + +## Fixes + +* N/A + +# 2.2.0 + +## Breaking + +* N/A + +## Improvements + +* Feature: [#256](https://github.com/Dynamoid/Dynamoid/pull/256) Support Rails 5.2 (@andrykonchin) + +## Fixes + +* Bug: [#255](https://github.com/Dynamoid/Dynamoid/pull/255) Fix Vagrant RVM configuration and upgrade to Ruby 2.4.1 (@richardhsu) + +# 2.1.0 + +## Breaking + +* N/A + +## Improvements + +* Feature: [#221](https://github.com/Dynamoid/Dynamoid/pull/221) Add field declaration option `of` to specify the type of `set` elements (@pratik60) +* Feature: [#223](https://github.com/Dynamoid/Dynamoid/pull/223) Add field declaration option `store_as_string` to store `datetime` as ISO-8601 formatted strings (@william101) +* Feature: [#228](https://github.com/Dynamoid/Dynamoid/pull/228) Add field declaration option `store_as_string` to store `date` as ISO-8601 formatted strings (@andrykonchin) +* Feature: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Support hash argument for `start` chain method (@mnussbaumer) +* Feature: [#236](https://github.com/Dynamoid/Dynamoid/pull/236) Change log level from `info` to `debug` for benchmark logging (@kicktheken) +* Feature: [#239](https://github.com/Dynamoid/Dynamoid/pull/239) Add methods for low-level updating: `.update`, `.update_fields` and `.upsert` (@andrykonchin) +* Feature: [#243](https://github.com/Dynamoid/Dynamoid/pull/243) Support `ne` condition operator (@andrykonchin) +* Feature: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Added support of backoff in batch operations (@andrykonchin) + * added global config options `backoff` and `backoff_strategies` to configure backoff + * added `constant` and `exponential` built-in backoff strategies + * `.find_all` and `.import` support new backoff options + +## Fixes + +* Bug: [#216](https://github.com/Dynamoid/Dynamoid/pull/216) Fix global index detection in queries with conditions other than equal (@andrykonchin) +* Bug: [#224](https://github.com/Dynamoid/Dynamoid/pull/224) Fix how `contains` operator works with `set` and `array` field types (@andrykonchin) +* Bug: [#225](https://github.com/Dynamoid/Dynamoid/pull/225) Fix equal conditions for `array` fields (@andrykonchin) +* Bug: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Repair support `start` chain method on Scan operation (@mnussbaumer) +* Bug: [#238](https://github.com/Dynamoid/Dynamoid/pull/238) Fix default value of `models_dir` config option (@baloran) +* Bug: [#244](https://github.com/Dynamoid/Dynamoid/pull/244) Allow to pass empty strings and sets to `.import` (@andrykonchin) +* Bug: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Batch operations (`batch_write_item` and `batch_read_item`) handle unprocessed items themselves (@andrykonchin) +* Bug: [#250](https://github.com/Dynamoid/Dynamoid/pull/250) Update outdated warning message about inefficient query and missing indices (@andrykonchin) +* Bug: [252](https://github.com/Dynamoid/Dynamoid/pull/252) Don't loose nanoseconds when store DateTime as float number + +# 2.0.0 + +## Breaking + +Breaking changes in this release generally bring Dynamoid behavior closer to the Rails-way. + +* Change: [#186](https://github.com/Dynamoid/Dynamoid/pull/186) Consistent behavior for `Model.where({}).all` (@andrykonchin) + * <= 1.3.x behaviour - + * load lazily if user specified batch size + * load all collection into memory otherwise + * New behaviour - + * always return lazy evaluated collection + * It means Model.where({}).all returns Enumerator instead of Array. + * If you need Array interface you have to convert collection to Array manually with to_a method call +* Change: [#195](https://github.com/Dynamoid/Dynamoid/pull/195) Failed `#find` returns error (@andrykonchin) + * <= 1.3.x behaviour - find returns nil or smaller array. + * New behaviour - it raises RecordNotFound if one or more records can not be found for the requested ids +* Change: [#196](https://github.com/Dynamoid/Dynamoid/pull/196) Return value of `#save` (@andrykonchin) + * <= 1.3.x behaviour - save returns self if model is saved successfully + * New behaviour - it returns true + +## Improvements + +* Feature: [#185](https://github.com/Dynamoid/Dynamoid/pull/185) `where`, finders and friends take into account STI (single table inheritance) now (@andrykonchin) + * query will return items of the model class and all subclasses +* Feature: [#190](https://github.com/Dynamoid/Dynamoid/pull/190) Allow passing options to range when defining attributes of the document (@richardhsu) + * Allows for serialized fields and passing the serializer option. +* Feature: [#198](https://github.com/Dynamoid/Dynamoid/pull/198) Enhanced `#create` and `#create!` to allow multiple document creation like `#import` (@andrykonchin) + * `User.create([{name: 'Josh'}, {name: 'Nick'}])` +* Feature: [#199](https://github.com/Dynamoid/Dynamoid/pull/199) Added `Document.import` method (@andrykonchin) +* Feature: [#205](https://github.com/Dynamoid/Dynamoid/pull/205) Use batch deletion via `batch_write_item` for `delete_all` (@andrykonchin) +* Rename: [#205](https://github.com/Dynamoid/Dynamoid/pull/205) `Chain#destroy_all` as `Chain#delete_all`, to better match Rails conventions when no callbacks are run (@andrykonchin) + * kept the old name as an alias, for backwards compatibility +* Feature: [#207](https://github.com/Dynamoid/Dynamoid/pull/207) Added slicing by 25 requests in #batch_write_item (@andrykonchin) +* Feature: [#211](https://github.com/Dynamoid/Dynamoid/pull/211) Improved Vagrant setup for testing (@richardhsu) +* Feature: [#212](https://github.com/Dynamoid/Dynamoid/pull/212) Add foreign_key option (@andrykonchin) +* Feature: [#213](https://github.com/Dynamoid/Dynamoid/pull/213) Support Boolean raw type (@andrykonchin) +* Improved Documentation (@pboling, @andrykonchin) + +## Fixes + +* Bug: [#191](https://github.com/Dynamoid/Dynamoid/pull/191), [#192](https://github.com/Dynamoid/Dynamoid/pull/192) Support lambdas as fix for value types were not able to be used as default values (@andrykonchin)(@richardhsu) +* Bug: [#202](https://github.com/Dynamoid/Dynamoid/pull/202) Fix several issues with associations (@andrykonchin) + * setting `nil` value raises an exception + * document doesn't keep assigned model and loads it from the storage + * delete call doesn't update cached ids of associated models + * fix clearing old `has_many` association while add model to new `has_many` association +* Bug: [#204](https://github.com/Dynamoid/Dynamoid/pull/204) Fixed issue where `Document.where(:"id.in" => [])` would do `Query` operation instead of `Scan` (@andrykonchin) + * Fixed `Chain#key_present?` +* Bug: [#205](https://github.com/Dynamoid/Dynamoid/pull/205) Fixed `delete_all` (@andrykonchin) + * Fixed exception when makes scan and sort key is declared in model + * Fixed exception when makes scan and any condition is specified in where clause (like Document.where().delete_all) + * Fixed exception when makes query and sort key isn't declared in model +* Bug: [#207](https://github.com/Dynamoid/Dynamoid/pull/207) Fixed `#delete` method for case `adapter.delete(table_name, [1, 2, 3], range_key: 1)` (@andrykonchin) + +# 1.3.4 + +## Improvements + +* Added `Chain#last` method (@andrykonchin) +* Added `date` field type (@andrykonchin) +* Added `application_timezone` config option (@andrykonchin) +* Allow consistent reading for Scan request (@andrykonchin) +* Use Query instead of Scan if there are no conditions for sort (range) key in where clause (@andrykonchin) +* Support condition operators for non-key fields for Query request (@andrykonchin) +* Support condition operators for Scan request (@andrykonchin) +* Support additional operators `in`, `contains`, `not_contains` (@andrykonchin) +* Support type casting in `where` clause (@andrykonchin) +* Rename `Chain#eval_limit` to `#record_limit` (@richardhsu) +* Add `Chain#scan_limit` (@richardhsu) +* Support batch loading for Query requests (@richardhsu) +* Support querying Global/Local Secondary Indices in `where` clause (@richardhsu) +* Only query on GSI if projects all attributes in `where` clause (@richardhsu) + +## Fixes + +* Fix incorrect applying of default field value (#36 and #117, @andrykonchin) +* Fix sync table creation/deletion (#160, @mirokuxy) +* Allow to override document timestamps (@andrykonchin) +* Fix storing empty array as nil (#8, @andrykonchin) +* Fix `limit` handling for Query requests (#85, @richardhsu) +* Fix `limit` handling for Scan requests (#85, @richardhsu) +* Fix paginating for Query requests (@richardhsu) +* Fix paginating for Scan requests (@richardhsu) +* Fix `batch_get_item` method call for integer partition key (@mudasirraza) + +# 1.3.3 + +* Allow configuration of the Dynamoid models directory, as not everyone keeps non AR models in app/models + - Dynamoid::Config.models_dir = "app/whatever" + +# 1.3.2 + +* Fix migrations by stopping the loading of all rails models outside the rails env. + +# 1.3.1 + +* Implements #135 + * dump values for :integer, :string, :boolean fields passed to where query + * e.g. You can search for booleans with any of: `[true, false, "t", "f", "true", "false"]` +* Adds support for Rails 5 without warnings. +* Adds rake tasks for working with a DynamoDB database: + * rake dynamoid:create_tables + * rake dynamoid:ping +* Automatically requires the Railtie when in Rails (which loads the rake tasks) +* Prevent duplicate entries in Dynamoid.included_models +* Added wwtd and appraisal to spec suite for easier verification of the compatibility matrix +* Support is now officially Ruby 2.0+, (including JRuby 9000) and Rails 4.0+ + +# 1.3.0 + +* Fixed specs (@AlexNisnevich & @pboling) +* Fix `blank?` and `present?` behavior for single associations (#110, @AlexNisnevich & @bayesimpact) +* Support BatchGet for more than 100 items (#80, @getninjas) +* Add ability to specify connection settings specific to Dynamoid (#116, @NielsKSchjoedt) +* Adds Support for Rails 5! (#109, @gastzars) +* Table Namespace Fix (#79, @alexperto) +* Improve Testing Docs (#103, @tadast) +* Query All Items by Looping (#102, @richardhsu) +* Store document in DocumentNotValid error for easier debugging (#98, holyketzer) +* Better support for raw datatype (#104, @OpenGov) +* Fix associative tables with non-id primary keys (#86, @everett-wetchler) + +# 1.2.1 + +* Remove accidental Gemfile.lock; fix .gitignore (#95, @pboling) +* Allow options to put_items (#95, @alexperto) +* Support range key in secondary index queries (#95, @pboling) +* Better handling of options generally (#95, @pboling) +* Support for batch_delete_item API (#95, @pboling) +* Support for batch_write_item API (#95, @alexperto) + +# 1.2.0 + +* Add create_table_syncronously, and sync: option to regular create_table (@pboling) + * make required for tables created with secondary indexes +* Expose and fix truncate method on adapter (#52, @pcorpet) +* Enable saving without updating timestamps (#58, @cignoir) +* Fix projected attributes by checking for :include (#56, @yoshida_tetsuhiro) +* Make behavior of association where method closer to AR by cloning instead of modifying (#51, @pcorpet) +* Add boolean field presence validator (#50, @pcorpet) +* Add association build method (#49, @pcorpet) +* Fix association create method (#47, #48, @pcorpet) +* Support range_between (#42, @ayemos) +* Fix problems with range query (#42, @ayemos) +* Don't prefix table names when namespace is nil (#40, @brenden) +* Added basic secondary index support (#34, @sumocoder) +* Fix query attribute behavior for booleans (#35, @amirmanji) +* Ignore unknown fields on model initialize (PR #33, @sumocoder) # 1.1.0 -* Added support for optimistic locking on delete (PR #29, sumocoder) -* upgrade concurrent-ruby requirement to 1.0 (PR #31, keithmgould) +* Added support for optimistic locking on delete (PR #29, @sumocoder) +* upgrade concurrent-ruby requirement to 1.0 (PR #31, @keithmgould) # 1.0.0 diff --git a/Gemfile b/Gemfile index d615c8b9..1a4488aa 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,6 @@ -source "http://www.rubygems.org" +source "https://rubygems.org" + +# Specify your gem's dependencies in dynamoid.gemspec gemspec + +gem "pry-byebug", platforms: :ruby diff --git a/LICENSE.txt b/LICENSE.txt index f3b63afa..0e0ce350 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,20 +1,22 @@ +MIT License + Copyright (c) 2012 Josh Symonds +Copyright (c) 2013 - 2018 Dynamoid, https://github.com/Dynamoid -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: +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: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -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. +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. diff --git a/README.markdown b/README.markdown deleted file mode 100644 index 0060c902..00000000 --- a/README.markdown +++ /dev/null @@ -1,377 +0,0 @@ -# Dynamoid - -Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It -provides similar functionality to ActiveRecord and improves on -Amazon's existing -[HashModel](http://docs.amazonwebservices.com/AWSRubySDK/latest/AWS/Record/HashModel.html) -by providing better searching tools and native association support. - -DynamoDB is not like other document-based databases you might know, and is very different indeed from relational databases. It sacrifices anything beyond the simplest relational queries and transactional support to provide a fast, cost-efficient, and highly durable storage solution. If your database requires complicated relational queries and transaction support, then this modest Gem cannot provide them for you, and neither can DynamoDB. In those cases you would do better to look elsewhere for your database needs. - -But if you want a fast, scalable, simple, easy-to-use database (and a Gem that supports it) then look no further! - -## Installation - -Installing Dynamoid is pretty simple. First include the Gem in your Gemfile: - -```ruby -gem 'dynamoid', '~> 1' -``` -## Prerequisities - -Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (~> 4). -Hence the configuration as needed for aws to work will be dealt with by aws setup. - -Here are the steps to setup aws-sdk. - -```ruby -gem 'aws-sdk', '~>2' -``` - -(or) include the aws-sdk in your Gemfile. - -**NOTE:** Dynamoid-1.0 doesn't support aws-sdk Version 1 (Use Dynamoid Major Version 0 for aws-sdk 1) - -Configure AWS access: -[Reference](https://github.com/aws/aws-sdk-ruby) - -For example, to configure AWS access: - -Create config/initializers/aws.rb as follows: - -```ruby - - Aws.config.update({ - region: 'us-west-2', - credentials: Aws::Credentials.new('REPLACE_WITH_ACCESS_KEY_ID', 'REPLACE_WITH_SECRET_ACCESS_KEY'), - }) - -``` - -For a full list of the DDB regions, you can go -[here](http://docs.aws.amazon.com/general/latest/gr/rande.html#ddb_region). - -Then you need to initialize Dynamoid config to get it going. Put code similar to this somewhere (a Rails initializer would be a great place for this if you're using Rails): - -```ruby - Dynamoid.configure do |config| - config.adapter = 'aws_sdk_v2' # This adapter establishes a connection to the DynamoDB servers using Amazon's own AWS gem. - config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have. - config.warn_on_scan = true # Output a warning to the logger when you perform a scan rather than a query on a table. - config.read_capacity = 100 # Read capacity for your tables - config.write_capacity = 20 # Write capacity for your tables - config.endpoint = 'http://localhost:3000' # [Optional]. If provided, it communicates with the DB listening at the endpoint. This is useful for testing with [Amazon Local DB] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html). - end - -``` - -Once you have the configuration set up, you need to move on to making models. - -## Setup - -You *must* include ```Dynamoid::Document``` in every Dynamoid model. - -```ruby -class User - include Dynamoid::Document - -end -``` - -### Table - -Dynamoid has some sensible defaults for you when you create a new table, including the table name and the primary key column. But you can change those if you like on table creation. - -```ruby -class User - include Dynamoid::Document - - table :name => :awesome_users, :key => :user_id, :read_capacity => 400, :write_capacity => 400 -end -``` - -These fields will not change an existing table: so specifying a new read_capacity and write_capacity here only works correctly for entirely new tables. Similarly, while Dynamoid will look for a table named `awesome_users` in your namespace, it won't change any existing tables to use that name; and if it does find a table with the correct name, it won't change its hash key, which it expects will be user_id. If this table doesn't exist yet, however, Dynamoid will create it with these options. - -### Fields - -You'll have to define all the fields on the model and the data type of each field. Every field on the object must be included here; if you miss any they'll be completely bypassed during DynamoDB's initialization and will not appear on the model objects. - -By default, fields are assumed to be of type ```:string```. Other built-in types are -```:integer```, ```:number```, ```:set```, ```:array```, ```:datetime```, ```:boolean```, and ```:serialized```. -If built-in types do not suit you, you can use a custom field type represented by an arbitrary class, provided that the class supports a compatible serialization interface. -The primary use case for using a custom field type is to represent your business logic with high-level types, while ensuring portability or backward-compatibility of the serialized representation. - -You get magic columns of id (string), created_at (datetime), and updated_at (datetime) for free. - -```ruby -class User - include Dynamoid::Document - - field :name - field :email - field :rank, :integer - field :number, :number - field :joined_at, :datetime - field :hash, :serialized - -end -``` - -You can optionally set a default value on a field using either a plain value or a lambda: - -```ruby - field :actions_taken, :integer, {default: 0} - field :joined_at, :datetime, {default: ->(){Time.now}} -``` - -To use a custom type for a field, suppose you have a `Money` type. - -```ruby - class Money - # ... your business logic ... - - def dynamoid_dump - "serialized representation as a string" - end - - def self.dynamoid_load(serialized_str) - # parse serialized representation and return a Money instance - Money.new(...) - end - end - - class User - include Dynamoid::Document - - field :balance, Money - end -``` - -If you want to use a third-party class (which does not support `#dynamoid_dump` and `.dynamoid_load`) -as your field type, you can use an adapter class providing `.dynamoid_dump` and `.dynamoid_load` class methods -for your third-party class. (`.dynamoid_load` can remain the same from the previous example; here we just -add a level of indirection for serializing.) Example: - -```ruby - # Third-party Money class - class Money; end - - class MoneyAdapter - def self.dynamoid_load(money_serialized_str) - Money.new(...) - end - - def self.dynamoid_dump(money_obj) - money_obj.value.to_s - end - end - - class User - include Dynamoid::Document - - field :balance, MoneyAdapter - end -``` - -Lastly, you can control the data type of your custom-class-backed field at the DynamoDB level. -This is especially important if you want to use your custom field as a numeric range or for -number-oriented queries. By default custom fields are persisted as a string attribute, but -your custom class can override this with a `.dynamoid_field_type` class method, which would -return either `:string` or `:number`. -(DynamoDB supports some other attribute types, but Dynamoid yet does not.) - - -### Associations - -Just like in ActiveRecord (or your other favorite ORM), Dynamoid uses associations to create links between models. - -The only supported associations (so far) are ```has_many```, ```has_one```, ```has_and_belongs_to_many```, and ```belongs_to```. Associations are very simple to create: just specify the type, the name, and then any options you'd like to pass to the association. If there's an inverse association either inferred or specified directly, Dynamoid will update both objects to point at each other. - -```ruby -class User - include Dynamoid::Document - - ... - - has_many :addresses - has_many :students, :class => User - belongs_to :teacher, :class_name => :user - belongs_to :group - has_one :role - has_and_belongs_to_many :friends, :inverse_of => :friending_users - -end - -class Address - include Dynamoid::Document - - ... - - belongs_to :address # Automatically links up with the user model - -end -``` - -Contrary to what you'd expect, association information is always contained on the object specifying the association, even if it seems like the association has a foreign key. This is a side effect of DynamoDB's structure: it's very difficult to find foreign keys without an index. Usually you won't find this to be a problem, but it does mean that association methods that build new models will not work correctly -- for example, ```user.addresses.new``` returns an address that is not associated to the user. We'll be correcting this soon. - -### Validations - -Dynamoid bakes in ActiveModel validations, just like ActiveRecord does. - -```ruby -class User - include Dynamoid::Document - - ... - - validates_presence_of :name - validates_format_of :email, :with => /@/ -end -``` - -To see more usage and examples of ActiveModel validations, check out the [ActiveModel validation documentation](http://api.rubyonrails.org/classes/ActiveModel/Validations.html). - -### Callbacks - -Dynamoid also employs ActiveModel callbacks. Right now, callbacks are defined on ```save```, ```update```, ```destroy```, which allows you to do ```before_``` or ```after_``` any of those. - -```ruby -class User - include Dynamoid::Document - - ... - - before_save :set_default_password - after_create :notify_friends - after_destroy :delete_addresses -end -``` - -## Usage - -### Object Creation - -Dynamoid's syntax is generally very similar to ActiveRecord's. Making new objects is simple: - -```ruby -u = User.new(:name => 'Josh') -u.email = 'josh@joshsymonds.com' -u.save -``` - -Save forces persistence to the datastore: a unique ID is also assigned, but it is a string and not an auto-incrementing number. - -```ruby -u.id # => "3a9f7216-4726-4aea-9fbc-8554ae9292cb" -``` - -To use associations, you use association methods very similar to ActiveRecord's: - -```ruby -address = u.addresses.create -address.city = 'Chicago' -address.save -``` - -### Querying - -Querying can be done in one of three ways: - -```ruby -Address.find(address.id) # Find directly by ID. -Address.where(:city => 'Chicago').all # Find by any number of matching criteria... though presently only "where" is supported. -Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax. -``` - -And you can also query on associations: - -```ruby -u.addresses.where(:city => 'Chicago').all -``` - -But keep in mind Dynamoid -- and document-based storage systems in general -- are not drop-in replacements for existing relational databases. The above query does not efficiently perform a conditional join, but instead finds all the user's addresses and naively filters them in Ruby. For large associations this is a performance hit compared to relational database engines. - -You can also limit the number of evaluated records, or select a record from which to start, to support pagination: - -```ruby -Address.eval_limit(5).start(address) # Only 5 addresses. -``` - -For large queries that return many rows, Dynamoid can use AWS' support for requesting documents in batches: - -```ruby -#Do some maintenance on the entire table without flooding DynamoDB -Address.all(batch_size: 100).each { |address| address.do_some_work; sleep(0.01) } -Address.limit(10_000).batch(100). each { … } #batch specified as part of a chain -``` - -### Consistent Reads - -Querying supports consistent reading. By default, DynamoDB reads are eventually consistent: if you do a write and then a read immediately afterwards, the results of the previous write may not be reflected. If you need to do a consistent read (that is, you need to read the results of a write immediately) you can do so, but keep in mind that consistent reads are twice as expensive as regular reads for DynamoDB. - -```ruby -Address.find(address.id, :consistent_read => true) # Find an address, ensure the read is consistent. -Address.where(:city => 'Chicago').consistent.all # Find all addresses where the city is Chicago, with a consistent read. -``` - -### Range Finding - -If you have a range index, Dynamoid provides a number of additional other convenience methods to make your life a little easier: - -```ruby -User.where("created_at.gt" => DateTime.now - 1.day).all -User.where("created_at.lt" => DateTime.now - 1.day).all -``` - -It also supports .gte and .lte. Turning those into symbols and allowing a Rails SQL-style string syntax is in the works. You can only have one range argument per query, because of DynamoDB's inherent limitations, so use it sensibly! - -## Concurrency - -Dynamoid supports basic, ActiveRecord-like optimistic locking on save operations. Simply add a `lock_version` column to your table like so: - -```ruby -class MyTable - ... - - field :lock_version, :integer - - ... -end -``` - -In this example, all saves to `MyTable` will raise an `Dynamoid::Errors::StaleObjectError` if a concurrent process loaded, edited, and saved the same row. Your code should trap this exception, reload the row (so that it will pick up the newest values), and try the save again. - -Calls to `update` and `update!` also increment the `lock_version`, however they do not check the existing value. This guarantees that a update operation will raise an exception in a concurrent save operation, however a save operation will never cause an update to fail. Thus, `update` is useful & safe only for doing atomic operations (e.g. increment a value, add/remove from a set, etc), but should not be used in a read-modify-write pattern. - -## Credits - -Dynamoid borrows code, structure, and even its name very liberally from the truly amazing [Mongoid](https://github.com/mongoid/mongoid). Without Mongoid to crib from none of this would have been possible, and I hope they don't mind me reusing their very awesome ideas to make DynamoDB just as accessible to the Ruby world as MongoDB. - -Also, without contributors the project wouldn't be nearly as awesome. So many thanks to: - -* [Logan Bowers](https://github.com/loganb) -* [Lane LaRue](https://github.com/luxx) -* [Craig Heneveld](https://github.com/cheneveld) -* [Anantha Kumaran](https://github.com/ananthakumaran) -* [Jason Dew](https://github.com/jasondew) -* [Luis Arias](https://github.com/luisantonioa) -* [Stefan Neculai](https://github.com/stefanneculai) -* [Philip White](https://github.com/philipmw) -* [Peeyush Kumar](https://github.com/peeyush1234) - -## Running the tests - -Running the tests is fairly simple. In one window, run `bin/start_dynamodblocal`, and in the other, use `rake`. - -[![Build Status](https://travis-ci.org/Dynamoid/Dynamoid.svg)](https://travis-ci.org/Dynamoid/Dynamoid) -[![Coverage Status](https://coveralls.io/repos/Dynamoid/Dynamoid/badge.svg?branch=master&service=github)](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) - -## Copyright - -Copyright (c) 2012 Josh Symonds. - -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: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8b13acea --- /dev/null +++ b/README.md @@ -0,0 +1,866 @@ +# Dynamoid + +You are viewing the README for version 2 of Dynamoid. See the [CHANGELOG](https://github.com/Dynamoid/Dynamoid/blob/master/CHANGELOG.md#200) for details on breaking changes since 1.3.x. + +For version 1.3.x use the [1-3-stable branch](https://github.com/Dynamoid/Dynamoid/blob/1-3-stable/README.md). + +Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It +provides similar functionality to ActiveRecord and improves on +Amazon's existing +[HashModel](http://docs.amazonwebservices.com/AWSRubySDK/latest/AWS/Record/HashModel.html) +by providing better searching tools and native association support. + +DynamoDB is not like other document-based databases you might know, and is very different indeed from relational databases. It sacrifices anything beyond the simplest relational queries and transactional support to provide a fast, cost-efficient, and highly durable storage solution. If your database requires complicated relational queries and transaction support, then this modest Gem cannot provide them for you, and neither can DynamoDB. In those cases you would do better to look elsewhere for your database needs. + +But if you want a fast, scalable, simple, easy-to-use database (and a Gem that supports it) then look no further! + + +| Project | Dynamoid | +|------------------------ | ----------------- | +| gem name | dynamoid | +| license | MIT | +| download rank | [![Total Downloads](https://img.shields.io/gem/rt/Dynamoid.svg)](https://rubygems.org/gems/dynamoid) | +| version | [![Gem Version](https://badge.fury.io/rb/dynamoid.svg)](https://rubygems.org/gems/dynamoid) | +| dependencies | [![Dependency Status](https://gemnasium.com/badges/github.com/Dynamoid/Dynamoid.svg)](https://gemnasium.com/github.com/Dynamoid/Dynamoid) [![Depfu](https://badges.depfu.com/badges/6661c063c8e77a5008344fc7283a50aa/status.svg)](https://depfu.com)| +| code quality | [![Code Climate](https://codeclimate.com/github/Dynamoid/Dynamoid.svg)](https://codeclimate.com/github/Dynamoid/Dynamoid) | +| continuous integration | [![Build Status](https://travis-ci.org/Dynamoid/Dynamoid.svg?branch=master)](https://travis-ci.org/Dynamoid/Dynamoid) | +| test coverage | [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/Dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) | +| triage helpers | [![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid) | +| homepage | [https://github.com/Dynamoid/Dynamoid](https://github.com/Dynamoid/Dynamoid) | +| documentation | [http://rdoc.info/github/Dynamoid/Dynamoid/frames](http://rdoc.info/github/Dynamoid/Dynamoid/frames) | + +## Installation + +Installing Dynamoid is pretty simple. First include the Gem in your Gemfile: + +```ruby +gem 'dynamoid', '~> 2' +``` +## Prerequisities + +Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (>= 4). +Hence the configuration as needed for aws to work will be dealt with by aws setup. + +Here are the steps to setup aws-sdk. + +```ruby +gem 'aws-sdk', '~>2' +``` + +(or) include the aws-sdk in your Gemfile. + +### AWS SDK Version Compatibility + +Make sure you are using the version for the right AWS SDK. + +| Dynamoid version | AWS SDK Version | +| ---------------- | --------------- | +| 0.x | 1.x | +| 1.x | 2.x | +| 2.x | 2.x | +| 3.x (unreleased) | 3.x | + +### AWS Configuration + +Configure AWS access: +[Reference](https://github.com/aws/aws-sdk-ruby) + +For example, to configure AWS access: + +Create config/initializers/aws.rb as follows: + +```ruby + + Aws.config.update({ + region: 'us-west-2', + credentials: Aws::Credentials.new('REPLACE_WITH_ACCESS_KEY_ID', 'REPLACE_WITH_SECRET_ACCESS_KEY'), + }) + +``` + +Alternatively, if you don't want Aws connection settings to be overwritten for you entire project, you can specify connection settings for Dynamoid only, by setting those in the `Dynamoid.configure` clause: + +```ruby + require 'dynamoid' + Dynamoid.configure do |config| + config.access_key = 'REPLACE_WITH_ACCESS_KEY_ID' + config.secret_key = 'REPLACE_WITH_SECRET_ACCESS_KEY' + config.region = 'us-west-2' + end +``` + +For a full list of the DDB regions, you can go +[here](http://docs.aws.amazon.com/general/latest/gr/rande.html#ddb_region). + +Then you need to initialize Dynamoid config to get it going. Put code similar to this somewhere (a Rails initializer would be a great place for this if you're using Rails): + +```ruby + require 'dynamoid' + Dynamoid.configure do |config| + config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have. Set to nil to avoid namespacing. + config.endpoint = 'http://localhost:3000' # [Optional]. If provided, it communicates with the DB listening at the endpoint. This is useful for testing with [Amazon Local DB] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html). + end + +``` + +### Ruby & Rails Compatibility Matrix + +| Ruby / Active Record | 4.0.x | 4.1.x | 4.2.x | 5.0.x | +|:---------------------:|:-----:|:-----:|:-----:|:-----:| +| 2.0.0 | ✓ | ✓ | ✓ | | +| 2.1.x | ✓ | ✓ | ✓ | | +| 2.2.0-2.2.1 | ✓ | ✓ | ✓ | | +| 2.2.2+ | ✓ | ✓ | ✓ | ✓ | +| 2.3.x | ✓ | ✓ | ✓ | ✓ | +| 2.3.x | ✓ | ✓ | ✓ | ✓ | +| 2.4.x | | | ✓ | ✓ | +| jruby-9.X | ✓ | ✓ | ✓ | ✓ | + +## Setup + +You *must* include ```Dynamoid::Document``` in every Dynamoid model. + +```ruby +class User + include Dynamoid::Document + +end +``` + +### Table + +Dynamoid has some sensible defaults for you when you create a new table, including the table name and the primary key column. But you can change those if you like on table creation. + +```ruby +class User + include Dynamoid::Document + + table :name => :awesome_users, :key => :user_id, :read_capacity => 5, :write_capacity => 5 +end +``` + +These fields will not change an existing table: so specifying a new read_capacity and write_capacity here only works correctly for entirely new tables. Similarly, while Dynamoid will look for a table named `awesome_users` in your namespace, it won't change any existing tables to use that name; and if it does find a table with the correct name, it won't change its hash key, which it expects will be user_id. If this table doesn't exist yet, however, Dynamoid will create it with these options. + +### Fields + +You'll have to define all the fields on the model and the data type of each field. Every field on the object must be included here; if you miss any they'll be completely bypassed during DynamoDB's initialization and will not appear on the model objects. + +By default, fields are assumed to be of type ```:string```. Other built-in types are +```:integer```, ```:number```, ```:set```, ```:array```, ```:datetime```, ```date```, ```:boolean```, ```:raw``` and ```:serialized```. +```raw``` type means you can store Ruby Array, Hash, String and numbers. +If built-in types do not suit you, you can use a custom field type represented by an arbitrary class, provided that the class supports a compatible serialization interface. +The primary use case for using a custom field type is to represent your business logic with high-level types, while ensuring portability or backward-compatibility of the serialized representation. + +#### Note on boolean type + +The boolean fields are stored as `"t", "f"` strings by default. DynamoDB +supports boolean type natively. So if you want to use native boolean +type or already have table with native boolean attribute you can easily +achieve this with `store_as_native_boolean` option: + +```ruby +class Document + include DynamoId::Document + + field :active, :boolean, store_as_native_boolean: true +end +``` + +#### Note on date type + +By default date fields are persisted as days count since 1 January 1970 like UNIX time. If you prefer dates to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true` + +```ruby +class Document + include DynamoId::Document + + field :sent_at, :datetime, store_as_string: true +end +``` + +#### Note on datetime type + +By default datetime fields are persisted as UNIX timestamps with milisecond precission in DynamoDB. If you prefer datetimes to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true` + +```ruby +class Document + include DynamoId::Document + + field :sent_at, :datetime, store_as_string: true +end +``` + +### Note on set type + +There is `of` option to declare the type of set elements. You can use +`:integer` value only + +```ruby +class Document + include DynamoId::Document + + field :tags, :set, of: :integer +end +``` + + +#### Magic Columns + +You get magic columns of id (string), created_at (datetime), and updated_at (datetime) for free. + +```ruby +class User + include Dynamoid::Document + + field :name + field :email + field :rank, :integer + field :number, :number + field :joined_at, :datetime + field :hash, :serialized + +end +``` + +#### Default Values + +You can optionally set a default value on a field using either a plain value or a lambda: + +```ruby + field :actions_taken, :integer, {default: 0} + field :joined_at, :datetime, {default: ->(){Time.now}} +``` + +#### Custom Types + +To use a custom type for a field, suppose you have a `Money` type. + +```ruby + class Money + # ... your business logic ... + + def dynamoid_dump + "serialized representation as a string" + end + + def self.dynamoid_load(serialized_str) + # parse serialized representation and return a Money instance + Money.new(1.23) + end + end + + class User + include Dynamoid::Document + + field :balance, Money + end +``` + +If you want to use a third-party class (which does not support `#dynamoid_dump` and `.dynamoid_load`) +as your field type, you can use an adapter class providing `.dynamoid_dump` and `.dynamoid_load` class methods +for your third-party class. (`.dynamoid_load` can remain the same from the previous example; here we just +add a level of indirection for serializing.) Example: + +```ruby + # Third-party Money class + class Money; end + + class MoneyAdapter + def self.dynamoid_load(money_serialized_str) + Money.new(1.23) + end + + def self.dynamoid_dump(money_obj) + money_obj.value.to_s + end + end + + class User + include Dynamoid::Document + + field :balance, MoneyAdapter + end +``` + +Lastly, you can control the data type of your custom-class-backed field at the DynamoDB level. +This is especially important if you want to use your custom field as a numeric range or for +number-oriented queries. By default custom fields are persisted as a string attribute, but +your custom class can override this with a `.dynamoid_field_type` class method, which would +return either `:string` or `:number`. + +DynamoDB may support some other attribute types that are not yet supported by Dynamoid. + +### Associations + +Just like in ActiveRecord (or your other favorite ORM), Dynamoid uses associations to create links between models. + +The only supported associations (so far) are ```has_many```, ```has_one```, ```has_and_belongs_to_many```, and ```belongs_to```. Associations are very simple to create: just specify the type, the name, and then any options you'd like to pass to the association. If there's an inverse association either inferred or specified directly, Dynamoid will update both objects to point at each other. + +```ruby +class User + include Dynamoid::Document + + # ... + + has_many :addresses + has_many :students, :class => User + belongs_to :teacher, :class_name => :user + belongs_to :group + belongs_to :group, :foreign_key => :group_id + has_one :role + has_and_belongs_to_many :friends, :inverse_of => :friending_users + +end + +class Address + include Dynamoid::Document + + # ... + + belongs_to :user # Automatically links up with the user model + +end +``` + +Contrary to what you'd expect, association information is always contained on the object specifying the association, even if it seems like the association has a foreign key. This is a side effect of DynamoDB's structure: it's very difficult to find foreign keys without an index. Usually you won't find this to be a problem, but it does mean that association methods that build new models will not work correctly -- for example, ```user.addresses.new``` returns an address that is not associated to the user. We'll be correcting this ~soon~ maybe someday, if we get a pull request. + +### Validations + +Dynamoid bakes in ActiveModel validations, just like ActiveRecord does. + +```ruby +class User + include Dynamoid::Document + + # ... + + validates_presence_of :name + validates_format_of :email, :with => /@/ +end +``` + +To see more usage and examples of ActiveModel validations, check out the [ActiveModel validation documentation](http://api.rubyonrails.org/classes/ActiveModel/Validations.html). + +If you want to bypass model validation, pass `validate: false` to `save` call: + +```ruby +model.save(validate: false) +``` + +### Callbacks + +Dynamoid also employs ActiveModel callbacks. Right now, callbacks are defined on ```save```, ```update```, ```destroy```, which allows you to do ```before_``` or ```after_``` any of those. + +```ruby +class User + include Dynamoid::Document + + # ... + + before_save :set_default_password + after_create :notify_friends + after_destroy :delete_addresses +end +``` + +### STI + +Dynamoid supports STI (Single Table Inheritance) like Active Record does. You need just specify `type` field in a base class. Example: + +```ruby +class Animal + include Dynamoid::Document + + field :name + field :type +end + +class Cat < Animal + field :lives, :integer +end + +cat = Cat.create(name: 'Morgan') +animal = Animal.find(cat.id) +animal.class +#=> Cat + +``` + +## Usage + +### Object Creation + +Dynamoid's syntax is generally very similar to ActiveRecord's. Making new objects is simple: + +```ruby +u = User.new(:name => 'Josh') +u.email = 'josh@joshsymonds.com' +u.save +``` + +Save forces persistence to the datastore: a unique ID is also assigned, but it is a string and not an auto-incrementing number. + +```ruby +u.id # => "3a9f7216-4726-4aea-9fbc-8554ae9292cb" +``` + +To use associations, you use association methods very similar to ActiveRecord's: + +```ruby +address = u.addresses.create +address.city = 'Chicago' +address.save +``` + +To create multiple documents at once: + +```ruby +User.create([{name: 'Josh'}, {name: 'Nick'}]) +``` + +There is an efficient and low-level way to create multiple documents +(without validation and callbacks running): + +```ruby +users = User.import([{name: 'Josh'}, {name: 'Nick'}]) +``` + +### Querying + +Querying can be done in one of three ways: + +```ruby +Address.find(address.id) # Find directly by ID. +Address.where(:city => 'Chicago').all # Find by any number of matching criteria... though presently only "where" is supported. +Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax. +``` + +And you can also query on associations: + +```ruby +u.addresses.where(:city => 'Chicago').all +``` + +But keep in mind Dynamoid -- and document-based storage systems in general -- are not drop-in replacements for existing relational databases. The above query does not efficiently perform a conditional join, but instead finds all the user's addresses and naively filters them in Ruby. For large associations this is a performance hit compared to relational database engines. + +#### Limits + +There are three types of limits that you can query with: + +1. `record_limit` - The number of evaluated records that are returned by the query. +2. `scan_limit` - The number of scanned records that DynamoDB will look at before returning. +3. `batch_size` - The number of records requested to DynamoDB per underlying request, good for large queries! + +Using these in various combinations results in the underlying requests to be made in the smallest size possible and +the query returns once `record_limit` or `scan_limit` is satisfied. It will attempt to batch whenever possible. + +You can thus limit the number of evaluated records, or select a record from which to start in order to support pagination. + +```ruby +Address.record_limit(5).start(address) # Only 5 addresses starting at `address` +``` +Where `address` is an instance of the model or a hash `{the_model_hash_key: 'value', the_model_range_key: 'value'}`: +Keep in mind that if you are passing a hash to `.start()` you need to explicitly define all required keys in it including range keys, depending on table or secondary indexes signatures, otherwise you'll get an `Aws::DynamoDB::Errors::ValidationException` either for `Exclusive Start Key must have same size as table's key schema` or `The provided starting key is invalid` + +If you are potentially running over a large data set and this is especially true when using certain filters, you may +want to consider limiting the number of scanned records (the number of records DynamoDB infrastructure looks through +when evaluating data to return): + +```ruby +Address.scan_limit(5).start(address) # Only scan at most 5 records and return what's found starting from `address` +``` + +For large queries that return many rows, Dynamoid can use AWS' support for requesting documents in batches: + +```ruby +# Do some maintenance on the entire table without flooding DynamoDB +Address.all(batch_size: 100).each { |address| address.do_some_work; sleep(0.01) } +Address.record_limit(10_000).batch(100). each { … } # Batch specified as part of a chain +``` + +The implication of batches is that the underlying requests are done in the batch sizes to make the request and responses +more manageable. Note that this batching is for `Query` and `Scans` and not `BatchGetItem` commands. + +#### Sort Conditions and Filters + +You are able to optimize query with condition for sort key. Following operators are available: `gt`, `lt`, `gte`, `lte`, +`begins_with`, `between` as well as equality: + +```ruby +Address.where(latitude: 10212) +Address.where('latitude.gt' => 10212) +Address.where('latitude.lt' => 10212) +Address.where('latitude.gte' => 10212) +Address.where('latitude.lte' => 10212) +Address.where('city.begins_with' => 'Lon') +Address.where('latitude.between' => [10212, 20000]) +``` + +You are able to filter results on the DynamoDB side and specify conditions for non-key fields. +Following operators are available: `in`, `contains`, `not_contains`: + +```ruby +Address.where('city.in' => ['London', 'Edenburg', 'Birmingham']) +Address.where('city.contains' => [on]) +Address.where('city.not_contains' => [ing]) +``` + +### Consistent Reads + +Querying supports consistent reading. By default, DynamoDB reads are eventually consistent: if you do a write and then a read immediately afterwards, the results of the previous write may not be reflected. If you need to do a consistent read (that is, you need to read the results of a write immediately) you can do so, but keep in mind that consistent reads are twice as expensive as regular reads for DynamoDB. + +```ruby +Address.find(address.id, :consistent_read => true) # Find an address, ensure the read is consistent. +Address.where(:city => 'Chicago').consistent.all # Find all addresses where the city is Chicago, with a consistent read. +``` + +### Range Finding + +If you have a range index, Dynamoid provides a number of additional other convenience methods to make your life a little easier: + +```ruby +User.where("created_at.gt" => DateTime.now - 1.day).all +User.where("created_at.lt" => DateTime.now - 1.day).all +``` + +It also supports .gte and .lte. Turning those into symbols and allowing a Rails SQL-style string syntax is in the works. You can only have one range argument per query, because of DynamoDB's inherent limitations, so use it sensibly! + + +### Updating + +In order to update document you can use high level methods +`#update_attributes`, `#update_attribute` and `.update`. +They run validation and collbacks. + +```ruby +Address.find(id).update_attributes(city: 'Chicago') +Address.find(id).update_attribute(city, 'Chicago') +Address.update(id, city: 'Chicago') +Address.update(id, { city: 'Chicago' }, if: { deliverable: true }) +``` + +There are also some low level methods `#update`, `.update_fields` and +`.upsert`. They don't run validation and callbacks (except `#update` - it +runs `update` callbacks). All of them support conditional updates. +`#upsert` will create new document if document with specified `id` +doesn't exist. + +```ruby +Adderess.find(id).update do |i| + i.set city: 'Chicago' + i.add latitude: 100 + i.delete set_of_numbers: 10 +end +Adderess.find(id).update(if: { deliverable: true }) do |i| + i.set city: 'Chicago' +end +Address.update_fields(id, city: 'Chicago') +Address.update_fields(id, { city: 'Chicago' }, if: { deliverable: true }) +Address.upsert(id, city: 'Chicago') +Address.upsert(id, { city: 'Chicago' }, if: { deliverable: true }) +``` + +### Deleting + +In order to delete some items `delete_all` method should be used. +Any callback wont be called. Items delete in efficient way in batch. + +```ruby +Address.where(city: "London").delete_all +``` + +### Global Secondary Indexes + +You can define index with `global_secondary_index`: + +```ruby +class User + include Dynamoid::Document + + field :name + field :age, :number + + global_secondary_index hash_key: :age +end +``` + +There are following options: +* `hash_key` - is used as hash key of an index, +* `range_key` - is used as range key of an index, +* `projected_attributes` - list of fields to store in an index or has a predefiled value `:keys_only`, `:all`; `:keys_only` is a default, +* `name` - an index will be created with this name when a table is created; by default name is generated and contains table name and keys names, +* `read_capacity` - is used when table creates and used as an index capacity; by default equals `Dynamoid::Config.read_capacity`, +* `write_capacity` - is used when table creates and used as an index capacity; by default equals `Dynamoid::Config.write_capacity` + +The only mandatory option is `name`. + +To use index in `Document.where` implicitly you need to project all the fields with option `projected_attributes: :all`. + +There are two ways to query Global Secondary Indexes (GSI). + +#### Explicit + +The first way explicitly uses your GSI and utilizes the `find_all_by_secondary_index` method which will lookup a valid +GSI to use based on the inputs, you MUST provide the correct keys to match the GSI you want: + +```ruby +find_all_by_secondary_index( + { + dynamo_primary_key_column_name => dynamo_primary_key_value + }, # The signature of find_all_by_secondary_index is ugly, so must be an explicit hash here + :range => { + "#{range_column}.#{range_modifier}" => range_value + }, + # false is the same as DESC in SQL (newest timestamp first) + # true is the same as ASC in SQL (oldest timestamp first) + :scan_index_forward => false # or true +) +``` + +Where the range modifier is one of `Dynamoid::Finders::RANGE_MAP.keys`, where the `RANGE_MAP` is: + +```ruby +RANGE_MAP = { + 'gt' => :range_greater_than, + 'lt' => :range_less_than, + 'gte' => :range_gte, + 'lte' => :range_lte, + 'begins_with' => :range_begins_with, + 'between' => :range_between, + 'eq' => :range_eq +} +``` + +Most range searches, like `eq`, need a single value, and searches like `between`, need an array with two values. + +#### Implicit + +The second way implicitly uses your GSI through the `where` clauses and deduces the index based on the query fields +provided. Another added benefit is that it is built into query chaining so you can use all the methods used in normal +querying. The explicit way from above would be rewritten as follows: + +```ruby +where(dynamo_primary_key_column_name => dynamo_primary_key_value, + "#{range_column}.#{range_modifier}" => range_value) + .scan_index_forward(false) +``` + +The only caveat with this method is that because it is also used for general querying, it WILL NOT use a GSI unless it +explicitly has defined `projected_attributes: :all` on the GSI in your model. This is because GSIs that do not have all +attributes projected will only contain the index keys and therefore will not return objects with fully resolved field +values. It currently opts to provide the complete results rather than partial results unless you've explicitly looked up +the data. + +*Future TODO could involve implementing `select` in chaining as well as resolving the fields with a second query against +the table since a query against GSI then a query on base table is still likely faster than scan on the base table* + +## Configuration + +Listed below are all configuration options. + +* `adapter` - usefull only for the gem developers to switch to a new adapter. Default and the only available value is `aws_sdk_v2` +* `namespace` - prefix for table names, default is `dynamoid_#{application_name}_#{environment}` for Rails application and `dynamoid` otherwise +* `logger` - by default it's a `Rails.logger` in Rails application and `stdout` otherwise. You can disable logging by setting `nil` or `false` values. Set `true` value to use defaults +* `access_key` - DynamoDb custom credentials for AWS, override global AWS credentials if they present +* `secret_key` - DynamoDb custom credentials for AWS, override global AWS credentials if they present +* `region` - DynamoDb custom credentials for AWS, override global AWS credentials if they present +* `batch_size` - when you try to load multiple items at once with `batch_get_item` call Dynamoid loads them not with one api call but piece by piece. Default is 100 items +* `read_capacity` - is used at table or indices creation. Default is 100 (units) +* `write_capacity` - is used at table or indices creation. Default is 20 (units) +* `warn_on_scan` - log warnings when scan table. Default is `true` +* `endpoint` - if provided, it communicates with the DynamoDB listening at the endpoint. This is useful for testing with [Amazon Local DB] +* `identity_map` - ensures that each object gets loaded only once by keeping every loaded object in a map. Looks up objects using the map when referring to them. Isn't thread safe. Default is `false`. + `Use Dynamoid::Middleware::IdentityMap` to clear identity map for each HTTP request +* `timestamps` - by default Dynamoid sets `created_at` and `updated_at` fields for model at creation and updating. You can disable this behavior by setting `false` value +* `sync_retry_max_times` - when Dynamoid creates or deletes table synchronously it checks for completion specified times. Default is 60 (times). It's a bit over 2 minutes by default +* `sync_retry_wait_seconds` - time to wait between retries. Default is 2 (seconds) +* `convert_big_decimal` - if `true` then Dynamoid converts numbers stored in `Hash` in `raw` field to float. Default is `false` +* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is `./app/models`. +* `application_timezone` - Dynamoid converts all `datetime` fields to specified time zone when loads data from the storage. + Acceptable values - `utc`, `local` (to use system time zone) and time zone name e.g. `Eastern Time (US & Canada)`. Default is `local` +* `store_datetime_as_string` - if `true` then Dynamoid stores :datetime fields in ISO 8601 string format. Default is `false` +* `store_date_as_string` - if `true` then Dynamoid stores :date fields in ISO 8601 string format. Default is `false` +* `backoff` - is a hash: key is a backoff strategy (symbol), value is parameters for the strategy. Is used in batch operations. Default id `nil` +* `backoff_strategies`: is a hash and contains all available strategies. Default is { constant: ..., exponential: ...} + + +## Concurrency + +Dynamoid supports basic, ActiveRecord-like optimistic locking on save operations. Simply add a `lock_version` column to your table like so: + +```ruby +class MyTable + # ... + + field :lock_version, :integer + + # ... +end +``` + +In this example, all saves to `MyTable` will raise an `Dynamoid::Errors::StaleObjectError` if a concurrent process loaded, edited, and saved the same row. Your code should trap this exception, reload the row (so that it will pick up the newest values), and try the save again. + +Calls to `update` and `update!` also increment the `lock_version`, however they do not check the existing value. This guarantees that a update operation will raise an exception in a concurrent save operation, however a save operation will never cause an update to fail. Thus, `update` is useful & safe only for doing atomic operations (e.g. increment a value, add/remove from a set, etc), but should not be used in a read-modify-write pattern. + + +### Backoff strategies + + +You can use several methods that run efficiently in batch mode like `.find_all` and `.import`. + +The backoff strategy will be used when, for any reason, some items could not be processed as part of a batch mode command. +Operations will be re-run to process these items. + +Exponential backoff is the recommended way to handle throughput limits exceeding and throttling on the table. + +There are two built-in strategies - constant delay and truncated binary exponential backoff. +By default no backoff is used but you can specify one of the built-in ones: + +```ruby +Dynamoid.configure do |config| + config.backoff = { constant: 2.second } +end + +Dynamoid.configure do |config| + config.backoff = { exponential: { base_backoff: 0.2.seconds, ceiling: 10 } } +end + +``` + +You can just specify strategy without any arguments to use default presets: + +```ruby +Dynamoid.configure do |config| + config.backoff = :constant +end +``` + +You can use your own strategy in following way: + +```ruby +Dynamoid.configure do |config| + config.backoff_strategies[:custom] = lambda do |n| + -> { sleep rand(n) } + end + + config.backoff = { custom: 10 } +end +``` + + +## Rake Tasks + + * `rake dynamoid:create_tables` + * `rake dynamoid:ping` + +## Test Environment + +In test environment you will most likely want to clean the database between test runs to keep tests completely isolated. This can be achieved like so + +```ruby +module DynamoidReset + def self.all + Dynamoid.adapter.list_tables.each do |table| + # Only delete tables in our namespace + if table =~ /^#{Dynamoid::Config.namespace}/ + Dynamoid.adapter.delete_table(table) + end + end + Dynamoid.adapter.tables.clear + # Recreate all tables to avoid unexpected errors + Dynamoid.included_models.each(&:create_table) + end +end + +# Reduce noise in test output +Dynamoid.logger.level = Logger::FATAL +``` + +If you're using RSpec you can invoke the above like so: + +```ruby +RSpec.configure do |config| + config.before(:each) do + DynamoidReset.all + end +end +``` + +In Rails, you may also want to ensure you do not delete non-test data accidentally by adding the following to your test environment setup: + +```ruby +raise "Tests should be run in 'test' environment only" if Rails.env != 'test' +Dynamoid.configure do |config| + config.namespace = "#{Rails.application.railtie_name}_#{Rails.env}" +end +``` + +## Credits + +Dynamoid borrows code, structure, and even its name very liberally from the truly amazing [Mongoid](https://github.com/mongoid/mongoid). Without Mongoid to crib from none of this would have been possible, and I hope they don't mind me reusing their very awesome ideas to make DynamoDB just as accessible to the Ruby world as MongoDB. + +Also, without contributors the project wouldn't be nearly as awesome. So many thanks to: + +* [Logan Bowers](https://github.com/loganb) +* [Lane LaRue](https://github.com/luxx) +* [Craig Heneveld](https://github.com/cheneveld) +* [Anantha Kumaran](https://github.com/ananthakumaran) +* [Jason Dew](https://github.com/jasondew) +* [Luis Arias](https://github.com/luisantonioa) +* [Stefan Neculai](https://github.com/stefanneculai) +* [Philip White](https://github.com/philipmw) * +* [Peeyush Kumar](https://github.com/peeyush1234) +* [Sumanth Ravipati](https://github.com/sumocoder) +* [Pascal Corpet](https://github.com/pcorpet) +* [Brian Glusman](https://github.com/bglusman) * +* [Peter Boling](https://github.com/pboling) * +* [Andrew Konchin](https://github.com/andrykonchin) * + +\* Current Maintainers + +## Running the tests + +Running the tests is fairly simple. You should have an instance of DynamoDB running locally. Follow these steps to setup your test environment. + + * First download and unpack the latest version of DynamoDB. We have a script that will do this for you if you use homebrew on a Mac. + + ```shell + bin/setup + ``` + + * Start the local instance of DynamoDB to listen in ***8000*** port + + ```shell + bin/start_dynamodblocal + ``` + + * and lastly, use `rake` to run the tests. + + ```shell + rake + ``` + + * When you are done, remember to stop the local test instance of dynamodb + + ```shell + bin/stop_dynamodblocal + ``` + +If you want to run all the specs that travis runs, use `bundle exec wwtd`, but first you will need to setup all the rubies, for each of `%w( 2.0.0-p648 2.1.10 2.2.6 2.3.3 2.4.1 jruby-9.1.8.0 )`. When you run `bundle exec wwtd` it will take care of starting and stopping the local dynamodb instance. + +```shell +rvm use 2.0.0-p648 +gem install rubygems-update +gem install bundler +bundle install +``` + +## Copyright + +Copyright (c) 2012 Josh Symonds. + +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: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +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. diff --git a/Rakefile b/Rakefile index 39180a40..ea16acf8 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,6 @@ -# encoding: utf-8 +require "bundler/gem_tasks" -require 'rubygems' -require 'bundler' +require "bundler/setup" begin Bundler.setup(:default, :development) rescue Bundler::BundlerError => e @@ -9,47 +8,25 @@ rescue Bundler::BundlerError => e $stderr.puts "Run `bundle install` to install missing gems" exit e.status_code end -require 'rake' - -require 'rspec/core' -require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = FileList['spec/**/*_spec.rb'] -end - -RSpec::Core::RakeTask.new(:rcov) do |spec| - spec.pattern = 'spec/**/*_spec.rb' - spec.rcov = true +if defined?(Rails) + load "./lib/dynamoid/tasks/database.rake" end -desc "Start DynamoDBLocal, run tests, clean up" -task :unattended_spec do |t| - - if system('bin/start_dynamodblocal') - puts 'DynamoDBLocal started; proceeding with specs.' - else - raise 'Unable to start DynamoDBLocal. Cannot run unattended specs.' - end - - #Cleanup - at_exit do - unless system('bin/stop_dynamodblocal') - $stderr.puts 'Unable to cleanly stop DynamoDBLocal.' - end - end - - Rake::Task["spec"].invoke +require "rake" +require "rspec/core/rake_task" +RSpec::Core::RakeTask.new(:spec) do |spec| + spec.pattern = FileList["spec/**/*_spec.rb"] end -require 'yard' +require "yard" YARD::Rake::YardocTask.new do |t| - t.files = ['lib/**/*.rb', "README", "LICENSE"] # optional - t.options = ['-m', 'markdown'] # optional + t.files = ["lib/**/*.rb", "README", "LICENSE"] # optional + t.options = ["-m", "markdown"] # optional end -desc 'Publish documentation to gh-pages' +desc "Publish documentation to gh-pages" task :publish do - Rake::Task['yard'].invoke + Rake::Task["yard"].invoke `git add .` `git commit -m 'Regenerated documentation'` `git checkout gh-pages` @@ -64,4 +41,6 @@ task :publish do `git checkout master` end +require "wwtd/tasks" + task :default => :spec diff --git a/Vagrantfile b/Vagrantfile new file mode 100755 index 00000000..de2b7660 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,27 @@ +Vagrant.configure('2') do |config| + # Choose base box + config.vm.box = 'bento/ubuntu-16.04' + + config.vm.provider 'virtualbox' do |vb| + # Prevent clock skew when host goes to sleep while VM is running + vb.customize ['guestproperty', 'set', :id, '/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold', 10_000] + + vb.cpus = 2 + vb.memory = 2048 + end + + # Defaults + config.vm.provision :salt do |salt| + salt.masterless = true + salt.minion_config = '.dev/vagrant/minion' + + # Pillars + salt.pillar({ + 'ruby' => { + 'version' => '2.4.1', + } + }) + + salt.run_highstate = true + end +end diff --git a/bin/_dynamodblocal b/bin/_dynamodblocal index 2e3e7173..2ec58dd5 100644 --- a/bin/_dynamodblocal +++ b/bin/_dynamodblocal @@ -1,2 +1,4 @@ -DIST_DIR=spec/DynamoDBLocal-2015-01-27 +DIST_DIR=spec/DynamoDBLocal-latest PIDFILE=dynamodb.pid +LISTEN_PORT=8000 +LOG_DIR="logs" diff --git a/bin/console b/bin/console new file mode 100755 index 00000000..56d5784a --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "dynamoid" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..06e7b478 --- /dev/null +++ b/bin/setup @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here +if ! brew info wget &>/dev/null; then + brew install wget +else + echo wget is already installed +fi +wget http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.zip --quiet -O spec/dynamodb_temp.zip +unzip -qq spec/dynamodb_temp.zip -d spec/DynamoDBLocal-latest +rm spec/dynamodb_temp.zip diff --git a/bin/start_dynamodblocal b/bin/start_dynamodblocal index b29b734f..b9f65a1c 100755 --- a/bin/start_dynamodblocal +++ b/bin/start_dynamodblocal @@ -1,7 +1,6 @@ #!/bin/sh -LISTEN_PORT=8000 - +# Source variables . $(dirname $0)/_dynamodblocal if [ -z $JAVA_HOME ]; then @@ -21,18 +20,20 @@ if [ ! -f DynamoDBLocal.jar ] || [ ! -d DynamoDBLocal_lib ]; then exit 1 fi +mkdir -p $LOG_DIR +echo "DynamoDB Local output will save to ${DIST_DIR}/${LOG_DIR}/" hash lsof 2>/dev/null && lsof -i :$LISTEN_PORT && { echo >&2 "Something is already listening on port $LISTEN_PORT; I will not attempt to start DynamoDBLocal."; exit 1; } -nohup $JAVA_HOME/bin/java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -inMemory -delayTransientStatuses -port $LISTEN_PORT 0<&- >/dev/null 2>&1 & -pid=$! -echo "Launched to listen on port $LISTEN_PORT." +NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +nohup $JAVA_HOME/bin/java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -delayTransientStatuses -port $LISTEN_PORT -inMemory 1>"${LOG_DIR}/${NOW}.out.log" 2>"${LOG_DIR}/${NOW}.err.log" & +PID=$! echo 'Verifying that DynamoDBLocal actually started...' # Allow some seconds for the JDK to start and die. counter=0 while [ $counter -le 5 ]; do - kill -0 $pid + kill -0 $PID if [ $? -ne 0 ]; then echo >&2 'ERROR: DynamoDBLocal died after we tried to start it!' exit 1 @@ -42,5 +43,5 @@ while [ $counter -le 5 ]; do fi done -echo "Started DynamoDBLocal at pid $pid to listen on port $LISTEN_PORT." -echo $pid > $PIDFILE +echo "DynamoDB Local started with pid $PID listening on port $LISTEN_PORT." +echo $PID > $PIDFILE diff --git a/bin/stop_dynamodblocal b/bin/stop_dynamodblocal index 44fecad8..08b26283 100755 --- a/bin/stop_dynamodblocal +++ b/bin/stop_dynamodblocal @@ -1,5 +1,6 @@ #!/bin/sh +# Source variables . $(dirname $0)/_dynamodblocal cd $DIST_DIR diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ad892acc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +version: '2' + +services: + dynamodb: + image: deangiberson/aws-dynamodb-local + ports: + - 8000:8000 diff --git a/dynamoid.gemspec b/dynamoid.gemspec index a88cc92b..14212813 100644 --- a/dynamoid.gemspec +++ b/dynamoid.gemspec @@ -1,76 +1,63 @@ -# -*- encoding: utf-8 -*- +# coding: utf-8 +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "dynamoid/version" + +Gem::Specification.new do |spec| + spec.name = "dynamoid" + spec.version = "3.0.0" -Gem::Specification.new do |s| - s.name = "dynamoid" - s.version = "2.0.0" # Keep in sync with README - s.authors = [ - 'Josh Symonds', - 'Logan Bowers', - 'Craig Heneveld', - 'Anatha Kumaran', - 'Jason Dew', - 'Luis Arias', - 'Stefan Neculai', - 'Philip White', - 'Peeyush Kumar', + spec.authors = [ + "Josh Symonds", + "Logan Bowers", + "Craig Heneveld", + "Anatha Kumaran", + "Jason Dew", + "Luis Arias", + "Stefan Neculai", + "Philip White", + "Peeyush Kumar", + "Sumanth Ravipati", + "Pascal Corpet", + "Brian Glusman", + "Peter Boling", + "Andrew Konchin" ] - s.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement." - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.markdown" + spec.email = ["peter.boling@gmail.com", "brian@stellaservice.com"] + + spec.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement." + spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB" + spec.extra_rdoc_files = [ + "LICENSE.txt", + "README.md" ] - # file list is generated with `git ls-files | grep -v -E -e '^spec/' -e '^\.' -e 'bin/'` - s.files = %w( - CHANGELOG.md - Gemfile - LICENSE.txt - README.markdown - Rakefile - dynamoid.gemspec - lib/dynamoid.rb - lib/dynamoid/adapter.rb - lib/dynamoid/adapter_plugin/aws_sdk_v2.rb - lib/dynamoid/associations.rb - lib/dynamoid/associations/association.rb - lib/dynamoid/associations/belongs_to.rb - lib/dynamoid/associations/has_and_belongs_to_many.rb - lib/dynamoid/associations/has_many.rb - lib/dynamoid/associations/has_one.rb - lib/dynamoid/associations/many_association.rb - lib/dynamoid/associations/single_association.rb - lib/dynamoid/components.rb - lib/dynamoid/config.rb - lib/dynamoid/config/options.rb - lib/dynamoid/criteria.rb - lib/dynamoid/criteria/chain.rb - lib/dynamoid/dirty.rb - lib/dynamoid/document.rb - lib/dynamoid/errors.rb - lib/dynamoid/fields.rb - lib/dynamoid/finders.rb - lib/dynamoid/identity_map.rb - lib/dynamoid/middleware/identity_map.rb - lib/dynamoid/persistence.rb - lib/dynamoid/validations.rb - ) - s.homepage = "http://github.com/Dynamoid/Dynamoid" - s.licenses = ["MIT"] - s.require_paths = ["lib"] - s.rubygems_version = "1.8.24" - s.summary = "Dynamoid is an ORM for Amazon's DynamoDB" + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin|test|spec|features|.dev|Vagrantfile)/}) } + spec.homepage = "http://github.com/Dynamoid/Dynamoid" + spec.licenses = ["MIT"] + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] - s.add_runtime_dependency(%q, ["~> 4"]) - s.add_runtime_dependency(%q, ["~> 2"]) - s.add_runtime_dependency(%q, [">= 1.0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, ["~> 3"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) + # This form of switching the gem dependencies is not compatible with wwtd & appraisal + # if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.2") + # spec.add_runtime_dependency(%q, [">= 4", "< 5.1.0"]) + # spec.add_development_dependency(%q, [">= 4", "< 5.1.0"]) + # else + # spec.add_runtime_dependency(%q, ["~> 4"]) + # spec.add_development_dependency(%q, ["~> 4"]) + # end + spec.add_runtime_dependency(%q, [">= 4"]) + spec.add_development_dependency(%q, [">= 4"]) + spec.add_runtime_dependency(%q, ["~> 2"]) + spec.add_runtime_dependency(%q, [">= 1.0"]) + spec.add_development_dependency "pry" + spec.add_development_dependency "bundler", "~> 1.14" + spec.add_development_dependency "rake", "~> 12.0" + spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "appraisal", "~> 2.1" + spec.add_development_dependency "wwtd", "~> 1.3" + spec.add_development_dependency(%q, [">= 0"]) + spec.add_development_dependency "coveralls", "~> 0.8" end - diff --git a/gemfiles/rails_4_0.gemfile b/gemfiles/rails_4_0.gemfile new file mode 100644 index 00000000..603cdb36 --- /dev/null +++ b/gemfiles/rails_4_0.gemfile @@ -0,0 +1,9 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry-byebug", platforms: :ruby +gem "rails", "~> 4.0.0" +gem "nokogiri", "~> 1.6.8" + +gemspec path: "../" diff --git a/gemfiles/rails_4_1.gemfile b/gemfiles/rails_4_1.gemfile new file mode 100644 index 00000000..d7089d7c --- /dev/null +++ b/gemfiles/rails_4_1.gemfile @@ -0,0 +1,9 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry-byebug", platforms: :ruby +gem "rails", "~> 4.1.0" +gem "nokogiri", "~> 1.6.8" + +gemspec path: "../" diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile new file mode 100644 index 00000000..77a3472d --- /dev/null +++ b/gemfiles/rails_4_2.gemfile @@ -0,0 +1,9 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry-byebug", platforms: :ruby +gem "rails", "~> 4.2.0" +gem "nokogiri", "~> 1.6.8" + +gemspec path: "../" diff --git a/gemfiles/rails_5_0.gemfile b/gemfiles/rails_5_0.gemfile new file mode 100644 index 00000000..e3e04b49 --- /dev/null +++ b/gemfiles/rails_5_0.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry-byebug", platforms: :ruby +gem "rails", "~> 5.0.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5_1.gemfile b/gemfiles/rails_5_1.gemfile new file mode 100644 index 00000000..f9b4a294 --- /dev/null +++ b/gemfiles/rails_5_1.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry-byebug", platforms: :ruby +gem "rails", "~> 5.1.0" + +gemspec path: "../" diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile new file mode 100644 index 00000000..49871db2 --- /dev/null +++ b/gemfiles/rails_5_2.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "pry-byebug", platforms: :ruby +gem "rails", "~> 5.2.0" + +gemspec path: "../" diff --git a/lib/dynamoid.rb b/lib/dynamoid.rb index 1fc59c9e..b6391814 100644 --- a/lib/dynamoid.rb +++ b/lib/dynamoid.rb @@ -1,14 +1,15 @@ -require "delegate" -require "time" -require "securerandom" -require "active_support" -require "active_support/core_ext" +require 'delegate' +require 'time' +require 'securerandom' +require 'active_support' +require 'active_support/core_ext' require 'active_support/json' -require "active_support/inflector" -require "active_support/lazy_load_hooks" -require "active_support/time_with_zone" -require "active_model" +require 'active_support/inflector' +require 'active_support/lazy_load_hooks' +require 'active_support/time_with_zone' +require 'active_model' +require 'dynamoid/version' require 'dynamoid/errors' require 'dynamoid/fields' require 'dynamoid/indexes' @@ -24,12 +25,18 @@ require 'dynamoid/document' require 'dynamoid/adapter' +require 'dynamoid/tasks/database' + require 'dynamoid/middleware/identity_map' +if defined?(Rails) + require 'dynamoid/railtie' +end + module Dynamoid extend self - MAX_ITEM_SIZE = 65_536 + MAX_ITEM_SIZE = 400_000 def configure block_given? ? yield(Dynamoid::Config) : Dynamoid::Config diff --git a/lib/dynamoid/adapter.rb b/lib/dynamoid/adapter.rb index a51a77a0..38e1b51e 100644 --- a/lib/dynamoid/adapter.rb +++ b/lib/dynamoid/adapter.rb @@ -17,7 +17,7 @@ def initialize def tables if !@tables_.value - @tables_.swap{|value, args| benchmark('Cache Tables') {list_tables}} + @tables_.swap{|value, args| benchmark('Cache Tables') { list_tables || [] } } end @tables_.value end @@ -51,7 +51,7 @@ def clear_cache! def benchmark(method, *args) start = Time.now result = yield - Dynamoid.logger.info "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }" + Dynamoid.logger.debug "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }" return result end @@ -80,12 +80,12 @@ def write(table, object, options = nil) # unless multiple ids are passed in. # # @since 0.2.0 - def read(table, ids, options = {}) + def read(table, ids, options = {}, &blk) range_key = options.delete(:range_key) if ids.respond_to?(:each) ids = ids.collect{|id| range_key ? [id, range_key] : id} - batch_get_item({table => ids}, options) + batch_get_item({table => ids}, options, &blk) else options[:range_key] = range_key if range_key get_item(table, ids, options) @@ -99,13 +99,13 @@ def read(table, ids, options = {}) # @param [Array] range_key of the record to delete, can also be a string of just one range_key # def delete(table, ids, options = {}) - range_key = options[:range_key] #array of range keys that matches the ids passed in + range_key = options[:range_key] # array of range keys that matches the ids passed in if ids.respond_to?(:each) if range_key.respond_to?(:each) - #turn ids into array of arrays each element being hash_key, range_key - ids = ids.each_with_index.map{|id,i| [id,range_key[i]]} + # turn ids into array of arrays each element being hash_key, range_key + ids = ids.each_with_index.map{|id, i| [id, range_key[i]]} else - ids = range_key ? [[ids, range_key]] : ids + ids = range_key ? ids.map { |id| [id, range_key] } : ids end batch_delete_item(table => ids) @@ -120,7 +120,7 @@ def delete(table, ids, options = {}) # @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan # # @since 0.2.0 - def scan(table, query, opts = {}) + def scan(table, query = {}, opts = {}) benchmark('Scan', table, query) {adapter.scan(table, query, opts)} end @@ -131,12 +131,25 @@ def create_table(table_name, key, options = {}) end end - [:batch_get_item, :delete_item, :delete_table, :get_item, :list_tables, :put_item].each do |m| + # @since 0.2.0 + def delete_table(table_name, options = {}) + if tables.include?(table_name) + benchmark('Delete Table') { adapter.delete_table(table_name, options) } + idx = tables.index(table_name) + tables.delete_at(idx) + end + end + + [:batch_get_item, :delete_item, :get_item, :list_tables, :put_item, :truncate, :batch_write_item, :batch_delete_item].each do |m| # Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing. # # @since 0.2.0 - define_method(m) do |*args| - benchmark("#{m.to_s}", args) {adapter.send(m, *args)} + define_method(m) do |*args, &blk| + if blk.present? + benchmark("#{m.to_s}", *args) { adapter.send(m, *args, &blk) } + else + benchmark("#{m.to_s}", *args) { adapter.send(m, *args) } + end end end diff --git a/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb b/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb index 400de68d..0d0158a3 100644 --- a/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +++ b/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb @@ -3,20 +3,80 @@ module AdapterPlugin # The AwsSdkV2 adapter provides support for the aws-sdk version 2 for ruby. class AwsSdkV2 + EQ = 'EQ'.freeze + RANGE_MAP = { + range_greater_than: 'GT', + range_less_than: 'LT', + range_gte: 'GE', + range_lte: 'LE', + range_begins_with: 'BEGINS_WITH', + range_between: 'BETWEEN', + range_eq: 'EQ' + } + + # Don't implement NULL and NOT_NULL because it doesn't make seanse - + # we declare schema in models + FIELD_MAP = { + eq: 'EQ', + ne: 'NE', + gt: 'GT', + lt: 'LT', + gte: 'GE', + lte: 'LE', + begins_with: 'BEGINS_WITH', + between: 'BETWEEN', + in: 'IN', + contains: 'CONTAINS', + not_contains: 'NOT_CONTAINS' + } + HASH_KEY = 'HASH'.freeze + RANGE_KEY = 'RANGE'.freeze + STRING_TYPE = 'S'.freeze + NUM_TYPE = 'N'.freeze + BINARY_TYPE = 'B'.freeze + TABLE_STATUSES = { + creating: 'CREATING', + updating: 'UPDATING', + deleting: 'DELETING', + active: 'ACTIVE' + }.freeze + PARSE_TABLE_STATUS = ->(resp, lookup = :table) { + # lookup is table for describe_table API + # lookup is table_description for create_table API + # because Amazon, damnit. + resp.send(lookup).table_status + } + BATCH_WRITE_ITEM_REQUESTS_LIMIT = 25 + attr_reader :table_cache # Establish the connection to DynamoDB. # # @return [Aws::DynamoDB::Client] the DynamoDB connection def connect! - @client = if Dynamoid::Config.endpoint? - Aws::DynamoDB::Client.new(endpoint: Dynamoid::Config.endpoint) - else - Aws::DynamoDB::Client.new - end + @client = Aws::DynamoDB::Client.new(connection_config) @table_cache = {} end + def connection_config + @connection_hash = {} + + if Dynamoid::Config.endpoint? + @connection_hash[:endpoint] = Dynamoid::Config.endpoint + end + if Dynamoid::Config.access_key? + @connection_hash[:access_key_id] = Dynamoid::Config.access_key + end + if Dynamoid::Config.secret_key? + @connection_hash[:secret_access_key] = Dynamoid::Config.secret_key + end + if Dynamoid::Config.region? + @connection_hash[:region] = Dynamoid::Config.region + end + + @connection_hash + end + # Return the client object. # # @since 1.0.0 @@ -24,75 +84,193 @@ def client @client end + # Puts multiple items in one table + # + # If optional block is passed it will be called for each written batch of items, meaning once per batch. + # Block receives boolean flag which is true if there are some unprocessed items, otherwise false. + # + # @example Saves several items to the table testtable + # Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', [{ id: '1', name: 'a' }, { id: '2', name: 'b'}]) + # + # @example Pass block + # Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', items) do |bool| + # if bool + # puts 'there are unprocessed items' + # end + # end + # + # @param [String] table_name the name of the table + # @param [Array] items to be processed + # @param [Hash] additional options + # @param [Proc] optional block + # + # See: + # * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method + def batch_write_item table_name, objects, options = {} + items = objects.map { |o| sanitize_item(o) } + + begin + while items.present? do + batch = items.shift(BATCH_WRITE_ITEM_REQUESTS_LIMIT) + requests = batch.map { |item| { put_request: { item: item } } } + + response = client.batch_write_item( + { + request_items: { + table_name => requests, + }, + return_consumed_capacity: 'TOTAL', + return_item_collection_metrics: 'SIZE' + }.merge!(options) + ) + + if block_given? + yield(response.unprocessed_items.present?) + end + + if response.unprocessed_items.present? + items += response.unprocessed_items[table_name].map { |r| r.put_request.item } + end + end + rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e + raise Dynamoid::Errors::ConditionalCheckFailedException, e + end + end + # Get many items at once from DynamoDB. More efficient than getting each item individually. # + # If optional block is passed `nil` will be returned and the block will be called for each read batch of items, + # meaning once per batch. + # + # Block receives parameters: + # * hash with items like `{ table_name: [items]}` + # * and boolean flag is true if there are some unprocessed keys, otherwise false. + # # @example Retrieve IDs 1 and 2 from the table testtable - # Dynamoid::Adapter::AwsSdkV2.batch_get_item({'table1' => ['1', '2']}) + # Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ['1', '2']) + # + # @example Pass block to receive each batch + # Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ids) do |hash, bool| + # puts hash['table1'] + # + # if bool + # puts 'there are unprocessed keys' + # end + # end # # @param [Hash] table_ids the hash of tables and IDs to retrieve # @param [Hash] options to be passed to underlying BatchGet call + # @param [Proc] optional block can be passed to handle each batch of items # # @return [Hash] a hash where keys are the table names and the values are the retrieved items # + # See: + # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method + # # @since 1.0.0 # - # @todo: Provide support for passing options to underlying batch_get_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method + # @todo: Provide support for passing options to underlying batch_get_item def batch_get_item(table_ids, options = {}) request_items = Hash.new{|h, k| h[k] = []} - return request_items if table_ids.all?{|k, v| v.empty?} + return request_items if table_ids.all?{|k, v| v.blank?} + + ret = Hash.new([].freeze) # Default for tables where no rows are returned table_ids.each do |t, ids| - next if ids.empty? + next if ids.blank? + ids = Array(ids).dup tbl = describe_table(t) hk = tbl.hash_key.to_s rng = tbl.range_key.to_s - keys = if rng.present? - Array(ids).map do |h,r| - { hk => h, rng => r } + while ids.present? do + batch = ids.shift(Dynamoid::Config.batch_size) + + request_items = Hash.new{|h, k| h[k] = []} + + keys = if rng.present? + Array(batch).map do |h, r| + { hk => h, rng => r } + end + else + Array(batch).map do |id| + { hk => id } + end end - else - Array(ids).map do |id| - { hk => id } + + request_items[t] = { + keys: keys + } + + results = client.batch_get_item( + request_items: request_items + ) + + unless block_given? + results.data[:responses].each do |table, rows| + ret[table] += rows.collect { |r| result_item_to_hash(r) } + end + else + batch_results = Hash.new([].freeze) + + results.data[:responses].each do |table, rows| + batch_results[table] += rows.collect { |r| result_item_to_hash(r) } + end + + yield(batch_results, results.unprocessed_keys.present?) end - end - request_items[t] = { - keys: keys - } + if results.unprocessed_keys.present? + ids += results.unprocessed_keys[t].keys.map { |h| h[hk] } + end + end end - results = client.batch_get_item( - request_items: request_items - ) - - ret = Hash.new([].freeze) # Default for tables where no rows are returned - results.data[:responses].each do |table, rows| - ret[table] = rows.collect { |r| result_item_to_hash(r) } + unless block_given? + ret end - ret end # Delete many items at once from DynamoDB. More efficient than delete each item individually. # # @example Delete IDs 1 and 2 from the table testtable - # Dynamoid::Adapter::AwsSdk.batch_delete_item('table1' => ['1', '2']) - #or - # Dynamoid::Adapter::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]])) + # Dynamoid::AdapterPlugin::AwsSdk.batch_delete_item('table1' => ['1', '2']) + # or + # Dynamoid::AdapterPlugin::AwsSdkV2.batch_delete_item('table1' => [['hk1', 'rk2'], ['hk1', 'rk2']]])) # # @param [Hash] options the hash of tables and IDs to delete # - # @return nil + # See: + # * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + # * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method # - # @todo: Provide support for passing options to underlying delete_item http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method + # TODO handle rejections because of internal processing failures def batch_delete_item(options) + requests = [] + options.each_pair do |table_name, ids| table = describe_table(table_name) - ids.each do |id| - client.delete_item(table_name: table_name, key: key_stanza(table, *id)) + + ids.each_slice(BATCH_WRITE_ITEM_REQUESTS_LIMIT) do |sliced_ids| + delete_requests = sliced_ids.map { |id| + {delete_request: {key: key_stanza(table, *id)}} + } + + requests << {table_name => delete_requests} + end + end + + begin + requests.map do |request_items| + client.batch_write_item( + request_items: request_items, + return_consumed_capacity: 'TOTAL', + return_item_collection_metrics: 'SIZE') end + rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e + raise Dynamoid::Errors::ConditionalCheckFailedException, e end - nil end # Create a table on DynamoDB. This usually takes a long time to complete. @@ -103,6 +281,7 @@ def batch_delete_item(options) # @option options [Array] local_secondary_indexes # @option options [Array] global_secondary_indexes # @option options [Symbol] hash_key_type The type of the hash key + # @option options [Boolean] sync Wait for table status to be ACTIVE? # @since 1.0.0 def create_table(table_name, key = :id, options = {}) Dynamoid.logger.info "Creating #{table_name} table. This could take a while." @@ -117,8 +296,8 @@ def create_table(table_name, key = :id, options = {}) gs_indexes = options[:global_secondary_indexes] key_schema = { - :hash_key_schema => { key => (options[:hash_key_type] || :string) }, - :range_key_schema => options[:range_key] + hash_key_schema: { key => (options[:hash_key_type] || :string) }, + range_key_schema: options[:range_key] } attribute_definitions = build_all_attribute_definitions( key_schema, @@ -150,11 +329,40 @@ def create_table(table_name, key = :id, options = {}) index_to_aws_hash(index) end end - client.create_table(client_opts) + resp = client.create_table(client_opts) + options[:sync] = true if !options.has_key?(:sync) && ls_indexes.present? || gs_indexes.present? + until_past_table_status(table_name, :creating) if options[:sync] && + (status = PARSE_TABLE_STATUS.call(resp, :table_description)) && + status == TABLE_STATUSES[:creating] + # Response to original create_table, which, if options[:sync] + # may have an outdated table_description.table_status of "CREATING" + resp rescue Aws::DynamoDB::Errors::ResourceInUseException => e Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists" end + # Create a table on DynamoDB *synchronously*. + # This usually takes a long time to complete. + # CreateTable is normally an asynchronous operation. + # You can optionally define secondary indexes on the new table, + # as part of the CreateTable operation. + # If you want to create multiple tables with secondary indexes on them, + # you must create the tables sequentially. + # Only one table with secondary indexes can be + # in the CREATING state at any given time. + # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#create_table-instance_method + # + # @param [String] table_name the name of the table to create + # @param [Symbol] key the table's primary key (defaults to :id) + # @param [Hash] options provide a range key here if the table has a composite key + # @option options [Array] local_secondary_indexes + # @option options [Array] global_secondary_indexes + # @option options [Symbol] hash_key_type The type of the hash key + # @since 1.2.0 + def create_table_synchronously(table_name, key = :id, options = {}) + create_table(table_name, key, options.merge(sync: true)) + end + # Removes an item from DynamoDB. # # @param [String] table_name the name of the table @@ -165,6 +373,7 @@ def create_table(table_name, key = :id, options = {}) # # @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#delete_item-instance_method def delete_item(table_name, key, options = {}) + options ||= {} range_key = options[:range_key] conditions = options[:conditions] table = describe_table(table_name) @@ -180,11 +389,22 @@ def delete_item(table_name, key, options = {}) # Deletes an entire table from DynamoDB. # # @param [String] table_name the name of the table to destroy + # @option options [Boolean] sync Wait for table status check to raise ResourceNotFoundException # # @since 1.0.0 - def delete_table(table_name) - client.delete_table(table_name: table_name) - table_cache.clear + def delete_table(table_name, options = {}) + resp = client.delete_table(table_name: table_name) + until_past_table_status(table_name, :deleting) if options[:sync] && + (status = PARSE_TABLE_STATUS.call(resp, :table_description)) && + status == TABLE_STATUSES[:deleting] + table_cache.delete(table_name) + rescue Aws::DynamoDB::Errors::ResourceInUseException => e + Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use" + raise e + end + + def delete_table_synchronously(table_name, options = {}) + delete_table(table_name, options.merge(sync: true)) end # @todo Add a DescribeTable method. @@ -201,6 +421,7 @@ def delete_table(table_name) # # @todo Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#get_item-instance_method def get_item(table_name, key, options = {}) + options ||= {} table = describe_table(table_name) range_key = options.delete(:range_key) @@ -232,7 +453,7 @@ def update_item(table_name, key, options = {}) key: key_stanza(table, key, range_key), attribute_updates: iu.to_h, expected: expected_stanza(conditions), - return_values: "ALL_NEW" + return_values: 'ALL_NEW' ) result_item_to_hash(result[:attributes]) rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e @@ -256,19 +477,18 @@ def list_tables # # @since 1.0.0 # - # @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method - def put_item(table_name, object, options = nil) - item = {} - - object.each do |k, v| - next if v.nil? || (v.respond_to?(:empty?) && v.empty?) - item[k.to_s] = v - end + # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method + def put_item(table_name, object, options = {}) + options ||= {} + item = sanitize_item(object) begin - client.put_item(table_name: table_name, - item: item, - expected: expected_stanza(options) + client.put_item( + { + table_name: table_name, + item: item, + expected: expected_stanza(options) + }.merge!(options) ) rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e raise Dynamoid::Errors::ConditionalCheckFailedException, e @@ -295,65 +515,93 @@ def put_item(table_name, object, options = nil) # @todo Provide support for various other options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#query-instance_method def query(table_name, opts = {}) table = describe_table(table_name) - hk = (opts[:hash_key].present? ? opts[:hash_key] : table.hash_key).to_s - rng = (opts[:range_key].present? ? opts[:range_key] : table.range_key).to_s + hk = (opts[:hash_key].present? ? opts.delete(:hash_key) : table.hash_key).to_s + rng = (opts[:range_key].present? ? opts.delete(:range_key) : table.range_key).to_s q = opts.slice( :consistent_read, :scan_index_forward, :select, :index_name ) - limit = opts.delete(:limit) - batch = opts.delete(:batch_size) - q[:limit] = batch || limit if (batch || limit) opts.delete(:consistent_read) opts.delete(:scan_index_forward) - opts.delete(:limit) opts.delete(:select) opts.delete(:index_name) - opts.delete(:next_token).tap do |token| - break unless token - q[:exclusive_start_key] = { - hk => token[:hash_key_element], - rng => token[:range_key_element] - } - end + # Deal with various limits and batching + record_limit = opts.delete(:record_limit) + scan_limit = opts.delete(:scan_limit) + batch_size = opts.delete(:batch_size) + exclusive_start_key = opts.delete(:exclusive_start_key) + limit = [record_limit, scan_limit, batch_size].compact.min key_conditions = { hk => { - # TODO: Provide option for other operators like NE, IN, LE, etc comparison_operator: EQ, - attribute_value_list: [ - opts.delete(:hash_value).freeze - ] + attribute_value_list: attribute_value_list(EQ, opts.delete(:hash_value).freeze) } } opts.each_pair do |k, v| - # TODO: ATM, only few comparison operators are supported, provide support for all operators next unless(op = RANGE_MAP[k]) key_conditions[rng] = { comparison_operator: op, - attribute_value_list: [ - opts.delete(k).freeze - ].flatten # Flatten as BETWEEN operator specifies array of two elements + attribute_value_list: attribute_value_list(op, opts.delete(k).freeze) } end + query_filter = {} + opts.reject {|k, _| k.in? RANGE_MAP.keys}.each do |attr, hash| + query_filter[attr] = { + comparison_operator: FIELD_MAP[hash.keys[0]], + attribute_value_list: attribute_value_list(FIELD_MAP[hash.keys[0]], hash.values[0].freeze) + } + end + + q[:limit] = limit if limit + q[:exclusive_start_key] = exclusive_start_key if exclusive_start_key q[:table_name] = table_name q[:key_conditions] = key_conditions + q[:query_filter] = query_filter Enumerator.new { |y| + record_count = 0 + scan_count = 0 loop do - result = client.query(q) + # Adjust the limit down if the remaining record and/or scan limit are + # lower to obey limits. We can assume the difference won't be + # negative due to break statements below but choose smaller limit + # which is why we have 2 separate if statements. + # NOTE: Adjusting based on record_limit can cause many HTTP requests + # being made. We may want to change this behavior, but it affects + # filtering on data with potentially large gaps. + # Example: + # User.where('created_at.gte' => 1.day.ago).record_limit(1000) + # Records 1-999 User's that fit criteria + # Records 1000-2000 Users's that do not fit criteria + # Record 2001 fits criteria + # The underlying implementation will have 1 page for records 1-999 + # then will request with limit 1 for records 1000-2000 (making 1000 + # requests of limit 1) until hit record 2001. + if q[:limit] && record_limit && record_limit - record_count < q[:limit] + q[:limit] = record_limit - record_count + end + if q[:limit] && scan_limit && scan_limit - scan_count < q[:limit] + q[:limit] = scan_limit - scan_count + end + + results = client.query(q) + results.items.each { |row| y << result_item_to_hash(row) } - result.items.each { |r| y << result_item_to_hash(r) } - last_key = result.last_evaluated_key + record_count += results.items.size + break if record_limit && record_count >= record_limit - if(last_key && batch) - q[:exclusive_start_key] = last_key + scan_count += results.scanned_count + break if scan_limit && scan_count >= scan_limit + + if(lk = results.last_evaluated_key) + q[:exclusive_start_key] = lk else break end @@ -361,18 +609,6 @@ def query(table_name, opts = {}) } end - EQ = "EQ".freeze - - RANGE_MAP = { - range_greater_than: 'GT', - range_less_than: 'LT', - range_gte: 'GE', - range_lte: 'LE', - range_begins_with: 'BEGINS_WITH', - range_between: 'BETWEEN', - range_eq: 'EQ' - } - # Scan the DynamoDB table. This is usually a very slow operation as it naively filters all data on # the DynamoDB servers. # @@ -384,29 +620,65 @@ def query(table_name, opts = {}) # @since 1.0.0 # # @todo: Provide support for various options http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#scan-instance_method - def scan(table_name, scan_hash, select_opts = {}) - limit = select_opts.delete(:limit) - batch = select_opts.delete(:batch_size) - + def scan(table_name, scan_hash = {}, select_opts = {}) request = { table_name: table_name } - request[:limit] = batch || limit if batch || limit - request[:scan_filter] = scan_hash.reduce({}) do |memo, kvp| - memo[kvp[0].to_s] = { - attribute_value_list: [kvp[1]], - # TODO: Provide support for all comparison operators - comparison_operator: EQ - } - memo - end if scan_hash.present? + request[:consistent_read] = true if select_opts.delete(:consistent_read) + + # Deal with various limits and batching + record_limit = select_opts.delete(:record_limit) + scan_limit = select_opts.delete(:scan_limit) + batch_size = select_opts.delete(:batch_size) + exclusive_start_key = select_opts.delete(:exclusive_start_key) + request_limit = [record_limit, scan_limit, batch_size].compact.min + request[:limit] = request_limit if request_limit + request[:exclusive_start_key] = exclusive_start_key if exclusive_start_key + + if scan_hash.present? + request[:scan_filter] = scan_hash.reduce({}) do |memo, (attr, cond)| + memo.merge(attr.to_s => { + comparison_operator: FIELD_MAP[cond.keys[0]], + attribute_value_list: attribute_value_list(FIELD_MAP[cond.keys[0]], cond.values[0].freeze) + }) + end + end Enumerator.new do |y| - # Batch loop, pulls multiple requests until done using the start_key + record_count = 0 + scan_count = 0 loop do + # Adjust the limit down if the remaining record and/or scan limit are + # lower to obey limits. We can assume the difference won't be + # negative due to break statements below but choose smaller limit + # which is why we have 2 separate if statements. + # NOTE: Adjusting based on record_limit can cause many HTTP requests + # being made. We may want to change this behavior, but it affects + # filtering on data with potentially large gaps. + # Example: + # User.where('created_at.gte' => 1.day.ago).record_limit(1000) + # Records 1-999 User's that fit criteria + # Records 1000-2000 Users's that do not fit criteria + # Record 2001 fits criteria + # The underlying implementation will have 1 page for records 1-999 + # then will request with limit 1 for records 1000-2000 (making 1000 + # requests of limit 1) until hit record 2001. + if request[:limit] && record_limit && record_limit - record_count < request[:limit] + request[:limit] = record_limit - record_count + end + if request[:limit] && scan_limit && scan_limit - scan_count < request[:limit] + request[:limit] = scan_limit - scan_count + end + results = client.scan(request) + results.items.each { |row| y << result_item_to_hash(row) } - results.data[:items].each { |row| y << result_item_to_hash(row) } + record_count += results.items.size + break if record_limit && record_count >= record_limit - if((lk = results[:last_evaluated_key]) && batch) + scan_count += results.scanned_count + break if scan_limit && scan_count >= scan_limit + + # Keep pulling if we haven't finished paging in all data + if(lk = results[:last_evaluated_key]) request[:exclusive_start_key] = lk else break @@ -415,7 +687,6 @@ def scan(table_name, scan_hash, select_opts = {}) end end - # # Truncates all records in the given table # @@ -428,7 +699,8 @@ def truncate(table_name) rk = table.range_key scan(table_name, {}, {}).each do |attributes| - opts = {range_key: attributes[rk.to_sym] } if rk + opts = {} + opts[:range_key] = attributes[rk.to_sym] if rk delete_item(table_name, attributes[hk], opts) end end @@ -439,11 +711,49 @@ def count(table_name) protected - STRING_TYPE = "S".freeze - NUM_TYPE = "N".freeze - BINARY_TYPE = "B".freeze + def check_table_status?(counter, resp, expect_status) + status = PARSE_TABLE_STATUS.call(resp) + again = counter < Dynamoid::Config.sync_retry_max_times && + status == TABLE_STATUSES[expect_status] + {again: again, status: status, counter: counter} + end + + def until_past_table_status(table_name, status = :creating) + counter = 0 + resp = nil + begin + check = {again: true} + while check[:again] + sleep Dynamoid::Config.sync_retry_wait_seconds + resp = client.describe_table(table_name: table_name) + check = check_table_status?(counter, resp, status) + Dynamoid.logger.info "Checked table status for #{table_name} (check #{check.inspect})" + counter += 1 + end + # If you issue a DescribeTable request immediately after a CreateTable + # request, DynamoDB might return a ResourceNotFoundException. + # This is because DescribeTable uses an eventually consistent query, + # and the metadata for your table might not be available at that moment. + # Wait for a few seconds, and then try the DescribeTable request again. + # See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#describe_table-instance_method + rescue Aws::DynamoDB::Errors::ResourceNotFoundException => e + case status + when :creating then + if counter >= Dynamoid::Config.sync_retry_max_times + Dynamoid.logger.warn "Waiting on table metadata for #{table_name} (check #{counter})" + retry # start over at first line of begin, does not reset counter + else + Dynamoid.logger.error "Exhausted max retries (Dynamoid::Config.sync_retry_max_times) waiting on table metadata for #{table_name} (check #{counter})" + raise e + end + else + # When deleting a table, "not found" is the goal. + Dynamoid.logger.info "Checked table status for #{table_name}: Not Found (check #{check.inspect})" + end + end + end - #Converts from symbol to the API string for the given data type + # Converts from symbol to the API string for the given data type # E.g. :number -> 'N' def api_type(type) case(type) @@ -468,22 +778,23 @@ def key_stanza(table, hash_key, range_key = nil) # @return an Expected stanza for the given conditions hash # def expected_stanza(conditions = nil) - expected = Hash.new { |h,k| h[k] = {} } + expected = Hash.new { |h, k| h[k] = {} } return expected unless conditions - conditions[:unless_exists].try(:each) do |col| + conditions.delete(:unless_exists).try(:each) do |col| expected[col.to_s][:exists] = false end - conditions[:if].try(:each) do |col,val| + conditions.delete(:if_exists).try(:each) do |col, val| + expected[col.to_s][:exists] = true + expected[col.to_s][:value] = val + end + conditions.delete(:if).try(:each) do |col, val| expected[col.to_s][:value] = val end expected end - HASH_KEY = "HASH".freeze - RANGE_KEY = "RANGE".freeze - # # New, semi-arbitrary API to get data on the table # @@ -498,7 +809,7 @@ def describe_table(table_name, reload = false) # def result_item_to_hash(item) {}.tap do |r| - item.each { |k,v| r[k.to_sym] = v } + item.each { |k, v| r[k.to_sym] = v } end end @@ -644,11 +955,26 @@ def attribute_definition_element(name, dynamoid_type) aws_type = api_type(dynamoid_type) { - :attribute_name => name.to_s, - :attribute_type => aws_type + attribute_name: name.to_s, + attribute_type: aws_type } end + # Build an array of values for Condition + # Is used in ScanFilter and QueryFilter + # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html + # @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc + # @params [Object] value: scalar value or array/set + def attribute_value_list(operator, value) + # For BETWEEN and IN operators we should keep value as is (it should be already an array) + # For all the other operators we wrap the value with array + if ["BETWEEN", "IN"].include?(operator) + [value].flatten + else + [value] + end + end + # # Represents a table. Exposes data from the "DescribeTable" API call, and also # provides methods for coercing values to the proper types based on the table's schema data @@ -670,7 +996,7 @@ def range_key def range_type range_type ||= schema[:attribute_definitions].find { |d| d[:attribute_name] == range_key - }.try(:fetch,:attribute_type, nil) + }.try(:fetch, :attribute_type, nil) end def hash_key @@ -739,19 +1065,19 @@ def set(values) def to_h ret = {} - @additions.each do |k,v| + @additions.each do |k, v| ret[k.to_s] = { action: ADD, value: v } end - @deletions.each do |k,v| + @deletions.each do |k, v| ret[k.to_s] = { action: DELETE, value: v } end - @updates.each do |k,v| + @updates.each do |k, v| ret[k.to_s] = { action: PUT, value: v @@ -761,9 +1087,15 @@ def to_h ret end - ADD = "ADD".freeze - DELETE = "DELETE".freeze - PUT = "PUT".freeze + ADD = 'ADD'.freeze + DELETE = 'DELETE'.freeze + PUT = 'PUT'.freeze + end + + def sanitize_item(attributes) + attributes.reject do |k, v| + v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?) + end end end end diff --git a/lib/dynamoid/associations.rb b/lib/dynamoid/associations.rb index 52342df1..ff2ca34d 100644 --- a/lib/dynamoid/associations.rb +++ b/lib/dynamoid/associations.rb @@ -16,16 +16,16 @@ module Dynamoid # * has_one module Associations extend ActiveSupport::Concern - + # Create the association tracking attribute and initialize it to an empty hash. included do - class_attribute :associations - + class_attribute :associations, instance_accessor: false + self.associations = {} end module ClassMethods - + # create a has_many association for this document. # # @param [Symbol] name the name of the association @@ -38,7 +38,7 @@ module ClassMethods def has_many(name, options = {}) association(:has_many, name, options) end - + # create a has_one association for this document. # # @param [Symbol] name the name of the association @@ -51,7 +51,7 @@ def has_many(name, options = {}) def has_one(name, options = {}) association(:has_one, name, options) end - + # create a belongs_to association for this document. # # @param [Symbol] name the name of the association @@ -64,7 +64,7 @@ def has_one(name, options = {}) def belongs_to(name, options = {}) association(:belongs_to, name, options) end - + # create a has_and_belongs_to_many association for this document. # # @param [Symbol] name the name of the association @@ -77,7 +77,7 @@ def belongs_to(name, options = {}) def has_and_belongs_to_many(name, options = {}) association(:has_and_belongs_to_many, name, options) end - + private # create getters and setters for an association. @@ -86,13 +86,23 @@ def has_and_belongs_to_many(name, options = {}) # @param [Symbol] name the name of the association # @param [Hash] options options to pass to the association constructor; see above for all valid options # - # @since 0.2.0 + # @since 0.2.0 def association(type, name, options = {}) - field "#{name}_ids".to_sym, :set - self.associations[name] = options.merge(:type => type) + # Declare document field. + # In simple case it's equivalent to + # field "#{name}_ids".to_sym, :set + assoc = Dynamoid::Associations.const_get(type.to_s.camelcase).new(nil, name, options) + field_name = assoc.declaration_field_name + field_type = assoc.declaration_field_type + + field field_name.to_sym, field_type + + self.associations[name] = options.merge(type: type) + define_method(name) do @associations[:"#{name}_ids"] ||= Dynamoid::Associations.const_get(type.to_s.camelcase).new(self, name, options) end + define_method("#{name}=".to_sym) do |objects| @associations[:"#{name}_ids"] ||= Dynamoid::Associations.const_get(type.to_s.camelcase).new(self, name, options) @associations[:"#{name}_ids"].setter(objects) @@ -100,7 +110,6 @@ def association(type, name, options = {}) end end - end - + end diff --git a/lib/dynamoid/associations/association.rb b/lib/dynamoid/associations/association.rb index 50da3df6..75f7d195 100644 --- a/lib/dynamoid/associations/association.rb +++ b/lib/dynamoid/associations/association.rb @@ -16,6 +16,7 @@ module Association # @option options [Class] :class the target class of the association; that is, the class to which the association objects belong # @option options [Symbol] :class_name the name of the target class of the association; only this or Class is necessary # @option options [Symbol] :inverse_of the name of the association on the target class + # @option options [Symbol] :foreign_key the name of the field for belongs_to association # # @return [Dynamoid::Association] the actual association instance itself # @@ -48,6 +49,14 @@ def reset @loaded = false end + def declaration_field_name + "#{name}_ids" + end + + def declaration_field_type + :set + end + private # The target class name, either inferred through the association's name or specified in options. @@ -68,7 +77,13 @@ def target_class # # @since 0.2.0 def target_attribute - "#{target_association}_ids".to_sym if target_association + # In simple case it's equivalent to + # "#{target_association}_ids".to_sym if target_association + if target_association + target_options = target_class.associations[target_association] + assoc = Dynamoid::Associations.const_get(target_options[:type].to_s.camelcase).new(nil, target_association, target_options) + assoc.send(:source_attribute) + end end # The ids in the target association. @@ -89,14 +104,26 @@ def source_class # # @since 0.2.0 def source_attribute - "#{name}_ids".to_sym + declaration_field_name.to_sym end # The ids in the source association. # # @since 0.2.0 def source_ids - source.send(source_attribute) || Set.new + # handle case when we store scalar value instead of collection (when foreign_key option is specified) + Array(source.send(source_attribute)).compact.to_set || Set.new + end + + # Create a new instance of the target class without trying to add it to the association. This creates a base, that caller can update before setting or adding it. + # + # @param [Hash] attribute hash for the new object + # + # @return [Dynamoid::Document] the newly-created object + # + # @since 1.1.1 + def build(attributes = {}) + target_class.build(attributes) end end diff --git a/lib/dynamoid/associations/belongs_to.rb b/lib/dynamoid/associations/belongs_to.rb index 39bac985..51cbfea5 100644 --- a/lib/dynamoid/associations/belongs_to.rb +++ b/lib/dynamoid/associations/belongs_to.rb @@ -1,12 +1,36 @@ # encoding: utf-8 module Dynamoid #:nodoc: - # The belongs_to association. For belongs_to, we reference only a single target instead of multiple records; that target is the + # The belongs_to association. For belongs_to, we reference only a single target instead of multiple records; that target is the # object to which the association object is associated. module Associations class BelongsTo include SingleAssociation + def declaration_field_name + options[:foreign_key] || "#{name}_ids" + end + + def declaration_field_type + if options[:foreign_key] + target_class.attributes[target_class.hash_key][:type] + else + :set + end + end + + # Override default implementation + # to handle case when we store id as scalar value, not as collection + def associate(hash_key) + target.send(target_association).disassociate(source.hash_key) if target && target_association + + if options[:foreign_key] + source.update_attribute(source_attribute, hash_key) + else + source.update_attribute(source_attribute, Set[hash_key]) + end + end + private # Find the target association, either has_many or has_one. Uses either options[:inverse_of] or the source class name and default parsing to @@ -24,21 +48,7 @@ def target_association return has_one_key_name if target_class.associations[has_one_key_name][:type] == :has_one end end - - # Associate a source object to this association. - # - # @since 0.2.0 - def associate_target(object) - object.update_attribute(target_attribute, target_ids.merge(Array(source.id))) - end - - # Disassociate a source object from this association. - # - # @since 0.2.0 - def disassociate_target(object) - source.update_attribute(source_attribute, target_ids - Array(source.id)) - end end end - + end diff --git a/lib/dynamoid/associations/has_and_belongs_to_many.rb b/lib/dynamoid/associations/has_and_belongs_to_many.rb index 476a2c5a..fd19e9be 100644 --- a/lib/dynamoid/associations/has_and_belongs_to_many.rb +++ b/lib/dynamoid/associations/has_and_belongs_to_many.rb @@ -18,22 +18,6 @@ def target_association return nil if guess.nil? || guess[:type] != :has_and_belongs_to_many key_name end - - # Associate a source object to this association. - # - # @since 0.2.0 - def associate_target(object) - ids = object.send(target_attribute) || Set.new - object.update_attribute(target_attribute, ids.merge(Array(source.id))) - end - - # Disassociate a source object from this association. - # - # @since 0.2.0 - def disassociate_target(object) - ids = object.send(target_attribute) || Set.new - object.update_attribute(target_attribute, ids - Array(source.id)) - end end end diff --git a/lib/dynamoid/associations/has_many.rb b/lib/dynamoid/associations/has_many.rb index b540fc84..5bbe17d8 100644 --- a/lib/dynamoid/associations/has_many.rb +++ b/lib/dynamoid/associations/has_many.rb @@ -8,7 +8,7 @@ class HasMany private - # Find the target association, always a :belongs_to association. Uses either options[:inverse_of] or the source class name + # Find the target association, always a :belongs_to association. Uses either options[:inverse_of] or the source class name # and default parsing to return the most likely name for the target association. # # @since 0.2.0 @@ -18,22 +18,7 @@ def target_association return nil if guess.nil? || guess[:type] != :belongs_to key_name end - - # Associate a source object to this association. - # - # @since 0.2.0 - def associate_target(object) - object.update_attribute(target_attribute, Set[source.id]) - end - - # Disassociate a source object from this association. - # - # @since 0.2.0 - def disassociate_target(object) - object.update_attribute(target_attribute, nil) - end - end end - + end diff --git a/lib/dynamoid/associations/has_one.rb b/lib/dynamoid/associations/has_one.rb index 231b23e4..6cae67e9 100644 --- a/lib/dynamoid/associations/has_one.rb +++ b/lib/dynamoid/associations/has_one.rb @@ -19,20 +19,6 @@ def target_association return nil if guess.nil? || guess[:type] != :belongs_to key_name end - - # Associate a source object to this association. - # - # @since 0.2.0 - def associate_target(object) - object.update_attribute(target_attribute, Set[source.id]) - end - - # Disassociate a source object from this association. - # - # @since 0.2.0 - def disassociate_target(object) - source.update_attribute(source_attribute, nil) - end end end diff --git a/lib/dynamoid/associations/many_association.rb b/lib/dynamoid/associations/many_association.rb index 6e536186..e6748d4a 100644 --- a/lib/dynamoid/associations/many_association.rb +++ b/lib/dynamoid/associations/many_association.rb @@ -14,7 +14,7 @@ def initialize(*args) include Enumerable # Delegate methods to the records the association represents. - delegate :first, :last, :empty?, :size, :class, :to => :records + delegate :first, :last, :empty?, :size, :class, to: :records # The records associated to the source. # @@ -52,12 +52,13 @@ def include?(object) # # @since 0.2.0 def delete(object) - source.update_attribute(source_attribute, source_ids - Array(object).collect(&:id)) - Array(object).each {|o| self.send(:disassociate_target, o)} if target_association + disassociate(Array(object).collect(&:hash_key)) + if target_association + Array(object).each { |obj| obj.send(target_association).disassociate(source.hash_key) } + end object end - # Add an object or array of objects to an association. This preserves the current records in the association (if any) # and adds the object to the target association if it is detected to exist. # @@ -67,8 +68,12 @@ def delete(object) # # @since 0.2.0 def <<(object) - source.update_attribute(source_attribute, source_ids.merge(Array(object).collect(&:id))) - Array(object).each {|o| self.send(:associate_target, o)} if target_association + associate(Array(object).collect(&:hash_key)) + + if target_association + Array(object).each { |obj| obj.send(target_association).associate(source.hash_key) } + end + object end @@ -145,8 +150,10 @@ def delete_all # # @since 0.2.0 def where(args) - args.each {|k, v| query[k] = v} - self + filtered = clone + filtered.query = query.clone + args.each {|k, v| filtered.query[k] = v} + filtered end # Is this array equal to the association's records? @@ -169,6 +176,14 @@ def method_missing(method, *args) end end + def associate(hash_key) + source.update_attribute(source_attribute, source_ids.merge(Array(hash_key))) + end + + def disassociate(hash_key) + source.update_attribute(source_attribute, source_ids - Array(hash_key)) + end + private # If a query exists, filter all existing results based on that query. diff --git a/lib/dynamoid/associations/single_association.rb b/lib/dynamoid/associations/single_association.rb index 0115879b..c01772e6 100644 --- a/lib/dynamoid/associations/single_association.rb +++ b/lib/dynamoid/associations/single_association.rb @@ -5,18 +5,23 @@ module Associations module SingleAssociation include Association - delegate :class, :to => :target + delegate :class, to: :target def setter(object) - delete - source.update_attribute(source_attribute, Set[object.id]) - self.send(:associate_target, object) if target_association + if object.nil? + delete + return + end + + associate(object.hash_key) + self.target = object + object.send(target_association).associate(source.hash_key) if target_association object end def delete - source.update_attribute(source_attribute, nil) - self.send(:disassociate_target, target) if target && target_association + target.send(target_association).disassociate(source.hash_key) if target && target_association + disassociate target end @@ -25,10 +30,9 @@ def create!(attributes = {}) end def create(attributes = {}) - setter(target_class.create!(attributes)) + setter(target_class.create(attributes)) end - # Is this object equal to the association's target? # # @return [Boolean] true/false @@ -53,6 +57,21 @@ def nil? target.nil? end + def empty? + # This is needed to that ActiveSupport's #blank? and #present? + # methods work as expected for SingleAssociations. + target.nil? + end + + def associate(hash_key) + target.send(target_association).disassociate(source.hash_key) if target && target_association + source.update_attribute(source_attribute, Set[hash_key]) + end + + def disassociate(hash_key=nil) + source.update_attribute(source_attribute, nil) + end + private # Find the target of the has_one association. @@ -64,6 +83,11 @@ def find_target return if source_ids.empty? target_class.find(source_ids.first) end + + def target=(object) + @target = object + @loaded = true + end end end end diff --git a/lib/dynamoid/components.rb b/lib/dynamoid/components.rb index 2c1ac00b..25a3df70 100644 --- a/lib/dynamoid/components.rb +++ b/lib/dynamoid/components.rb @@ -23,7 +23,7 @@ module Components include ActiveModel::Naming include ActiveModel::Observing if defined?(ActiveModel::Observing) include ActiveModel::Serializers::JSON - include ActiveModel::Serializers::Xml + include ActiveModel::Serializers::Xml if defined?(ActiveModel::Serializers::Xml) include Dynamoid::Fields include Dynamoid::Indexes include Dynamoid::Persistence diff --git a/lib/dynamoid/config.rb b/lib/dynamoid/config.rb index c4d14ebd..891cf666 100644 --- a/lib/dynamoid/config.rb +++ b/lib/dynamoid/config.rb @@ -1,6 +1,8 @@ # encoding: utf-8 -require "uri" -require "dynamoid/config/options" +require 'uri' +require 'dynamoid/config/options' +require 'dynamoid/config/backoff_strategies/constant_backoff' +require 'dynamoid/config/backoff_strategies/exponential_backoff' module Dynamoid @@ -11,18 +13,30 @@ module Config include ActiveModel::Observing if defined?(ActiveModel::Observing) # All the default options. - option :adapter, :default => 'aws_sdk_v2' - option :namespace, :default => defined?(Rails) ? "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" : "dynamoid" - option :logger, :default => defined?(Rails) - option :access_key - option :secret_key - option :read_capacity, :default => 100 - option :write_capacity, :default => 20 - option :warn_on_scan, :default => true - option :endpoint, :default => nil - option :use_ssl, :default => true - option :port, :default => '443' - option :identity_map, :default => false + option :adapter, default: 'aws_sdk_v2' + option :namespace, default: defined?(Rails) ? "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" : 'dynamoid' + option :access_key, default: nil + option :secret_key, default: nil + option :region, default: nil + option :batch_size, default: 100 + option :read_capacity, default: 100 + option :write_capacity, default: 20 + option :warn_on_scan, default: true + option :endpoint, default: nil + option :identity_map, default: false + option :timestamps, default: true + option :sync_retry_max_times, default: 60 # a bit over 2 minutes + option :sync_retry_wait_seconds, default: 2 + option :convert_big_decimal, default: false + option :models_dir, default: './app/models' # perhaps you keep your dynamoid models in a different directory? + option :application_timezone, default: :local # available values - :utc, :local, time zone names + option :store_datetime_as_string, default: false # store Time fields in ISO 8601 string format + option :store_date_as_string, default: false # store Date fields in ISO 8601 string format + option :backoff, default: nil # callable object to handle exceeding of table throughput limit + option :backoff_strategies, default: { + constant: BackoffStrategies::ConstantBackoff, + exponential: BackoffStrategies::ExponentialBackoff + } # The default logger for Dynamoid: either the Rails logger or just stdout. # @@ -50,5 +64,15 @@ def logger=(logger) end end + def build_backoff + if backoff.is_a?(Hash) + name = backoff.keys[0] + args = backoff.values[0] + + backoff_strategies[name].call(args) + else + backoff_strategies[backoff].call + end + end end end diff --git a/lib/dynamoid/config/backoff_strategies/constant_backoff.rb b/lib/dynamoid/config/backoff_strategies/constant_backoff.rb new file mode 100644 index 00000000..6ea2b4c0 --- /dev/null +++ b/lib/dynamoid/config/backoff_strategies/constant_backoff.rb @@ -0,0 +1,11 @@ +module Dynamoid + module Config + module BackoffStrategies + class ConstantBackoff + def self.call(n = 1) + -> { sleep n } + end + end + end + end +end diff --git a/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb b/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb new file mode 100644 index 00000000..97a074d7 --- /dev/null +++ b/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb @@ -0,0 +1,25 @@ +module Dynamoid + module Config + module BackoffStrategies + # Truncated binary exponential backoff algorithm + # See https://en.wikipedia.org/wiki/Exponential_backoff + class ExponentialBackoff + def self.call(opts = {}) + opts = { base_backoff: 0.5, ceiling: 3 }.merge(opts) + base_backoff = opts[:base_backoff] + ceiling = opts[:ceiling] + + times = 1 + + lambda do + power = [times - 1, ceiling - 1].min + backoff = base_backoff * (2 ** power) + sleep backoff + + times += 1 + end + end + end + end + end +end diff --git a/lib/dynamoid/config/options.rb b/lib/dynamoid/config/options.rb index 7547c6a5..2d741de0 100644 --- a/lib/dynamoid/config/options.rb +++ b/lib/dynamoid/config/options.rb @@ -43,7 +43,7 @@ def #{name}=(value) def #{name}? #{name} end - + def reset_#{name} settings[#{name.inspect}] = defaults[#{name.inspect}] end diff --git a/lib/dynamoid/criteria.rb b/lib/dynamoid/criteria.rb index 7bc2b39a..b8aa1327 100644 --- a/lib/dynamoid/criteria.rb +++ b/lib/dynamoid/criteria.rb @@ -6,11 +6,11 @@ module Dynamoid # Allows classes to be queried by where, all, first, and each and return criteria chains. module Criteria extend ActiveSupport::Concern - + module ClassMethods - - [:where, :all, :first, :each, :eval_limit, :start, :scan_index_forward].each do |meth| - # Return a criteria chain in response to a method that will begin or end a chain. For more information, + + [:where, :all, :first, :last, :each, :record_limit, :scan_limit, :batch, :start, :scan_index_forward].each do |meth| + # Return a criteria chain in response to a method that will begin or end a chain. For more information, # see Dynamoid::Criteria::Chain. # # @since 0.2.0 @@ -25,5 +25,5 @@ module ClassMethods end end end - + end diff --git a/lib/dynamoid/criteria/chain.rb b/lib/dynamoid/criteria/chain.rb index 95c4c931..5dd928d6 100644 --- a/lib/dynamoid/criteria/chain.rb +++ b/lib/dynamoid/criteria/chain.rb @@ -5,9 +5,11 @@ module Criteria # The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from # chain to relation). It is a chainable object that builds up a query and eventually executes it by a Query or Scan. class Chain + # TODO: Should we transform any other types of query values? + TYPES_TO_DUMP_FOR_QUERY = [:string, :integer, :boolean, :serialized] attr_accessor :query, :source, :values, :consistent_read + attr_reader :hash_key, :range_key, :index_name include Enumerable - # Create a new criteria chain. # # @param [Class] source the class upon which the ultimate query will be performed. @@ -16,6 +18,11 @@ def initialize(source) @source = source @consistent_read = false @scan_index_forward = true + + # Honor STI and :type field if it presents + if @source.attributes.key?(:type) + @query[:'type.in'] = @source.deep_subclasses.map(&:name) << @source.name + end end # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the @@ -30,7 +37,14 @@ def initialize(source) # # @since 0.2.0 def where(args) - args.each {|k, v| query[k.to_sym] = v} + args.each do |k, v| + sym = k.to_sym + query[sym] = if (field_options = source.attributes[sym]) && (type = field_options[:type]) && TYPES_TO_DUMP_FOR_QUERY.include?(type) + source.dump_field(v, field_options) + else + v + end + end self end @@ -46,30 +60,51 @@ def all records end + # Returns the last fetched record matched the criteria + # Enumerable doesn't implement `last`, only `first` + # So we have to implement it ourselves + # + def last + all.to_a.last + end + # Destroys all the records matching the criteria. # - def destroy_all + def delete_all ids = [] + ranges = [] if key_present? - ranges = [] Dynamoid.adapter.query(source.table_name, range_query).collect do |hash| ids << hash[source.hash_key.to_sym] - ranges << hash[source.range_key.to_sym] + ranges << hash[source.range_key.to_sym] if source.range_key end - Dynamoid.adapter.delete(source.table_name, ids,{:range_key => ranges}) + Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence) else - Dynamoid.adapter.scan(source.table_name, query, scan_opts).collect do |hash| + Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).collect do |hash| ids << hash[source.hash_key.to_sym] + ranges << hash[source.range_key.to_sym] if source.range_key end - Dynamoid.adapter.delete(source.table_name, ids) + Dynamoid.adapter.delete(source.table_name, ids, range_key: ranges.presence) end end + alias_method :destroy_all, :delete_all - def eval_limit(limit) - @eval_limit = limit + # The record limit is the limit of evaluated records returned by the + # query or scan. + def record_limit(limit) + @record_limit = limit + self + end + + # The scan limit which is the limit of records that DynamoDB will + # internally query or scan. This is different from the record limit + # as with filtering DynamoDB may look at N scanned records but return 0 + # records if none pass the filter. + def scan_limit(limit) + @scan_limit = limit self end @@ -95,10 +130,6 @@ def each(&block) records.each(&block) end - def consistent_opts - { :consistent_read => consistent_read } - end - private # The actual records referenced by the association. @@ -107,12 +138,11 @@ def consistent_opts # # @since 0.2.0 def records - results = if key_present? + if key_present? records_via_query else records_via_scan end - @batch_size ? results : Array(results) end def records_via_query @@ -131,78 +161,212 @@ def records_via_query def records_via_scan if Dynamoid::Config.warn_on_scan Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!' - Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{source.attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]" - end - - if @consistent_read - raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation' + Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.downcase}.rb:" + Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'" + Dynamoid.logger.warn "* local_secondary_indexe range_key: 'some-name'" + Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}" end Enumerator.new do |yielder| - Dynamoid.adapter.scan(source.table_name, query, scan_opts).each do |hash| + Dynamoid.adapter.scan(source.table_name, scan_query, scan_opts).each do |hash| yielder.yield source.from_database(hash) end end end def range_hash(key) - val = query[key] + name, operation = key.to_s.split('.') + val = type_cast_condition_parameter(name, query[key]) + + case operation + when 'gt' + { range_greater_than: val } + when 'lt' + { range_less_than: val } + when 'gte' + { range_gte: val } + when 'lte' + { range_lte: val } + when 'between' + { range_between: val } + when 'begins_with' + { range_begins_with: val } + end + end - return { :range_value => query[key] } if query[key].is_a?(Range) + def field_hash(key) + name, operation = key.to_s.split('.') + val = type_cast_condition_parameter(name, query[key]) - case key.to_s.split('.').last + hash = case operation + when 'ne' + { ne: val } when 'gt' - { :range_greater_than => val.to_f } + { gt: val } when 'lt' - { :range_less_than => val.to_f } + { lt: val } when 'gte' - { :range_gte => val.to_f } + { gte: val } when 'lte' - { :range_lte => val.to_f } + { lte: val } + when 'between' + { between: val } when 'begins_with' - { :range_begins_with => val } + { begins_with: val } + when 'in' + { in: val } + when 'contains' + { contains: val } + when 'not_contains' + { not_contains: val } end + + return { name.to_sym => hash } + end + + def consistent_opts + { consistent_read: consistent_read } end def range_query - opts = { :hash_value => query[source.hash_key] } - if key = query.keys.find { |k| k.to_s.include?('.') } - opts.merge!(range_hash(key)) + opts = {} + + # Add hash key + opts[:hash_key] = @hash_key + opts[:hash_value] = type_cast_condition_parameter(@hash_key, query[@hash_key]) + + # Add range key + if @range_key + opts[:range_key] = @range_key + if query[@range_key].present? + value = type_cast_condition_parameter(@range_key, query[@range_key]) + opts.update(range_eq: value) + end + + query.keys.select { |k| k.to_s =~ /^#{@range_key}\./ }.each do |key| + opts.merge!(range_hash(key)) + end + end + + (query.keys.map(&:to_sym) - [@hash_key.to_sym, @range_key.try(:to_sym)]) + .reject { |k, _| k.to_s =~ /^#{@range_key}\./ } + .each do |key| + if key.to_s.include?('.') + opts.update(field_hash(key)) + else + value = type_cast_condition_parameter(key, query[key]) + opts[key] = {eq: value} + end end + opts.merge(query_opts).merge(consistent_opts) end - def query_keys - query.keys.collect{|k| k.to_s.split('.').first} + def type_cast_condition_parameter(key, value) + return value if [:array, :set].include?(source.attributes[key.to_sym][:type]) + + if !value.respond_to?(:to_ary) + source.dump_field(value, source.attributes[key.to_sym]) + else + value.to_ary.map { |el| source.dump_field(el, source.attributes[key.to_sym]) } + end end - # [hash_key] or [hash_key, range_key] is specified in query keys. def key_present? - query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set) + query_keys = query.keys.collect { |k| k.to_s.split('.').first } + + # See if querying based on table hash key + if query.keys.map(&:to_s).include?(source.hash_key.to_s) + @hash_key = source.hash_key + + # Use table's default range key + if query_keys.include?(source.range_key.to_s) + @range_key = source.range_key + return true + end + + # See if can use any local secondary index range key + # Chooses the first LSI found that can be utilized for the query + source.local_secondary_indexes.each do |_, lsi| + next unless query_keys.include?(lsi.range_key.to_s) + @range_key = lsi.range_key + @index_name = lsi.name + end + + return true + end + + # See if can use any global secondary index + # Chooses the first GSI found that can be utilized for the query + # But only do so if projects ALL attributes otherwise we won't + # get back full data + source.global_secondary_indexes.each do |_, gsi| + next unless query.keys.map(&:to_s).include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all + @hash_key = gsi.hash_key + @range_key = gsi.range_key + @index_name = gsi.name + return true + end + + # Could not utilize any indices so we'll have to scan + false end + # Start key needs to be set up based on the index utilized + # If using a secondary index then we must include the index's composite key + # as well as the tables composite key. def start_key - key = { :hash_key_element => @start.hash_key } - if range_key = @start.class.range_key - key.merge!({:range_key_element => @start.send(range_key) }) + return @start if @start.is_a?(Hash) + hash_key = @hash_key || source.hash_key + range_key = @range_key || source.range_key + + key = {} + key[hash_key] = type_cast_condition_parameter(hash_key, @start.send(hash_key)) + if range_key + key[range_key] = type_cast_condition_parameter(range_key, @start.send(range_key)) + end + # Add table composite keys if they differ from secondary index used composite key + if hash_key != source.hash_key + key[source.hash_key] = type_cast_condition_parameter(source.hash_key, @start.hash_key) + end + if source.range_key && range_key != source.range_key + key[source.range_key] = type_cast_condition_parameter(source.range_key, @start.range_value) end key end def query_opts opts = {} + opts[:index_name] = @index_name if @index_name opts[:select] = 'ALL_ATTRIBUTES' - opts[:limit] = @eval_limit if @eval_limit - opts[:next_token] = start_key if @start + opts[:record_limit] = @record_limit if @record_limit + opts[:scan_limit] = @scan_limit if @scan_limit + opts[:batch_size] = @batch_size if @batch_size + opts[:exclusive_start_key] = start_key if @start opts[:scan_index_forward] = @scan_index_forward opts end + def scan_query + {}.tap do |opts| + query.keys.map(&:to_sym).each do |key| + if key.to_s.include?('.') + opts.update(field_hash(key)) + else + value = type_cast_condition_parameter(key, query[key]) + opts[key] = {eq: value} + end + end + end + end + def scan_opts opts = {} - opts[:limit] = @eval_limit if @eval_limit - opts[:next_token] = start_key if @start + opts[:record_limit] = @record_limit if @record_limit + opts[:scan_limit] = @scan_limit if @scan_limit opts[:batch_size] = @batch_size if @batch_size + opts[:exclusive_start_key] = start_key if @start + opts[:consistent_read] = true if @consistent_read opts end end diff --git a/lib/dynamoid/dirty.rb b/lib/dynamoid/dirty.rb index 5a128502..433cc61a 100644 --- a/lib/dynamoid/dirty.rb +++ b/lib/dynamoid/dirty.rb @@ -5,7 +5,7 @@ module Dirty module ClassMethods def from_database(*) - super.tap { |d| d.changed_attributes.clear } + super.tap { |d| d.send(:clear_changes_information) } end end @@ -15,7 +15,7 @@ def save(*) def update!(*) ret = super - clear_changes #update! completely reloads all fields on the class, so any extant changes are wiped out + clear_changes # update! completely reloads all fields on the class, so any extant changes are wiped out ret end @@ -26,9 +26,9 @@ def reload def clear_changes previous = changes (block_given? ? yield : true).tap do |result| - unless result == false #failed validation; nil is OK. + unless result == false # failed validation; nil is OK. @previously_changed = previous - changed_attributes.clear + clear_changes_information end end end @@ -43,5 +43,24 @@ def write_attribute(name, value) def attribute_method?(attr) super || self.class.attributes.has_key?(attr.to_sym) end + + if ActiveModel::VERSION::STRING >= '5.2.0' + # The ActiveModel::Dirty API was changed + # https://github.com/rails/rails/commit/c3675f50d2e59b7fc173d7b332860c4b1a24a726#diff-aaddd42c7feb0834b1b5c66af69814d3 + # So we just try to disable new functionality + + def mutations_from_database + @mutations_from_database ||= ActiveModel::NullMutationTracker.instance + end + + def forget_attribute_assignments + end + end + + if ActiveModel::VERSION::STRING < '4.2.0' + def clear_changes_information + changed_attributes.clear + end + end end end diff --git a/lib/dynamoid/document.rb b/lib/dynamoid/document.rb index 0a5c3803..40964f81 100644 --- a/lib/dynamoid/document.rb +++ b/lib/dynamoid/document.rb @@ -8,12 +8,12 @@ module Document include Dynamoid::Components included do - class_attribute :options, :read_only_attributes, :base_class + class_attribute :options, :read_only_attributes, :base_class, instance_accessor: false self.options = {} self.read_only_attributes = [] self.base_class = self - Dynamoid.included_models << self + Dynamoid.included_models << self unless Dynamoid.included_models.include? self end module ClassMethods @@ -72,7 +72,11 @@ def count # # @since 0.2.0 def create(attrs = {}) - attrs[:type] ? attrs[:type].constantize.new(attrs).tap(&:save) : new(attrs).tap(&:save) + if attrs.is_a?(Array) + attrs.map { |attr| create(attr) } + else + build(attrs).tap(&:save) + end end # Initialize a new object and immediately save it to the database. Raise an exception if persistence failed. @@ -83,7 +87,11 @@ def create(attrs = {}) # # @since 0.2.0 def create!(attrs = {}) - attrs[:type] ? attrs[:type].constantize.new(attrs).tap(&:save!) : new(attrs).tap(&:save!) + if attrs.is_a?(Array) + attrs.map { |attr| create!(attr) } + else + build(attrs).tap(&:save!) + end end # Initialize a new object. @@ -106,9 +114,84 @@ def build(attrs = {}) # @since 0.2.0 def exists?(id_or_conditions = {}) case id_or_conditions - when Hash then ! where(id_or_conditions).all.empty? - else !! find(id_or_conditions) + when Hash then where(id_or_conditions).first.present? + else !! find_by_id(id_or_conditions) + end + end + + def update(hash_key, range_key_value=nil, attrs) + if range_key.present? + range_key_value = dump_field(range_key_value, attributes[self.range_key]) + else + range_key_value = nil end + + model = find(hash_key, range_key: range_key_value, consistent_read: true) + model.update_attributes(attrs) + model + end + + def update_fields(hash_key_value, range_key_value=nil, attrs={}, conditions={}) + optional_params = [range_key_value, attrs, conditions].compact + if optional_params.first.is_a?(Hash) + range_key_value = nil + attrs, conditions = optional_params[0 .. 1] + else + range_key_value = optional_params.first + attrs, conditions = optional_params[1 .. 2] + end + + options = if range_key + { range_key: dump_field(range_key_value, attributes[range_key]) } + else + {} + end + + (conditions[:if_exists] ||= {})[hash_key] = hash_key_value + options[:conditions] = conditions + + begin + new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t| + attrs.symbolize_keys.each do |k, v| + t.set k => dump_field(v, attributes[k]) + end + end + new(new_attrs) + rescue Dynamoid::Errors::ConditionalCheckFailedException + end + end + + def upsert(hash_key_value, range_key_value=nil, attrs={}, conditions={}) + optional_params = [range_key_value, attrs, conditions].compact + if optional_params.first.is_a?(Hash) + range_key_value = nil + attrs, conditions = optional_params[0 .. 1] + else + range_key_value = optional_params.first + attrs, conditions = optional_params[1 .. 2] + end + + options = if range_key + { range_key: dump_field(range_key_value, attributes[range_key]) } + else + {} + end + + options[:conditions] = conditions + + begin + new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t| + attrs.symbolize_keys.each do |k, v| + t.set k => dump_field(v, attributes[k]) + end + end + new(new_attrs) + rescue Dynamoid::Errors::ConditionalCheckFailedException + end + end + + def deep_subclasses + subclasses + subclasses.map(&:deep_subclasses).flatten end end @@ -120,6 +203,10 @@ def exists?(id_or_conditions = {}) # # @since 0.2.0 def initialize(attrs = {}) + # we need this hack for Rails 4.0 only + # because `run_callbacks` calls `attributes` getter while it is still nil + @attributes = {} + run_callbacks :initialize do @new_record = true @attributes ||= {} @@ -163,7 +250,7 @@ def hash # @since 0.2.0 def reload range_key_value = range_value ? dumped_range_value : nil - self.attributes = self.class.find(hash_key, :range_key => range_key_value, :consistent_read => true).attributes + self.attributes = self.class.find(hash_key, range_key: range_key_value, consistent_read: true).attributes @associations.values.each(&:reset) self end diff --git a/lib/dynamoid/errors.rb b/lib/dynamoid/errors.rb index dc1619f6..fb3aedc8 100644 --- a/lib/dynamoid/errors.rb +++ b/lib/dynamoid/errors.rb @@ -27,7 +27,7 @@ class RecordNotDestroyed < Error attr_reader :record def initialize(record) - super("Failed to destroy item") + super('Failed to destroy item') @record = record end end @@ -61,9 +61,15 @@ def initialize(record, attempted_action) end end + class RecordNotFound < Error + end + class DocumentNotValid < Error + attr_reader :document + def initialize(document) super("Validation failed: #{document.errors.full_messages.join(", ")}") + @document = document end end diff --git a/lib/dynamoid/fields.rb b/lib/dynamoid/fields.rb index 008e7d8a..54e133d3 100644 --- a/lib/dynamoid/fields.rb +++ b/lib/dynamoid/fields.rb @@ -5,23 +5,26 @@ module Dynamoid #:nodoc: module Fields extend ActiveSupport::Concern + + # Types allowed in indexes: PERMITTED_KEY_TYPES = [ :number, :integer, :string, - :datetime + :datetime, + :serialized ] # Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at. included do - class_attribute :attributes + class_attribute :attributes, instance_accessor: false class_attribute :range_key self.attributes = {} field :created_at, :datetime field :updated_at, :datetime - field :id #Default primary key + field :id # Default primary key end module ClassMethods @@ -29,7 +32,7 @@ module ClassMethods # Specify a field for a document. # # Its type determines how it is coerced when read in and out of the datastore. - # You can specify :integer, :number, :set, :array, :datetime, and :serialized, + # You can specify :integer, :number, :set, :array, :datetime, :date and :serialized, # or specify a class that defines a serialization strategy. # # If you specify a class for field type, Dynamoid will serialize using @@ -48,20 +51,30 @@ def field(name, type = :string, options = {}) Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.") type = :number end - self.attributes = attributes.merge(name => {:type => type}.merge(options)) - - define_method(named) { read_attribute(named) } - define_method("#{named}?") { !read_attribute(named).nil? } - define_method("#{named}=") {|value| write_attribute(named, value) } + self.attributes = attributes.merge(name => {type: type}.merge(options)) + + generated_methods.module_eval do + define_method(named) { read_attribute(named) } + define_method("#{named}?") do + value = read_attribute(named) + case value + when true then true + when false, nil then false + else + !value.nil? + end + end + define_method("#{named}=") {|value| write_attribute(named, value) } + end end - def range(name, type = :string) - field(name, type) + def range(name, type = :string, options = {}) + field(name, type, options) self.range_key = name end def table(options) - #a default 'id' column is created when Dynamoid::Document is included + # a default 'id' column is created when Dynamoid::Document is included unless(attributes.has_key? hash_key) remove_field :id field(hash_key) @@ -70,10 +83,23 @@ def table(options) def remove_field(field) field = field.to_sym - attributes.delete(field) or raise "No such field" - remove_method field - remove_method :"#{field}=" - remove_method :"#{field}?" + attributes.delete(field) or raise 'No such field' + + generated_methods.module_eval do + remove_method field + remove_method :"#{field}=" + remove_method :"#{field}?" + end + end + + private + + def generated_methods + @generated_methods ||= begin + Module.new.tap do |mod| + include(mod) + end + end end end @@ -88,10 +114,6 @@ def remove_field(field) # # @since 0.2.0 def write_attribute(name, value) - if (size = value.to_s.size) > MAX_ITEM_SIZE - Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}." - end - if association = @associations[name] association.reset end @@ -137,14 +159,16 @@ def update_attribute(attribute, value) # # @since 0.2.0 def set_created_at - self.created_at = DateTime.now + self.created_at ||= DateTime.now.in_time_zone(Time.zone) if Dynamoid::Config.timestamps end # Automatically called during the save callback to set the updated_at time. # # @since 0.2.0 def set_updated_at - self.updated_at = DateTime.now + if Dynamoid::Config.timestamps && !self.updated_at_changed? + self.updated_at = DateTime.now.in_time_zone(Time.zone) + end end def set_type diff --git a/lib/dynamoid/finders.rb b/lib/dynamoid/finders.rb index b85cbdf2..1e5514f0 100644 --- a/lib/dynamoid/finders.rb +++ b/lib/dynamoid/finders.rb @@ -36,15 +36,27 @@ def find(*ids) ids = Array(ids.flatten.uniq) if ids.count == 1 result = self.find_by_id(ids.first, options) + if result.nil? + message = "Couldn't find #{self.name} with '#{self.hash_key}'=#{ids[0]}" + raise Errors::RecordNotFound.new(message) + end expects_array ? Array(result) : result else - find_all(ids) + result = find_all(ids) + if result.size != ids.size + message = "Couldn't find all #{self.name.pluralize} with '#{self.hash_key}': (#{ids.join(', ')}) " + message << "(found #{result.size} results, but was looking for #{ids.size})" + raise Errors::RecordNotFound.new(message) + end + result end end - # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGet. + # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGetItem. # Returns empty array if no results found. # + # Uses backoff specified by `Dynamoid::Config.backoff` config option + # # @param [Array] ids # @param [Hash] options: Passed to the underlying query. # @@ -55,8 +67,26 @@ def find(*ids) # find all the tweets using hash key and range key with consistent read # Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true) def find_all(ids, options = {}) - items = Dynamoid.adapter.read(self.table_name, ids, options) - items ? items[self.table_name].map{|i| from_database(i)} : [] + results = unless Dynamoid.config.backoff + items = Dynamoid.adapter.read(self.table_name, ids, options) + items ? items[self.table_name] : [] + else + items = [] + backoff = nil + Dynamoid.adapter.read(self.table_name, ids, options) do |hash, has_unprocessed_items| + items += hash[self.table_name] + + if has_unprocessed_items + backoff ||= Dynamoid.config.build_backoff + backoff.call + else + backoff = nil + end + end + items + end + + results ? results.map {|i| from_database(i) } : [] end # Find one object directly by id. @@ -81,7 +111,7 @@ def find_by_id(id, options = {}) # @param [String/Number] range_key of the object to find # def find_by_composite_key(hash_key, range_key, options = {}) - find_by_id(hash_key, options.merge({:range_key => range_key})) + find_by_id(hash_key, options.merge(range_key: range_key)) end # Find all objects by hash and range keys. @@ -106,7 +136,7 @@ def find_by_composite_key(hash_key, range_key, options = {}) # @return [Array] an array of all matching items # def find_all_by_composite_key(hash_key, options = {}) - Dynamoid.adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item| + Dynamoid.adapter.query(self.table_name, options.merge(hash_value: hash_key)).collect do |item| from_database(item) end end @@ -121,14 +151,15 @@ def find_all_by_composite_key(hash_key, options = {}) # field :gender, :string # field :rank :number # table :key => :email - # global_secondary_index :hash_key => :age, :range_key => :gender + # global_secondary_index :hash_key => :age, :range_key => :rank # end - # User.find_all_by_secondary_index(:age => 5, :range => {"rank.lte" => 10}) + # # NOTE: the first param and the second param are both hashes, + # # so curly braces must be used on first hash param if sending both params + # User.find_all_by_secondary_index({:age => 5}, :range => {"rank.lte" => 10}) # - # @param [Hash] hash eg: {:age => 5} - # @param [Hash] options - @TODO support more options in future such as query filter, projected keys etc - # @option options [Hash] :range {"rank.lte" => 10} - # @option options [Boolean] :batch_size Fetch all records instead of limiting to 1MB + # @param [Hash] eg: {:age => 5} + # @param [Hash] eg: {"rank.lte" => 10} + # @param [Hash] options - query filter, projected keys, scan_index_forward etc # @return [Array] an array of all matching items def find_all_by_secondary_index(hash, options = {}) range = options[:range] || {} @@ -138,29 +169,29 @@ def find_all_by_secondary_index(hash, options = {}) if range_key_field range_key_field = range_key_field.to_s - range_key_op = "eq" - if range_key_field.include?(".") - range_key_field, range_key_op = range_key_field.split(".", 2) + range_key_op = 'eq' + if range_key_field.include?('.') + range_key_field, range_key_op = range_key_field.split('.', 2) end range_op_mapped = RANGE_MAP.fetch(range_key_op) end # Find the index index = self.find_index(hash_key_field, range_key_field) - raise Dynamoid::Errors::MissingIndex if index.nil? + raise Dynamoid::Errors::MissingIndex.new("attempted to find #{[hash_key_field, range_key_field]}") if index.nil? # query opts = { - :hash_key => hash_key_field.to_s, - :hash_value => hash_key_value, - :index_name => index.name, + hash_key: hash_key_field.to_s, + hash_value: hash_key_value, + index_name: index.name, } - opts[:batch_size] = options[:batch_size] if options[:batch_size] if range_key_field opts[:range_key] = range_key_field opts[range_op_mapped] = range_key_value end - Dynamoid.adapter.query(self.table_name, opts).map do |item| + dynamo_options = opts.merge(options.reject {|key, _| key == :range }) + Dynamoid.adapter.query(self.table_name, dynamo_options).map do |item| from_database(item) end end diff --git a/lib/dynamoid/identity_map.rb b/lib/dynamoid/identity_map.rb index 25de26be..45a188db 100644 --- a/lib/dynamoid/identity_map.rb +++ b/lib/dynamoid/identity_map.rb @@ -80,7 +80,6 @@ def delete(*args) super end - def identity_map_key key = hash_key.to_s if self.class.range_key diff --git a/lib/dynamoid/indexes.rb b/lib/dynamoid/indexes.rb index ea7666c1..54ef28e5 100644 --- a/lib/dynamoid/indexes.rb +++ b/lib/dynamoid/indexes.rb @@ -3,8 +3,8 @@ module Indexes extend ActiveSupport::Concern included do - class_attribute :local_secondary_indexes - class_attribute :global_secondary_indexes + class_attribute :local_secondary_indexes, instance_accessor: false + class_attribute :global_secondary_indexes, instance_accessor: false self.local_secondary_indexes = {} self.global_secondary_indexes = {} end @@ -39,8 +39,8 @@ def global_secondary_index(options={}) end index_opts = { - :read_capacity => Dynamoid::Config.read_capacity, - :write_capacity => Dynamoid::Config.write_capacity + read_capacity: Dynamoid::Config.read_capacity, + write_capacity: Dynamoid::Config.write_capacity }.merge(options) index_opts[:dynamoid_class] = self @@ -52,7 +52,6 @@ def global_secondary_index(options={}) self end - # Defines a local secondary index on a table. Will use the same primary # hash key as the table. # @@ -83,11 +82,10 @@ def local_secondary_index(options={}) ' must use a different :range_key than the primary key') end - index_opts = options.merge({ - :dynamoid_class => self, - :type => :local_secondary, - :hash_key => primary_hash_key - }) + index_opts = options.merge( + dynamoid_class: self, + type: :local_secondary, + hash_key: primary_hash_key) index = Dynamoid::Indexes::Index.new(index_opts) key = index_key(primary_hash_key, index_range_key) @@ -95,13 +93,11 @@ def local_secondary_index(options={}) self end - def find_index(hash, range=nil) index = self.indexes[index_key(hash, range)] index end - # Returns true iff the provided hash[,range] key combo is a local # secondary index. # @@ -113,7 +109,6 @@ def is_local_secondary_index?(hash, range=nil) self.local_secondary_indexes[index_key(hash, range)].present? end - # Returns true iff the provided hash[,range] key combo is a global # secondary index. # @@ -125,7 +120,6 @@ def is_global_secondary_index?(hash, range=nil) self.global_secondary_indexes[index_key(hash, range)].present? end - # Generates a convenient lookup key name for a hash/range index. # Should normally not be used directly. # @@ -140,7 +134,6 @@ def index_key(hash, range=nil) name end - # Generates a default index name. # # @param [Symbol] hash hash key name. @@ -150,7 +143,6 @@ def index_name(hash, range=nil) "#{self.table_name}_index_#{self.index_key(hash, range)}" end - # Convenience method to return all indexes on the table. # # @return [Hash] the combined hash of global and local @@ -166,7 +158,6 @@ def indexed_hash_keys end end - # Represents the attributes of a DynamoDB index. class Index include ActiveModel::Validations @@ -186,7 +177,6 @@ class Index validate_projected_attributes end - def initialize(attrs={}) unless attrs[:dynamoid_class].present? raise Dynamoid::Errors::InvalidIndex.new(':dynamoid_class is required') @@ -205,7 +195,6 @@ def initialize(attrs={}) raise Dynamoid::Errors::InvalidIndex.new(self) unless self.valid? end - # Convenience method to determine the projection type for an index. # Projection types are: :keys_only, :all, :include. # @@ -218,19 +207,16 @@ def projection_type end end - private def validate_projected_attributes - unless (@projected_attributes.is_a?(Array) || - PROJECTION_TYPES.include?(@projected_attributes)) + unless @projected_attributes.is_a?(Array) || PROJECTION_TYPES.include?(@projected_attributes) errors.add(:projected_attributes, 'Invalid projected attributes specified.') end end def validate_index_type - unless (@type.present? && - [:local_secondary, :global_secondary].include?(@type)) + unless @type.present? && [:local_secondary, :global_secondary].include?(@type) errors.add(:type, 'Invalid index :type specified') end end diff --git a/lib/dynamoid/persistence.rb b/lib/dynamoid/persistence.rb index c1d8cf9e..c09d9085 100644 --- a/lib/dynamoid/persistence.rb +++ b/lib/dynamoid/persistence.rb @@ -13,10 +13,15 @@ module Persistence attr_accessor :new_record alias :new_record? :new_record + UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze + module ClassMethods def table_name - @table_name ||= "#{Dynamoid::Config.namespace}_#{options[:name] || base_class.name.split('::').last.downcase.pluralize}" + table_base_name = options[:name] || base_class.name.split('::').last + .downcase.pluralize + + @table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_') end # Creates a table. @@ -36,14 +41,14 @@ def create_table(options = {}) range_key_hash = nil end options = { - :id => self.hash_key, - :table_name => self.table_name, - :write_capacity => self.write_capacity, - :read_capacity => self.read_capacity, - :range_key => range_key_hash, - :hash_key_type => dynamo_type(attributes[self.hash_key][:type]), - :local_secondary_indexes => self.local_secondary_indexes.values, - :global_secondary_indexes => self.global_secondary_indexes.values + id: self.hash_key, + table_name: self.table_name, + write_capacity: self.write_capacity, + read_capacity: self.read_capacity, + range_key: range_key_hash, + hash_key_type: dynamo_type(attributes[self.hash_key][:type]), + local_secondary_indexes: self.local_secondary_indexes.values, + global_secondary_indexes: self.global_secondary_indexes.values }.merge(options) Dynamoid.adapter.create_table(options[:table_name], options[:id], options) @@ -66,7 +71,13 @@ def undump(incoming = nil) incoming = (incoming || {}).symbolize_keys Hash.new.tap do |hash| self.attributes.each do |attribute, options| - hash[attribute] = undump_field(incoming[attribute], options) + if incoming.has_key?(attribute) + hash[attribute] = undump_field(incoming[attribute], options) + elsif options.has_key?(:default) + hash[attribute] = evaluate_default_value(options[:default]) + else + hash[attribute] = nil + end end incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute } end @@ -89,11 +100,7 @@ def undump_field(value, options) value end else - if value.nil? && (default_value = options[:default]) - value = default_value.respond_to?(:call) ? default_value.call : default_value - end - - if !value.nil? + unless value.nil? case options[:type] when :string value.to_s @@ -105,22 +112,29 @@ def undump_field(value, options) value.to_a when :hash value + when :raw + if value.is_a?(Hash) + undump_hash(value) + else + value + end when :set - Set.new(value) + undump_set(options, value) when :datetime + parse_datetime(value, options) + when :date if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time) - value + value.to_date else - Time.at(value).to_datetime + parse_date(value, options) end when :boolean - # persisted as 't', but because undump is called during initialize it can come in as true if value == 't' || value == true true elsif value == 'f' || value == false false else - raise ArgumentError, "Boolean column neither true nor false" + raise ArgumentError, 'Boolean column neither true nor false' end else raise ArgumentError, "Unknown type #{options[:type]}" @@ -129,12 +143,91 @@ def undump_field(value, options) end end + def undump_set(options, value) + case options[:of] + when :integer + value.map { |v| Integer(v) }.to_set + when :number + value.map { |v| BigDecimal.new(v.to_s) }.to_set + else + value.is_a?(Set) ? value : Set.new(value) + end + end + + def dump_field(value, options) + if (field_class = options[:type]).is_a?(Class) + if value.respond_to?(:dynamoid_dump) + value.dynamoid_dump + elsif field_class.respond_to?(:dynamoid_dump) + field_class.dynamoid_dump(value) + else + raise ArgumentError, "Neither #{field_class} nor #{value} support serialization for Dynamoid." + end + else + case options[:type] + when :string + !value.nil? ? value.to_s : nil + when :integer + !value.nil? ? Integer(value) : nil + when :number + !value.nil? ? value : nil + when :set + !value.nil? ? dump_object(Set.new(value)) : nil + when :array + !value.nil? ? dump_object(value) : nil + when :hash + !value.nil? ? dump_object(value) : nil + when :datetime + # !value.nil? ? value.to_time.to_f : nil + !value.nil? ? format_datetime(value, options) : nil + when :date + !value.nil? ? format_date(value, options) : nil + when :serialized + options[:serializer] ? options[:serializer].dump(value) : value.to_yaml + when :raw + !value.nil? ? value : nil + when :boolean + if !value.nil? + if options[:store_as_native_boolean] + !!value # native boolean type + else + value.to_s[0] # => "f" or "t" + end + else + nil + end + else + raise ArgumentError, "Unknown type #{options[:type]}" + end + end + end + + # Convert empty strings to nil in objects since DynamoDB does not allow + # empty strings in the database. + def dump_object(obj) + case obj + when Hash + obj.inject({}) do |new_hash, (key, value)| + new_hash[key] = (value == '' ? nil : dump_object(value)) + new_hash + end + when Array, Set + new_obj = obj.class.new + obj.each do |value| + new_obj << (value == '' ? nil : dump_object(value)) + end + new_obj + else + obj + end + end + def dynamo_type(type) if type.is_a?(Class) type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string else case type - when :integer, :number, :datetime + when :integer, :number, :datetime, :date :number when :string, :serialized :string @@ -150,7 +243,140 @@ def dynamo_type(type) # @option opts [Boolean] :skip_lock_check - skips checking the lock version def destroy_all(opts = {}) batch_size = opts[:batch_size] || 100 - self.eval_limit(batch_size).all.each {|i| i.destroy(opts)} + self.record_limit(batch_size).all.each {|i| i.destroy(opts)} + end + + # Creates several models at once. + # Neither callbacks nor validations run. + # It works efficiently because of using BatchWriteItem. + # + # Returns array of models + # + # Uses backoff specified by `Dynamoid::Config.backoff` config option + # + # @param [Array] items + # + # @example + # User.import([{ name: 'a' }, { name: 'b' }]) + def import(objects) + documents = objects.map do |attrs| + self.build(attrs).tap do |item| + item.hash_key = SecureRandom.uuid if item.hash_key.blank? + end + end + + unless Dynamoid.config.backoff + Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump)) + else + backoff = nil + Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump)) do |has_unprocessed_items| + if has_unprocessed_items + backoff ||= Dynamoid.config.build_backoff + backoff.call + else + backoff = nil + end + end + end + + documents.each { |d| d.new_record = false } + documents + end + + private + + def undump_hash(hash) + {}.tap do |h| + hash.each { |key, value| h[key.to_sym] = undump_hash_value(value) } + end + end + + def undump_hash_value(val) + case val + when BigDecimal + if Dynamoid::Config.convert_big_decimal + val.to_f + else + val + end + when Hash + undump_hash(val) + when Array + val.map { |v| undump_hash_value(v) } + else + val + end + end + + def format_datetime(value, options) + use_string_format = options[:store_as_string].nil? \ + ? Dynamoid.config.store_datetime_as_string \ + : options[:store_as_string] + + if use_string_format + value.to_time.iso8601 + else + unless value.respond_to?(:to_i) && value.respond_to?(:nsec) + value = value.to_time + end + BigDecimal("%d.%09d" % [value.to_i, value.nsec]) + end + end + + def format_date(value, options) + use_string_format = options[:store_as_string].nil? \ + ? Dynamoid.config.store_date_as_string \ + : options[:store_as_string] + + unless use_string_format + (value.to_date - UNIX_EPOCH_DATE).to_i + else + value.to_date.iso8601 + end + end + + def parse_datetime(value, options) + return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time) + + use_string_format = options[:store_as_string].nil? \ + ? Dynamoid.config.store_datetime_as_string \ + : options[:store_as_string] + value = DateTime.iso8601(value).to_time.to_i if use_string_format + + case Dynamoid::Config.application_timezone + when :utc + ActiveSupport::TimeZone['UTC'].at(value).to_datetime + when :local + Time.at(value).to_datetime + when String + ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime + end + end + + def parse_date(value, options) + use_string_format = options[:store_as_string].nil? \ + ? Dynamoid.config.store_date_as_string \ + : options[:store_as_string] + + unless use_string_format + UNIX_EPOCH_DATE + value.to_i + else + Date.iso8601(value) + end + end + + # Evaluates the default value given, this is used by undump + # when determining the value of the default given for a field options. + # + # @param [Object] :value the attribute's default value + def evaluate_default_value(val) + if val.respond_to?(:call) + val.call + elsif val.duplicable? + val.dup + else + val + end end end @@ -177,15 +403,13 @@ def save(options = {}) self.class.create_table if new_record? - conditions = { :unless_exists => [self.class.hash_key]} + conditions = { unless_exists: [self.class.hash_key]} conditions[:unless_exists] << range_key if(range_key) run_callbacks(:create) { persist(conditions) } else persist end - - self end # @@ -195,10 +419,10 @@ def save(options = {}) # def update!(conditions = {}, &block) run_callbacks(:update) do - options = range_key ? {:range_key => dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {} + options = range_key ? {range_key: dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {} begin - new_attrs = Dynamoid.adapter.update_item(self.class.table_name, self.hash_key, options.merge(:conditions => conditions)) do |t| + new_attrs = Dynamoid.adapter.update_item(self.class.table_name, self.hash_key, options.merge(conditions: conditions)) do |t| if(self.class.attributes[:lock_version]) t.add(lock_version: 1) end @@ -278,66 +502,13 @@ def dump # # @since 0.2.0 def dump_field(value, options) - if (field_class = options[:type]).is_a?(Class) - if value.respond_to?(:dynamoid_dump) - value.dynamoid_dump - elsif field_class.respond_to?(:dynamoid_dump) - field_class.dynamoid_dump(value) - else - raise ArgumentError, "Neither #{field_class} nor #{value} support serialization for Dynamoid." - end - else - case options[:type] - when :string - !value.nil? ? value.to_s : nil - when :integer - !value.nil? ? Integer(value) : nil - when :number - !value.nil? ? value : nil - when :set - !value.nil? ? dump_object(Set.new(value)) : nil - when :array - !value.nil? ? dump_object(value) : nil - when :hash - !value.nil? ? dump_object(value) : nil - when :datetime - !value.nil? ? value.to_time.to_f : nil - when :serialized - options[:serializer] ? options[:serializer].dump(value) : value.to_yaml - when :boolean - if(!value.nil?) - if [true, false].include?(value) - value - else - raise ArgumentError, "Boolean column neither true nor false" - end - else - nil - end - else - raise ArgumentError, "Unknown type #{options[:type]}" - end - end + self.class.dump_field(value, options) end # Convert empty strings to nil in objects since DynamoDB does not allow # empty strings in the database. def dump_object(obj) - case obj - when Hash - obj.inject({}) do |new_hash, (key, value)| - new_hash[key] = (value == '' ? nil : dump_object(value)) - new_hash - end - when Array, Set - new_obj = obj.class.new - obj.each do |value| - new_obj << (value == '' ? nil : dump_object(value)) - end - new_obj - else - obj - end + self.class.dump_object(obj) end # Persist the object into the datastore. Assign it an id first if it doesn't have one. @@ -345,7 +516,7 @@ def dump_object(obj) # @since 0.2.0 def persist(conditions = nil) run_callbacks(:save) do - self.hash_key = SecureRandom.uuid if self.hash_key.nil? || self.hash_key.blank? + self.hash_key = SecureRandom.uuid if self.hash_key.blank? # Add an exists check to prevent overwriting existing records with new ones if(new_record?) @@ -357,7 +528,7 @@ def persist(conditions = nil) if(self.class.attributes[:lock_version]) conditions ||= {} self.lock_version = (lock_version || 0) + 1 - #Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually + # Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if(changes[:lock_version][0]) end diff --git a/lib/dynamoid/railtie.rb b/lib/dynamoid/railtie.rb new file mode 100644 index 00000000..28b2aae7 --- /dev/null +++ b/lib/dynamoid/railtie.rb @@ -0,0 +1,11 @@ +if defined? (Rails) + + module Dynamoid + class Railtie < Rails::Railtie + rake_tasks do + Dir[File.join(File.dirname(__FILE__), 'tasks/*.rake')].each { |f| load f } + end + end + end + +end diff --git a/lib/dynamoid/tasks/database.rake b/lib/dynamoid/tasks/database.rake new file mode 100644 index 00000000..20af0ab3 --- /dev/null +++ b/lib/dynamoid/tasks/database.rake @@ -0,0 +1,41 @@ +require 'dynamoid' +require 'dynamoid/tasks/database' + +namespace :dynamoid do + desc "Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables" + task :create_tables => :environment do + # Load models so Dynamoid will be able to discover tables expected. + Dir[ File.join(Dynamoid::Config.models_dir, "*.rb") ].sort.each { |file| require file } + if Dynamoid.included_models.any? + tables = Dynamoid::Tasks::Database.create_tables + result = tables[:created].map{ |c| "#{c} created" } + tables[:existing].map{ |e| "#{e} already exists" } + result.sort.each{ |r| puts r } + else + puts "Dynamoid models are not loaded, or you have no Dynamoid models." + end + end + + desc 'Tests if the DynamoDB instance can be contacted using your configuration' + task :ping => :environment do + success = false + failure_reason = nil + + begin + Dynamoid::Tasks::Database.ping + success = true + rescue Exception => e + failure_reason = e.message + end + + msg = "Connection to DynamoDB #{success ? 'OK' : 'FAILED'}" + msg << if Dynamoid.config.endpoint + " at local endpoint '#{Dynamoid.config.endpoint}'" + else + ' at remote AWS endpoint' + end + if not success + msg << ", reason being '#{failure_reason}'" + end + puts msg + end +end diff --git a/lib/dynamoid/tasks/database.rb b/lib/dynamoid/tasks/database.rb new file mode 100644 index 00000000..11836f74 --- /dev/null +++ b/lib/dynamoid/tasks/database.rb @@ -0,0 +1,30 @@ +module Dynamoid + module Tasks + module Database + extend self + + # Create any new tables for the models. Existing tables are not + # modified. + def create_tables + results = { created: [], existing: [] } + # We can't quite rely on Dynamoid.included_models alone, we need to select only viable models + Dynamoid.included_models.select{ |m| not m.base_class.try(:name).blank? }.uniq(&:table_name).each do |model| + if Dynamoid.adapter.list_tables.include? model.table_name + results[:existing] << model.table_name + else + model.create_table + results[:created] << model.table_name + end + end + results + end + + # Is the DynamoDB reachable? + def ping + Dynamoid.adapter.list_tables + true + end + + end + end +end diff --git a/lib/dynamoid/validations.rb b/lib/dynamoid/validations.rb index 53ec4bfa..337ac8c6 100644 --- a/lib/dynamoid/validations.rb +++ b/lib/dynamoid/validations.rb @@ -1,6 +1,6 @@ # encoding: utf-8 module Dynamoid - + # Provide ActiveModel validations to Dynamoid documents. module Validations extend ActiveSupport::Concern @@ -12,7 +12,7 @@ module Validations # # @since 0.2.0 def save(options = {}) - options.reverse_merge!(:validate => true) + options.reverse_merge!(validate: true) return false if options[:validate] and (not valid?) super end @@ -30,7 +30,35 @@ def valid?(context = nil) # @since 0.2.0 def save! raise Dynamoid::Errors::DocumentNotValid.new(self) unless valid? - save(:validate => false) + save(validate: false) + self + end + + module ClassMethods + + # Override validates_presence_of to handle false values as present. + # + # @since 1.1.1 + def validates_presence_of(*attr_names) + validates_with PresenceValidator, _merge_attributes(attr_names) + end + + private + + # Validates that the specified attributes are present (false or not blank). + class PresenceValidator < ActiveModel::EachValidator + # Validate the record for the record and value. + def validate_each(record, attr_name, value) + record.errors.add(attr_name, :blank, options) if not_present?(value) + end + + private + + # Check whether a value is not present. + def not_present?(value) + value.blank? && value != false + end + end end end end diff --git a/lib/dynamoid/version.rb b/lib/dynamoid/version.rb new file mode 100644 index 00000000..56e4817f --- /dev/null +++ b/lib/dynamoid/version.rb @@ -0,0 +1,3 @@ +module Dynamoid + VERSION = '2.2.0' +end diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal.jar deleted file mode 100644 index a43b5ced..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/antlr-runtime-4.1.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/antlr-runtime-4.1.jar deleted file mode 100644 index e2b192cd..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/antlr-runtime-4.1.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/aws-java-sdk-1.9.16.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/aws-java-sdk-1.9.16.jar deleted file mode 100644 index c217600e..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/aws-java-sdk-1.9.16.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/commons-cli-1.2.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/commons-cli-1.2.jar deleted file mode 100644 index aa0f5f2f..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/commons-cli-1.2.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/commons-lang3-3.x.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/commons-lang3-3.x.jar deleted file mode 100644 index c591e60a..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/commons-lang3-3.x.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-annotations-2.3.2.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-annotations-2.3.2.jar deleted file mode 100644 index 7ea186fd..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-annotations-2.3.2.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-core-2.3.2.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-core-2.3.2.jar deleted file mode 100644 index bdf112fe..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-core-2.3.2.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-databind-2.3.2.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-databind-2.3.2.jar deleted file mode 100644 index 5859ab51..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jackson-databind-2.3.2.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-ajp-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-ajp-8.1.12.v20130726.jar deleted file mode 100644 index 05b7c5d8..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-ajp-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-annotations-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-annotations-8.1.12.v20130726.jar deleted file mode 100644 index 3127071b..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-annotations-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-client-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-client-8.1.12.v20130726.jar deleted file mode 100644 index e5311af0..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-client-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-continuation-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-continuation-8.1.12.v20130726.jar deleted file mode 100644 index f253f0d5..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-continuation-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-deploy-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-deploy-8.1.12.v20130726.jar deleted file mode 100644 index 405d2d03..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-deploy-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-http-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-http-8.1.12.v20130726.jar deleted file mode 100644 index 30adfb56..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-http-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-io-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-io-8.1.12.v20130726.jar deleted file mode 100644 index e37e7159..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-io-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-jmx-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-jmx-8.1.12.v20130726.jar deleted file mode 100644 index 47c654bc..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-jmx-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-jndi-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-jndi-8.1.12.v20130726.jar deleted file mode 100644 index 457685bc..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-jndi-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-overlay-deployer-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-overlay-deployer-8.1.12.v20130726.jar deleted file mode 100644 index d291bca4..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-overlay-deployer-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-plus-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-plus-8.1.12.v20130726.jar deleted file mode 100644 index 17c41b6b..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-plus-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-policy-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-policy-8.1.12.v20130726.jar deleted file mode 100644 index 61fa61bd..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-policy-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-rewrite-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-rewrite-8.1.12.v20130726.jar deleted file mode 100644 index b7fe16f3..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-rewrite-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-security-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-security-8.1.12.v20130726.jar deleted file mode 100644 index fd9ea3e5..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-security-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-server-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-server-8.1.12.v20130726.jar deleted file mode 100644 index 49cdbb0c..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-server-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-servlet-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-servlet-8.1.12.v20130726.jar deleted file mode 100644 index 07e6068e..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-servlet-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-servlets-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-servlets-8.1.12.v20130726.jar deleted file mode 100644 index bc870609..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-servlets-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-start.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-start.jar deleted file mode 100644 index 8a5ee39a..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-start.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-util-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-util-8.1.12.v20130726.jar deleted file mode 100644 index 41a76e05..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-util-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-webapp-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-webapp-8.1.12.v20130726.jar deleted file mode 100644 index 4a7a6893..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-webapp-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-websocket-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-websocket-8.1.12.v20130726.jar deleted file mode 100644 index 1b93315d..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-websocket-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-xml-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-xml-8.1.12.v20130726.jar deleted file mode 100644 index f8282133..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/jetty-xml-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/joda-time-2.3.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/joda-time-2.3.jar deleted file mode 100644 index 9dce4f92..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/joda-time-2.3.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-linux-amd64.so b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-linux-amd64.so deleted file mode 100644 index 88461578..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-linux-amd64.so and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-linux-i386.so b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-linux-i386.so deleted file mode 100644 index 15e7469e..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-linux-i386.so and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-osx.dylib b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-osx.dylib deleted file mode 100644 index 02761626..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/libsqlite4java-osx.dylib and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/log4j-api-2.1.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/log4j-api-2.1.jar deleted file mode 100644 index d18d67c4..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/log4j-api-2.1.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/log4j-core-2.1.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/log4j-core-2.1.jar deleted file mode 100644 index be2e9170..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/log4j-core-2.1.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/servlet-api-3.0.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/servlet-api-3.0.jar deleted file mode 100644 index b26b213a..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/servlet-api-3.0.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-core-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-core-8.1.12.v20130726.jar deleted file mode 100644 index 1f4f26e2..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-core-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-jetty-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-jetty-8.1.12.v20130726.jar deleted file mode 100644 index 3bdf0c63..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-jetty-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-jetty-http-8.1.12.v20130726.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-jetty-http-8.1.12.v20130726.jar deleted file mode 100644 index 02f0e537..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/spdy-jetty-http-8.1.12.v20130726.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java-win32-x64.dll b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java-win32-x64.dll deleted file mode 100644 index 70d258f2..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java-win32-x64.dll and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java-win32-x86.dll b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java-win32-x86.dll deleted file mode 100644 index c988e5a6..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java-win32-x86.dll and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java.jar b/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java.jar deleted file mode 100644 index cfa8d8cf..00000000 Binary files a/spec/DynamoDBLocal-2015-01-27/DynamoDBLocal_lib/sqlite4java.jar and /dev/null differ diff --git a/spec/DynamoDBLocal-2015-01-27/LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/LICENSE.txt deleted file mode 100644 index d8cfda6b..00000000 --- a/spec/DynamoDBLocal-2015-01-27/LICENSE.txt +++ /dev/null @@ -1,28 +0,0 @@ -DynamoDB Local License Agreement -THIS IS AN AGREEMENT BETWEEN YOU AND AMAZON WEB SERVICES, INC. (WITH ITS AFFILIATES, "AWS" OR "WE") THAT GOVERNS YOUR USE OF THE DYNAMODB LOCAL SOFTWARE (TOGETHER WITH ANY UPDATES AND ENHANCEMENTS TO IT, AND ACCOMPANYING DOCUMENTATION, THE“SOFTWARE”) THAT WE MAKE AVAILABLE TO YOU. IF YOU INSTALL OR USE THE SOFTWARE, YOU WILL BE BOUND BY THIS LICENSE AGREEMENT. UNLESS OTHERWISE DEFINED IN THIS LICENSE AGREEMENT, CAPTIALIZED TERMS WILL HAVE THE SAME MEANING AS SET FORTH IN THE AWS CUSTOMER AGREEMENT POSTED AT AWS.AMAZON.COM/AGREEMENT (THE “AWS AGREEMENT”). -1. Use of the Software -We hereby grant you a personal, limited, nonexclusive, non-transferable, non-sublicenseable license to (a) install the Software on computer equipment owned or controlled by you and (b) use the Software solely (i) for your internal business purposes and (ii) in connection with the Services. You may not use the Software if you do not have an account in good standing with AWS. Some components of the Software (whether developed by AWS or third parties) may also be governed by applicable open source software licenses located in the software component's source code. Your license rights with respect to these individual components are defined by the applicable open source software license, and nothing in this Agreement will restrict, limit, or otherwise affect any rights or obligations you may have, or conditions to which you may be subject, under such open source software licenses. -2. Limitations -You may not, and you will not encourage, assist or authorize any other person to, (a) incorporate any portion of it into your own programs or compile any portion of it in combination with your own programs; or (b) sell, rent, lease, lend, loan, distribute, act as a service bureau, publicly communicate, transform, or sub-license the Software or otherwise assign any rights to the Software in whole or in part; (c) modify, alter, tamper with, repair, or otherwise create derivative works of the Software, or (d) reverse engineer, disassemble, or decompile the Software or apply any other process or procedure to derive the source code of any software included in the Software. All rights granted to you are conditioned on your continued compliance this License Agreement, and will immediately and automatically terminate if you do not comply with any term or condition of this License Agreement or the AWS Customer Agreement, including any failure to remit timely payment for the Software or the Service. -3. Reservation of Rights -You may not use the Software for any illegal purpose. The Software is the intellectual property of AWS or its licensors. The structure, organization, and code of the Software are valuable trade secrets and confidential information of AWS. The Software is protected by law, including without limitation copyright laws and international treaty provisions. Except for the rights explicitly granted to you in this License Agreement, all right, title and interest in the Software are reserved and retained by us and our licensors. You do not acquire any intellectual property or other rights in the Software as a result of downloading the Software. -4. Updates -In order to keep the Software up-to-date, we may offer automatic or manual updates at any time. If we elect to provide maintenance or support of any kind, we may terminate that maintenance or support at any time without notice to you. -5. Termination -You may terminate this License Agreement at any time by uninstalling or destroying all copies of the Software that are in your possession or control. Your rights under this License Agreement will automatically terminate without notice from us if you fail to comply with any of its terms or fail to make timely payment. In the case of termination, you must cease all use and destroy all copies of the Software. We may modify, suspend, discontinue, or terminate your right to use part or all of the Software at any time without notice to you, and in that event we may modify the Software to make it inoperable. AWS will not be liable to you should it exercise those rights. Our failure to insist upon or enforce your strict compliance with this License Agreement will not constitute a waiver of any of our rights. -6. Disclaimer of Warranties and Limitation of Liability -a. YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT INSTALLATION AND USE OF, AND ANY OTHER ACCESS TO, THE APPLICATION IS AT YOUR SOLE RISK. THE APPLICATION IS DELIVERED TO YOU “AS IS” WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, AND AWS, ITS LICENSORS AND DISTRIBUTORS, AND EACH OF THEIR RESPECTIVE AFFILIATES AND SUPPLIERS (COLLECTIVELY, THE “RELEASED PARTIES”) DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, QUIET ENJOYMENT, AND NON-INFRINGEMENT. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY A RELEASED PARTY OR AN AUTHORIZED REPRESENTATIVE OF A RELEASED PARTY WILL CREATE A WARRANTY. THE LAWS OF CERTAIN JURISDICTIONS DO NOT ALLOW THE DISCLAIMER OF IMPLIED WARRANTIES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE DISCLAIMERS, EXCLUSIONS, OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL RIGHTS. -b. TO THE EXTENT NOT PROHIBITED BY LAW, NO RELEASED PARTY WILL BE LIABLE TO YOU FOR ANY INCIDENTAL OR CONSEQUENTIAL DAMAGES FOR BREACH OF ANY EXPRESS OR IMPLIED WARRANTY, BREACH OF CONTRACT, NEGLIGENCE, STRICT LIABILITY, OR ANY OTHER LEGAL THEORY RELATED TO THE APPLICATION, INCLUDING WITHOUT LIMITATION ANY DAMAGES ARISING OUT OF LOSS OF PROFITS, REVENUE, DATA, OR USE OF THE APPLICATION, EVEN IF A RELEASED PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN ANY CASE, ANY RELEASED PARTY’S AGGREGATE LIABILITY UNDER THE AGREEMENT WILL BE LIMITED TO $50.00. THE LAWS OF CERTAIN JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE EXCLUSIONS OR LIMITATIONS MAY NOT APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL RIGHTS. -7. Indemnification -You are liable for and will defend, indemnify, and hold harmless the Released Parties and their officers, directors, agents, and employees, from and against any liability, loss, damage, cost, or expense (including reasonable attorneys’ fees) arising out of your use of the Software, violation of the Agreement, violation of applicable law, or violation of any right of any person or entity, including without limitation intellectual property rights. -8. Export Regulations -You will comply with all export and re-export restrictions and regulations of the United States Department of Commerce and other United States and foreign agencies and authorities that may apply to the Software, and not to transfer, or encourage, assist, or authorize the transfer of the Software to a prohibited country or otherwise in violation of any applicable restrictions or regulations. -9. U.S. Government End Users -The Software is provided to the U.S. Government as “commercial items,” “commercial computer software,” “commercial computer software documentation,” and “technical data” with the same rights and restrictions generally applicable to the Software. If you are using the Software on behalf of the U.S. Government and these terms fail to meet the U.S. Government’s needs or are inconsistent in any respect with federal law, you will immediately discontinue your use of the Software. The terms “commercial item” “commercial computer software,” “commercial computer software documentation,” and “technical data” are defined in the Federal Acquisition Regulation and the Defense Federal Acquisition Regulation Supplement. -10. Amendment -We may amend this License Agreement at our sole discretion by posting the revised terms on the AWS website (aws.amazon.com) or within the Software. Your continued use of the Software after any amendment's effective date evidences your agreement to be bound by it. -11. Conflicts -The terms of this License Agreement govern the Software and any updates or upgrades to the Software that we may provide that replace or supplement the original Software, unless the update or upgrade is accompanied by a separate license, in which case the terms of that license will govern. - -NOTE -Other license terms may apply to certain, identified software files contained within or distributed with the accompanying software if such terms are included in the directory third_party_licenses/. Such other license terms will then apply in lieu of the terms of the software license above. diff --git a/spec/DynamoDBLocal-2015-01-27/README.txt b/spec/DynamoDBLocal-2015-01-27/README.txt deleted file mode 100644 index 029b2d9c..00000000 --- a/spec/DynamoDBLocal-2015-01-27/README.txt +++ /dev/null @@ -1,26 +0,0 @@ -README -======== - -For an overview of DynamoDB Local please refer to the documentation at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html - - -Enhancements in this release ------------------------------ - -http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/Welcome.html - -* Add support for online indexing - -Note the following difference in DynamoDBLocal: - -* Local's exception messages may differ from those returned by the service. - - - -Running DynamoDB Local (There are two new command line options available for running DynamoDB Local) ---------------------------------------------------------------- - -java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar [options] - -For more information on available options, run with the -help option: - java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -help diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/THIRD_PARTY_JAVASCRIPT_NOTICES.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/THIRD_PARTY_JAVASCRIPT_NOTICES.txt deleted file mode 100644 index f85d936e..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/THIRD_PARTY_JAVASCRIPT_NOTICES.txt +++ /dev/null @@ -1,24 +0,0 @@ -REPL.IT, jq-console, page.js, jquery, jsrepl, and bootstrap - -DynamoDB Local includes REPL.IT, Copyright (c) 2014 REPL.IT; jq-console, Copyright (c) 2014 jq-console; page.js, Copyright (c) 2012 TJ Holowaychuk ; jquery, Copyright 2014 jQuery Foundation and other contributors, http://jquery.com/; jsrepl, Copyright (c) 2014 jsrepl; and bootstrap, Copyright (c) 2014 Twitter, each of which is subject to the terms and conditions of the MIT license that states as follows: - -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: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -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. - - -ace - -DynamoDB Local includes ace, Copyright (c) 2010, Ajax.org B.V., which is subject to the terms and conditions of the BSD license that states as follows: - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - * Neither the name of Ajax.org B.V. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/antlr-runtime-4.1_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/antlr-runtime-4.1_LICENSE.txt deleted file mode 100644 index 5ddbbcfc..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/antlr-runtime-4.1_LICENSE.txt +++ /dev/null @@ -1,26 +0,0 @@ -[The "BSD license"] -Copyright (c) 2014 Terence Parr, Sam Harwell -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - 3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-cli-1.2_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-cli-1.2_LICENSE.txt deleted file mode 100644 index 57bc88a1..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-cli-1.2_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-cli-1.2_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-cli-1.2_NOTICE.txt deleted file mode 100644 index 72eb32a9..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-cli-1.2_NOTICE.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apache Commons CLI -Copyright 2001-2009 The Apache Software Foundation - -This product includes software developed by -The Apache Software Foundation (http://www.apache.org/). diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-lang-3.3.x_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-lang-3.3.x_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-lang-3.3.x_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-lang-3.3.x_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-lang-3.3.x_NOTICE.txt deleted file mode 100644 index 2f0ca384..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/commons-lang-3.3.x_NOTICE.txt +++ /dev/null @@ -1,8 +0,0 @@ -Apache Commons Lang -Copyright 2001-2011 The Apache Software Foundation - -This product includes software developed by -The Apache Software Foundation (http://www.apache.org/). - -This product includes software from the Spring Framework, -under the Apache License 2.0 (see: StringUtils.containsWhitespace()) diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-annotations-2.3.2_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-annotations-2.3.2_NOTICE.txt deleted file mode 100644 index 0cae638a..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-annotations-2.3.2_NOTICE.txt +++ /dev/null @@ -1,7 +0,0 @@ -This product currently only contains code developed by authors -of specific components, as identified by the source code files; -if such notes are missing files have been created by -Tatu Saloranta. - -For additional credits (generally to people who reported problems) -see CREDITS file. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-core-2.3.2_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-core-2.3.2_NOTICE.txt deleted file mode 100644 index 0cae638a..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-core-2.3.2_NOTICE.txt +++ /dev/null @@ -1,7 +0,0 @@ -This product currently only contains code developed by authors -of specific components, as identified by the source code files; -if such notes are missing files have been created by -Tatu Saloranta. - -For additional credits (generally to people who reported problems) -see CREDITS file. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-databind-2.3.2_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-databind-2.3.2_NOTICE.txt deleted file mode 100644 index 0cae638a..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson-databind-2.3.2_NOTICE.txt +++ /dev/null @@ -1,7 +0,0 @@ -This product currently only contains code developed by authors -of specific components, as identified by the source code files; -if such notes are missing files have been created by -Tatu Saloranta. - -For additional credits (generally to people who reported problems) -see CREDITS file. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jackson_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jetty_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jetty_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jetty_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jetty_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jetty_NOTICE.txt deleted file mode 100644 index a04070ec..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/jetty_NOTICE.txt +++ /dev/null @@ -1,30 +0,0 @@ -============================================================== - Jetty Web Container - Copyright 1995-2012 Mort Bay Consulting Pty Ltd. -============================================================== - -The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd -unless otherwise noted. - -Jetty is dual licensed under both - - * The Apache 2.0 License - http://www.apache.org/licenses/LICENSE-2.0.html - - and - - * The Eclipse Public 1.0 License - http://www.eclipse.org/legal/epl-v10.html - -Jetty may be distributed under either license. - -The javax.servlet package used was sourced from the Apache -Software Foundation and is distributed under the apache 2.0 -license. - -The UnixCrypt.java code implements the one way cryptography used by -Unix systems for simple password protection. Copyright 1996 Aki Yoshida, -modified April 2001 by Iris Van den Broeke, Daniel Deville. -Permission to use, copy, modify and distribute UnixCrypt -for non-commercial or commercial purposes and without fee is -granted provided that the copyright notice appears in all copies. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/joda-time-2.3_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/joda-time-2.3_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/joda-time-2.3_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/joda-time-2.3_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/joda-time-2.3_NOTICE.txt deleted file mode 100644 index dffbcf31..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/joda-time-2.3_NOTICE.txt +++ /dev/null @@ -1,5 +0,0 @@ -============================================================================= -= NOTICE file corresponding to section 4d of the Apache License Version 2.0 = -============================================================================= -This product includes software developed by -Joda.org (http://www.joda.org/). diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/log4j-1.2.17_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/log4j-1.2.17_LICENSE.txt deleted file mode 100644 index 6279e520..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/log4j-1.2.17_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 1999-2005 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/log4j-1.2.17_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/log4j-1.2.17_NOTICE.txt deleted file mode 100644 index 03757323..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/log4j-1.2.17_NOTICE.txt +++ /dev/null @@ -1,5 +0,0 @@ -Apache log4j -Copyright 2007 The Apache Software Foundation - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). \ No newline at end of file diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/servlet-api-3.0_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/servlet-api-3.0_LICENSE.txt deleted file mode 100644 index 926f436f..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/servlet-api-3.0_LICENSE.txt +++ /dev/null @@ -1,552 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - -APACHE TOMCAT SUBCOMPONENTS: - -Apache Tomcat includes a number of subcomponents with separate copyright notices -and license terms. Your use of these subcomponents is subject to the terms and -conditions of the following licenses. - - -For the following XML Schemas for Java EE Deployment Descriptors: - - javaee_5.xsd - - javaee_web_services_1_2.xsd - - javaee_web_services_client_1_2.xsd - - javaee_6.xsd - - javaee_web_services_1_3.xsd - - javaee_web_services_client_1_3.xsd - - web-app_3_0.xsd - - web-common_3_0.xsd - - web-fragment_3_0.xsd - -COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 - -1. Definitions. - - 1.1. Contributor. means each individual or entity that creates or contributes - to the creation of Modifications. - - 1.2. Contributor Version. means the combination of the Original Software, - prior Modifications used by a Contributor (if any), and the - Modifications made by that particular Contributor. - - 1.3. Covered Software. means (a) the Original Software, or (b) Modifications, - or (c) the combination of files containing Original Software with files - containing Modifications, in each case including portions thereof. - - 1.4. Executable. means the Covered Software in any form other than Source - Code. - - 1.5. Initial Developer. means the individual or entity that first makes - Original Software available under this License. - - 1.6. Larger Work. means a work which combines Covered Software or portions - thereof with code not governed by the terms of this License. - - 1.7. License. means this document. - - 1.8. Licensable. means having the right to grant, to the maximum extent - possible, whether at the time of the initial grant or subsequently - acquired, any and all of the rights conveyed herein. - - 1.9. Modifications. means the Source Code and Executable form of any of the - following: - - A. Any file that results from an addition to, deletion from or - modification of the contents of a file containing Original Software - or previous Modifications; - - B. Any new file that contains any part of the Original Software or - previous Modification; or - - C. Any new file that is contributed or otherwise made available under - the terms of this License. - - 1.10. Original Software. means the Source Code and Executable form of - computer software code that is originally released under this License. - - 1.11. Patent Claims. means any patent claim(s), now owned or hereafter - acquired, including without limitation, method, process, and apparatus - claims, in any patent Licensable by grantor. - - 1.12. Source Code. means (a) the common form of computer software code in - which modifications are made and (b) associated documentation included - in or with such code. - - 1.13. You. (or .Your.) means an individual or a legal entity exercising - rights under, and complying with all of the terms of, this License. For - legal entities, .You. includes any entity which controls, is controlled - by, or is under common control with You. For purposes of this - definition, .control. means (a) the power, direct or indirect, to cause - the direction or management of such entity, whether by contract or - otherwise, or (b) ownership of more than fifty percent (50%) of the - outstanding shares or beneficial ownership of such entity. - -2. License Grants. - - 2.1. The Initial Developer Grant. - - Conditioned upon Your compliance with Section 3.1 below and subject to - third party intellectual property claims, the Initial Developer hereby - grants You a world-wide, royalty-free, non-exclusive license: - - (a) under intellectual property rights (other than patent or trademark) - Licensable by Initial Developer, to use, reproduce, modify, display, - perform, sublicense and distribute the Original Software (or - portions thereof), with or without Modifications, and/or as part of - a Larger Work; and - - (b) under Patent Claims infringed by the making, using or selling of - Original Software, to make, have made, use, practice, sell, and - offer for sale, and/or otherwise dispose of the Original Software - (or portions thereof). - - (c) The licenses granted in Sections 2.1(a) and (b) are effective on the - date Initial Developer first distributes or otherwise makes the - Original Software available to a third party under the terms of this - License. - - (d) Notwithstanding Section 2.1(b) above, no patent license is granted: - (1) for code that You delete from the Original Software, or (2) for - infringements caused by: (i) the modification of the Original - Software, or (ii) the combination of the Original Software with - other software or devices. - - 2.2. Contributor Grant. - - Conditioned upon Your compliance with Section 3.1 below and subject to third - party intellectual property claims, each Contributor hereby grants You a - world-wide, royalty-free, non-exclusive license: - - (a) under intellectual property rights (other than patent or trademark) - Licensable by Contributor to use, reproduce, modify, display, - perform, sublicense and distribute the Modifications created by such - Contributor (or portions thereof), either on an unmodified basis, - with other Modifications, as Covered Software and/or as part of a - Larger Work; and - - (b) under Patent Claims infringed by the making, using, or selling of - Modifications made by that Contributor either alone and/or in - combination with its Contributor Version (or portions of such - combination), to make, use, sell, offer for sale, have made, and/or - otherwise dispose of: (1) Modifications made by that Contributor (or - portions thereof); and (2) the combination of Modifications made by - that Contributor with its Contributor Version (or portions of such - combination). - - (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on - the date Contributor first distributes or otherwise makes the - Modifications available to a third party. - - (d) Notwithstanding Section 2.2(b) above, no patent license is granted: - (1) for any code that Contributor has deleted from the Contributor - Version; (2) for infringements caused by: (i) third party - modifications of Contributor Version, or (ii) the combination of - Modifications made by that Contributor with other software (except - as part of the Contributor Version) or other devices; or (3) under - Patent Claims infringed by Covered Software in the absence of - Modifications made by that Contributor. - -3. Distribution Obligations. - - 3.1. Availability of Source Code. - Any Covered Software that You distribute or otherwise make available in - Executable form must also be made available in Source Code form and that - Source Code form must be distributed only under the terms of this License. - You must include a copy of this License with every copy of the Source Code - form of the Covered Software You distribute or otherwise make available. - You must inform recipients of any such Covered Software in Executable form - as to how they can obtain such Covered Software in Source Code form in a - reasonable manner on or through a medium customarily used for software - exchange. - - 3.2. Modifications. - The Modifications that You create or to which You contribute are governed - by the terms of this License. You represent that You believe Your - Modifications are Your original creation(s) and/or You have sufficient - rights to grant the rights conveyed by this License. - - 3.3. Required Notices. - You must include a notice in each of Your Modifications that identifies - You as the Contributor of the Modification. You may not remove or alter - any copyright, patent or trademark notices contained within the Covered - Software, or any notices of licensing or any descriptive text giving - attribution to any Contributor or the Initial Developer. - - 3.4. Application of Additional Terms. - You may not offer or impose any terms on any Covered Software in Source - Code form that alters or restricts the applicable version of this License - or the recipients. rights hereunder. You may choose to offer, and to - charge a fee for, warranty, support, indemnity or liability obligations to - one or more recipients of Covered Software. However, you may do so only on - Your own behalf, and not on behalf of the Initial Developer or any - Contributor. You must make it absolutely clear that any such warranty, - support, indemnity or liability obligation is offered by You alone, and - You hereby agree to indemnify the Initial Developer and every Contributor - for any liability incurred by the Initial Developer or such Contributor as - a result of warranty, support, indemnity or liability terms You offer. - - 3.5. Distribution of Executable Versions. - You may distribute the Executable form of the Covered Software under the - terms of this License or under the terms of a license of Your choice, - which may contain terms different from this License, provided that You are - in compliance with the terms of this License and that the license for the - Executable form does not attempt to limit or alter the recipient.s rights - in the Source Code form from the rights set forth in this License. If You - distribute the Covered Software in Executable form under a different - license, You must make it absolutely clear that any terms which differ - from this License are offered by You alone, not by the Initial Developer - or Contributor. You hereby agree to indemnify the Initial Developer and - every Contributor for any liability incurred by the Initial Developer or - such Contributor as a result of any such terms You offer. - - 3.6. Larger Works. - You may create a Larger Work by combining Covered Software with other code - not governed by the terms of this License and distribute the Larger Work - as a single product. In such a case, You must make sure the requirements - of this License are fulfilled for the Covered Software. - -4. Versions of the License. - - 4.1. New Versions. - Sun Microsystems, Inc. is the initial license steward and may publish - revised and/or new versions of this License from time to time. Each - version will be given a distinguishing version number. Except as provided - in Section 4.3, no one other than the license steward has the right to - modify this License. - - 4.2. Effect of New Versions. - You may always continue to use, distribute or otherwise make the Covered - Software available under the terms of the version of the License under - which You originally received the Covered Software. If the Initial - Developer includes a notice in the Original Software prohibiting it from - being distributed or otherwise made available under any subsequent version - of the License, You must distribute and make the Covered Software - available under the terms of the version of the License under which You - originally received the Covered Software. Otherwise, You may also choose - to use, distribute or otherwise make the Covered Software available under - the terms of any subsequent version of the License published by the - license steward. - - 4.3. Modified Versions. - When You are an Initial Developer and You want to create a new license for - Your Original Software, You may create and use a modified version of this - License if You: (a) rename the license and remove any references to the - name of the license steward (except to note that the license differs from - this License); and (b) otherwise make it clear that the license contains - terms which differ from this License. - -5. DISCLAIMER OF WARRANTY. - - COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN .AS IS. BASIS, WITHOUT - WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT - LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, - MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK - AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD - ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL - DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY - SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN - ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED - HEREUNDER EXCEPT UNDER THIS DISCLAIMER. - -6. TERMINATION. - - 6.1. This License and the rights granted hereunder will terminate - automatically if You fail to comply with terms herein and fail to - cure such breach within 30 days of becoming aware of the breach. - Provisions which, by their nature, must remain in effect beyond the - termination of this License shall survive. - - 6.2. If You assert a patent infringement claim (excluding declaratory - judgment actions) against Initial Developer or a Contributor (the - Initial Developer or Contributor against whom You assert such claim - is referred to as .Participant.) alleging that the Participant - Software (meaning the Contributor Version where the Participant is a - Contributor or the Original Software where the Participant is the - Initial Developer) directly or indirectly infringes any patent, then - any and all rights granted directly or indirectly to You by such - Participant, the Initial Developer (if the Initial Developer is not - the Participant) and all Contributors under Sections 2.1 and/or 2.2 - of this License shall, upon 60 days notice from Participant terminate - prospectively and automatically at the expiration of such 60 day - notice period, unless if within such 60 day period You withdraw Your - claim with respect to the Participant Software against such - Participant either unilaterally or pursuant to a written agreement - with Participant. - - 6.3. In the event of termination under Sections 6.1 or 6.2 above, all end - user licenses that have been validly granted by You or any - distributor hereunder prior to termination (excluding licenses - granted to You by any distributor) shall survive termination. - -7. LIMITATION OF LIABILITY. - - UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING - NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY - OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF - ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, - INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT - LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK STOPPAGE, - COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR - LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF - SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR - DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY.S NEGLIGENCE TO THE EXTENT - APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE - EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS - EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. - -8. U.S. GOVERNMENT END USERS. - - The Covered Software is a .commercial item,. as that term is defined in 48 - C.F.R. 2.101 (Oct. 1995), consisting of .commercial computer software. (as - that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and commercial - computer software documentation. as such terms are used in 48 C.F.R. 12.212 - (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 - through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered - Software with only those rights set forth herein. This U.S. Government Rights - clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or - provision that addresses Government rights in computer software under this - License. - -9. MISCELLANEOUS. - - This License represents the complete agreement concerning subject matter - hereof. If any provision of this License is held to be unenforceable, such - provision shall be reformed only to the extent necessary to make it - enforceable. This License shall be governed by the law of the jurisdiction - specified in a notice contained within the Original Software (except to the - extent applicable law, if any, provides otherwise), excluding such - jurisdiction's conflict-of-law provisions. Any litigation relating to this - License shall be subject to the jurisdiction of the courts located in the - jurisdiction and venue specified in a notice contained within the Original - Software, with the losing party responsible for costs, including, without - limitation, court costs and reasonable attorneys. fees and expenses. The - application of the United Nations Convention on Contracts for the - International Sale of Goods is expressly excluded. Any law or regulation - which provides that the language of a contract shall be construed against - the drafter shall not apply to this License. You agree that You alone are - responsible for compliance with the United States export administration - regulations (and the export control laws and regulation of any other - countries) when You use, distribute or otherwise make available any Covered - Software. - -10. RESPONSIBILITY FOR CLAIMS. - - As between Initial Developer and the Contributors, each party is responsible - for claims and damages arising, directly or indirectly, out of its - utilization of rights under this License and You agree to work with Initial - Developer and Contributors to distribute such responsibility on an equitable - basis. Nothing herein is intended or shall be deemed to constitute any - admission of liability. - - NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION - LICENSE (CDDL) - - The code released under the CDDL shall be governed by the laws of the State - of California (excluding conflict-of-law provisions). Any litigation relating - to this License shall be subject to the jurisdiction of the Federal Courts of - the Northern District of California and the state courts of the State of - California, with venue lying in Santa Clara County, California. - diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/servlet-api-3.0_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/servlet-api-3.0_NOTICE.txt deleted file mode 100644 index 7eab0f63..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/servlet-api-3.0_NOTICE.txt +++ /dev/null @@ -1,17 +0,0 @@ -Apache Tomcat -Copyright 1999-2010 The Apache Software Foundation - -This product includes software developed by -The Apache Software Foundation (http://www.apache.org/). - -The original XML Schemas for Java EE Deployment Descriptors: - - javaee_5.xsd - - javaee_web_services_1_2.xsd - - javaee_web_services_client_1_2.xsd - - javaee_6.xsd - - javaee_web_services_1_3.xsd - - javaee_web_services_client_1_3.xsd - - web-app_3_0.xsd - - web-common_3_0.xsd - - web-fragment_3_0.xsd -may be obtained from http://java.sun.com/xml/ns/javaee/ diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-core_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-core_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-core_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-core_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-core_NOTICE.txt deleted file mode 100644 index a04070ec..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-core_NOTICE.txt +++ /dev/null @@ -1,30 +0,0 @@ -============================================================== - Jetty Web Container - Copyright 1995-2012 Mort Bay Consulting Pty Ltd. -============================================================== - -The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd -unless otherwise noted. - -Jetty is dual licensed under both - - * The Apache 2.0 License - http://www.apache.org/licenses/LICENSE-2.0.html - - and - - * The Eclipse Public 1.0 License - http://www.eclipse.org/legal/epl-v10.html - -Jetty may be distributed under either license. - -The javax.servlet package used was sourced from the Apache -Software Foundation and is distributed under the apache 2.0 -license. - -The UnixCrypt.java code implements the one way cryptography used by -Unix systems for simple password protection. Copyright 1996 Aki Yoshida, -modified April 2001 by Iris Van den Broeke, Daniel Deville. -Permission to use, copy, modify and distribute UnixCrypt -for non-commercial or commercial purposes and without fee is -granted provided that the copyright notice appears in all copies. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-jetty_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-jetty_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-jetty_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-jetty_NOTICE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-jetty_NOTICE.txt deleted file mode 100644 index a04070ec..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/spdy-jetty_NOTICE.txt +++ /dev/null @@ -1,30 +0,0 @@ -============================================================== - Jetty Web Container - Copyright 1995-2012 Mort Bay Consulting Pty Ltd. -============================================================== - -The Jetty Web Container is Copyright Mort Bay Consulting Pty Ltd -unless otherwise noted. - -Jetty is dual licensed under both - - * The Apache 2.0 License - http://www.apache.org/licenses/LICENSE-2.0.html - - and - - * The Eclipse Public 1.0 License - http://www.eclipse.org/legal/epl-v10.html - -Jetty may be distributed under either license. - -The javax.servlet package used was sourced from the Apache -Software Foundation and is distributed under the apache 2.0 -license. - -The UnixCrypt.java code implements the one way cryptography used by -Unix systems for simple password protection. Copyright 1996 Aki Yoshida, -modified April 2001 by Iris Van den Broeke, Daniel Deville. -Permission to use, copy, modify and distribute UnixCrypt -for non-commercial or commercial purposes and without fee is -granted provided that the copyright notice appears in all copies. diff --git a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/sqlite4java_LICENSE.txt b/spec/DynamoDBLocal-2015-01-27/third_party_licenses/sqlite4java_LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/spec/DynamoDBLocal-2015-01-27/third_party_licenses/sqlite4java_LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/spec/app/models/address.rb b/spec/app/models/address.rb index a7965a8f..88f1b0fd 100644 --- a/spec/app/models/address.rb +++ b/spec/app/models/address.rb @@ -6,10 +6,12 @@ class Address field :deliverable, :boolean field :latitude, :number field :info, :hash + field :config, :raw + field :registered_on, :date - field :lock_version, :integer #Provides Optimistic Locking + field :lock_version, :integer # Provides Optimistic Locking def zip_code=(zip_code) - self.city = "Chicago" + self.city = 'Chicago' end end diff --git a/spec/app/models/bar.rb b/spec/app/models/bar.rb new file mode 100644 index 00000000..4e5ef319 --- /dev/null +++ b/spec/app/models/bar.rb @@ -0,0 +1,16 @@ +class Bar + include Dynamoid::Document + + table name: :bar, + key: :bar_id, + range_key: :visited_at, + read_capacity: 200, + write_capacity: 200 + + field :name + field :visited_at, :integer + + validates_presence_of :name, :visited_at + + global_secondary_index hash_key: :name, range_key: :visited_at +end diff --git a/spec/app/models/cadillac.rb b/spec/app/models/cadillac.rb new file mode 100644 index 00000000..4d9fa22d --- /dev/null +++ b/spec/app/models/cadillac.rb @@ -0,0 +1,5 @@ +require_relative 'car' + +class Cadillac < Car +end + diff --git a/spec/app/models/camel_case.rb b/spec/app/models/camel_case.rb index 45e6f21f..c252558d 100644 --- a/spec/app/models/camel_case.rb +++ b/spec/app/models/camel_case.rb @@ -7,18 +7,18 @@ class CamelCase has_many :users has_one :sponsor has_and_belongs_to_many :subscriptions - + before_create :doing_before_create after_create :doing_after_create before_update :doing_before_update after_update :doing_after_update - + private - + def doing_before_create true end - + def doing_after_create true end diff --git a/spec/app/models/car.rb b/spec/app/models/car.rb index b2f54281..3ef18362 100644 --- a/spec/app/models/car.rb +++ b/spec/app/models/car.rb @@ -1,6 +1,6 @@ require_relative 'vehicle' class Car < Vehicle - + field :power_locks, :boolean end \ No newline at end of file diff --git a/spec/app/models/magazine.rb b/spec/app/models/magazine.rb index 7590b77c..5f75de0d 100644 --- a/spec/app/models/magazine.rb +++ b/spec/app/models/magazine.rb @@ -1,11 +1,13 @@ class Magazine include Dynamoid::Document - + table key: :title + field :title - + field :size, :number + has_many :subscriptions has_many :camel_cases has_one :sponsor - belongs_to :owner, :class_name => 'User', :inverse_of => :books + belongs_to :owner, class_name: 'User', inverse_of: :books end diff --git a/spec/app/models/nuclear_submarine.rb b/spec/app/models/nuclear_submarine.rb index 6a7dbbf8..0d511b4a 100644 --- a/spec/app/models/nuclear_submarine.rb +++ b/spec/app/models/nuclear_submarine.rb @@ -1,5 +1,5 @@ require_relative 'vehicle' class NuclearSubmarine < Vehicle - + field :torpedoes, :integer end \ No newline at end of file diff --git a/spec/app/models/post.rb b/spec/app/models/post.rb index d3ba3a0b..c9485493 100644 --- a/spec/app/models/post.rb +++ b/spec/app/models/post.rb @@ -9,7 +9,7 @@ class Post field :length field :name - local_secondary_index :range_key => :name - global_secondary_index :hash_key => :name, :range_key => :posted_at - global_secondary_index :hash_key => :length + local_secondary_index range_key: :name + global_secondary_index hash_key: :name, range_key: :posted_at + global_secondary_index hash_key: :length end diff --git a/spec/app/models/sponsor.rb b/spec/app/models/sponsor.rb index 37738c18..6d71e2fc 100644 --- a/spec/app/models/sponsor.rb +++ b/spec/app/models/sponsor.rb @@ -1,6 +1,6 @@ class Sponsor include Dynamoid::Document - + belongs_to :magazine has_many :subscriptions diff --git a/spec/app/models/subscription.rb b/spec/app/models/subscription.rb index 30ed7088..9542176c 100644 --- a/spec/app/models/subscription.rb +++ b/spec/app/models/subscription.rb @@ -6,7 +6,7 @@ class Subscription belongs_to :magazine has_and_belongs_to_many :users - belongs_to :customer, :class_name => 'User', :inverse_of => :monthly + belongs_to :customer, class_name: 'User', inverse_of: :monthly has_and_belongs_to_many :camel_cases end diff --git a/spec/app/models/user.rb b/spec/app/models/user.rb index db073259..ed4ebf5b 100644 --- a/spec/app/models/user.rb +++ b/spec/app/models/user.rb @@ -4,6 +4,7 @@ class User field :name field :email field :password + field :admin, :boolean field :last_logged_in_at, :datetime field :favorite_colors, :serialized @@ -11,11 +12,11 @@ class User has_and_belongs_to_many :subscriptions - has_many :books, :class_name => 'Magazine', :inverse_of => :owner - has_one :monthly, :class_name => 'Subscription', :inverse_of => :customer + has_many :books, class_name: 'Magazine', inverse_of: :owner + has_one :monthly, class_name: 'Subscription', inverse_of: :customer - has_and_belongs_to_many :followers, :class_name => 'User', :inverse_of => :following - has_and_belongs_to_many :following, :class_name => 'User', :inverse_of => :followers + has_and_belongs_to_many :followers, class_name: 'User', inverse_of: :following + has_and_belongs_to_many :following, class_name: 'User', inverse_of: :followers belongs_to :camel_case diff --git a/spec/app/models/vehicle.rb b/spec/app/models/vehicle.rb index dae48098..9da1f449 100644 --- a/spec/app/models/vehicle.rb +++ b/spec/app/models/vehicle.rb @@ -1,7 +1,7 @@ class Vehicle include Dynamoid::Document - + field :type - + field :description end \ No newline at end of file diff --git a/spec/dynamodb_local.rb b/spec/dynamodb_local.rb index 97ea2d70..fa457f2c 100644 --- a/spec/dynamodb_local.rb +++ b/spec/dynamodb_local.rb @@ -1,15 +1,10 @@ -class DynamoDBLocal - DIST_DIR = 'DynamoDBLocal-2015-01-27' - - def self.start! - raise 'DynamoDBLocal requires JAVA_HOME to be set' unless ENV.has_key?('JAVA_HOME') - - Dir.chdir(DIST_DIR) do - pid = Kernel.spawn("#{ENV['JAVA_HOME']}/bin/java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -inMemory -delayTransientStatuses") - STDERR.puts "Started DynamoDBLocal at pid #{pid}." +module DynamoDBLocal + def self.delete_all_specified_tables! + if !Dynamoid.adapter.tables.empty? + Dynamoid.adapter.list_tables.each do |table| + Dynamoid.adapter.delete_table(table) if table =~ /^#{Dynamoid::Config.namespace}/ + end + Dynamoid.adapter.tables.clear end end - - def self.stop! - end end diff --git a/spec/dynamoid/adapter_plugin/aws_sdk_v2_spec.rb b/spec/dynamoid/adapter_plugin/aws_sdk_v2_spec.rb index faff7525..a7111b88 100644 --- a/spec/dynamoid/adapter_plugin/aws_sdk_v2_spec.rb +++ b/spec/dynamoid/adapter_plugin/aws_sdk_v2_spec.rb @@ -10,9 +10,10 @@ { 1 => [:id], 2 => [:id], - 3 => [:id, {:range_key => {:range => :number}}], - 4 => [:id, {:range_key => {:range => :number}}], - 5 => [:id, {:range_key => {:range => :string}}] + 3 => [:id, {range_key: {range: :number}}], + 4 => [:id, {range_key: {range: :number}}], + 5 => [:id, { read_capacity: 10_000, write_capacity: 1000 }], + 6 => [:id, {range_key: {range: :string}}] }.each do |n, args| name = "dynamoid_tests_TestTable#{n}" let(:"test_table#{n}") do @@ -21,37 +22,261 @@ end end + # + # Test limit controls in querys and scans + # + # Since query and scans have different interface, then including this shared example + # requires some inputs. The internal aspects will configure request parameters and + # the Dynamoid adapter call correctly. + # + # @param [Symbol] request_type the name of the request, either :query or :scan + # + shared_examples 'correctly handling limits' do |request_type| + before(:each) do + @request_type = request_type + end + + def request_params + return {hash_value: '1'} if @request_type == :query + {} + end + + def dynamo_request(table_name, scan_hash = {}, select_opts = {}) + return Dynamoid.adapter.query(table_name, scan_hash.merge(select_opts)) if @request_type == :query + Dynamoid.adapter.scan(table_name, scan_hash, select_opts) + end + + context 'multiple name entities' do + before(:each) do + (1..4).each do |i| + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: i.to_f) + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Pascal', range: (i + 4).to_f) + end + end + + it 'returns correct records' do + expect(dynamo_request(test_table3, request_params, {}).count).to eq(8) + end + + it 'returns correct record limit' do + expect(dynamo_request(test_table3, request_params, record_limit: 1).count).to eq(1) + expect(dynamo_request(test_table3, request_params, record_limit: 3).count).to eq(3) + end + + it 'returns correct batch' do + # Receives 8 times for each item and 1 more for empty page + expect(Dynamoid.adapter.client).to receive(request_type).exactly(9).times.and_call_original + expect(dynamo_request(test_table3, request_params, batch_size: 1).count).to eq(8) + end + + it 'returns correct batch and paginates in batches' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(3).times.and_call_original + expect(dynamo_request(test_table3, request_params, batch_size: 3).count).to eq(8) + end + + it 'returns correct record limit and batch' do + expect(dynamo_request(test_table3, request_params, record_limit: 1, batch_size: 1).count).to eq(1) + end + + it 'returns correct record limit with filter' do + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Josh'}), record_limit: 1).count) + .to eq(1) + end + + it 'obeys correct scan limit with filter' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(1).times.and_call_original + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Josh'}), scan_limit: 2).count) + .to eq(2) + end + + it 'obeys correct scan limit over record limit with filter' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(1).times.and_call_original + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Josh'}), + scan_limit: 2, + record_limit: 10, # Won't be able to return more than 2 due to scan limit + ).count).to eq(2) + end + + it 'obeys correct scan limit with filter with some return' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(1).times.and_call_original + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Pascal'}), + scan_limit: 5 + ).count).to eq(1) + end + + it 'obeys correct scan limit and batch size with filter with some return' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(2).times.and_call_original + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Josh'}), + scan_limit: 3, + batch_size: 2, # This would force batching of size 2 for potential of 4 results! + ).count).to eq(3) + end + + it 'obeys correct scan limit with filter and batching for some return' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(5).times.and_call_original + # We should paginate through 5 responses each of size 1 (batch) and + # only scan through 5 records at most which with our given filter + # should return 1 result since first 4 are Josh and last is Pascal. + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Pascal'}), + batch_size: 1, + scan_limit: 5, + record_limit: 3 + ).count).to eq(1) + end + + it 'obeys correct record limit with filter, batching, and scan limit' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(6).times.and_call_original + # We should paginate through 6 responses each of size 1 (batch) and + # only scan through 6 records at most which with our given filter + # should return 2 results, and hit record limit before scan limit. + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Pascal'}), + batch_size: 1, + scan_limit: 10, + record_limit: 2 + ).count).to eq(2) + end + end + + # + # Tests that even with large records we are paginating to pull more data + # even if we hit response data size limits + # + context 'large records still returns as much data' do + before(:each) do + # 64 of these items will exceed the 1MB result record_limit thus query won't return all results on first loop + # We use :age since :range won't work for filtering in queries + 200.times do |i| + Dynamoid.adapter.put_item(test_table3, + id: '1', + range: i.to_f, + age: i.to_f, + data: 'A'*1024*16) + end + end + + it 'returns correct for limits and scan limit' do + expect(dynamo_request(test_table3, request_params, + scan_limit: 100 + ).count).to eq(100) + end + + it 'returns correct for scan limit with filtering' do + # Not sure why there is difference but :query will do 1 page and see 100 records and filter out 10 + # while :scan will do 2 pages and see 64 records on first page similar to the 1MB return limit + # and then look at 36 records and find 10 on the second page. + pages = request_type == :query ? 1 : 2 + expect(Dynamoid.adapter.client).to receive(request_type).exactly(pages).times.and_call_original + expect(dynamo_request(test_table3, request_params.merge(age: {gte: 90.0}), + scan_limit: 100 + ).count).to eq(10) + end + + it 'returns correct for record limit' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(2).times.and_call_original + expect(dynamo_request(test_table3, request_params.merge(age: {gte: 5.0}), + record_limit: 100 + ).count).to eq(100) + end + + it 'returns correct record limit with filtering' do + expect(dynamo_request(test_table3, request_params.merge(age: {gte: 133.0}), + record_limit: 100 + ).count).to eq(67) + end + + it 'returns correct with batching' do + # Since we hit the data size limit 3 times, so we must make 4 requests + # which is limitation of DynamoDB and therefore batch limit is + # restricted by this limitation as well! + expect(Dynamoid.adapter.client).to receive(request_type).exactly(4).times.and_call_original + expect(dynamo_request(test_table3, request_params, + batch_size: 100 + ).count).to eq(200) + end + + it 'returns correct with batching and record limit beyond data size limit' do + # Since we hit limit once, we need to make sure the second request only + # requests for as many as we have left for our record limit. + expect(Dynamoid.adapter.client).to receive(request_type).exactly(2).times.and_call_original + expect(dynamo_request(test_table3, request_params, + record_limit: 83, + batch_size: 100 + ).count).to eq(83) + end + + it 'returns correct with batching and record limit' do + expect(Dynamoid.adapter.client).to receive(request_type).exactly(11).times.and_call_original + # Since we do age >= 5.0 we lose the first 5 results so we make 11 paginated requests + expect(dynamo_request(test_table3, request_params.merge(age: {gte: 5.0}), + record_limit: 100, + batch_size: 10 + ).count).to eq(100) + end + end + + it 'correctly limits edge case of record and scan counts approaching limits' do + (1..4).each do |i| + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: i.to_f) + end + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Pascal', range: 5.0) + (6..10).each do |i| + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: i.to_f) + end + + expect(Dynamoid.adapter.client).to receive(request_type).exactly(2).times.and_call_original + # In faulty code, the record limit would adjust limit to 2 thus on second page + # we would get the 5th Josh (range value 6.0) whereas correct implementation would + # adjust limit to 1 since can only scan 1 more record therefore would see Pascal + # and not go to next valid record. + expect(dynamo_request(test_table3, request_params.merge(name: {eq: 'Josh'}), + batch_size: 4, + scan_limit: 5, # Scan limit would adjust requested limit to 1 + record_limit: 6, # Record limit would adjust requested limit to 2 + ).count).to eq(4) + end + end + # # Tests adapter against ranged tables # shared_examples 'range queries' do before do - Dynamoid.adapter.put_item(test_table3, {:id => "1", :range => 1.0}) - Dynamoid.adapter.put_item(test_table3, {:id => "1", :range => 3.0}) + Dynamoid.adapter.put_item(test_table3, id: '1', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '1', range: 3.0) end it 'performs query on a table with a range and selects items in a range' do - expect(Dynamoid.adapter.query(test_table3, :hash_value => '1', :range_between => [0.0,3.0]).to_a).to eq [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}] + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_between: [0.0, 3.0]).to_a).to eq [{id: '1', range: BigDecimal.new(1)}, {id: '1', range: BigDecimal.new(3)}] end it 'performs query on a table with a range and selects items in a range with :select option' do - expect(Dynamoid.adapter.query(test_table3, :hash_value => '1', :range_between => [0.0,3.0], :select => 'ALL_ATTRIBUTES').to_a).to eq [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}] + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_between: [0.0, 3.0], select: 'ALL_ATTRIBUTES').to_a).to eq [{id: '1', range: BigDecimal.new(1)}, {id: '1', range: BigDecimal.new(3)}] end it 'performs query on a table with a range and selects items greater than' do - expect(Dynamoid.adapter.query(test_table3, :hash_value => '1', :range_greater_than => 1.0).to_a).to eq [{:id => '1', :range => BigDecimal.new(3)}] + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_greater_than: 1.0).to_a).to eq [{id: '1', range: BigDecimal.new(3)}] end it 'performs query on a table with a range and selects items less than' do - expect(Dynamoid.adapter.query(test_table3, :hash_value => '1', :range_less_than => 2.0).to_a).to eq [{:id => '1', :range => BigDecimal.new(1)}] + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_less_than: 2.0).to_a).to eq [{id: '1', range: BigDecimal.new(1)}] end it 'performs query on a table with a range and selects items gte' do - expect(Dynamoid.adapter.query(test_table3, :hash_value => '1', :range_gte => 1.0).to_a).to eq [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}] + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_gte: 1.0).to_a).to eq [{id: '1', range: BigDecimal.new(1)}, {id: '1', range: BigDecimal.new(3)}] end it 'performs query on a table with a range and selects items lte' do - expect(Dynamoid.adapter.query(test_table3, :hash_value => '1', :range_lte => 3.0).to_a).to eq [{:id => '1', :range => BigDecimal.new(1)}, {:id => '1', :range => BigDecimal.new(3)}] + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_lte: 3.0).to_a).to eq [{id: '1', range: BigDecimal.new(1)}, {id: '1', range: BigDecimal.new(3)}] + end + + it 'performs query on a table and returns items based on returns correct limit' do + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_greater_than: 0.0, record_limit: 1).count).to eq(1) + end + + it 'performs query on a table with a range and selects all items' do + 200.times { |i| Dynamoid.adapter.put_item(test_table3, id: '1', range: i.to_f, data: 'A'*1024*16) } + # 64 of these items will exceed the 1MB result limit thus query won't return all results on first loop + expect(Dynamoid.adapter.query(test_table3, hash_value: '1', range_gte: 0.0).count).to eq(200) end end @@ -60,40 +285,39 @@ # shared_examples 'correct ordering' do before(:each) do - Dynamoid.adapter.put_item(test_table4, {:id => "1", :order => 1, :range => 1.0}) - Dynamoid.adapter.put_item(test_table4, {:id => "1", :order => 2, :range => 2.0}) - Dynamoid.adapter.put_item(test_table4, {:id => "1", :order => 3, :range => 3.0}) - Dynamoid.adapter.put_item(test_table4, {:id => "1", :order => 4, :range => 4.0}) - Dynamoid.adapter.put_item(test_table4, {:id => "1", :order => 5, :range => 5.0}) - Dynamoid.adapter.put_item(test_table4, {:id => "1", :order => 6, :range => 6.0}) + Dynamoid.adapter.put_item(test_table4, id: '1', order: 1, range: 1.0) + Dynamoid.adapter.put_item(test_table4, id: '1', order: 2, range: 2.0) + Dynamoid.adapter.put_item(test_table4, id: '1', order: 3, range: 3.0) + Dynamoid.adapter.put_item(test_table4, id: '1', order: 4, range: 4.0) + Dynamoid.adapter.put_item(test_table4, id: '1', order: 5, range: 5.0) + Dynamoid.adapter.put_item(test_table4, id: '1', order: 6, range: 6.0) end it 'performs query on a table with a range and selects items less than that is in the correct order, scan_index_forward true' do - query = Dynamoid.adapter.query(test_table4, :hash_value => '1', :range_greater_than => 0, :scan_index_forward => true).to_a - expect(query[0]).to eq({:id => '1', :order => 1, :range => BigDecimal.new(1)}) - expect(query[1]).to eq({:id => '1', :order => 2, :range => BigDecimal.new(2)}) - expect(query[2]).to eq({:id => '1', :order => 3, :range => BigDecimal.new(3)}) - expect(query[3]).to eq({:id => '1', :order => 4, :range => BigDecimal.new(4)}) - expect(query[4]).to eq({:id => '1', :order => 5, :range => BigDecimal.new(5)}) - expect(query[5]).to eq({:id => '1', :order => 6, :range => BigDecimal.new(6)}) + query = Dynamoid.adapter.query(test_table4, hash_value: '1', range_greater_than: 0, scan_index_forward: true).to_a + expect(query[0]).to eq(id: '1', order: 1, range: BigDecimal.new(1)) + expect(query[1]).to eq(id: '1', order: 2, range: BigDecimal.new(2)) + expect(query[2]).to eq(id: '1', order: 3, range: BigDecimal.new(3)) + expect(query[3]).to eq(id: '1', order: 4, range: BigDecimal.new(4)) + expect(query[4]).to eq(id: '1', order: 5, range: BigDecimal.new(5)) + expect(query[5]).to eq(id: '1', order: 6, range: BigDecimal.new(6)) end it 'performs query on a table with a range and selects items less than that is in the correct order, scan_index_forward false' do - query = Dynamoid.adapter.query(test_table4, :hash_value => '1', :range_greater_than => 0, :scan_index_forward => false).to_a - expect(query[5]).to eq({:id => '1', :order => 1, :range => BigDecimal.new(1)}) - expect(query[4]).to eq({:id => '1', :order => 2, :range => BigDecimal.new(2)}) - expect(query[3]).to eq({:id => '1', :order => 3, :range => BigDecimal.new(3)}) - expect(query[2]).to eq({:id => '1', :order => 4, :range => BigDecimal.new(4)}) - expect(query[1]).to eq({:id => '1', :order => 5, :range => BigDecimal.new(5)}) - expect(query[0]).to eq({:id => '1', :order => 6, :range => BigDecimal.new(6)}) + query = Dynamoid.adapter.query(test_table4, hash_value: '1', range_greater_than: 0, scan_index_forward: false).to_a + expect(query[5]).to eq(id: '1', order: 1, range: BigDecimal.new(1)) + expect(query[4]).to eq(id: '1', order: 2, range: BigDecimal.new(2)) + expect(query[3]).to eq(id: '1', order: 3, range: BigDecimal.new(3)) + expect(query[2]).to eq(id: '1', order: 4, range: BigDecimal.new(4)) + expect(query[1]).to eq(id: '1', order: 5, range: BigDecimal.new(5)) + expect(query[0]).to eq(id: '1', order: 6, range: BigDecimal.new(6)) end end - context 'without a preexisting table' do # CreateTable and DeleteTable it 'performs CreateTable and DeleteTable' do - table = Dynamoid.adapter.create_table('CreateTable', :id, :range_key => { :created_at => :number }) + table = Dynamoid.adapter.create_table('CreateTable', :id, range_key: { created_at: :number }) expect(Dynamoid.adapter.list_tables).to include 'CreateTable' @@ -104,7 +328,7 @@ let(:doc_class) do Class.new do include Dynamoid::Document - range :range => :number + range range: :number field :range2 field :hash2 end @@ -112,72 +336,73 @@ it 'creates table with local_secondary_index' do # setup - doc_class.table({:name => 'table_lsi', :key => :id}) + doc_class.table(name: 'table_lsi', key: :id) doc_class.local_secondary_index ({ - :range_key => :range2, + range_key: :range2, }) - Dynamoid.adapter.create_table('table_lsi', :id, { - :local_secondary_indexes => doc_class.local_secondary_indexes.values, - :range_key => { :range => :number } - }) + Dynamoid.adapter.create_table('table_lsi', :id, + local_secondary_indexes: doc_class.local_secondary_indexes.values, + range_key: { range: :number } + ) # execute - data = Dynamoid.adapter.client.describe_table(table_name: 'table_lsi').data + resp = Dynamoid.adapter.client.describe_table(table_name: 'table_lsi') + data = resp.data lsi = data.table.local_secondary_indexes.first # test - expect(lsi.index_name).to eql "dynamoid_tests_table_lsi_index_id_range2" + expect(Dynamoid::AdapterPlugin::AwsSdkV2::PARSE_TABLE_STATUS.call(resp)).to eq(Dynamoid::AdapterPlugin::AwsSdkV2::TABLE_STATUSES[:active]) + expect(lsi.index_name).to eql 'dynamoid_tests_table_lsi_index_id_range2' expect(lsi.key_schema.map(&:to_hash)).to eql [ - {:attribute_name=>"id", :key_type=>"HASH"}, - {:attribute_name=>"range2", :key_type=>"RANGE"} + {attribute_name: 'id', key_type: 'HASH'}, + {attribute_name: 'range2', key_type: 'RANGE'} ] - expect(lsi.projection.to_hash).to eql ({:projection_type=>"KEYS_ONLY"}) + expect(lsi.projection.to_hash).to eql ({projection_type: 'KEYS_ONLY'}) end it 'creates table with global_secondary_index' do # setup - doc_class.table({:name => 'table_gsi', :key => :id}) + doc_class.table(name: 'table_gsi', key: :id) doc_class.global_secondary_index ({ - :hash_key => :hash2, - :range_key => :range2, - :write_capacity => 10, - :read_capacity => 20 - - }) - Dynamoid.adapter.create_table('table_gsi', :id, { - :global_secondary_indexes => doc_class.global_secondary_indexes.values, - :range_key => { :range => :number } + hash_key: :hash2, + range_key: :range2, + write_capacity: 10, + read_capacity: 20 }) + Dynamoid.adapter.create_table('table_gsi', :id, + global_secondary_indexes: doc_class.global_secondary_indexes.values, + range_key: { range: :number }) # execute - data = Dynamoid.adapter.client.describe_table(table_name: 'table_gsi').data + resp = Dynamoid.adapter.client.describe_table(table_name: 'table_gsi') + data = resp.data gsi = data.table.global_secondary_indexes.first # test - expect(gsi.index_name).to eql "dynamoid_tests_table_gsi_index_hash2_range2" + expect(Dynamoid::AdapterPlugin::AwsSdkV2::PARSE_TABLE_STATUS.call(resp)).to eq(Dynamoid::AdapterPlugin::AwsSdkV2::TABLE_STATUSES[:active]) + expect(gsi.index_name).to eql 'dynamoid_tests_table_gsi_index_hash2_range2' expect(gsi.key_schema.map(&:to_hash)).to eql [ - {:attribute_name=>"hash2", :key_type=>"HASH"}, - {:attribute_name=>"range2", :key_type=>"RANGE"} + {attribute_name: 'hash2', key_type: 'HASH'}, + {attribute_name: 'range2', key_type: 'RANGE'} ] - expect(gsi.projection.to_hash).to eql ({:projection_type=>"KEYS_ONLY"}) - expect(gsi.provisioned_throughput.read_capacity_units).to eql 20 + expect(gsi.projection.to_hash).to eql ({projection_type: 'KEYS_ONLY'}) expect(gsi.provisioned_throughput.write_capacity_units).to eql 10 + expect(gsi.provisioned_throughput.read_capacity_units).to eql 20 end end end - context 'with a preexisting table' do # GetItem, PutItem and DeleteItem - it "performs GetItem for an item that does not exist" do + it 'performs GetItem for an item that does not exist' do expect(Dynamoid.adapter.get_item(test_table1, '1')).to be_nil end - it "performs GetItem for an item that does exist" do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) + it 'performs GetItem for an item that does exist' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq({:name => 'Josh', :id => '1'}) + expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(name: 'Josh', id: '1') Dynamoid.adapter.delete_item(test_table1, '1') @@ -185,13 +410,13 @@ end it 'performs GetItem for an item that does exist with a range key' do - Dynamoid.adapter.put_item(test_table3, {:id => '1', :name => 'Josh', :range => 2.0}) + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 2.0) - expect(Dynamoid.adapter.get_item(test_table3, '1', :range_key => 2.0)).to eq({:name => 'Josh', :id => '1', :range => 2.0}) + expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 2.0)).to eq(name: 'Josh', id: '1', range: 2.0) - Dynamoid.adapter.delete_item(test_table3, '1', :range_key => 2.0) + Dynamoid.adapter.delete_item(test_table3, '1', range_key: 2.0) - expect(Dynamoid.adapter.get_item(test_table3, '1', :range_key => 2.0)).to be_nil + expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 2.0)).to be_nil end it 'performs DeleteItem for an item that does not exist' do @@ -201,62 +426,154 @@ end it 'performs PutItem for an item that does not exist' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq({:id => '1', :name => 'Josh'}) + expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(id: '1', name: 'Josh') end # BatchGetItem it 'passes options to underlying BatchGet call' do - pending "at the moment passing the options to underlying batch get is not supported" - expect_any_instance_of(Aws::DynamoDB::Client).to receive(:batch_get_item).with(:request_items => {test_table1 => {:keys => [{'id' => '1'}, {'id' => '2'}], :consistent_read => true}}).and_call_original - described_class.batch_get_item({test_table1 => ['1', '2']}, :consistent_read => true) + pending 'at the moment passing the options to underlying batch get is not supported' + expect_any_instance_of(Aws::DynamoDB::Client).to receive(:batch_get_item).with(request_items: {test_table1 => {keys: [{'id' => '1'}, {'id' => '2'}], consistent_read: true}}).and_call_original + described_class.batch_get_item({test_table1 => ['1', '2']}, consistent_read: true) end - it "performs BatchGetItem with singular keys" do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table2, {:id => '1', :name => 'Justin'}) + it 'performs BatchGetItem with singular keys' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table2, id: '1', name: 'Justin') results = Dynamoid.adapter.batch_get_item(test_table1 => '1', test_table2 => '1') expect(results.size).to eq 2 - expect(results[test_table1]).to include({:name => 'Josh', :id => '1'}) - expect(results[test_table2]).to include({:name => 'Justin', :id => '1'}) + expect(results[test_table1]).to include(name: 'Josh', id: '1') + expect(results[test_table2]).to include(name: 'Justin', id: '1') end - it "performs BatchGetItem with multiple keys" do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table1, {:id => '2', :name => 'Justin'}) + it 'performs BatchGetItem with multiple keys' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') results = Dynamoid.adapter.batch_get_item(test_table1 => ['1', '2']) expect(results.size).to eq 1 - expect(results[test_table1]).to include({:name => 'Josh', :id => '1'}) - expect(results[test_table1]).to include({:name => 'Justin', :id => '2'}) + expect(results[test_table1]).to include(name: 'Josh', id: '1') + expect(results[test_table1]).to include(name: 'Justin', id: '2') end it 'performs BatchGetItem with one ranged key' do - Dynamoid.adapter.put_item(test_table3, {:id => '1', :name => 'Josh', :range => 1.0}) - Dynamoid.adapter.put_item(test_table3, {:id => '2', :name => 'Justin', :range => 2.0}) + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0]]) expect(results.size).to eq 1 - expect(results[test_table3]).to include({:name => 'Josh', :id => '1', :range => 1.0}) + expect(results[test_table3]).to include(name: 'Josh', id: '1', range: 1.0) end it 'performs BatchGetItem with multiple ranged keys' do - Dynamoid.adapter.put_item(test_table3, {:id => '1', :name => 'Josh', :range => 1.0}) - Dynamoid.adapter.put_item(test_table3, {:id => '2', :name => 'Justin', :range => 2.0}) + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) - results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0],['2', 2.0]]) + results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0], ['2', 2.0]]) expect(results.size).to eq 1 - expect(results[test_table3]).to include({:name => 'Josh', :id => '1', :range => 1.0}) - expect(results[test_table3]).to include({:name => 'Justin', :id => '2', :range => 2.0}) + expect(results[test_table3]).to include(name: 'Josh', id: '1', range: 1.0) + expect(results[test_table3]).to include(name: 'Justin', id: '2', range: 2.0) + end + + it 'performs BatchGetItem with ranges of 100 keys' do + table_ids = [] + + (1..101).each do |i| + id, range = i.to_s, i.to_f + Dynamoid.adapter.put_item(test_table3, id: id, name: "Josh_#{i}", range: range) + table_ids << [id, range] + end + + results = Dynamoid.adapter.batch_get_item(test_table3 => table_ids) + + expect(results.size).to eq 1 + + expect(results[test_table3]).to include(name: 'Josh_101', id: '101', range: 101.0) + end + + it 'loads unprocessed items' do + # batch_get_item has following limitations: + # * up to 100 items at once + # * up to 16 MB at once + # + # So we write data as large as possible and read it back + # 100 * 400 KB (limit for item) = ~40 MB + # 40 MB / 16 MB = 3 times + + ids = (1 .. 100).map(&:to_s) + ids.each do |id| + text = ' ' * (400.kilobytes - 9) # length('id' + 'text' + 1-100) = 9 bytes + Dynamoid.adapter.put_item(test_table5, id: id, text: text) + end + + expect(Dynamoid.adapter.client).to receive(:batch_get_item).exactly(3).times.and_call_original + + results = Dynamoid.adapter.batch_get_item(test_table5 => ids) + items = results[test_table5] + expect(items.size).to eq 100 + expect(items.map { |h| h[:id] }).to match_array(ids) + end + + context 'optional block passed' do + it 'returns nil' do + ids = (1 .. 110).map(&:to_s) + ids.each do |id| + Dynamoid.adapter.put_item(test_table1, id: id) + end + + results = Dynamoid.adapter.batch_get_item(test_table1 => ids) do + end + + expect(results).to eq nil + end + + it 'passes as block arguments items for each batch' do + ids = (1 .. 110).map(&:to_s) + ids.each do |id| + Dynamoid.adapter.put_item(test_table1, id: id) + end + + args = [] + results = Dynamoid.adapter.batch_get_item(test_table1 => ids) do |hash| + args << hash + end + + expect(args.size).to eq 2 + + expect(args[0].keys[0]).to eq test_table1 + expect(args[0].values[0].size).to eq 100 + + expect(args[1].keys[0]).to eq test_table1 + expect(args[1].values[0].size).to eq 10 + + expect((args[0].values[0] + args[1].values[0]).map { |h| h[:id] }).to match_array(ids) + end + + it 'passes as block arguments flag if there are unprocessed items for each batch' do + # 50 * 400KB = ~20 MB + # It should be enough to exceed limit of 16 MB per call + ids = (1 .. 50).map(&:to_s) + ids.each do |id| + text = ' ' * (400.kilobytes - 9) # length('id' + 'text' + 1-100) = 9 bytes + Dynamoid.adapter.put_item(test_table5, id: id, text: text) + end + + args = [] + results = Dynamoid.adapter.batch_get_item(test_table5 => ids) do |hash, flag| + args << flag + end + + expect(args).to eq [true, false] + end end # BatchDeleteItem - it "performs BatchDeleteItem with singular keys" do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table2, {:id => '1', :name => 'Justin'}) + it 'performs BatchDeleteItem with singular keys' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table2, id: '1', name: 'Justin') Dynamoid.adapter.batch_delete_item(test_table1 => ['1'], test_table2 => ['1']) @@ -267,9 +584,9 @@ expect(results[test_table2]).to be_blank end - it "performs BatchDeleteItem with multiple keys" do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table1, {:id => '2', :name => 'Justin'}) + it 'performs BatchDeleteItem with multiple keys' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') Dynamoid.adapter.batch_delete_item(test_table1 => ['1', '2']) @@ -280,8 +597,8 @@ end it 'performs BatchDeleteItem with one ranged key' do - Dynamoid.adapter.put_item(test_table3, {:id => '1', :name => 'Josh', :range => 1.0}) - Dynamoid.adapter.put_item(test_table3, {:id => '2', :name => 'Justin', :range => 2.0}) + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0]]) results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0]]) @@ -291,94 +608,247 @@ end it 'performs BatchDeleteItem with multiple ranged keys' do - Dynamoid.adapter.put_item(test_table3, {:id => '1', :name => 'Josh', :range => 1.0}) - Dynamoid.adapter.put_item(test_table3, {:id => '2', :name => 'Justin', :range => 2.0}) + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) - Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0],['2', 2.0]]) - results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0],['2', 2.0]]) + Dynamoid.adapter.batch_delete_item(test_table3 => [['1', 1.0], ['2', 2.0]]) + results = Dynamoid.adapter.batch_get_item(test_table3 => [['1', 1.0], ['2', 2.0]]) expect(results.size).to eq 1 expect(results[test_table3]).to be_blank end - # ListTables - it 'performs ListTables' do - #Force creation of the tables - test_table1; test_table2; test_table3; test_table4 + it 'performs BatchDeleteItem with more than 25 items' do + (25 + 1).times do |i| + Dynamoid.adapter.put_item(test_table1, id: i.to_s) + end - expect(Dynamoid.adapter.list_tables).to include test_table1 - expect(Dynamoid.adapter.list_tables).to include test_table2 + expect(Dynamoid.adapter.client).to receive(:batch_write_item) + .exactly(2).times.and_call_original + Dynamoid.adapter.batch_delete_item(test_table1 => (0 .. 25).map(&:to_s)) + + results = Dynamoid.adapter.scan(test_table1) + expect(results.to_a.size).to eq 0 end - # Query - it 'performs query on a table and returns items' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) + it 'performs BatchDeleteItem with more than 25 items and different tables' do + 13.times do |i| + Dynamoid.adapter.put_item(test_table1, id: i.to_s) + Dynamoid.adapter.put_item(test_table2, id: i.to_s) + end + + expect(Dynamoid.adapter.client).to receive(:batch_write_item) + .exactly(2).times.and_call_original + Dynamoid.adapter.batch_delete_item( + test_table1 => (0 .. 12).map(&:to_s), + test_table2 => (0 .. 12).map(&:to_s)) + + results = Dynamoid.adapter.scan(test_table1) + expect(results.to_a.size).to eq 0 - expect(Dynamoid.adapter.query(test_table1, :hash_value => '1').first).to eq({ :id=> '1', :name=>"Josh" }) + results = Dynamoid.adapter.scan(test_table2) + expect(results.to_a.size).to eq 0 end - describe 'query :batch_size param' do - before(:each) do - name = "a"*1024*300 - 5.times do |i| - Dynamoid.adapter.put_item(test_table5, { - :id => "1", - :range => i.to_s, - :name => name - } - ) - end + describe '#batch_write_item' do + it 'creates several items at once' do + Dynamoid.adapter.batch_write_item(test_table3, [ + {id: '1', range: 1.0}, + {id: '2', range: 2.0}, + {id: '3', range: 3.0} + ]) + + results = Dynamoid.adapter.scan(test_table3) + expect(results.to_a).to contain_exactly( + {id: '1', range: 1.0}, + {id: '2', range: 2.0}, + {id: '3', range: 3.0} + ) + end + it 'performs BatchDeleteItem with more than 25 items' do + items = (1 .. 26).map { |i| {id: i.to_s} } + + expect(Dynamoid.adapter.client).to receive(:batch_write_item) + .exactly(2).times.and_call_original + + Dynamoid.adapter.batch_write_item(test_table1, items) end - it 'fetches all results when :batch_size provided' do - result = Dynamoid.adapter.query(test_table5, - :hash_value => '1', - :batch_size => 2 + + it 'writes unprocessed items' do + # batch_write_item has following limitations: + # * up to 25 items at once + # * up to 16 MB at once + # + # dynamodb-local ignores provisioned throughput settings + # so we cannot emulate unprocessed items - let's stub + + ids = (1 .. 3).map(&:to_s) + items = ids.map { |id| { id: id } } + + records = [] + responses = [ + double('response 1', unprocessed_items: { test_table1 => [ + double(put_request: double(item: { id: '2' })), + double(put_request: double(item: { id: '3' })) + ]}), + double('response 2', unprocessed_items: { test_table1 => [ + double(put_request: double(item: { id: '3' })) + ]}), + double('response 3', unprocessed_items: nil) + ] + allow(Dynamoid.adapter.client).to receive(:batch_write_item) do |args| + records << args[:request_items][test_table1].map { |h| h[:put_request][:item] } + responses.shift + end + + Dynamoid.adapter.batch_write_item(test_table1, items) + expect(records).to eq( + [ + [{ id: '1' }, { id: '2' }, { id: '3' }], + [{ id: '2' }, { id: '3' }], + [{ id: '3' }], + ] ) - expect(result.count).to eq(5) end - it 'limits to 1MB when :batch_size is not provided' do - result = Dynamoid.adapter.query(test_table5, :hash_value => '1') - expect(result.count).to eq(4) + context 'optional block passed' do + it 'passes as block arguments flag if there are unprocessed items for each batch' do + # dynamodb-local ignores provisioned throughput settings + # so we cannot emulate unprocessed items - let's stub + + responses = [ + double('response 1', unprocessed_items: { test_table1 => [ + double(put_request: double(item: { id: '25' })) # fail + ]}), + double('response 2', unprocessed_items: nil), # success + double('response 3', unprocessed_items: { test_table1 => [ + double(put_request: double(item: { id: '25' })) # fail + ]}), + double('response 4', unprocessed_items: nil) # success + ] + allow(Dynamoid.adapter.client).to receive(:batch_write_item).and_return(*responses) + + args = [] + items = (1 .. 50).map(&:to_s).map { |id| { id: id } } # the limit is 25 items at once + Dynamoid.adapter.batch_write_item(test_table1, items) do |has_unprocessed_items| + args << has_unprocessed_items + end + expect(args).to eq [true, false, true, false] + end end end + # ListTables + it 'performs ListTables' do + # Force creation of the tables + test_table1; test_table2; test_table3; test_table4 + + expect(Dynamoid.adapter.list_tables).to include test_table1 + expect(Dynamoid.adapter.list_tables).to include test_table2 + end + + # Query + it 'performs query on a table and returns items' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + + expect(Dynamoid.adapter.query(test_table1, hash_value: '1').first).to eq(id: '1', name: 'Josh') + end + it 'performs query on a table and returns items if there are multiple items' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table1, {:id => '2', :name => 'Justin'}) + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') - expect(Dynamoid.adapter.query(test_table1, :hash_value => '1').first).to eq({ :id=> '1', :name=>"Josh" }) + expect(Dynamoid.adapter.query(test_table1, hash_value: '1').first).to eq(id: '1', name: 'Josh') end it_behaves_like 'range queries' + describe 'query' do + include_examples 'correctly handling limits', :query + end + # Scan it 'performs scan on a table and returns items' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') - expect(Dynamoid.adapter.scan(test_table1, :name => 'Josh').to_a).to eq [{ :id=> '1', :name=>"Josh" }] + expect(Dynamoid.adapter.scan(test_table1, name: {eq: 'Josh'}).to_a).to eq [{ id: '1', name: 'Josh' }] end it 'performs scan on a table and returns items if there are multiple items but only one match' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table1, {:id => '2', :name => 'Justin'}) + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Justin') - expect(Dynamoid.adapter.scan(test_table1, :name => 'Josh').to_a).to eq [{ :id=> '1', :name=>"Josh" }] + expect(Dynamoid.adapter.scan(test_table1, name: {eq: 'Josh'}).to_a).to eq [{ id: '1', name: 'Josh' }] end it 'performs scan on a table and returns multiple items if there are multiple matches' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table1, {:id => '2', :name => 'Josh'}) + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') - expect(Dynamoid.adapter.scan(test_table1, :name => 'Josh')).to include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"}) + expect(Dynamoid.adapter.scan(test_table1, name: {eq: 'Josh'})).to include({name: 'Josh', id: '2'}, name: 'Josh', id: '1') end it 'performs scan on a table and returns all items if no criteria are specified' do - Dynamoid.adapter.put_item(test_table1, {:id => '1', :name => 'Josh'}) - Dynamoid.adapter.put_item(test_table1, {:id => '2', :name => 'Josh'}) + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + + expect(Dynamoid.adapter.scan(test_table1, {})).to include({name: 'Josh', id: '2'}, name: 'Josh', id: '1') + end + + it 'performs scan on a table and returns correct limit' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') + + expect(Dynamoid.adapter.scan(test_table1, {}, record_limit: 1).count).to eq(1) + end + + it 'performs scan on a table and returns correct batch' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') + + expect(Dynamoid.adapter.scan(test_table1, {}, batch_size: 1).count).to eq(4) + end + + it 'performs scan on a table and returns correct limit and batch' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '3', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '4', name: 'Josh') + + expect(Dynamoid.adapter.scan(test_table1, {}, record_limit: 1, batch_size: 1).count).to eq(1) + end + + describe 'scans' do + it_behaves_like 'correctly handling limits', :scan + end + + # Truncate + it 'performs truncate on an existing table' do + Dynamoid.adapter.put_item(test_table1, id: '1', name: 'Josh') + Dynamoid.adapter.put_item(test_table1, id: '2', name: 'Pascal') + + expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq(name: 'Josh', id: '1') + expect(Dynamoid.adapter.get_item(test_table1, '2')).to eq(name: 'Pascal', id: '2') + + Dynamoid.adapter.truncate(test_table1) + + expect(Dynamoid.adapter.get_item(test_table1, '1')).to be_nil + expect(Dynamoid.adapter.get_item(test_table1, '2')).to be_nil + end + + it 'performs truncate on an existing table with a range key' do + Dynamoid.adapter.put_item(test_table3, id: '1', name: 'Josh', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', name: 'Justin', range: 2.0) + + Dynamoid.adapter.truncate(test_table3) - expect(Dynamoid.adapter.scan(test_table1, {})).to include({:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"}) + expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 1.0)).to be_nil + expect(Dynamoid.adapter.get_item(test_table3, '2', range_key: 2.0)).to be_nil end it_behaves_like 'correct ordering' diff --git a/spec/dynamoid/adapter_spec.rb b/spec/dynamoid/adapter_spec.rb index 14d14a11..ff14d32e 100644 --- a/spec/dynamoid/adapter_spec.rb +++ b/spec/dynamoid/adapter_spec.rb @@ -7,6 +7,19 @@ def test_table; 'dynamoid_tests_TestTable'; end let(:single_id){'123'} let(:many_ids){%w(1 2)} + { + 1 => [:id], + 2 => [:id], + 3 => [:id, {range_key: {range: :number}}], + 4 => [:id, {range_key: {range: :number}}] + }.each do |n, args| + name = "dynamoid_tests_TestTable#{n}" + let(:"test_table#{n}") do + Dynamoid.adapter.create_table(name, *args) + name + end + end + describe 'connection management' do it 'does not auto-establish a connection' do expect_any_instance_of(described_class.adapter_plugin_class).to_not receive(:connect!) @@ -53,49 +66,92 @@ def test_table; 'dynamoid_tests_TestTable'; end it 'raises NoMethodError if we try a method that is not on the child' do expect {subject.foobar}.to raise_error(NoMethodError) end - + it 'writes through the adapter' do - expect(subject).to receive(:put_item).with(test_table, {:id => single_id}, nil).and_return(true) - subject.write(test_table, {:id => single_id}) + expect(subject).to receive(:put_item).with(test_table, {id: single_id}, nil).and_return(true) + subject.write(test_table, id: single_id) end - it 'reads through the adapter for one ID' do - expect(subject).to receive(:get_item).with(test_table, single_id, {}).and_return(true) - subject.read(test_table, single_id) - end + describe '#read' do + it 'reads through the adapter for one ID' do + expect(subject).to receive(:get_item).with(test_table, single_id, {}).and_return(true) + subject.read(test_table, single_id) + end - it 'reads through the adapter for many IDs' do - expect(subject).to receive(:batch_get_item).with({test_table => many_ids}, {}).and_return(true) - subject.read(test_table, many_ids) - end + it 'reads through the adapter for many IDs' do + expect(subject).to receive(:batch_get_item).with({test_table => many_ids}, {}).and_return(true) + subject.read(test_table, many_ids) + end - it 'delete through the adapter for one ID' do - expect(subject).to receive(:delete_item).with(test_table, single_id, {}).and_return(nil) - subject.delete(test_table, single_id) - end + it 'reads through the adapter for one ID and a range key' do + expect(subject).to receive(:get_item).with(test_table, single_id, range_key: 2.0).and_return(true) + subject.read(test_table, single_id, range_key: 2.0) + end - it 'deletes through the adapter for many IDs' do - expect(subject).to receive(:batch_delete_item).with({test_table => many_ids}).and_return(nil) - subject.delete(test_table, many_ids) + it 'reads through the adapter for many IDs and a range key' do + expect(subject).to receive(:batch_get_item).with({test_table => [['1', 2.0], ['2', 2.0]]}, {}).and_return(true) + subject.read(test_table, many_ids, range_key: 2.0) + end end - it 'reads through the adapter for one ID and a range key' do - expect(subject).to receive(:get_item).with(test_table, single_id, :range_key => 2.0).and_return(true) - subject.read(test_table, single_id, :range_key => 2.0) - end + describe '#delete' do + it 'deletes through the adapter for one ID' do + Dynamoid.adapter.put_item(test_table1, id: '1') + Dynamoid.adapter.put_item(test_table1, id: '2') - it 'reads through the adapter for many IDs and a range key' do - expect(subject).to receive(:batch_get_item).with({test_table => [['1', 2.0], ['2', 2.0]]}, {}).and_return(true) - subject.read(test_table, many_ids, :range_key => 2.0) - end + expect { + subject.delete(test_table1, '1') + }.to change { + Dynamoid.adapter.scan(test_table1).to_a.size + }.from(2).to(1) - it 'deletes through the adapter for one ID and a range key' do - expect(subject).to receive(:delete_item).with(test_table, single_id, :range_key => 2.0).and_return(nil) - subject.delete(test_table, single_id, :range_key => 2.0) - end + expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq nil + end + + it 'deletes through the adapter for many IDs' do + Dynamoid.adapter.put_item(test_table1, id: '1') + Dynamoid.adapter.put_item(test_table1, id: '2') + Dynamoid.adapter.put_item(test_table1, id: '3') + + expect { + subject.delete(test_table1, ['1', '2']) + }.to change { + Dynamoid.adapter.scan(test_table1).to_a.size + }.from(3).to(1) + + expect(Dynamoid.adapter.get_item(test_table1, '1')).to eq nil + expect(Dynamoid.adapter.get_item(test_table1, '2')).to eq nil + end + + it 'deletes through the adapter for one ID and a range key' do + Dynamoid.adapter.put_item(test_table3, id: '1', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', range: 2.0) + + expect { + subject.delete(test_table3, '1', range_key: 1.0) + }.to change { + Dynamoid.adapter.scan(test_table3).to_a.size + }.from(2).to(1) - it 'deletes through the adapter for many IDs and a range key' do - expect(subject).to receive(:batch_delete_item).with({test_table => [['1', 2.0], ['2', 2.0]]}).and_return(nil) - subject.delete(test_table, many_ids, :range_key => [2.0,2.0]) + expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 1.0)).to eq nil + end + + it 'deletes through the adapter for many IDs and a range key' do + Dynamoid.adapter.put_item(test_table3, id: '1', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '1', range: 2.0) + Dynamoid.adapter.put_item(test_table3, id: '2', range: 1.0) + Dynamoid.adapter.put_item(test_table3, id: '2', range: 2.0) + + expect(subject).to receive(:batch_delete_item).and_call_original + + expect { + subject.delete(test_table3, ['1', '2'], range_key: 1.0) + }.to change { + Dynamoid.adapter.scan(test_table3).to_a.size + }.from(4).to(2) + + expect(Dynamoid.adapter.get_item(test_table3, '1', range_key: 1.0)).to eq nil + expect(Dynamoid.adapter.get_item(test_table3, '2', range_key: 1.0)).to eq nil + end end end diff --git a/spec/dynamoid/associations/association_spec.rb b/spec/dynamoid/associations/association_spec.rb index a3b8f88a..1a07ba2f 100644 --- a/spec/dynamoid/associations/association_spec.rb +++ b/spec/dynamoid/associations/association_spec.rb @@ -1,15 +1,14 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') describe Dynamoid::Associations::Association do + let(:subscription) {Subscription.create} + let(:magazine) {Magazine.create} it 'returns an empty array if there are no associations' do - expect(Magazine.create.subscriptions).to be_empty + expect(magazine.subscriptions).to be_empty end it 'adds an item to an association' do - subscription = Subscription.create - magazine = Magazine.create - magazine.subscriptions << subscription expect(magazine.subscriptions.size).to eq 1 @@ -17,9 +16,6 @@ end it 'deletes an item from an association' do - subscription = Subscription.create - magazine = Magazine.create - magazine.subscriptions << subscription magazine.subscriptions.delete(subscription) @@ -27,7 +23,6 @@ end it 'creates an item from an association' do - magazine = Magazine.create subscription = magazine.subscriptions.create expect(subscription.class).to eq Subscription @@ -36,7 +31,6 @@ end it 'returns the number of items in the association' do - magazine = Magazine.create magazine.subscriptions.create expect(magazine.subscriptions.size).to eq 1 @@ -51,16 +45,12 @@ end it 'assigns directly via the equals operator' do - magazine = Magazine.create - subscription = Subscription.create magazine.subscriptions = [subscription] expect(magazine.subscriptions).to eq [subscription] end it 'assigns directly via the equals operator and reflects to the target association' do - magazine = Magazine.create - subscription = Subscription.create magazine.subscriptions = [subscription] expect(subscription.magazine).to eq magazine @@ -77,7 +67,6 @@ end it 'deletes all items from the association' do - magazine = Magazine.create magazine.subscriptions << Subscription.create magazine.subscriptions << Subscription.create magazine.subscriptions << Subscription.create @@ -89,7 +78,6 @@ end it 'uses where inside an association and returns a result' do - magazine = Magazine.create included_subscription = magazine.subscriptions.create(length: 10) unincldued_subscription = magazine.subscriptions.create(length: 8) @@ -97,7 +85,6 @@ end it 'uses where inside an association and returns an empty set' do - magazine = Magazine.create included_subscription = magazine.subscriptions.create(length: 10) unincldued_subscription = magazine.subscriptions.create(length: 8) @@ -105,22 +92,18 @@ end it 'includes enumerable' do - magazine = Magazine.create subscription1 = magazine.subscriptions.create subscription2 = magazine.subscriptions.create subscription3 = magazine.subscriptions.create - expect(magazine.subscriptions.collect(&:id).sort).to eq [subscription1.id, subscription2.id, subscription3.id].sort + expect(magazine.subscriptions.collect(&:hash_key).sort).to eq [subscription1.hash_key, subscription2.hash_key, subscription3.hash_key].sort end it 'works for camel-cased associations' do - expect(Magazine.create.camel_cases.create.class).to eq CamelCase + expect(magazine.camel_cases.create.class).to eq CamelCase end it 'destroys associations' do - subscription = Subscription.create - magazine = Magazine.create - expect(magazine.subscriptions).to receive(:target).and_return([subscription]) expect(subscription).to receive(:destroy) @@ -128,8 +111,6 @@ end it 'deletes associations' do - subscription = Subscription.new - magazine = Magazine.create expect(magazine.subscriptions).to receive(:target).and_return([subscription]) expect(subscription).to receive(:delete) @@ -137,24 +118,22 @@ end it 'replaces existing associations when using the setter' do - magazine = Magazine.create subscription1 = magazine.subscriptions.create subscription2 = magazine.subscriptions.create - subscription3 = Subscription.create + subscription3 = subscription expect(subscription1.reload.magazine_ids).to be_present expect(subscription2.reload.magazine_ids).to be_present magazine.subscriptions = subscription3 - expect(magazine.subscriptions_ids).to eq Set[subscription3.id] + expect(magazine.subscriptions_ids).to eq Set[subscription3.hash_key] expect(subscription1.reload.magazine_ids).to be_blank expect(subscription2.reload.magazine_ids).to be_blank - expect(subscription3.reload.magazine_ids).to eq Set[magazine.id] + expect(subscription3.reload.magazine_ids).to eq Set[magazine.hash_key] end it 'destroys all objects and removes them from the association' do - magazine = Magazine.create subscription1 = magazine.subscriptions.create subscription2 = magazine.subscriptions.create subscription3 = magazine.subscriptions.create @@ -162,11 +141,10 @@ magazine.subscriptions.destroy_all expect(magazine.subscriptions).to be_blank - expect(Subscription.all).to be_empty + expect(Subscription.all.to_a).to be_empty end it 'deletes all objects and removes them from the association' do - magazine = Magazine.create subscription1 = magazine.subscriptions.create subscription2 = magazine.subscriptions.create subscription3 = magazine.subscriptions.create @@ -174,12 +152,10 @@ magazine.subscriptions.delete_all expect(magazine.subscriptions).to be_blank - expect(Subscription.all).to be_empty + expect(Subscription.all.to_a).to be_empty end it 'delegates class to the association object' do - magazine = Magazine.create - expect(magazine.sponsor.class).to eq nil.class magazine.sponsor.create expect(magazine.sponsor.class).to eq Sponsor @@ -191,13 +167,12 @@ it 'loads association one time only' do pending("FIXME: find_target doesn't exist anymore") - magazine = Magazine.create sponsor = magazine.sponsor.create expect(magazine.sponsor).to receive(:find_target).once.and_return(sponsor) - magazine.sponsor.id - magazine.sponsor.id + magazine.sponsor.hash_key + magazine.sponsor.hash_key end end diff --git a/spec/dynamoid/associations/belongs_to_spec.rb b/spec/dynamoid/associations/belongs_to_spec.rb index 3655cea3..d23dda46 100644 --- a/spec/dynamoid/associations/belongs_to_spec.rb +++ b/spec/dynamoid/associations/belongs_to_spec.rb @@ -1,10 +1,14 @@ +require 'active_support' +require 'active_support/core_ext/object' + require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') describe Dynamoid::Associations::BelongsTo do - context 'has many' do - let!(:subscription) {Subscription.create} - let!(:camel_case) {CamelCase.create} + let(:subscription) {Subscription.create} + let(:camel_case) {CamelCase.create} + let(:magazine) {subscription.magazine.create} + let(:user) {magazine.owner.create} it 'determines nil if it has no associated record' do expect(subscription.magazine).to be_nil @@ -14,54 +18,310 @@ expect(camel_case.magazine.send(:target_association)).to eq :camel_cases end - it 'delegates equality to its source record' do - magazine = subscription.magazine.create - expect(subscription.magazine).to eq magazine - end - - it 'associates has_many automatically' do - magazine = subscription.magazine.create - expect(magazine.subscriptions).to include subscription + end - magazine = Magazine.create - user = magazine.owner.create + it 'associates has_many automatically' do expect(user.books.size).to eq 1 expect(user.books).to include magazine end - - it 'behaves like the object it is trying to be' do - magazine = subscription.magazine.create - subscription.magazine.update_attribute(:title, 'Test Title') + it 'behaves like the object it is trying to be' do + expect(magazine.subscriptions).to include subscription + subscription.magazine.update_attribute(:size, 101) - expect(Magazine.first.title).to eq 'Test Title' + expect(Magazine.first.size).to eq 101 end end - + context 'has one' do + let(:subscription) {Subscription.create} let(:sponsor) {Sponsor.create} - let!(:subscription) {Subscription.create} + let(:magazine) {sponsor.magazine.create} + let(:user) {subscription.customer.create} - it 'determins nil if it has no associated record' do + it 'considers an association nil/blank if it has no associated record' do expect(sponsor.magazine).to be_nil + expect(sponsor.magazine).to be_blank end - + + it 'considers an association present if it has an associated record' do + sponsor.magazine.create + + expect(magazine.sponsor).to be_present + end + it 'delegates equality to its source record' do - magazine = sponsor.magazine.create - expect(sponsor.magazine).to eq magazine end - + it 'associates has_one automatically' do - magazine = sponsor.magazine.create - expect(magazine.sponsor).to eq sponsor - - user = subscription.customer.create expect(user.monthly).to eq subscription end end + + describe 'assigning' do + context 'has many' do + let(:subscription) { Subscription.create } + + it 'associates model on this side' do + magazine = Magazine.create + subscription.magazine = magazine + + expect(subscription.magazine).to eq(magazine) + end + + it 'associates model on that side' do + magazine = Magazine.create + subscription.magazine = magazine + + expect(magazine.subscriptions.to_a).to eq([subscription]) + end + + it 're-associates model on this side' do + magazine_old = Magazine.create + magazine_new = Magazine.create + subscription.magazine = magazine_old + + expect { + subscription.magazine = magazine_new + }.to change { subscription.magazine.target }.from(magazine_old).to(magazine_new) + end + + it 're-associates model on that side' do + magazine_old = Magazine.create + magazine_new = Magazine.create + subscription.magazine = magazine_old + + expect { + subscription.magazine = magazine_new + }.to change { magazine_new.subscriptions.target }.from([]).to([subscription]) + end + + it 'deletes previous model from association' do + magazine_old = Magazine.create + magazine_new = Magazine.create + subscription.magazine = magazine_old + + expect { + subscription.magazine = magazine_new + }.to change { magazine_old.subscriptions.to_a }.from([subscription]).to([]) + end + + it 'stores the same object on this side' do + magazine = Magazine.create + subscription.magazine = magazine + + expect(subscription.magazine.target.object_id).to eq(magazine.object_id) + end + + it 'does not store the same object on that side' do + magazine = Magazine.create + subscription.magazine = magazine + + expect(magazine.subscriptions.target[0].object_id).to_not eq(subscription.object_id) + end + end + + context 'has one' do + let(:sponsor) { Sponsor.create } + + it 'associates model on this side' do + magazine = Magazine.create + sponsor.magazine = magazine + + expect(sponsor.magazine).to eq(magazine) + end + + it 'associates model on that side' do + magazine = Magazine.create + sponsor.magazine = magazine + + expect(magazine.sponsor).to eq(sponsor) + end + + it 're-associates model on this side' do + magazine_old = Magazine.create + magazine_new = Magazine.create + sponsor.magazine = magazine_old + + expect { + sponsor.magazine = magazine_new + }.to change { sponsor.magazine.target }.from(magazine_old).to(magazine_new) + end + + it 're-associates model on this side' do + magazine_old = Magazine.create + magazine_new = Magazine.create + sponsor.magazine = magazine_old + + expect { + sponsor.magazine = magazine_new + }.to change { magazine_new.sponsor.target }.from(nil).to(sponsor) + end + + it 'deletes previous model from association' do + magazine_old = Magazine.create + magazine_new = Magazine.create + sponsor.magazine = magazine_old + + expect { + sponsor.magazine = magazine_new + }.to change { magazine_old.sponsor.target }.from(sponsor).to(nil) + end + + it 'stores the same object on this side' do + magazine = Magazine.create + sponsor.magazine = magazine + + expect(sponsor.magazine.target.object_id).to eq(magazine.object_id) + end + + it 'does not store the same object on that side' do + magazine = Magazine.create + + sponsor.magazine = magazine + expect(magazine.sponsor.target.object_id).to_not eq(sponsor.object_id) + end + end + end + + context 'set to nil' do + it 'can be set to nil' do + subscription = Subscription.new + + expect { subscription.magazine = nil }.not_to raise_error + expect(subscription.magazine).to eq nil + + subscription.save! + expect(Subscription.find(subscription.id).magazine).to eq nil + end + + it 'overrides previous saved value' do + magazine = Magazine.create! + subscription = Subscription.create!(magazine: magazine) + + expect { + subscription.magazine = nil + subscription.save! + }.to change { + Subscription.find(subscription.id).magazine.target + }.from(magazine).to(nil) + end + + it 'updates association on the other side' do + magazine = Magazine.create! + subscription = Subscription.create!(magazine: magazine) + + expect { + subscription.magazine = nil + subscription.save! + }.to change { + Magazine.find(magazine.title).subscriptions.to_a + }.from([subscription]).to([]) + end + end + + describe '#delete' do + it 'clears association on this side' do + subscription = Subscription.create + magazine = subscription.magazine.create + + expect { + subscription.magazine.delete + }.to change { subscription.magazine.target }.from(magazine).to(nil) + end + + it 'persists changes on this side' do + subscription = Subscription.create + magazine = subscription.magazine.create + + expect { + subscription.magazine.delete + }.to change { Subscription.find(subscription.id).magazine.target }.from(magazine).to(nil) + end + + context 'has many' do + let(:subscription) { Subscription.create } + let!(:magazine) { subscription.magazine.create } + + it 'clears association on that side' do + expect { + subscription.magazine.delete + }.to change { magazine.subscriptions.target }.from([subscription]).to([]) + end + + it 'persists changes on that side' do + expect { + subscription.magazine.delete + }.to change { Magazine.find(magazine.title).subscriptions.target }.from([subscription]).to([]) + end + end + + context 'has one' do + let(:sponsor) { Sponsor.create } + let!(:magazine) { sponsor.magazine.create } + + it 'clears association on that side' do + expect { + sponsor.magazine.delete + }.to change { magazine.sponsor.target }.from(sponsor).to(nil) + end + + it 'persists changes on that side' do + expect { + sponsor.magazine.delete + }.to change { Magazine.find(magazine.title).sponsor.target }.from(sponsor).to(nil) + end + end + end + + describe 'foreign_key option' do + before :each do + @directory_class = directory_class = new_class(table_name: :directories) + + @file_class = file_class = new_class(table_name: :files) do + belongs_to :directory, class: directory_class, foreign_key: :directory_id + + def self.to_s; 'File' end + end + + @directory_class.instance_eval do + has_many :files, class: file_class + + def self.to_s; 'Directory' end + end + end + + it 'specifies field name' do + file = @file_class.new + expect(file.respond_to? :directory_id).to eq(true) + end + + it 'forces to store :id as a scalar value and not as collection' do + directory = @directory_class.create! + file = @file_class.new(directory: directory) + expect(file.directory_id).to eq(directory.id) + end + + it 'assigns and persists id correctly on this side of association' do + directory = @directory_class.create! + file = @file_class.create!(directory: directory) + + expect(@file_class.find(file.id).directory_id).to eq(directory.id) + expect(file.directory).to eq(directory) + expect(@file_class.find(file.id).directory).to eq(directory) + end + + it 'assigns and persists id correctly on the other side of association' do + directory = @directory_class.create! + file = @file_class.create!(directory: directory) + + expect(directory.files.to_a).to eq [file] + expect(@directory_class.find(directory.id).files.to_a).to eq [file] + end + end end diff --git a/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb b/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb index 898d429c..8d4127e6 100644 --- a/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +++ b/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb @@ -6,7 +6,7 @@ it 'determines equality from its records' do user = subscription.users.create - + expect(subscription.users.size).to eq 1 expect(subscription.users).to include user end @@ -15,14 +15,14 @@ expect(subscription.users.send(:target_association)).to eq :subscriptions expect(camel_case.subscriptions.send(:target_association)).to eq :camel_cases end - + it 'determines target attribute' do expect(subscription.users.send(:target_attribute)).to eq :subscriptions_ids end - + it 'associates has_and_belongs_to_many automatically' do user = subscription.users.create - + expect(user.subscriptions.size).to eq 1 expect(user.subscriptions).to include subscription expect(subscription.users.size).to eq 1 @@ -33,12 +33,64 @@ expect(follower.following).to include user expect(user.followers).to include follower end - + it 'disassociates has_and_belongs_to_many automatically' do user = subscription.users.create - + subscription.users.delete(user) expect(subscription.users.size).to eq 0 expect(user.subscriptions.size).to eq 0 end + + describe 'assigning' do + let(:subscription) { Subscription.create } + let(:user) { User.create } + + it 'associates model on this side' do + subscription.users << user + expect(subscription.users.to_a).to eq([user]) + end + + it 'associates model on that side' do + subscription.users << user + expect(user.subscriptions.to_a).to eq([subscription]) + end + end + + describe '#delete' do + it 'clears association on this side' do + subscription = Subscription.create + user = subscription.users.create + + expect { + subscription.users.delete(user) + }.to change { subscription.users.target }.from([user]).to([]) + end + + it 'persists changes on this side' do + subscription = Subscription.create + user = subscription.users.create + + expect { + subscription.users.delete(user) + }.to change { Subscription.find(subscription.id).users.target }.from([user]).to([]) + end + + context 'has and belongs to many' do + let(:subscription) { Subscription.create } + let!(:user) { subscription.users.create } + + it 'clears association on that side' do + expect { + subscription.users.delete(user) + }.to change { subscription.users.target }.from([user]).to([]) + end + + it 'persists changes on that side' do + expect { + subscription.users.delete(user) + }.to change { Subscription.find(subscription.id).users.target }.from([user]).to([]) + end + end + end end diff --git a/spec/dynamoid/associations/has_many_spec.rb b/spec/dynamoid/associations/has_many_spec.rb index ff97ecac..7c4f9896 100644 --- a/spec/dynamoid/associations/has_many_spec.rb +++ b/spec/dynamoid/associations/has_many_spec.rb @@ -1,13 +1,13 @@ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') describe Dynamoid::Associations::HasMany do - let!(:magazine) {Magazine.create} - let!(:user) {User.create} - let!(:camel_case) {CamelCase.create} + let(:magazine) {Magazine.create} + let(:user) {User.create} + let(:camel_case) {CamelCase.create} it 'determines equality from its records' do subscription = magazine.subscriptions.create - + expect(magazine.subscriptions).to eq subscription end @@ -21,19 +21,127 @@ expect(magazine.subscriptions.send(:target_class)).to eq Subscription expect(user.books.send(:target_class)).to eq Magazine end - + it 'determines target attribute' do expect(magazine.subscriptions.send(:target_attribute)).to eq :magazine_ids expect(user.books.send(:target_attribute)).to eq :owner_ids end - + it 'associates belongs_to automatically' do subscription = magazine.subscriptions.create - + expect(subscription.magazine).to eq magazine magazine = user.books.create expect(magazine.owner).to eq user end + it 'has a where method to filter associates' do + red = magazine.camel_cases.create + red.color = 'red' + red.save + + blue = magazine.camel_cases.create + blue.color = 'blue' + blue.save + + expect(magazine.camel_cases.count).to eq 2 + expect(magazine.camel_cases.where(color: 'red').count).to eq 1 + end + + it 'is not modified by the where method' do + red = magazine.camel_cases.create + red.color = 'red' + red.save + + blue = magazine.camel_cases.create + blue.color = 'blue' + blue.save + + expect(magazine.camel_cases.where(color: 'red').count).to eq 1 + expect(magazine.camel_cases.where(color: 'yellow').count).to eq 0 + expect(magazine.camel_cases.count).to eq 2 + end + + describe 'assigning' do + let(:magazine) { Magazine.create } + let(:subscription) { Subscription.create } + + it 'associates model on this side' do + magazine.subscriptions << subscription + expect(magazine.subscriptions.to_a).to eq([subscription]) + end + + it 'associates model on that side' do + magazine.subscriptions << subscription + expect(subscription.magazine).to eq(magazine) + end + + it 're-associates new model on this side' do + magazine_old = Magazine.create + magazine_new = Magazine.create + magazine_old.subscriptions << subscription + + expect { + magazine_new.subscriptions << subscription + }.to change { magazine_new.subscriptions.to_a }.from([]).to([subscription]) + end + + it 're-associates new model on that side' do + magazine_old = Magazine.create + magazine_new = Magazine.create + magazine_old.subscriptions << subscription + + expect { + magazine_new.subscriptions << subscription + }.to change { subscription.magazine.target }.from(magazine_old).to(magazine_new) + end + + it 'deletes previous model from association' do + magazine_old = Magazine.create + magazine_new = Magazine.create + magazine_old.subscriptions << subscription + + expect { + magazine_new.subscriptions << subscription + }.to change { Magazine.find(magazine_old.title).subscriptions.to_a }.from([subscription]).to([]) + end + end + + describe '#delete' do + it 'clears association on this side' do + magazine = Magazine.create + subscription = magazine.subscriptions.create + + expect { + magazine.subscriptions.delete(subscription) + }.to change { magazine.subscriptions.target }.from([subscription]).to([]) + end + + it 'persists changes on this side' do + magazine = Magazine.create + subscription = magazine.subscriptions.create + + expect { + magazine.subscriptions.delete(subscription) + }.to change { Magazine.find(magazine.title).subscriptions.target }.from([subscription]).to([]) + end + + context 'belongs to' do + let(:magazine) { Magazine.create } + let!(:subscription) { magazine.subscriptions.create } + + it 'clears association on that side' do + expect { + magazine.subscriptions.delete(subscription) + }.to change { magazine.subscriptions.target }.from([subscription]).to([]) + end + + it 'persists changes on that side' do + expect { + magazine.subscriptions.delete(subscription) + }.to change { Magazine.find(magazine.title).subscriptions.target }.from([subscription]).to([]) + end + end + end end diff --git a/spec/dynamoid/associations/has_one_spec.rb b/spec/dynamoid/associations/has_one_spec.rb index a767989a..d3edf7f6 100644 --- a/spec/dynamoid/associations/has_one_spec.rb +++ b/spec/dynamoid/associations/has_one_spec.rb @@ -1,36 +1,46 @@ +require 'active_support' +require 'active_support/core_ext/object' + require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') describe Dynamoid::Associations::HasOne do - let!(:magazine) {Magazine.create} - let!(:user) {User.create} - let!(:camel_case) {CamelCase.create} + let(:magazine) {Magazine.create} + let(:user) {User.create} + let(:camel_case) {CamelCase.create} - it 'determines nil if it has no associated record' do + it 'considers an association nil/blank if it has no associated record' do expect(magazine.sponsor).to be_nil + expect(magazine.sponsor).to be_blank + end + + it 'considers an association present if it has an associated record' do + magazine.sponsor.create + + expect(magazine.sponsor).to be_present end it 'determines target association correctly' do expect(camel_case.sponsor.send(:target_association)).to eq :camel_case end - + it 'returns only one object when associated' do magazine.sponsor.create - + expect(magazine.sponsor).to_not be_a_kind_of Array end - + it 'delegates equality to its source record' do sponsor = magazine.sponsor.create - + expect(magazine.sponsor).to eq sponsor end - + it 'is equal from its target record' do sponsor = magazine.sponsor.create - + expect(magazine.sponsor).to eq sponsor end - + it 'associates belongs_to automatically' do sponsor = magazine.sponsor.create expect(sponsor.magazine).to eq magazine @@ -39,4 +49,141 @@ subscription = user.monthly.create expect(subscription.customer).to eq user end + + describe 'assigning' do + context 'belongs to' do + let(:magazine) { Magazine.create } + + it 'associates model on this side' do + sponsor = Sponsor.create + magazine.sponsor = sponsor + + expect(magazine.sponsor).to eq(sponsor) + end + + it 'associates model on that side' do + sponsor = Sponsor.create + magazine.sponsor = sponsor + + expect(sponsor.magazine).to eq(magazine) + end + + it 're-associates model on this side' do + sponsor_old = Sponsor.create + sponsor_new = Sponsor.create + magazine.sponsor = sponsor_old + + expect { + magazine.sponsor = sponsor_new + }.to change { magazine.sponsor.target }.from(sponsor_old).to(sponsor_new) + end + + it 're-associates model on that side' do + sponsor_old = Sponsor.create + sponsor_new = Sponsor.create + + magazine.sponsor = sponsor_old + expect { + magazine.sponsor = sponsor_new + }.to change { sponsor_new.magazine.target }.from(nil).to(magazine) + end + + it 'deletes previous model from association' do + sponsor_old = Sponsor.create + sponsor_new = Sponsor.create + + magazine.sponsor = sponsor_old + expect { + magazine.sponsor = sponsor_new + }.to change { sponsor_old.magazine.target }.from(magazine).to(nil) + end + + it 'stores the same object on this side' do + sponsor = Sponsor.create + magazine.sponsor = sponsor + + expect(magazine.sponsor.target.object_id).to eq(sponsor.object_id) + end + + it 'does not store the same object on that side' do + sponsor = Sponsor.create! + magazine.sponsor = sponsor + + expect(sponsor.magazine.target.object_id).not_to eq(magazine.object_id) + end + end + end + + context 'set to nil' do + it 'can be set to nil' do + magazine = Magazine.create! + + expect { magazine.sponsor = nil }.not_to raise_error + expect(magazine.sponsor).to eq nil + + magazine.save! + expect(Magazine.find(magazine.title).sponsor).to eq nil + end + + it 'overrides previous saved value' do + sponsor = Sponsor.create! + magazine = Magazine.create!(sponsor: sponsor) + + expect { + magazine.sponsor = nil + magazine.save! + }.to change { + Magazine.find(magazine.title).sponsor.target + }.from(sponsor).to(nil) + end + + it 'updates association on the other side' do + sponsor = Sponsor.create! + magazine = Magazine.create!(sponsor: sponsor) + + expect { + magazine.sponsor = nil + magazine.save! + }.to change { + Sponsor.find(sponsor.id).magazine.target + }.from(magazine).to(nil) + end + end + + describe '#delete' do + it 'clears association on this side' do + magazine = Magazine.create + sponsor = magazine.sponsor.create + + expect { + magazine.sponsor.delete + }.to change { magazine.sponsor.target }.from(sponsor).to(nil) + end + + it 'persists changes on this side' do + magazine = Magazine.create + sponsor = magazine.sponsor.create + + expect { + magazine.sponsor.delete + }.to change { Magazine.find(magazine.title).sponsor.target }.from(sponsor).to(nil) + end + + context 'belongs to' do + let(:magazine) { Magazine.create } + let!(:sponsor) { magazine.sponsor.create } + + it 'clears association on that side' do + expect { + magazine.sponsor.delete + }.to change { sponsor.magazine.target }.from(magazine).to(nil) + end + + it 'persists changes on that side' do + expect { + magazine.sponsor.delete + }.to change { Sponsor.find(sponsor.id).magazine.target }.from(magazine).to(nil) + end + end + end end diff --git a/spec/dynamoid/associations_spec.rb b/spec/dynamoid/associations_spec.rb index a4fb41f6..e4259ce0 100644 --- a/spec/dynamoid/associations_spec.rb +++ b/spec/dynamoid/associations_spec.rb @@ -2,11 +2,11 @@ describe Dynamoid::Associations do let(:magazine) { Magazine.create } - + it 'defines a getter' do expect(magazine).to respond_to :subscriptions end - + it 'defines a setter' do expect(magazine).to respond_to :subscriptions= end diff --git a/spec/dynamoid/config/backoff_strategies/exponential_backoff_spec.rb b/spec/dynamoid/config/backoff_strategies/exponential_backoff_spec.rb new file mode 100644 index 00000000..8190558f --- /dev/null +++ b/spec/dynamoid/config/backoff_strategies/exponential_backoff_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +RSpec.describe Dynamoid::Config::BackoffStrategies::ExponentialBackoff do + let(:base_backoff) { 1 } + let(:ceiling) { 5 } + let(:backoff) { described_class.call(base_backoff: base_backoff, ceiling: ceiling) } + + it 'sleeps the first time for specified base backoff time' do + expect(described_class).to receive(:sleep).with(base_backoff) + backoff.call + end + + it 'sleeps for exponentialy increasing time' do + seconds = [] + allow(described_class).to receive(:sleep) do |s| + seconds << s + end + + backoff.call + expect(seconds).to eq [base_backoff] + + backoff.call + expect(seconds).to eq [base_backoff, base_backoff * 2] + + backoff.call + expect(seconds).to eq [base_backoff, base_backoff * 2, base_backoff * 4] + + backoff.call + expect(seconds).to eq [base_backoff, base_backoff * 2, base_backoff * 4, base_backoff * 8] + end + + it 'stops to increase time after ceiling times' do + seconds = [] + allow(described_class).to receive(:sleep) do |s| + seconds << s + end + + 6.times { backoff.call } + expect(seconds).to eq [ + base_backoff, + base_backoff * 2, + base_backoff * 4, + base_backoff * 8, + base_backoff * 16, + base_backoff * 16 + ] + end + + it 'can be called without parameters' do + backoff = nil + expect do + backoff = described_class.call + end.not_to raise_error + end + + it 'uses base backoff = 0.5 and ceiling = 3 by default' do + backoff = described_class.call + + seconds = [] + allow(described_class).to receive(:sleep) do |s| + seconds << s + end + + 4.times { backoff.call } + expect(seconds).to eq([ + 0.5, + 0.5 * 2, + 0.5 * 4, + 0.5 * 4 + ]) + end +end diff --git a/spec/dynamoid/criteria/chain_spec.rb b/spec/dynamoid/criteria/chain_spec.rb index e3af9967..4cda8ca4 100644 --- a/spec/dynamoid/criteria/chain_spec.rb +++ b/spec/dynamoid/criteria/chain_spec.rb @@ -2,7 +2,7 @@ describe Dynamoid::Criteria::Chain do let(:time) { DateTime.now } - let!(:user) { User.create(:name => 'Josh', :email => 'josh@joshsymonds.com', :password => 'Test123') } + let!(:user) { User.create(name: 'Josh', email: 'josh@joshsymonds.com', password: 'Test123') } let(:chain) { Dynamoid::Criteria::Chain.new(User) } describe 'Query vs Scan' do @@ -15,74 +15,1115 @@ it 'Queries when query is only ID' do chain = Dynamoid::Criteria::Chain.new(Address) - chain.query = { :id => 'test' } + chain.query = { id: 'test' } + expect(chain).to receive(:records_via_query) + chain.all + end + + it 'Queries when query contains ID' do + chain = Dynamoid::Criteria::Chain.new(Address) + chain.query = { id: 'test', city: 'Bucharest' } expect(chain).to receive(:records_via_query) chain.all end it 'Scans when query includes keys that are neither a hash nor a range' do chain = Dynamoid::Criteria::Chain.new(Address) - chain.query = { :id => 'test', :city => 'Bucharest' } + chain.query = { city: 'Bucharest' } expect(chain).to receive(:records_via_scan) chain.all end it 'Scans when query is only a range' do chain = Dynamoid::Criteria::Chain.new(Tweet) - chain.query = { :group => 'xx' } + chain.query = { group: 'xx' } expect(chain).to receive(:records_via_scan) chain.all end + + it 'Scans when there is only not-equal operator for hash key' do + chain = Dynamoid::Criteria::Chain.new(Address) + chain.query = { 'id.in' => ['test'] } + expect(chain).to receive(:records_via_scan) + chain.all + end + end + + describe 'Limits' do + shared_examples 'correct handling chain limits' do |request_type| + let(:model) { + Class.new do + include Dynamoid::Document + table name: :customer, key: :id + range :age, :integer + field :name + end + } + + before(:each) do + @request_type = request_type + (1..10).each do |i| + model.create(id: '1', name: 'Josh', age: i) + model.create(id: '1', name: 'Pascal', age: i + 100) + end + end + + def request_params + return { id: '1' } if @request_type == :query + {} + end + + it 'supports record_limit' do + expect(model.where(request_params.merge(name: 'Josh')).record_limit(1).count).to eq(1) + expect(model.where(request_params.merge(name: 'Josh')).record_limit(3).count).to eq(3) + end + + it 'supports scan_limit' do + expect(model.where(request_params.merge(name: 'Pascal')).scan_limit(1).count).to eq(0) + expect(model.where(request_params.merge(name: 'Pascal')).scan_limit(11).count).to eq(1) + end + + it 'supports batch' do + expect(model.where(request_params.merge(name: 'Josh')).batch(1).count).to eq(10) + expect(model.where(request_params.merge(name: 'Josh')).batch(3).count).to eq(10) + end + + it 'supports combined limits with batch size 1' do + # Scanning through 13 means it'll see 10 Josh objects and then + # 3 Pascal objects but it'll hit record_limit first with 2 objects + # so we'd only see 12 requests due to batching. + expect(Dynamoid.adapter.client).to receive(request_type).exactly(12).times.and_call_original + expect(model.where(request_params.merge(name: 'Pascal')) + .record_limit(2) + .scan_limit(13) + .batch(1).count).to eq(2) + end + + it 'supports combined limits with batch size other than 1' do + # Querying in batches of 3 so we'd see: + # 3 Josh, 3 Josh, 3 Josh, 1 Josh + 2 Pascal, 3 Pascal, 3 Pascal, 2 Pascal + # So total of 7 requests + expect(Dynamoid.adapter.client).to receive(request_type).exactly(7).times.and_call_original + expect(model.where(request_params.merge(name: 'Pascal')) + .record_limit(10) + .batch(3).count).to eq(10) + end + end + + describe 'Query' do + it_behaves_like 'correct handling chain limits', :query + end + + describe 'Scan' do + it_behaves_like 'correct handling chain limits', :scan + end + end + + describe 'Query with keys conditions' do + let(:model) { + Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :age, :integer + end + } + + it 'supports eq' do + customer1 = model.create(name: 'Bob', age: 10) + customer2 = model.create(name: 'Bob', age: 30) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(name: 'Bob', age: '10').all).to contain_exactly(customer1) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to eq(:age) + expect(chain.index_name).to be_nil + end + + it 'supports lt' do + customer1 = model.create(name: 'Bob', age: 5) + customer2 = model.create(name: 'Bob', age: 9) + customer3 = model.create(name: 'Bob', age: 12) + + expect(model.where(name: 'Bob', 'age.lt' => 10).all).to contain_exactly(customer1, customer2) + end + + it 'supports gt' do + customer1 = model.create(name: 'Bob', age: 11) + customer2 = model.create(name: 'Bob', age: 12) + customer3 = model.create(name: 'Bob', age: 9) + + expect(model.where(name: 'Bob', 'age.gt' => 10).all).to contain_exactly(customer1, customer2) + end + + it 'supports lte' do + customer1 = model.create(name: 'Bob', age: 5) + customer2 = model.create(name: 'Bob', age: 9) + customer3 = model.create(name: 'Bob', age: 12) + + expect(model.where(name: 'Bob', 'age.lte' => 9).all).to contain_exactly(customer1, customer2) + end + + it 'supports gte' do + customer1 = model.create(name: 'Bob', age: 11) + customer2 = model.create(name: 'Bob', age: 12) + customer3 = model.create(name: 'Bob', age: 9) + + expect(model.where(name: 'Bob', 'age.gte' => 11).all).to contain_exactly(customer1, customer2) + end + + it 'supports begins_with' do + model = Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :job_title, :string + end + + customer1 = model.create(name: 'Bob', job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(name: 'Bob', job_title: 'Environmental Project Manager') + customer3 = model.create(name: 'Bob', job_title: 'Creative Consultant') + + expect(model.where(name: 'Bob', 'job_title.begins_with' => 'Environmental').all) + .to contain_exactly(customer1, customer2) + end + + it 'supports between' do + customer1 = model.create(name: 'Bob', age: 10) + customer2 = model.create(name: 'Bob', age: 20) + customer3 = model.create(name: 'Bob', age: 30) + customer4 = model.create(name: 'Bob', age: 40) + + expect(model.where(name: 'Bob', 'age.between' => [19, 31]).all).to contain_exactly(customer2, customer3) + end + end + + # http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.QueryFilter.html?shortFooter=true + describe 'Query with not-keys conditions' do + let(:model) { + new_class do + table name: :customer, key: :name + range :last_name + field :age, :integer + end + } + + it 'supports eq' do + customer1 = model.create(name: 'a', last_name: 'a', age: 10) + customer2 = model.create(name: 'a', last_name: 'b', age: 30) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(name: 'a', age: '10').all).to contain_exactly(customer1) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to be_nil + expect(chain.index_name).to be_nil + end + + it 'supports eq for set' do + klass = new_class do + range :last_name + field :set, :set + end + + document1 = klass.create(id: 1, last_name: 'a', set: [1, 2].to_set) + document2 = klass.create(id: 1, last_name: 'b', set: [3, 4].to_set) + + chain = Dynamoid::Criteria::Chain.new(klass) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(id: 1, set: [1, 2].to_set).all).to contain_exactly(document1) + end + + it 'supports eq for array' do + klass = new_class do + range :last_name + field :array, :array + end + + document1 = klass.create(id: 1, last_name: 'a', array: [1, 2]) + document2 = klass.create(id: 1, last_name: 'b', array: [3, 4]) + + chain = Dynamoid::Criteria::Chain.new(klass) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(id: 1, array: [1, 2]).all).to contain_exactly(document1) + end + + it 'supports ne' do + customer1 = model.create(name: 'a', last_name: 'a', age: 5) + customer2 = model.create(name: 'a', last_name: 'b', age: 9) + + expect(model.where(name: 'a', 'age.ne' => 9).all).to contain_exactly(customer1) + end + + it 'supports lt' do + customer1 = model.create(name: 'a', last_name: 'a', age: 5) + customer2 = model.create(name: 'a', last_name: 'b', age: 9) + customer3 = model.create(name: 'a', last_name: 'c', age: 12) + + expect(model.where(name: 'a', 'age.lt' => 10).all).to contain_exactly(customer1, customer2) + end + + it 'supports gt' do + customer1 = model.create(name: 'a', last_name: 'a', age: 11) + customer2 = model.create(name: 'a', last_name: 'b', age: 12) + customer3 = model.create(name: 'a', last_name: 'c', age: 9) + + expect(model.where(name: 'a', 'age.gt' => 10).all).to contain_exactly(customer1, customer2) + end + + it 'supports lte' do + customer1 = model.create(name: 'a', last_name: 'a', age: 5) + customer2 = model.create(name: 'a', last_name: 'b', age: 9) + customer3 = model.create(name: 'a', last_name: 'c', age: 12) + + expect(model.where(name: 'a', 'age.lte' => 9).all).to contain_exactly(customer1, customer2) + end + + it 'supports gte' do + customer1 = model.create(name: 'a', last_name: 'a', age: 11) + customer2 = model.create(name: 'a', last_name: 'b', age: 12) + customer3 = model.create(name: 'a', last_name: 'c', age: 9) + + expect(model.where(name: 'a', 'age.gte' => 11).all).to contain_exactly(customer1, customer2) + end + + it 'supports begins_with' do + model = Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :last_name + field :job_title, :string + end + + customer1 = model.create(name: 'a', last_name: 'a', job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(name: 'a', last_name: 'b', job_title: 'Environmental Project Manager') + customer3 = model.create(name: 'a', last_name: 'c', job_title: 'Creative Consultant') + + expect(model.where(name: 'a', 'job_title.begins_with' => 'Environmental').all) + .to contain_exactly(customer1, customer2) + end + + it 'supports between' do + customer1 = model.create(name: 'a', last_name: 'a', age: 10) + customer2 = model.create(name: 'a', last_name: 'b', age: 20) + customer3 = model.create(name: 'a', last_name: 'c', age: 30) + customer4 = model.create(name: 'a', last_name: 'd', age: 40) + + expect(model.where(name: 'a', 'age.between' => [19, 31]).all).to contain_exactly(customer2, customer3) + end + + it 'supports in' do + customer1 = model.create(name: 'a', last_name: 'a', age: 10) + customer2 = model.create(name: 'a', last_name: 'b', age: 20) + customer3 = model.create(name: 'a', last_name: 'c', age: 30) + + expect(model.where(name: 'a', 'age.in' => [10, 20]).all).to contain_exactly(customer1, customer2) + end + + it 'supports contains' do + model = Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :last_name + field :job_title, :string + end + + customer1 = model.create(name: 'a', last_name: 'a', job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(name: 'a', last_name: 'b', job_title: 'Environmental Project Manager') + customer3 = model.create(name: 'a', last_name: 'c', job_title: 'Creative Consultant') + + expect(model.where(name: 'a', 'job_title.contains' => 'Consul').all) + .to contain_exactly(customer1, customer3) + end + + it 'supports not_contains' do + model = Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :last_name + field :job_title, :string + end + + customer1 = model.create(name: 'a', last_name: 'a', job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(name: 'a', last_name: 'b', job_title: 'Environmental Project Manager') + customer3 = model.create(name: 'a', last_name: 'c', job_title: 'Creative Consultant') + + expect(model.where(name: 'a', 'job_title.not_contains' => 'Consul').all) + .to contain_exactly(customer2) + end + end + + # http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.ScanFilter.html?shortFooter=true + describe 'Scan conditions ' do + let(:model) { + Class.new do + include Dynamoid::Document + table name: :customer + field :age, :integer + field :job_title, :string + end + } + + it 'supports eq' do + customer1 = model.create(age: 10) + customer2 = model.create(age: 30) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_scan).and_call_original + expect(chain.where(age: '10').all).to contain_exactly(customer1) + expect(chain.hash_key).to be_nil + expect(chain.range_key).to be_nil + expect(chain.index_name).to be_nil + end + + it 'supports eq for set' do + klass = new_class do + field :set, :set + end + document1 = klass.create(set: ['a', 'b']) + document2 = klass.create(set: ['b', 'c']) + + expect(klass.where(set: ['a', 'b'].to_set).all).to contain_exactly(document1) + end + + it 'supports eq for array' do + klass = new_class do + field :array, :array + end + document1 = klass.create(array: ['a', 'b']) + document2 = klass.create(array: ['b', 'c']) + + expect(klass.where(array: ['a', 'b']).all).to contain_exactly(document1) + end + + it 'supports ne' do + customer1 = model.create(age: 5) + customer2 = model.create(age: 9) + + expect(model.where('age.ne' => 9).all).to contain_exactly(customer1) + end + + it 'supports lt' do + customer1 = model.create(age: 5) + customer2 = model.create(age: 9) + customer3 = model.create(age: 12) + + expect(model.where('age.lt' => 10).all).to contain_exactly(customer1, customer2) + end + + it 'supports gt' do + customer1 = model.create(age: 11) + customer2 = model.create(age: 12) + customer3 = model.create(age: 9) + + expect(model.where('age.gt' => 10).all).to contain_exactly(customer1, customer2) + end + + it 'supports lte' do + customer1 = model.create(age: 5) + customer2 = model.create(age: 9) + customer3 = model.create(age: 12) + + expect(model.where('age.lte' => 9).all).to contain_exactly(customer1, customer2) + end + + it 'supports gte' do + customer1 = model.create(age: 11) + customer2 = model.create(age: 12) + customer3 = model.create(age: 9) + + expect(model.where('age.gte' => 11).all).to contain_exactly(customer1, customer2) + end + + it 'supports begins_with' do + customer1 = model.create(job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(job_title: 'Environmental Project Manager') + customer3 = model.create(job_title: 'Creative Consultant') + + expect(model.where('job_title.begins_with' => 'Environmental').all) + .to contain_exactly(customer1, customer2) + end + + it 'supports between' do + customer1 = model.create(age: 10) + customer2 = model.create(age: 20) + customer3 = model.create(age: 30) + customer4 = model.create(age: 40) + + expect(model.where('age.between' => [19, 31]).all).to contain_exactly(customer2, customer3) + end + + it 'supports in' do + customer1 = model.create(age: 10) + customer2 = model.create(age: 20) + customer3 = model.create(age: 30) + + expect(model.where('age.in' => [10, 20]).all).to contain_exactly(customer1, customer2) + end + + it 'supports contains' do + customer1 = model.create(job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(job_title: 'Environmental Project Manager') + customer3 = model.create(job_title: 'Creative Consultant') + + expect(model.where('job_title.contains' => 'Consul').all) + .to contain_exactly(customer1, customer3) + end + + it 'supports contains for set' do + klass = new_class do + field :set, :set + end + document1 = klass.create(set: ['a', 'b']) + document2 = klass.create(set: ['b', 'c']) + + expect(klass.where('set.contains' => 'a').all).to contain_exactly(document1) + expect(klass.where('set.contains' => 'b').all).to contain_exactly(document1, document2) + expect(klass.where('set.contains' => 'c').all).to contain_exactly(document2) + end + + it 'supports contains for array' do + klass = new_class do + field :array, :array + end + document1 = klass.create(array: ['a', 'b']) + document2 = klass.create(array: ['b', 'c']) + + expect(klass.where('array.contains' => 'a').all).to contain_exactly(document1) + expect(klass.where('array.contains' => 'b').all).to contain_exactly(document1, document2) + expect(klass.where('array.contains' => 'c').all).to contain_exactly(document2) + end + + it 'supports not_contains' do + customer1 = model.create(job_title: 'Environmental Air Quality Consultant') + customer2 = model.create(job_title: 'Environmental Project Manager') + customer3 = model.create(job_title: 'Creative Consultant') + + expect(model.where('job_title.not_contains' => 'Consul').all) + .to contain_exactly(customer2) + end + end + + describe 'Lazy loading' do + describe '.all' do + it 'does load result lazily' do + Vehicle.create + + expect(Dynamoid.adapter.client).to receive(:scan).exactly(0).times.and_call_original + Vehicle.record_limit(1).all + end + end + end + + describe 'local secondary indexes used for `where` clauses' do + let(:model) { + Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :range, :integer + field :range2, :integer + field :range3, :integer + + local_secondary_index range_key: :range2, name: :range2index + local_secondary_index range_key: :range3, name: :range3index + end + } + + before(:each) do + @customer1 = model.create(name: 'Bob', range: 1, range2: 11, range3: 111) + @customer2 = model.create(name: 'Bob', range: 2, range2: 22, range3: 222) + @customer3 = model.create(name: 'Bob', range: 3, range2: 33, range3: 333) + end + + it 'supports query on local secondary index but always defaults to table range key' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:name => 'Bob', 'range.lt' => 3, 'range2.gt' => 15).count).to eq(1) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to eq(:range) + expect(chain.index_name).to be_nil + end + + it 'supports query on local secondary index' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:name => 'Bob', 'range2.gt' => 15).count).to eq(2) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to eq(:range2) + expect(chain.index_name).to eq(:range2index) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:name => 'Bob', 'range3.lt' => 200).count).to eq(1) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to eq(:range3) + expect(chain.index_name).to eq(:range3index) + end + + it 'supports query on local secondary index with start' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:name => 'Bob', 'range2.gt' => 15).count).to eq(2) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to eq(:range2) + expect(chain.index_name).to eq(:range2index) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:name => 'Bob', 'range2.gt' => 15).start(@customer2).all).to contain_exactly(@customer3) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to eq(:range2) + expect(chain.index_name).to eq(:range2index) + end + end + + describe 'global secondary indexes used for `where` clauses' do + it 'does not use global secondary index if does not project all attributes' do + model = Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :customerid, :integer + field :city + field :age, :integer + field :gender + + global_secondary_index hash_key: :city, range_key: :age, name: :cityage + end + + customer1 = model.create(name: 'Bob', city: 'San Francisco', age: 10, gender: 'male', customerid: 1) + customer2 = model.create(name: 'Jeff', city: 'San Francisco', age: 15, gender: 'male', customerid: 2) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_scan).and_call_original + expect(chain.where(city: 'San Francisco').count).to eq(2) + # Does not use GSI since not projecting all attributes + expect(chain.hash_key).to be_nil + expect(chain.range_key).to be_nil + expect(chain.index_name).to be_nil + end + + context 'with full composite key for table' do + let(:model) { + Class.new do + include Dynamoid::Document + table name: :customer, key: :name + range :customerid, :integer + field :city + field :email + field :age, :integer + field :gender + + global_secondary_index hash_key: :city, range_key: :age, name: :cityage, projected_attributes: :all + global_secondary_index hash_key: :email, range_key: :age, name: :emailage, projected_attributes: :all + end + } + + before(:each) do + @customer1 = model.create(name: 'Bob', city: 'San Francisco', email: 'bob@test.com', age: 10, gender: 'male', + customerid: 1) + @customer2 = model.create(name: 'Jeff', city: 'San Francisco', email: 'jeff@test.com', age: 15, gender: 'male', + customerid: 2) + @customer3 = model.create(name: 'Mark', city: 'San Francisco', email: 'mark@test.com', age: 20, gender: 'male', + customerid: 3) + @customer4 = model.create(name: 'Greg', city: 'New York', email: 'greg@test.com', age: 25, gender: 'male', + customerid: 4) + end + + it 'supports query on global secondary index but always defaults to table hash key' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(name: 'Bob').count).to eq(1) + expect(chain.hash_key).to eq(:name) + expect(chain.range_key).to be_nil + expect(chain.index_name).to be_nil + end + + it 'supports query on global secondary index' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(city: 'San Francisco').count).to eq(3) + expect(chain.hash_key).to eq(:city) + expect(chain.range_key).to eq(:age) + expect(chain.index_name).to eq(:cityage) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:city => 'San Francisco', 'age.gt' => 12).count).to eq(2) + expect(chain.hash_key).to eq(:city) + expect(chain.range_key).to eq(:age) + expect(chain.index_name).to eq(:cityage) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(email: 'greg@test.com').count).to eq(1) + expect(chain.hash_key).to eq(:email) + expect(chain.range_key).to eq(:age) + expect(chain.index_name).to eq(:emailage) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(:email => 'greg@test.com', 'age.gt' => 12).count).to eq(1) + expect(chain.hash_key).to eq(:email) + expect(chain.range_key).to eq(:age) + expect(chain.index_name).to eq(:emailage) + end + + it 'supports scan when no global secondary index available' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_scan).and_call_original + expect(chain.where(gender: 'male').count).to eq(4) + expect(chain.hash_key).to be_nil + expect(chain.range_key).to be_nil + expect(chain.index_name).to be_nil + end + + it 'supports query on global secondary index with start' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(city: 'San Francisco').count).to eq(3) + expect(chain.hash_key).to eq(:city) + expect(chain.range_key).to eq(:age) + expect(chain.index_name).to eq(:cityage) + + # Now query with start at customer2 and we should only see customer3 + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(city: 'San Francisco').start(@customer2).all).to contain_exactly(@customer3) + # Repeat with hash notation + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(city: 'San Francisco').start({city: @customer2.city, age: @customer2.age, name: @customer2.name, customerid: @customer2.customerid}).all).to contain_exactly(@customer3) + end + + it 'supports scan with start on hash key & range key' do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_scan).and_call_original + expect(chain.scan_limit(1).start(@customer2)).to contain_exactly(@customer4) + # Repeat with hash notation + expect(chain).to receive(:records_via_scan).and_call_original + expect(chain.scan_limit(1).start({name: @customer2.name, customerid: @customer2.customerid})).to contain_exactly(@customer4) + end + + it "does not use index if a condition for index hash key is other than 'equal'" do + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_scan).and_call_original + expect(chain.where('city.begins_with' => 'San').count).to eq(3) + expect(chain.hash_key).to be_nil + expect(chain.range_key).to be_nil + expect(chain.index_name).to be_nil + end + end + + it 'supports query on global secondary index with correct start key without table range key' do + model = Class.new do + include Dynamoid::Document + table name: :customer, key: :name + field :city + field :age, :integer + + global_secondary_index hash_key: :city, range_key: :age, name: :cityage, projected_attributes: :all + end + + customer1 = model.create(name: 'Bob', city: 'San Francisco', age: 10) + customer2 = model.create(name: 'Jeff', city: 'San Francisco', age: 15) + + chain = Dynamoid::Criteria::Chain.new(model) + expect(chain).to receive(:records_via_query).and_call_original + expect(chain.where(city: 'San Francisco').start(customer1).all).to contain_exactly(customer2) + end + end + + describe 'type casting in `where` clause' do + it 'casts datetime' do + model = Class.new do + include Dynamoid::Document + table name: :customers + + field :activated_at, :datetime + end + + customer1 = model.create(activated_at: Time.now) + customer2 = model.create(activated_at: Time.now - 1.hour) + customer3 = model.create(activated_at: Time.now - 2.hour) + + expect( + model.where('activated_at.gt' => Time.now - 1.5.hours).all + ).to contain_exactly(customer1, customer2) + end + + it 'casts date' do + model = Class.new do + include Dynamoid::Document + table name: :customers + + field :registered_on, :date + end + + customer1 = model.create(registered_on: Date.today) + customer2 = model.create(registered_on: Date.today - 2.day) + customer3 = model.create(registered_on: Date.today - 4.days) + + expect( + model.where('registered_on.gt' => Date.today - 3.days).all + ).to contain_exactly(customer1, customer2) + end + + it 'casts array elements' do + model = Class.new do + include Dynamoid::Document + table name: :customers + + field :birthday, :date + end + + customer1 = model.create(birthday: '1978-08-21'.to_date) + customer2 = model.create(birthday: '1984-05-13'.to_date) + customer3 = model.create(birthday: '1991-11-28'.to_date) + + expect( + model.where('birthday.between' => ['1980-01-01'.to_date, '1990-01-01'.to_date]).all + ).to contain_exactly(customer2) + end + + context 'Query' do + it 'casts partition key `equal` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers, key: :registered_on + + field :registered_on, :date + end + + customer1 = model.create(registered_on: Date.today) + customer2 = model.create(registered_on: Date.today - 2.day) + + expect( + model.where(registered_on: Date.today).all + ).to contain_exactly(customer1) + end + + it 'casts sort key `equal` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers, key: :first_name + + field :first_name + range :registered_on, :date + end + + customer1 = model.create(first_name: 'Alice', registered_on: Date.today) + customer2 = model.create(first_name: 'Alice', registered_on: Date.today - 2.day) + + expect( + model.where(first_name: 'Alice', registered_on: Date.today).all + ).to contain_exactly(customer1) + end + + it 'casts sort key `range` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers, key: :first_name + + field :first_name + range :registered_on, :date + end + + customer1 = model.create(first_name: 'Alice', registered_on: Date.today) + customer2 = model.create(first_name: 'Alice', registered_on: Date.today - 2.day) + customer3 = model.create(first_name: 'Alice', registered_on: Date.today - 4.days) + + expect( + model.where(first_name: 'Alice', 'registered_on.gt' => Date.today - 3.days).all + ).to contain_exactly(customer1, customer2) + end + + it 'casts non-key field `equal` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers, key: :first_name + + field :first_name + range :last_name + field :registered_on, :date # <==== not range key + end + + customer1 = model.create(first_name: 'Alice', last_name: 'Cooper', registered_on: Date.today) + customer2 = model.create(first_name: 'Alice', last_name: 'Morgan', registered_on: Date.today - 2.day) + + expect( + model.where(first_name: 'Alice', registered_on: Date.today).all + ).to contain_exactly(customer1) + end + + it 'casts non-key field `range` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers, key: :first_name + + field :first_name + range :last_name + field :registered_on, :date # <==== not range key + end + + customer1 = model.create(first_name: 'Alice', last_name: 'Cooper', registered_on: Date.today) + customer2 = model.create(first_name: 'Alice', last_name: 'Morgan', registered_on: Date.today - 2.day) + customer3 = model.create(first_name: 'Alice', last_name: 'Smit', registered_on: Date.today - 4.days) + + expect( + model.where(first_name: 'Alice', 'registered_on.gt' => Date.today - 3.days).all + ).to contain_exactly(customer1, customer2) + end + end + + context 'Scan' do + it 'casts field for `equal` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers + + field :birthday, :date + end + + customer1 = model.create(birthday: '1978-08-21'.to_date) + customer2 = model.create(birthday: '1984-05-13'.to_date) + + expect(model.where(birthday: '1978-08-21').all).to contain_exactly(customer1) + end + + it 'casts field for `range` condition' do + model = Class.new do + include Dynamoid::Document + table name: :customers + + field :birthday, :date + end + + customer1 = model.create(birthday: '1978-08-21'.to_date) + customer2 = model.create(birthday: '1984-05-13'.to_date) + + expect(model.where('birthday.gt' => '1980-01-01').all).to contain_exactly(customer2) + end + end + end + + context 'single table inheritance' do + describe 'where' do + it 'honors STI' do + Vehicle.create(description: 'Description') + car = Car.create(description: 'Description') + + expect(Car.where(description: 'Description').all.to_a).to eq [car] + end + end + + describe 'all' do + it 'honors STI' do + Vehicle.create(description: 'Description') + car = Car.create + + expect(Car.all.to_a).to eq [car] + end + end + end + + describe '#delete_all' do + it 'deletes in batch' do + klass = new_class + klass.create! + + chain = Dynamoid::Criteria::Chain.new(klass) + + expect(Dynamoid.adapter.client).to receive(:batch_write_item).and_call_original + chain.delete_all + end + + context 'when some conditions specified' do + it 'deletes only proper items' do + klass = new_class do + field :title + end + + document1 = klass.create!(title: 'Doc #1') + klass.create!(title: 'Doc #2') + document3 = klass.create!(title: 'Doc #3') + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {title: 'Doc #2'} + + expect { chain.delete_all }.to change { klass.count }.by(-1) + expect(klass.all).to contain_exactly(document1, document3) + end + + it 'loads items with Query if can' do + klass = new_class do + range :title + end + + document = klass.create!(title: 'Doc #1') + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {id: document.id} + + expect(Dynamoid.adapter.client).to receive(:query).and_call_original + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + + it 'loads items with Scan if cannot use Query' do + klass = new_class do + range :title + field :author + end + + klass.create!(title: "The Cuckoo's Calling", author: 'J. K. Rowling') + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {author: 'J. K. Rowling'} + + expect(Dynamoid.adapter.client).to receive(:scan).and_call_original + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + + context 'Query (partition key specified)' do + it 'works well with composite primary key' do + klass = new_class do + range :title + end + + document = klass.create!(title: 'Doc #1') + klass.create!(title: 'Doc #2') + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {id: document.id} + + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + + it 'works well when there is partition key only' do + klass = new_class do + field :title + end + + document = klass.create! + klass.create! + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {id: document.id} + + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + end + + context 'Scan (partition key is not specified)' do + it 'works well with composite primary key' do + klass = new_class do + range :title + end + + klass.create!(title: 'Doc #1') + klass.create!(title: 'Doc #2') + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {title: 'Doc #1'} + + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + + it 'works well when there is partition key only' do + klass = new_class do + field :title + end + + klass.create!(title: 'Doc #1') + klass.create!(title: 'Doc #2') + + chain = Dynamoid::Criteria::Chain.new(klass) + chain.query = {title: 'Doc #1'} + + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + end + end + + context 'there are no conditions' do + it 'deletes all the items' do + klass = new_class do + field :title + end + + 3.times { klass.create! } + chain = Dynamoid::Criteria::Chain.new(klass) + expect { chain.delete_all }.to change { klass.count }.from(3).to(0) + end + + context 'Scan' do + it 'works well with composite primary key' do + klass = new_class do + range :title + end + + klass.create!(title: 'Doc #1') + chain = Dynamoid::Criteria::Chain.new(klass) + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + + it 'works well when there is partition key only' do + klass = new_class + + klass.create! + chain = Dynamoid::Criteria::Chain.new(klass) + expect { chain.delete_all }.to change { klass.count }.by(-1) + end + end + end end describe 'User' do let(:chain) { described_class.new(User) } it 'defines each' do - chain.query = {:name => 'Josh'} + chain.query = {name: 'Josh'} chain.each {|u| u.update_attribute(:name, 'Justin')} expect(User.find(user.id).name).to eq 'Justin' end it 'includes Enumerable' do - chain.query = {:name => 'Josh'} + chain.query = {name: 'Josh'} expect(chain.collect {|u| u.name}).to eq ['Josh'] end end describe 'Tweet' do - let!(:tweet1) { Tweet.create(:tweet_id => "x", :group => "one") } - let!(:tweet2) { Tweet.create(:tweet_id => "x", :group => "two") } - let!(:tweet3) { Tweet.create(:tweet_id => "xx", :group => "two") } + let!(:tweet1) { Tweet.create(tweet_id: 'x', group: 'one') } + let!(:tweet2) { Tweet.create(tweet_id: 'x', group: 'two') } + let!(:tweet3) { Tweet.create(tweet_id: 'xx', group: 'two') } let(:tweets) { [tweet1, tweet2, tweet3] } let(:chain) { Dynamoid::Criteria::Chain.new(Tweet) } it 'limits evaluated records' do chain.query = {} - expect(chain.eval_limit(1).count).to eq 1 + expect(chain.record_limit(1).count).to eq 1 + expect(chain.record_limit(2).count).to eq 2 end it 'finds tweets with a start' do - chain.query = { :tweet_id => "x" } + chain.query = { tweet_id: 'x' } chain.start(tweet1) expect(chain.count).to eq 1 expect(chain.first).to eq tweet2 end it 'finds one specific tweet' do - chain.query = { :tweet_id => "xx", :group => "two" } - expect(chain.all).to eq [tweet3] + chain.query = { tweet_id: 'xx', group: 'two' } + expect(chain.all.to_a).to eq [tweet3] end - it 'finds posts with "where" method' do + it 'finds posts with "where" method with "gt" query' do ts_epsilon = 0.001 # 1 ms time = DateTime.now - post1 = Post.create(:post_id => 'x', :posted_at => time) - post2 = Post.create(:post_id => 'x', :posted_at => (time + 1.hour)) + post1 = Post.create(post_id: 'x', posted_at: time) + post2 = Post.create(post_id: 'x', posted_at: (time + 1.hour)) chain = Dynamoid::Criteria::Chain.new(Post) - query = { :post_id => "x", "posted_at.gt" => time + ts_epsilon } + query = { :post_id => 'x', 'posted_at.gt' => (time + ts_epsilon) } resultset = chain.send(:where, query) expect(resultset.count).to eq 1 stored_record = resultset.first @@ -93,21 +1134,38 @@ expect(stored_record.attributes[:updated_at]).to be_within(ts_epsilon).of(post2.attributes[:updated_at]) end - describe 'destroy' do - it 'destroys tweet with a range simple range query' do - chain.query = { :tweet_id => "x" } - expect(chain.all.size).to eq 2 - chain.destroy_all - expect(chain.consistent.all.size).to eq 0 - end + it 'finds posts with "where" method with "lt" query' do + ts_epsilon = 0.001 # 1 ms + time = DateTime.now + post1 = Post.create(post_id: 'x', posted_at: time) + post2 = Post.create(post_id: 'x', posted_at: (time + 1.hour)) + chain = Dynamoid::Criteria::Chain.new(Post) + query = { :post_id => 'x', 'posted_at.lt' => (time + 1.hour - ts_epsilon) } + resultset = chain.send(:where, query) + expect(resultset.count).to eq 1 + stored_record = resultset.first + expect(stored_record.attributes[:post_id]).to eq post2.attributes[:post_id] + # Must use an epsilon to compare timestamps after round-trip: https://github.com/Dynamoid/Dynamoid/issues/2 + expect(stored_record.attributes[:created_at]).to be_within(ts_epsilon).of(post1.attributes[:created_at]) + expect(stored_record.attributes[:posted_at]).to be_within(ts_epsilon).of(post1.attributes[:posted_at]) + expect(stored_record.attributes[:updated_at]).to be_within(ts_epsilon).of(post1.attributes[:updated_at]) + end - it 'deletes one specific tweet with range' do - chain = Dynamoid::Criteria::Chain.new(Tweet) - chain.query = { :tweet_id => "xx", :group => "two" } - expect(chain.all.size).to eq 1 - chain.destroy_all - expect(chain.consistent.all.size).to eq 0 - end + it 'finds posts with "where" method with "between" query' do + ts_epsilon = 0.001 # 1 ms + time = DateTime.now + post1 = Post.create(post_id: 'x', posted_at: time) + post2 = Post.create(post_id: 'x', posted_at: (time + 1.hour)) + chain = Dynamoid::Criteria::Chain.new(Post) + query = { :post_id => 'x', 'posted_at.between' => [time - ts_epsilon, time + ts_epsilon]} + resultset = chain.send(:where, query) + expect(resultset.count).to eq 1 + stored_record = resultset.first + expect(stored_record.attributes[:post_id]).to eq post2.attributes[:post_id] + # Must use an epsilon to compare timestamps after round-trip: https://github.com/Dynamoid/Dynamoid/issues/2 + expect(stored_record.attributes[:created_at]).to be_within(ts_epsilon).of(post1.attributes[:created_at]) + expect(stored_record.attributes[:posted_at]).to be_within(ts_epsilon).of(post1.attributes[:posted_at]) + expect(stored_record.attributes[:updated_at]).to be_within(ts_epsilon).of(post1.attributes[:updated_at]) end describe 'batch queries' do diff --git a/spec/dynamoid/criteria_spec.rb b/spec/dynamoid/criteria_spec.rb index d9da0090..606473b8 100644 --- a/spec/dynamoid/criteria_spec.rb +++ b/spec/dynamoid/criteria_spec.rb @@ -1,17 +1,35 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe Dynamoid::Criteria do - let!(:user1) {User.create(:name => 'Josh', :email => 'josh@joshsymonds.com')} - let!(:user2) {User.create(:name => 'Justin', :email => 'justin@joshsymonds.com')} + let!(:user1) {User.create(name: 'Josh', email: 'josh@joshsymonds.com', admin: true)} + let!(:user2) {User.create(name: 'Justin', email: 'justin@joshsymonds.com', admin: false)} it 'finds first using where' do - expect(User.where(:name => 'Josh').first).to eq user1 + expect(User.where(name: 'Josh').first).to eq user1 end - + + it 'finds last using where' do + expect(User.where(admin: false).last).to eq user2 + end + it 'finds all using where' do - expect(User.where(:name => 'Josh').all).to eq [user1] + expect(User.where(name: 'Josh').all.to_a).to eq [user1] + end + + context 'transforms booleans' do + it 'accepts native' do + expect(User.where(admin: 't').all.to_a).to eq [user1] + end + + it 'accepts string' do + expect(User.where(admin: 'true').all.to_a).to eq [user1] + end + + it 'accepts boolean' do + expect(User.where(admin: true).all.to_a).to eq [user1] + end end - + it 'returns all records' do expect(Set.new(User.all)).to eq Set.new([user1, user2]) expect(User.all.first.new_record).to be_falsey @@ -23,11 +41,11 @@ end it 'returns empty attributes for where' do - expect(Magazine.where(:name => 'Josh').all).to eq [] + expect(Magazine.where(title: 'Josh').all.to_a).to eq [] end it 'returns empty attributes for all' do - expect(Magazine.all).to eq [] + expect(Magazine.all.to_a).to eq [] end end @@ -39,8 +57,8 @@ end it 'returns N records' do - 5.times { |i| User.create(:name => 'Josh', :email => 'josh_#{i}@joshsymonds.com') } - expect(User.eval_limit(2).all.size).to eq(2) + 5.times { |i| User.create(name: 'Josh', email: 'josh_#{i}@joshsymonds.com') } + expect(User.record_limit(2).all.count).to eq(2) end # TODO This test is broken using the AWS SDK adapter. @@ -54,21 +72,40 @@ #end it 'send consistent option to adapter' do - pending "This test is broken as we are overriding the consistent_read option to true inside the adapter" + pending 'This test is broken as we are overriding the consistent_read option to true inside the adapter' expect(Dynamoid::Adapter).to receive(:get_item) { |table_name, key, options| options[:consistent_read] == true } - User.where(:name => 'x').consistent.first + User.where(name: 'x').consistent.first expect(Dynamoid::Adapter).to receive(:query) { |table_name, options| options[:consistent_read] == true }.returns([]) - Tweet.where(:tweet_id => 'xx', :group => 'two').consistent.all + Tweet.where(tweet_id: 'xx', group: 'two').consistent.all expect(Dynamoid::Adapter).to receive(:query) { |table_name, options| options[:consistent_read] == false }.returns([]) - Tweet.where(:tweet_id => 'xx', :group => 'two').all + Tweet.where(tweet_id: 'xx', group: 'two').all end - it 'raises exception when consistent_read is used with scan' do + it 'does not raise exception when consistent_read is used with scan' do expect do - User.where(:password => 'password').consistent.first - end.to raise_error(Dynamoid::Errors::InvalidQuery) + User.where(password: 'password').consistent.first + end.not_to raise_error(Dynamoid::Errors::InvalidQuery) + end + + context 'when scans and warn_on_scan config option is true' do + before do + @warn_on_scan = Dynamoid::Config.warn_on_scan + Dynamoid::Config.warn_on_scan = true + end + after do + Dynamoid::Config.warn_on_scan = @warn_on_scan + end + + it 'logs warnings' do + expect(Dynamoid.logger).to receive(:warn).with('Queries without an index are forced to use scan and are generally much slower than indexed queries!') + expect(Dynamoid.logger).to receive(:warn).with("You can index this query by adding index declaration to user.rb:") + expect(Dynamoid.logger).to receive(:warn).with("* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'") + expect(Dynamoid.logger).to receive(:warn).with("* local_secondary_indexe range_key: 'some-name'") + expect(Dynamoid.logger).to receive(:warn).with("Not indexed attributes: :name, :password") + + User.where(name: 'x', password: 'password').all + end end - end diff --git a/spec/dynamoid/dirty_spec.rb b/spec/dynamoid/dirty_spec.rb index d175bce0..803905aa 100644 --- a/spec/dynamoid/dirty_spec.rb +++ b/spec/dynamoid/dirty_spec.rb @@ -9,30 +9,30 @@ end it 'is not empty' do - tweet = Tweet.new(:tweet_id => "1", :group => 'abc') + tweet = Tweet.new(tweet_id: '1', group: 'abc') expect(tweet).to be_changed expect(tweet.group_was).to be_nil end it 'is empty when loaded from database' do - Tweet.create!(:tweet_id => "1", :group => 'abc') - tweet = Tweet.where(:tweet_id => "1", :group => 'abc').first + Tweet.create!(tweet_id: '1', group: 'abc') + tweet = Tweet.where(tweet_id: '1', group: 'abc').first expect(tweet).to_not be_changed tweet.group = 'abc' tweet.reload expect(tweet).to_not be_changed end - + it 'is empty after an update' do - tweet = Tweet.create!(:tweet_id => "1", :group => 'abc') + tweet = Tweet.create!(tweet_id: '1', group: 'abc') tweet.update! do |t| - t.set(msg: "foo") + t.set(msg: 'foo') end expect(tweet).to_not be_changed end it 'tracks changes after saves' do - tweet = Tweet.new(:tweet_id => "1", :group => 'abc') + tweet = Tweet.new(tweet_id: '1', group: 'abc') tweet.save! expect(tweet).to_not be_changed @@ -47,7 +47,7 @@ end it 'clears changes on save' do - tweet = Tweet.new(:tweet_id => "1", :group => 'abc') + tweet = Tweet.new(tweet_id: '1', group: 'abc') tweet.group = 'xyz' expect(tweet.group_changed?).to be_truthy tweet.save! diff --git a/spec/dynamoid/document_spec.rb b/spec/dynamoid/document_spec.rb index 6f3abef2..d78221a8 100644 --- a/spec/dynamoid/document_spec.rb +++ b/spec/dynamoid/document_spec.rb @@ -6,15 +6,17 @@ address = Address.new expect(address.new_record).to be_truthy - expect(address.attributes).to eq({id: nil, - created_at: nil, - updated_at: nil, - city: nil, - options: nil, - deliverable: nil, - latitude: nil, - info: nil, - lock_version: nil}) + expect(address.attributes).to eq(id: nil, + created_at: nil, + updated_at: nil, + city: nil, + options: nil, + deliverable: nil, + latitude: nil, + config: nil, + info: nil, + registered_on: nil, + lock_version: nil) end it 'responds to will_change! methods for all fields' do @@ -26,90 +28,352 @@ end it 'initializes a new document with attributes' do - address = Address.new(:city => 'Chicago') + address = Address.new(city: 'Chicago') expect(address.new_record).to be_truthy - expect(address.attributes).to eq({id: nil, - created_at: nil, - updated_at: nil, - city: 'Chicago', - options: nil, - deliverable: nil, - latitude: nil, - info: nil, - lock_version: nil}) + expect(address.attributes).to eq(id: nil, + created_at: nil, + updated_at: nil, + city: 'Chicago', + options: nil, + deliverable: nil, + latitude: nil, + info: nil, + config: nil, + registered_on: nil, + lock_version: nil) end it 'initializes a new document with a virtual attribute' do - address = Address.new(:zip_code => '12345') + address = Address.new(zip_code: '12345') expect(address.new_record).to be_truthy - expect(address.attributes).to eq({id: nil, - created_at: nil, - updated_at: nil, - city: 'Chicago', - options: nil, - deliverable: nil, - latitude: nil, - info: nil, - lock_version: nil}) + expect(address.attributes).to eq(id: nil, + created_at: nil, + updated_at: nil, + city: 'Chicago', + options: nil, + deliverable: nil, + latitude: nil, + info: nil, + config: nil, + registered_on: nil, + lock_version: nil) end it 'allows interception of write_attribute on load' do - class Model + klass = Class.new do include Dynamoid::Document field :city def city=(value); self[:city] = value.downcase; end end - expect(Model.new(:city => "Chicago").city).to eq "chicago" + expect(klass.new(city: 'Chicago').city).to eq 'chicago' end it 'ignores unknown fields (does not raise error)' do - class Model + klass = Class.new do include Dynamoid::Document field :city end - model = Model.new(:unknown_field => "test", :city => "Chicago") - expect(model.city).to eql "Chicago" + model = klass.new(unknown_field: 'test', city: 'Chicago') + expect(model.city).to eql 'Chicago' end it 'creates a new document' do - address = Address.create(:city => 'Chicago') + address = Address.create(city: 'Chicago') expect(address.new_record).to be_falsey expect(address.id).to_not be_nil end + it 'creates multiple documents' do + addresses = Address.create([{city: 'Chicago'}, {city: 'New York'}]) + + expect(addresses.size).to eq 2 + expect(addresses.all?(&:persisted?)).to be true + expect(addresses[0].city).to eq 'Chicago' + expect(addresses[1].city).to eq 'New York' + end + + it 'raises error when tries to save multiple invalid objects' do + klass = Class.new do + include Dynamoid::Document + field :city + validates :city, presence: true + + def self.name; 'Address'; end + end + + expect { + klass.create!([{city: 'Chicago'}, {city: nil}]) + }.to raise_error(Dynamoid::Errors::DocumentNotValid) + end + it 'knows if a document exists or not' do - address = Address.create(:city => 'Chicago') + address = Address.create(city: 'Chicago') expect(Address.exists?(address.id)).to be_truthy - expect(Address.exists?("does-not-exist")).to be_falsey - expect(Address.exists?(:city => address.city)).to be_truthy - expect(Address.exists?(:city => "does-not-exist")).to be_falsey + expect(Address.exists?('does-not-exist')).to be_falsey + expect(Address.exists?(city: address.city)).to be_truthy + expect(Address.exists?(city: 'does-not-exist')).to be_falsey end it 'gets errors courtesy of ActiveModel' do - address = Address.create(:city => 'Chicago') + address = Address.create(city: 'Chicago') expect(address.errors).to be_empty expect(address.errors.full_messages).to be_empty end + describe '.update' do + let(:document_class) do + new_class do + field :name + + validates :name, presence: true, length: { minimum: 5 } + def self.name; 'Document' end + end + end + + it 'loads and saves document' do + d = document_class.create(name: 'Document#1') + + expect { + document_class.update(d.id, name: '[Updated]') + }.to change { d.reload.name }.from('Document#1').to('[Updated]') + end + + it 'returns updated document' do + d = document_class.create(name: 'Document#1') + d2 = document_class.update(d.id, name: '[Updated]') + + expect(d2).to be_a(document_class) + expect(d2.name).to eq '[Updated]' + end + + it 'does not save invalid document' do + d = document_class.create(name: 'Document#1') + d2 = nil + + expect { + d2 = document_class.update(d.id, name: '[Up') + }.not_to change { d.reload.name } + expect(d2).not_to be_valid + end + + it 'accepts range key value if document class declares it' do + klass = new_class do + field :name + range :status + end + + d = klass.create(status: 'old', name: 'Document#1') + expect { + klass.update(d.id, 'old', name: '[Updated]') + }.to change { d.reload.name }.to('[Updated]') + end + + it 'converts range key value to proper format' do + klass = new_class do + field :name + range :activated_on, :date + field :another_date, :datetime + end + + d = klass.create(activated_on: '2018-01-14'.to_date, name: 'Document#1') + expect { + klass.update(d.id, '2018-01-14'.to_date, name: '[Updated]') + }.to change { d.reload.name }.to('[Updated]') + end + end + + describe '.update_fields' do + let(:document_class) do + new_class do + field :title + field :version, :integer + field :published_on, :date + end + end + + it 'changes field value' do + obj = document_class.create(title: 'Old title') + expect { + document_class.update_fields(obj.id, title: 'New title') + }.to change { document_class.find(obj.id).title }.from('Old title').to('New title') + end + + it 'changes field value to nil' do + obj = document_class.create(title: 'New Document') + expect { + document_class.update_fields(obj.id, title: nil) + }.to change { document_class.find(obj.id).title }.from('New Document').to(nil) + end + + it 'returns updated document' do + obj = document_class.create(title: 'Old title') + result = document_class.update_fields(obj.id, title: 'New title') + + expect(result.id).to eq obj.id + expect(result.title).to eq 'New title' + end + + it 'checks the conditions on update' do + obj = document_class.create(title: 'Old title', version: 1) + expect { + document_class.update_fields(obj.id, { title: 'New title' }, if: { version: 1 }) + }.to change { document_class.find(obj.id).title }.to('New title') + + obj = document_class.create(title: 'Old title', version: 1) + expect { + result = document_class.update_fields(obj.id, { title: 'New title' }, if: { version: 6 }) + }.not_to change { document_class.find(obj.id).title } + end + + it 'does not create new document if it does not exist yet' do + document_class.create_table + + expect { + document_class.update_fields('some-fake-id', title: 'Title') + }.not_to change { document_class.count } + end + + it 'accepts range key if it is declared' do + document_class_with_range = new_class do + field :title + range :category + end + + obj = document_class_with_range.create(category: 'New') + + expect { + document_class_with_range.update_fields(obj.id, 'New', title: '[Updated]') + }.to change { + document_class_with_range.find(obj.id, range_key: 'New').title + }.to('[Updated]') + end + + it 'converts range key value' do + document_class_with_range = new_class do + field :title + range :published_on, :date + end + + obj = document_class_with_range.create(title: 'Old', published_on: '2018-02-23'.to_date) + document_class_with_range.update_fields(obj.id, '2018-02-23'.to_date, title: 'New') + expect(obj.reload.title).to eq 'New' + end + + it 'converts attributes values' do + obj = document_class.create + document_class.update_fields(obj.id, published_on: '2018-02-23'.to_date) + attributes = Dynamoid.adapter.get_item(document_class.table_name, obj.id) + expect(attributes[:published_on]).to eq 17585 + end + end + + describe '.upsert' do + let(:document_class) do + new_class do + field :title + field :version, :integer + field :published_on, :date + end + end + + it 'changes field value' do + obj = document_class.create(title: 'Old title') + expect { + document_class.upsert(obj.id, title: 'New title') + }.to change { document_class.find(obj.id).title }.from('Old title').to('New title') + end + + it 'changes field value to nil' do + obj = document_class.create(title: 'New Document') + expect { + document_class.upsert(obj.id, title: nil) + }.to change { document_class.find(obj.id).title }.from('New Document').to(nil) + end + + it 'returns updated document' do + obj = document_class.create(title: 'Old title') + result = document_class.upsert(obj.id, title: 'New title') + + expect(result.id).to eq obj.id + expect(result.title).to eq 'New title' + end + + it 'checks the conditions on update' do + obj = document_class.create(title: 'Old title', version: 1) + expect { + document_class.upsert(obj.id, { title: 'New title' }, if: { version: 1 }) + }.to change { document_class.find(obj.id).title }.to('New title') + + obj = document_class.create(title: 'Old title', version: 1) + expect { + result = document_class.upsert(obj.id, { title: 'New title' }, if: { version: 6 }) + }.not_to change { document_class.find(obj.id).title } + end + + it 'creates new document if it does not exist yet' do + document_class.create_table + + expect { + document_class.upsert('not-existed-id', title: 'Title') + }.to change { document_class.count } + + obj = document_class.find('not-existed-id') + expect(obj.title).to eq 'Title' + end + + it 'accepts range key if it is declared' do + document_class_with_range = new_class do + field :title + range :category + end + + obj = document_class_with_range.create(category: 'New') + + expect { + document_class_with_range.upsert(obj.id, 'New', title: '[Updated]') + }.to change { + document_class_with_range.find(obj.id, range_key: 'New').title + }.to('[Updated]') + end + + it 'converts range key value' do + document_class_with_range = new_class do + field :title + range :published_on, :date + end + + obj = document_class_with_range.create(title: 'Old', published_on: '2018-02-23'.to_date) + document_class_with_range.upsert(obj.id, '2018-02-23'.to_date, title: 'New') + expect(obj.reload.title).to eq 'New' + end + + it 'converts attributes values' do + obj = document_class.create + document_class.upsert(obj.id, published_on: '2018-02-23'.to_date) + attributes = Dynamoid.adapter.get_item(document_class.table_name, obj.id) + expect(attributes[:published_on]).to eq 17585 + end + end + context '.reload' do let(:address){ Address.create } - let(:message){ Message.create({:text => 'Nice, supporting datetime range!', :time => Time.now.to_datetime}) } - let(:tweet){ tweet = Tweet.create(:tweet_id => 'x', :group => 'abc') } + let(:message){ Message.create(text: 'Nice, supporting datetime range!', time: Time.now.to_datetime) } + let(:tweet){ tweet = Tweet.create(tweet_id: 'x', group: 'abc') } it 'reflects persisted changes' do - address.update_attributes(:city => 'Chicago') + address.update_attributes(city: 'Chicago') expect(address.reload.city).to eq 'Chicago' end it 'uses a :consistent_read' do - expect(Tweet).to receive(:find).with(tweet.hash_key, {:range_key => tweet.range_value, :consistent_read => true}).and_return(tweet) + expect(Tweet).to receive(:find).with(tweet.hash_key, range_key: tweet.range_value, consistent_read: true).and_return(tweet) tweet.reload end @@ -133,7 +397,7 @@ class Model end it 'follows any table options provided to it' do - tweet = Tweet.create(:group => 12345) + tweet = Tweet.create(group: 12345) expect{tweet.id}.to raise_error(NoMethodError) expect(tweet.tweet_id).to_not be_nil @@ -165,7 +429,7 @@ class Model end it 'hashes documents with the keys to the same value' do - expect({document => 1}).to have_key(same) + expect(document => 1).to have_key(same) end end @@ -179,25 +443,25 @@ class Model context 'with a range key' do it_behaves_like 'it has equality testing and hashing' do - let(:document){ Tweet.create(:tweet_id => 'x', :group => 'abc', :msg => 'foo') } - let(:different) { Tweet.create(:tweet_id => 'y', :group => 'abc', :msg => 'foo') } - let(:same) { Tweet.new(:tweet_id => 'x', :group => 'abc', :msg => 'bar') } + let(:document){ Tweet.create(tweet_id: 'x', group: 'abc', msg: 'foo') } + let(:different) { Tweet.create(tweet_id: 'y', group: 'abc', msg: 'foo') } + let(:same) { Tweet.new(tweet_id: 'x', group: 'abc', msg: 'bar') } end it 'is not equal to another document with the same hash key but a different range value' do - document = Tweet.create(:tweet_id => 'x', :group => 'abc') - different = Tweet.create(:tweet_id => 'x', :group => 'xyz') + document = Tweet.create(tweet_id: 'x', group: 'abc') + different = Tweet.create(tweet_id: 'x', group: 'xyz') expect(document).to_not eq different end end context 'single table inheritance' do - it "should have a type" do - expect(Vehicle.new.type).to eq "Vehicle" + it 'should have a type' do + expect(Vehicle.new.type).to eq 'Vehicle' end - it "reports the same table name for both base and derived classes" do + it 'reports the same table name for both base and derived classes' do expect(Vehicle.table_name).to eq Car.table_name expect(Vehicle.table_name).to eq NuclearSubmarine.table_name end @@ -205,10 +469,20 @@ class Model context '#count' do it 'returns the number of documents in the table' do - document = Tweet.create(:tweet_id => 'x', :group => 'abc') - different = Tweet.create(:tweet_id => 'x', :group => 'xyz') + document = Tweet.create(tweet_id: 'x', group: 'abc') + different = Tweet.create(tweet_id: 'x', group: 'xyz') expect(Tweet.count).to eq 2 end end + + describe '.deep_subclasses' do + it 'returns direct children' do + expect(Car.deep_subclasses).to eq [Cadillac] + end + + it 'returns grandchildren too' do + expect(Vehicle.deep_subclasses).to include(Cadillac) + end + end end diff --git a/spec/dynamoid/fields_spec.rb b/spec/dynamoid/fields_spec.rb index cae79058..a286d9de 100644 --- a/spec/dynamoid/fields_spec.rb +++ b/spec/dynamoid/fields_spec.rb @@ -13,17 +13,33 @@ end it 'declares a query attribute' do - expect(address.city?).to be_falsey - address.city = 'Chicago' - - expect(address.city?).to be_truthy end it 'automatically declares id' do expect{address.id}.to_not raise_error end + it 'allows range key serializers' do + date_serializer = Class.new do + def self.dump(v) + v && v.strftime('%m/%d/%Y') + end + + def self.load(v) + v && DateTime.strptime(v, '%m/%d/%Y') + end + end + + expect do + model = Class.new do + include Dynamoid::Document + table name: :special + range :special_date, :serialized, serializer: date_serializer + end + end.to_not raise_error + end + it 'automatically declares and fills in created_at and updated_at' do address.save @@ -32,6 +48,29 @@ expect(address.updated_at).to be_a DateTime end + context 'query attributes' do + it 'are declared' do + expect(address.city?).to be_falsey + + address.city = 'Chicago' + + expect(address.city?).to be_truthy + end + + it 'return false when boolean attributes are nil or false' do + address.deliverable = nil + expect(address.deliverable?).to be_falsey + + address.deliverable = false + expect(address.deliverable?).to be_falsey + end + + it 'return true when boolean attributes are true' do + address.deliverable = true + expect(address.deliverable?).to be_truthy + end + end + context 'with a saved address' do let(:address) { Address.create(deliverable: true) } let(:original_id) { address.id } @@ -56,7 +95,7 @@ it 'should update all attributes' do expect(address).to receive(:save).once.and_return(true) - address.update_attributes(:city => 'Chicago') + address.update_attributes(city: 'Chicago') expect(address[:city]).to eq 'Chicago' expect(address.id).to eq original_id end @@ -68,12 +107,45 @@ expect(address.id).to eq original_id end - it 'should update only created_at when no params are passed' do + it 'should update only updated_at when no params are passed' do initial_updated_at = address.updated_at address.update_attributes([]) expect(address.updated_at).to_not eq initial_updated_at end + context 'when timestamps are specified' do + it 'uses specified created_at at creation' do + time = Time.now - 1.day + address = Address.create!(created_at: time) + expect(address.created_at).to eq(time) + end + + it 'uses specified updated_at at creation' do + time = Time.now - 1.day + address = Address.create!(updated_at: time) + expect(address.updated_at).to eq(time) + end + + it 'uses specified updated_at at updating' do + time = Time.now - 1.day + + expect do + address.city = 'foobar' + address.updated_at = time + address.save! + end.to change { address.updated_at }.to(time) + end + + it 'changes updated_at at updating' do + expect do + address.city = 'foobar' + address.save! + end.to change { address.updated_at } + + expect(address.updated_at).to be_present + end + end + it 'adds in dirty methods for attributes' do address.city = 'Chicago' address.save @@ -84,23 +156,24 @@ end it 'returns all attributes' do - expect(Address.attributes).to eq({id: {type: :string}, - created_at: {type: :datetime}, - updated_at: {type: :datetime}, - city: {type: :string}, - options: {type: :serialized}, - deliverable: {type: :boolean}, - latitude: {type: :number}, - info: {type: :hash}, - lock_version: {type: :integer}}) + expect(Address.attributes).to eq(id: {type: :string}, + created_at: {type: :datetime}, + updated_at: {type: :datetime}, + city: {type: :string}, + options: {type: :serialized}, + deliverable: {type: :boolean}, + latitude: {type: :number}, + info: {type: :hash}, + config: {type: :raw}, + registered_on: {type: :date}, + lock_version: {type: :integer}) end end - it "gives a warning when setting a single value larger than the maximum item size" do - expect(Dynamoid.logger).to receive(:warn) do |input| - expect(input).to include "city field has a length of 66000" - end - Address.new city: ("Ten chars " * 6_600) + it 'raises an exception when items size exceeds 400kb' do + expect { + Address.create(city: 'Ten chars ' * 500_000) + }.to raise_error(Aws::DynamoDB::Errors::ValidationException, 'Item size has exceeded the maximum allowed size') end context '.remove_attribute' do @@ -128,28 +201,61 @@ end context 'default values for fields' do - let(:doc) do + let(:doc_class) do Class.new do include Dynamoid::Document - field :name, :string, :default => 'x' - field :uid, :integer, :default => lambda { 42 } + field :name, :string, default: 'x' + field :uid, :integer, default: lambda { 42 } + field :config, :serialized, default: {} + field :version, :integer, default: 1 + field :hidden, :boolean, default: false def self.name 'Document' end - end.new + end + end + + it 'returns default value specified as object' do + expect(doc_class.new.name).to eq('x') + end + + it 'returns default value specified as lamda/block (callable object)' do + expect(doc_class.new.uid).to eq(42) + end + + it 'returns default value as is for serializable field' do + expect(doc_class.new.config).to eq({}) + end + + it 'supports `false` as default value' do + expect(doc_class.new.hidden).to eq(false) end - it 'returns default value' do + it 'can modify default value independently for every instance' do + doc = doc_class.new + doc.name << 'y' + expect(doc_class.new.name).to eq('x') + end + + it 'returns default value specified as object even if value cannot be duplicated' do + expect(doc_class.new.version).to eq(1) + end + + it 'should save default values' do + doc = doc_class.create! + doc = doc_class.find(doc.id) expect(doc.name).to eq('x') expect(doc.uid).to eq(42) + expect(doc.config).to eq({}) + expect(doc.version).to eq(1) + expect(doc.hidden).to be false end - it 'should save default value' do - doc.save! - expect(doc.reload.name).to eq('x') - expect(doc.uid).to eq(42) + it 'does not use default value if nil value assigns explicitly' do + doc = doc_class.new(name: nil) + expect(doc.name).to eq nil end end @@ -180,14 +286,46 @@ def self.name end context 'single table inheritance' do - it "has only base class fields on the base class" do + it 'has only base class fields on the base class' do expect(Vehicle.attributes.keys.to_set).to eq Set.new([:type, :description, :created_at, :updated_at, :id]) end - it "has only the base and derived fields on a sub-class" do - #Only NuclearSubmarines have torpedoes + it 'has only the base and derived fields on a sub-class' do + # Only NuclearSubmarines have torpedoes expect(Car.attributes).to_not have_key(:torpedoes) end end + context 'extention overides field accessors' do + let(:klass) { + extention = Module.new do + def name + super.upcase + end + + def name=(s) + super(s.try(:downcase)) + end + end + + Class.new do + include Dynamoid::Document + include extention + + field :name + end + } + + it 'can access new setter' do + address = klass.new + address.name = 'AB cd' + expect(address[:name]).to eq('ab cd') + end + + it 'can access new getter' do + address = klass.new + address.name = 'ABcd' + expect(address.name).to eq('ABCD') + end + end end diff --git a/spec/dynamoid/finders_spec.rb b/spec/dynamoid/finders_spec.rb index d25ef454..59a89629 100644 --- a/spec/dynamoid/finders_spec.rb +++ b/spec/dynamoid/finders_spec.rb @@ -1,7 +1,7 @@ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe Dynamoid::Finders do - let!(:address) { Address.create(:city => 'Chicago') } + let!(:address) { Address.create(city: 'Chicago') } it 'finds an existing address' do found = Address.find(address.id) @@ -16,16 +16,26 @@ expect(found.new_record).to be_falsey end - it 'returns nil when nothing is found' do - expect(Address.find('1234')).to be_nil + it 'raises error when nothing is found' do + expect { Address.find('1234') }.to raise_error( + Dynamoid::Errors::RecordNotFound, "Couldn't find Address with 'id'=1234") end it 'finds multiple ids' do - address2 = Address.create(:city => 'Illinois') + address2 = Address.create(city: 'Illinois') expect(Set.new(Address.find(address.id, address2.id))).to eq Set.new([address, address2]) end + it 'raises error when passed several ids and some models were not found' do + a1 = Address.create + a2 = Address.create + expect { Address.find(a1.id, a2.id, 'fake-id') }.to raise_error( + Dynamoid::Errors::RecordNotFound, + "Couldn't find all Addresses with 'id': (#{a1.id}, #{a2.id}, fake-id) " + + '(found 2 results, but was looking for 3)') + end + it 'returns array if passed in array' do expect(Address.find([address.id])).to eq [address] end @@ -34,12 +44,15 @@ expect(Address.find(address.id)).to eq address end - it 'returns nil if non-array id is passed in and no result found' do - expect(Address.find("not existing id")).to be_nil + it 'raises error if non-array id is passed in and no result found' do + expect { Address.find('not-existing-id') }.to raise_error( + Dynamoid::Errors::RecordNotFound, + "Couldn't find Address with 'id'=not-existing-id") end - it 'returns empty array if array of ids is passed in and no result found' do - expect(Address.find(["not existing id"])).to eq [] + it 'raises error if array of ids is passed in and no result found' do + expect { Address.find(['not-existing-id']) }.to raise_error( + Dynamoid::Errors::RecordNotFound, "Couldn't find Address with 'id'=not-existing-id") end # TODO: ATM, adapter sets consistent read to be true for all query. Provide option for setting consistent_read option @@ -61,18 +74,18 @@ end it 'finds using method_missing for multiple attributes' do - user = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') + user = User.create(name: 'Josh', email: 'josh@joshsymonds.com') - array = User.find_all_by_name_and_email('Josh', 'josh@joshsymonds.com') + array = User.find_all_by_name_and_email('Josh', 'josh@joshsymonds.com').to_a expect(array).to eq [user] end it 'finds using method_missing for single attributes and multiple results' do - user1 = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') - user2 = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') + user1 = User.create(name: 'Josh', email: 'josh@joshsymonds.com') + user2 = User.create(name: 'Josh', email: 'josh@joshsymonds.com') - array = User.find_all_by_name('Josh') + array = User.find_all_by_name('Josh').to_a expect(array.size).to eq 2 expect(array).to include user1 @@ -80,10 +93,10 @@ end it 'finds using method_missing for multiple attributes and multiple results' do - user1 = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') - user2 = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') + user1 = User.create(name: 'Josh', email: 'josh@joshsymonds.com') + user2 = User.create(name: 'Josh', email: 'josh@joshsymonds.com') - array = User.find_all_by_name_and_email('Josh', 'josh@joshsymonds.com') + array = User.find_all_by_name_and_email('Josh', 'josh@joshsymonds.com').to_a expect(array.size).to eq 2 expect(array).to include user1 @@ -91,42 +104,42 @@ end it 'finds using method_missing for multiple attributes and no results' do - user1 = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') - user2 = User.create(:name => 'Justin', :email => 'justin@joshsymonds.com') + user1 = User.create(name: 'Josh', email: 'josh@joshsymonds.com') + user2 = User.create(name: 'Justin', email: 'justin@joshsymonds.com') - array = User.find_all_by_name_and_email('Gaga','josh@joshsymonds.com') + array = User.find_all_by_name_and_email('Gaga', 'josh@joshsymonds.com').to_a expect(array).to be_empty end it 'finds using method_missing for a single attribute and no results' do - user1 = User.create(:name => 'Josh', :email => 'josh@joshsymonds.com') - user2 = User.create(:name => 'Justin', :email => 'justin@joshsymonds.com') + user1 = User.create(name: 'Josh', email: 'josh@joshsymonds.com') + user2 = User.create(name: 'Justin', email: 'justin@joshsymonds.com') - array = User.find_all_by_name('Gaga') + array = User.find_all_by_name('Gaga').to_a expect(array).to be_empty end it 'should find on a query that is not indexed' do - user = User.create(:password => 'Test') + user = User.create(password: 'Test') - array = User.find_all_by_password('Test') + array = User.find_all_by_password('Test').to_a expect(array).to eq [user] end it 'should find on a query on multiple attributes that are not indexed' do - user = User.create(:password => 'Test', :name => 'Josh') + user = User.create(password: 'Test', name: 'Josh') - array = User.find_all_by_password_and_name('Test', 'Josh') + array = User.find_all_by_password_and_name('Test', 'Josh').to_a expect(array).to eq [user] end it 'should return an empty array when fields exist but nothing is found' do User.create_table - array = User.find_all_by_password('Test') + array = User.find_all_by_password('Test').to_a expect(array).to be_empty end @@ -141,7 +154,7 @@ end it 'should return a array of tweets' do - tweets = (1..10).map { |i| Tweet.create(:tweet_id => "#{i}", :group => "group_#{i}") } + tweets = (1..10).map { |i| Tweet.create(tweet_id: "#{i}", group: "group_#{i}") } expect(Tweet.find_all(tweets.map { |t| [t.tweet_id, t.group] })).to match_array(tweets) end @@ -154,10 +167,63 @@ end it 'passes options to the adapter' do - pending "This test is broken as we are overriding the consistent_read option to true inside the adapter" + pending 'This test is broken as we are overriding the consistent_read option to true inside the adapter' user_ids = [%w(1 red), %w(1 green)] - Dynamoid.adapter.expects(:read).with(anything, user_ids, :consistent_read => true) - User.find_all(user_ids, :consistent_read => true) + Dynamoid.adapter.expects(:read).with(anything, user_ids, consistent_read: true) + User.find_all(user_ids, consistent_read: true) + end + + context 'backoff is specified' do + before do + @old_backoff = Dynamoid.config.backoff + @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup + + @counter = 0 + Dynamoid.config.backoff_strategies[:simple] = ->(_) { -> { @counter += 1 } } + Dynamoid.config.backoff = { simple: nil } + end + + after do + Dynamoid.config.backoff = @old_backoff + Dynamoid.config.backoff_strategies = @old_backoff_strategies + end + + it 'returns items' do + users = (1..10).map { User.create } + + results = User.find_all(users.map(&:id)) + expect(results).to match_array(users) + end + + it 'returns empty array when there are no results' do + User.create_table + expect(User.find_all(['some-fake-id'])).to eq [] + end + + it 'uses specified backoff when some items are not processed' do + # batch_get_item has following limitations: + # * up to 100 items at once + # * up to 16 MB at once + # + # So we write data as large as possible and read it back + # 100 * 400 KB (limit for item) = ~40 MB + # 40 MB / 16 MB = 3 times + + ids = (1 .. 100).map(&:to_s) + users = ids.map do |id| + name = ' ' * (400.kilobytes - 120) # 400KB - length(attribute names) + User.create(id: id, name: name) + end + + results = User.find_all(users.map(&:id)) + expect(results).to match_array(users) + + expect(@counter).to eq 2 + end + + it 'uses new backoff after successful call without unprocessed items' do + skip 'it is difficult to test' + end end end @@ -193,14 +259,14 @@ end describe '.find_all_by_secondary_index' do - def time_to_f(time) - time.to_time.to_f + def time_to_decimal(time) + BigDecimal("%d.%09d" % [time.to_i, time.nsec]) end it 'returns exception if index could not be found' do - Post.create(:post_id => 1, :posted_at => Time.now) + Post.create(post_id: 1, posted_at: Time.now) expect do - Post.find_all_by_secondary_index(:posted_at => Time.now.to_i) + Post.find_all_by_secondary_index(posted_at: Time.now.to_i) end.to raise_exception(Dynamoid::Errors::MissingIndex) end @@ -218,44 +284,79 @@ def time_to_f(time) context 'local secondary index' do it 'queries the local secondary index' do time = DateTime.now - p1 = Post.create(:name => "p1", :post_id => 1, :posted_at => time) - p2 = Post.create(:name => "p2", :post_id => 1, :posted_at => time + 1.day) - p3 = Post.create(:name => "p3", :post_id => 2, :posted_at => time) + p1 = Post.create(name: 'p1', post_id: 1, posted_at: time) + p2 = Post.create(name: 'p2', post_id: 1, posted_at: time + 1.day) + p3 = Post.create(name: 'p3', post_id: 2, posted_at: time) posts = Post.find_all_by_secondary_index( - {:post_id => p1.post_id}, - :range => {:name => "p1"} + {post_id: p1.post_id}, + range: {name: 'p1'} ) post = posts.first expect(posts.count).to eql 1 - expect(post.name).to eql "p1" - expect(post.post_id).to eql "1" + expect(post.name).to eql 'p1' + expect(post.post_id).to eql '1' end end context 'global secondary index' do + it 'can sort' do + time = DateTime.now + first_visit = Bar.create(name: 'Drank', visited_at: (time - 1.day).to_i) + Bar.create(name: 'Drank', visited_at: time.to_i) + last_visit = Bar.create(name: 'Drank', visited_at: (time + 1.day).to_i) + + bars = Bar.find_all_by_secondary_index( + {name: 'Drank'}, range: {'visited_at.lte' => (time + 10.days).to_i} + ) + first_bar = bars.first + last_bar = bars.last + expect(bars.count).to eql 3 + expect(first_bar.name).to eql first_visit.name + expect(first_bar.bar_id).to eql first_visit.bar_id + expect(last_bar.name).to eql last_visit.name + expect(last_bar.bar_id).to eql last_visit.bar_id + end + it 'honors :scan_index_forward => false' do + time = DateTime.now + first_visit = Bar.create(name: 'Drank', visited_at: time - 1.day) + Bar.create(name: 'Drank', visited_at: time) + last_visit = Bar.create(name: 'Drank', visited_at: time + 1.day) + different_bar = Bar.create(name: 'Junk', visited_at: time + 7.days) + bars = Bar.find_all_by_secondary_index( + {name: 'Drank'}, range: {'visited_at.lte' => (time + 10.days).to_i}, + scan_index_forward: false + ) + first_bar = bars.first + last_bar = bars.last + expect(bars.count).to eql 3 + expect(first_bar.name).to eql last_visit.name + expect(first_bar.bar_id).to eql last_visit.bar_id + expect(last_bar.name).to eql first_visit.name + expect(last_bar.bar_id).to eql first_visit.bar_id + end it 'queries gsi with hash key' do time = DateTime.now - p1 = Post.create(:post_id => 1, :posted_at => time, :length => "10") - p2 = Post.create(:post_id => 2, :posted_at => time, :length => "30") - p3 = Post.create(:post_id => 3, :posted_at => time, :length => "10") + p1 = Post.create(post_id: 1, posted_at: time, length: '10') + p2 = Post.create(post_id: 2, posted_at: time, length: '30') + p3 = Post.create(post_id: 3, posted_at: time, length: '10') - posts = Post.find_all_by_secondary_index(:length => "10") - expect(posts.map(&:post_id).sort).to eql ["1", "3"] + posts = Post.find_all_by_secondary_index(length: '10') + expect(posts.map(&:post_id).sort).to eql ['1', '3'] end it 'queries gsi with hash and range key' do - time = DateTime.now - p1 = Post.create(:post_id => 1, :posted_at => time, :name => "post1") - p2 = Post.create(:post_id => 2, :posted_at => time + 1.day, :name => "post1") - p3 = Post.create(:post_id => 3, :posted_at => time, :name => "post3") + time = Time.now + p1 = Post.create(post_id: 1, posted_at: time, name: 'post1') + p2 = Post.create(post_id: 2, posted_at: time + 1.day, name: 'post1') + p3 = Post.create(post_id: 3, posted_at: time, name: 'post3') posts = Post.find_all_by_secondary_index( - {:name => "post1"}, - :range => {:posted_at => time_to_f(time)} + {name: 'post1'}, + range: {posted_at: time_to_decimal(time)} ) - expect(posts.map(&:post_id).sort).to eql ["1"] + expect(posts.map(&:post_id).sort).to eql ['1'] end end @@ -263,63 +364,63 @@ def time_to_f(time) describe 'string comparisons' do it 'filters based on begins_with operator' do time = DateTime.now - Post.create(:post_id => 1, :posted_at => time, :name => "fb_post") - Post.create(:post_id => 1, :posted_at => time + 1.day, :name => "blog_post") + Post.create(post_id: 1, posted_at: time, name: 'fb_post') + Post.create(post_id: 1, posted_at: time + 1.day, name: 'blog_post') posts = Post.find_all_by_secondary_index( - {:post_id => "1"}, :range => {"name.begins_with" => "blog_"} + {post_id: '1'}, range: {'name.begins_with' => 'blog_'} ) - expect(posts.map(&:name)).to eql ["blog_post"] + expect(posts.map(&:name)).to eql ['blog_post'] end end describe 'numeric comparisons' do before(:each) do @time = DateTime.now - p1 = Post.create(:post_id => 1, :posted_at => @time, :name => "post") - p2 = Post.create(:post_id => 2, :posted_at => @time + 1.day, :name => "post") - p3 = Post.create(:post_id => 3, :posted_at => @time + 2.days, :name => "post") + p1 = Post.create(post_id: 1, posted_at: @time, name: 'post') + p2 = Post.create(post_id: 2, posted_at: @time + 1.day, name: 'post') + p3 = Post.create(post_id: 3, posted_at: @time + 2.days, name: 'post') end it 'filters based on gt (greater than)' do posts = Post.find_all_by_secondary_index( - {:name => "post"}, - :range => {"posted_at.gt" => time_to_f(@time + 1.day)} + {name: 'post'}, + range: {'posted_at.gt' => time_to_decimal(@time + 1.day)} ) - expect(posts.map(&:post_id).sort).to eql ["3"] + expect(posts.map(&:post_id).sort).to eql ['3'] end it 'filters based on lt (less than)' do posts = Post.find_all_by_secondary_index( - {:name => "post"}, - :range => {"posted_at.lt" => time_to_f(@time + 1.day)} + {name: 'post'}, + range: {'posted_at.lt' => time_to_decimal(@time + 1.day)} ) - expect(posts.map(&:post_id).sort).to eql ["1"] + expect(posts.map(&:post_id).sort).to eql ['1'] end it 'filters based on gte (greater than or equal to)' do posts = Post.find_all_by_secondary_index( - {:name => "post"}, - :range => {"posted_at.gte" => time_to_f(@time + 1.day)} + {name: 'post'}, + range: {'posted_at.gte' => time_to_decimal(@time + 1.day)} ) - expect(posts.map(&:post_id).sort).to eql ["2", "3"] + expect(posts.map(&:post_id).sort).to eql ['2', '3'] end it 'filters based on lte (less than or equal to)' do posts = Post.find_all_by_secondary_index( - {:name => "post"}, - :range => {"posted_at.lte" => time_to_f(@time + 1.day)} + {name: 'post'}, + range: {'posted_at.lte' => time_to_decimal(@time + 1.day)} ) - expect(posts.map(&:post_id).sort).to eql ["1", "2"] + expect(posts.map(&:post_id).sort).to eql ['1', '2'] end it 'filters based on between operator' do - between = [time_to_f(@time - 1.day), time_to_f(@time + 1.5.day)] + between = [time_to_decimal(@time - 1.day), time_to_decimal(@time + 1.5.day)] posts = Post.find_all_by_secondary_index( - {:name => "post"}, - :range => {"posted_at.between" => between} + {name: 'post'}, + range: {'posted_at.between' => between} ) - expect(posts.map(&:post_id).sort).to eql ["1", "2"] + expect(posts.map(&:post_id).sort).to eql ['1', '2'] end end end diff --git a/spec/dynamoid/identity_map_spec.rb b/spec/dynamoid/identity_map_spec.rb index dd9c997d..5006334e 100644 --- a/spec/dynamoid/identity_map_spec.rb +++ b/spec/dynamoid/identity_map_spec.rb @@ -11,31 +11,31 @@ context 'object identity' do it 'maintains a single object' do - tweet = Tweet.create(:tweet_id => "x", :group => "one") - tweet1 = Tweet.where(:tweet_id => "x", :group => "one").first + tweet = Tweet.create(tweet_id: 'x', group: 'one') + tweet1 = Tweet.where(tweet_id: 'x', group: 'one').first expect(tweet).to equal(tweet1) end end context 'cache' do it 'uses cache' do - tweet = Tweet.create(:tweet_id => "x", :group => "one") + tweet = Tweet.create(tweet_id: 'x', group: 'one') expect(Dynamoid::Adapter).to receive(:read).never - tweet1 = Tweet.find_by_id("x", :range_key => "one") + tweet1 = Tweet.find_by_id('x', range_key: 'one') expect(tweet).to equal(tweet1) end it 'clears cache on delete' do - tweet = Tweet.create(:tweet_id => "x", :group => "one") + tweet = Tweet.create(tweet_id: 'x', group: 'one') tweet.delete - expect(Tweet.find_by_id("x", :range_key => "one")).to be_nil + expect(Tweet.find_by_id('x', range_key: 'one')).to be_nil end end context 'clear' do it 'clears the identiy map' do - Tweet.create(:tweet_id => "x", :group => "one") - Tweet.create(:tweet_id => "x", :group => "two") + Tweet.create(tweet_id: 'x', group: 'one') + Tweet.create(tweet_id: 'x', group: 'two') expect(Tweet.identity_map.size).to eq(2) Dynamoid::IdentityMap.clear diff --git a/spec/dynamoid/indexes_spec.rb b/spec/dynamoid/indexes_spec.rb index bfec2db3..b1535ed1 100644 --- a/spec/dynamoid/indexes_spec.rb +++ b/spec/dynamoid/indexes_spec.rb @@ -25,7 +25,7 @@ it 'adds the index to the global_secondary_indexes hash' do index_key = doc_class.index_key(:some_hash_field) - doc_class.global_secondary_index(:hash_key => :some_hash_field) + doc_class.global_secondary_index(hash_key: :some_hash_field) expected_index = doc_class.global_secondary_indexes[index_key] expect(expected_index).to eq(@dummy_index) @@ -33,10 +33,9 @@ it 'with a range key, also adds the index to the global_secondary_indexes hash' do index_key = doc_class.index_key(:some_hash_field, :some_range_field) - doc_class.global_secondary_index({ - :hash_key => :some_hash_field, - :range_key => :some_range_field - }) + doc_class.global_secondary_index( + hash_key: :some_hash_field, + range_key: :some_range_field) expected_index = doc_class.global_secondary_indexes[index_key] expect(expected_index).to eq(@dummy_index) @@ -45,24 +44,24 @@ context 'with optional parameters' do context 'with a hash-only index' do let(:doc_class_with_gsi) do - doc_class.global_secondary_index(:hash_key => :secondary_hash_field) + doc_class.global_secondary_index(hash_key: :secondary_hash_field) end it 'creates the index with the correct options' do test_class = doc_class_with_gsi index_opts = { - :dynamoid_class => test_class, - :type => :global_secondary, - :read_capacity => Dynamoid::Config.read_capacity, - :write_capacity => Dynamoid::Config.write_capacity, - :hash_key => :secondary_hash_field + dynamoid_class: test_class, + type: :global_secondary, + read_capacity: Dynamoid::Config.read_capacity, + write_capacity: Dynamoid::Config.write_capacity, + hash_key: :secondary_hash_field } expect(Dynamoid::Indexes::Index).to have_received(:new).with(index_opts) end it 'adds the index to the global_secondary_indexes hash' do test_class = doc_class_with_gsi - index_key = "secondary_hash_field" + index_key = 'secondary_hash_field' expect(test_class.global_secondary_indexes.keys).to eql [index_key] expect(test_class.global_secondary_indexes[index_key]).to eq(@dummy_index) end @@ -70,28 +69,27 @@ context 'with a hash and range index' do let(:doc_class_with_gsi) do - doc_class.global_secondary_index({ - :hash_key => :secondary_hash_field, - :range_key => :secondary_range_field - }) + doc_class.global_secondary_index( + hash_key: :secondary_hash_field, + range_key: :secondary_range_field) end it 'creates the index with the correct options' do test_class = doc_class_with_gsi index_opts = { - :dynamoid_class => test_class, - :type => :global_secondary, - :read_capacity => Dynamoid::Config.read_capacity, - :write_capacity => Dynamoid::Config.write_capacity, - :hash_key => :secondary_hash_field, - :range_key => :secondary_range_field + dynamoid_class: test_class, + type: :global_secondary, + read_capacity: Dynamoid::Config.read_capacity, + write_capacity: Dynamoid::Config.write_capacity, + hash_key: :secondary_hash_field, + range_key: :secondary_range_field } expect(Dynamoid::Indexes::Index).to have_received(:new).with(index_opts) end it 'adds the index to the global_secondary_indexes hash' do test_class = doc_class_with_gsi - index_key = "secondary_hash_field_secondary_range_field" + index_key = 'secondary_hash_field_secondary_range_field' expect(test_class.global_secondary_indexes[index_key]).to eq(@dummy_index) end end @@ -106,7 +104,7 @@ end it 'with no :hash_key, throws an error' do expect do - doc_class.global_secondary_index(:range_key => :something) + doc_class.global_secondary_index(range_key: :something) end.to raise_error( Dynamoid::Errors::InvalidIndex, /hash_key.*specified/) end @@ -123,26 +121,26 @@ let(:doc_class_with_lsi) do Class.new do include Dynamoid::Document - table :name => :mytable, :key => :some_hash_field - range :some_range_field #@WHAT + table name: :mytable, key: :some_hash_field + range :some_range_field # @WHAT - local_secondary_index(:range_key => :secondary_range_field) + local_secondary_index(range_key: :secondary_range_field) end end it 'creates the index with the correct options' do test_class = doc_class_with_lsi index_opts = { - :dynamoid_class => test_class, - :type => :local_secondary, - :hash_key => :some_hash_field, - :range_key => :secondary_range_field + dynamoid_class: test_class, + type: :local_secondary, + hash_key: :some_hash_field, + range_key: :secondary_range_field } expect(Dynamoid::Indexes::Index).to have_received(:new).with(index_opts) end it 'adds the index to the local_secondary_indexes hash' do test_class = doc_class_with_lsi - index_key = "some_hash_field_secondary_range_field" + index_key = 'some_hash_field_secondary_range_field' expect(test_class.local_secondary_indexes.keys).to eql [index_key] expect(test_class.local_secondary_indexes[index_key]).to eq(@dummy_index) end @@ -152,7 +150,7 @@ let(:doc_class_with_table) do Class.new do include Dynamoid::Document - table :name => :mytable, :key => :some_hash_field + table name: :mytable, key: :some_hash_field range :some_range_field end end @@ -166,14 +164,14 @@ it 'throws an error if the range_key isn`t specified' do test_class = doc_class_with_table expect do - test_class.local_secondary_index(:projected_attributes => :all) + test_class.local_secondary_index(projected_attributes: :all) end.to raise_error(Dynamoid::Errors::InvalidIndex, /range_key.*specified/) end it 'throws an error if the range_key is the same as the primary range key' do test_class = doc_class_with_table expect do - test_class.local_secondary_index(:range_key => :some_range_field) + test_class.local_secondary_index(range_key: :some_range_field) end.to raise_error(Dynamoid::Errors::InvalidIndex, /different.*:range_key/) end end @@ -183,19 +181,19 @@ context 'when hash specified' do it 'generates an index key of the form if only hash is specified' do index_key = doc_class.index_key(:some_hash_field) - expect(index_key).to eq("some_hash_field") + expect(index_key).to eq('some_hash_field') end end context 'when hash and range specified' do it 'generates an index key of the form _' do index_key = doc_class.index_key(:some_hash_field, :some_range_field) - expect(index_key).to eq("some_hash_field_some_range_field") + expect(index_key).to eq('some_hash_field_some_range_field') end it 'generates an index key of the form when range is nil' do index_key = doc_class.index_key(:some_hash_field, nil) - expect(index_key).to eq("some_hash_field") + expect(index_key).to eq('some_hash_field') end end end @@ -204,7 +202,7 @@ let(:doc_class) do Class.new do include Dynamoid::Document - table :name => :mytable + table name: :mytable end end @@ -215,20 +213,20 @@ end end - # Index nested class. describe 'Index' do describe '#initialize' do let(:doc_class) do Class.new do include Dynamoid::Document - table :name => :mytable, :key => :some_hash_field + table name: :mytable, key: :some_hash_field field :primary_hash_field field :primary_range_field field :secondary_hash_field field :secondary_range_field field :array_field, :array + field :serialized_field, :serialized end end @@ -242,70 +240,69 @@ it 'throws an error if :type is invalid' do expect do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :primary_hash_field, - :type => :garbage) + dynamoid_class: doc_class, + hash_key: :primary_hash_field, + type: :garbage) end.to raise_error(Dynamoid::Errors::InvalidIndex, /Invalid.*:type/) end it 'throws an error when :hash_key is not a table attribute' do expect do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :garbage, - :type => :global_secondary) + dynamoid_class: doc_class, + hash_key: :garbage, + type: :global_secondary) end.to raise_error(Dynamoid::Errors::InvalidIndex, /No such field/) end it 'throws an error when :hash_key is of invalid type' do expect do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :array_field, - :type => :global_secondary) + dynamoid_class: doc_class, + hash_key: :array_field, + type: :global_secondary) end.to raise_error(Dynamoid::Errors::InvalidIndex, /hash_key.*/) end it 'throws an error when :range_key is of invalid type' do expect do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :primary_hash_field, - :type => :global_secondary, - :range_key => :array_field) + dynamoid_class: doc_class, + hash_key: :primary_hash_field, + type: :global_secondary, + range_key: :array_field) end.to raise_error(Dynamoid::Errors::InvalidIndex, /range_key.*/) end it 'throws an error when :range_key is not a table attribute' do expect do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :primary_hash_field, - :type => :global_secondary, - :range_key => :garbage) + dynamoid_class: doc_class, + hash_key: :primary_hash_field, + type: :global_secondary, + range_key: :garbage) end.to raise_error(Dynamoid::Errors::InvalidIndex, /No such field/) end it 'throws an error if :projected_attributes are invalid' do expect do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :primary_hash_field, - :type => :global_secondary, - :projected_attributes => :garbage) + dynamoid_class: doc_class, + hash_key: :primary_hash_field, + type: :global_secondary, + projected_attributes: :garbage) end.to raise_error(Dynamoid::Errors::InvalidIndex, /Invalid projected attributes/) end end - context 'correct parameters' do context 'with only required params' do let(:defaults_index) do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :primary_hash_field, - :range_key => :secondary_range_field, - :type => :local_secondary + dynamoid_class: doc_class, + hash_key: :primary_hash_field, + range_key: :secondary_range_field, + type: :local_secondary ) end @@ -318,12 +315,12 @@ end it 'sets the hash_key_schema' do - expected = {:primary_hash_field => :string} + expected = {primary_hash_field: :string} expect(defaults_index.hash_key_schema).to eql expected end it 'sets the range_key_schema' do - expected = {:secondary_range_field => :string} + expected = {secondary_range_field: :string} expect(defaults_index.range_key_schema).to eql expected end @@ -342,13 +339,13 @@ context 'with other params specified' do let(:other_index) do Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :name => :mont_blanc, - :hash_key => :secondary_hash_field, - :type => :global_secondary, - :projected_attributes => [:secondary_hash_field, :array_field], - :read_capacity => 100, - :write_capacity => 200 + dynamoid_class: doc_class, + name: :mont_blanc, + hash_key: :secondary_hash_field, + type: :global_secondary, + projected_attributes: [:secondary_hash_field, :array_field], + read_capacity: 100, + write_capacity: 200 ) end it 'sets the provided attributes' do @@ -367,13 +364,12 @@ end end - describe '#projection_type' do let(:doc_class) do Class.new do include Dynamoid::Document - table :name => :mytable, :key => :primary_hash_field + table name: :mytable, key: :primary_hash_field field :primary_hash_field field :secondary_hash_field @@ -383,20 +379,20 @@ it 'projection type is :include' do projection_include = Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :secondary_hash_field, - :type => :global_secondary, - :projected_attributes => [:secondary_hash_field, :array_field] + dynamoid_class: doc_class, + hash_key: :secondary_hash_field, + type: :global_secondary, + projected_attributes: [:secondary_hash_field, :array_field] ).projection_type expect(projection_include).to eq(:include) end it 'projection type is :all' do projection_all = Dynamoid::Indexes::Index.new( - :dynamoid_class => doc_class, - :hash_key => :secondary_hash_field, - :type => :global_secondary, - :projected_attributes => :all + dynamoid_class: doc_class, + hash_key: :secondary_hash_field, + type: :global_secondary, + projected_attributes: :all ).projection_type expect(projection_all).to eq(:all) diff --git a/spec/dynamoid/persistence_spec.rb b/spec/dynamoid/persistence_spec.rb index 64629867..3b9dce33 100644 --- a/spec/dynamoid/persistence_spec.rb +++ b/spec/dynamoid/persistence_spec.rb @@ -10,13 +10,13 @@ end it 'creates a table' do - Address.create_table(:table_name => Address.table_name) + Address.create_table(table_name: Address.table_name) expect(Dynamoid.adapter.list_tables).to include 'dynamoid_tests_addresses' end it 'checks if a table already exists' do - Address.create_table(:table_name => Address.table_name) + Address.create_table(table_name: Address.table_name) expect(Address.table_exists?(Address.table_name)).to be_truthy expect(Address.table_exists?('crazytable')).to be_falsey @@ -38,19 +38,28 @@ let(:klass) do Class.new do include Dynamoid::Document - table :name => :addresses + table name: :addresses field :city - before_destroy {|i| false } + before_destroy {|i| + # Halting the callback chain in active record changed with Rails >= 5.0.0.beta1 + # We now have to throw :abort to halt the callback chain + # See: https://github.com/rails/rails/commit/bb78af73ab7e86fd9662e8810e346b082a1ae193 + if ActiveModel::VERSION::MAJOR < 5 + false + else + throw :abort + end + } end end describe 'destroy' do it 'deletes an item completely' do - @user = User.create(:name => 'Josh') + @user = User.create(name: 'Josh') @user.destroy - expect(Dynamoid.adapter.read("dynamoid_tests_users", @user.id)).to be_nil + expect(Dynamoid.adapter.read('dynamoid_tests_users', @user.id)).to be_nil end it 'returns false when destroy fails (due to callback)' do @@ -121,7 +130,7 @@ it 'assigns itself an id on save' do address.save - expect(Dynamoid.adapter.read("dynamoid_tests_addresses", address.id)[:id]).to eq address.id + expect(Dynamoid.adapter.read('dynamoid_tests_addresses', address.id)[:id]).to eq address.id end it 'prevents concurrent writes to tables with a lock_version' do @@ -140,45 +149,194 @@ address.id = 'test123' address.save - expect(Dynamoid.adapter.read("dynamoid_tests_addresses", 'test123')).to_not be_empty + expect(Dynamoid.adapter.read('dynamoid_tests_addresses', 'test123')).to_not be_empty end it 'has a table name' do expect(Address.table_name).to eq 'dynamoid_tests_addresses' end + context 'with namespace is empty' do + def reload_address + Object.send(:remove_const, 'Address') + load 'app/models/address.rb' + end + + namespace = Dynamoid::Config.namespace + + before do + reload_address + Dynamoid.configure do |config| + config.namespace = '' + end + end + + after do + reload_address + Dynamoid.configure do |config| + config.namespace = namespace + end + end + + it 'does not add a namespace prefix to table names' do + table_name = Address.table_name + expect(Dynamoid::Config.namespace).to be_empty + expect(table_name).to eq 'addresses' + end + end + + context 'with timestamps set to false' do + def reload_address + Object.send(:remove_const, 'Address') + load 'app/models/address.rb' + end + + timestamps = Dynamoid::Config.timestamps + + before do + reload_address + Dynamoid.configure do |config| + config.timestamps = false + end + end + + after do + reload_address + Dynamoid.configure do |config| + config.timestamps = timestamps + end + end + + it 'sets nil to created_at and updated_at' do + address = Address.create + expect(address.created_at).to be_nil + expect(address.updated_at).to be_nil + end + end + + it 'deletes an item completely' do + @user = User.create(name: 'Josh') + @user.destroy + + expect(Dynamoid.adapter.read('dynamoid_tests_users', @user.id)).to be_nil + end + it 'keeps string attributes as strings' do - @user = User.new(:name => 'Josh') + @user = User.new(name: 'Josh') expect(@user.send(:dump)[:name]).to eq 'Josh' end - it 'dumps datetime attributes' do - @user = User.create(:name => 'Josh') - expect(@user.send(:dump)[:name]).to eq 'Josh' + it 'keeps raw Hash attributes as a Hash' do + config = {acres: 5, trees: {cyprus: 30, poplar: 10, joshua: 1}, horses: ['Lucky', 'Dummy'], lake: 1, tennis_court: 1} + @addr = Address.new(config: config) + expect(@addr.send(:dump)[:config]).to eq config end - it 'dumps integer attributes' do - @subscription = Subscription.create(:length => 10) - expect(@subscription.send(:dump)[:length]).to eq 10 + it 'keeps raw Array attributes as an Array' do + config = ['windows', 'roof', 'doors'] + @addr = Address.new(config: config) + expect(@addr.send(:dump)[:config]).to eq config end - it 'dumps set attributes' do - @subscription = Subscription.create(:length => 10) - @magazine = @subscription.magazine.create + it 'keeps raw String attributes as a String' do + config = 'Configy' + @addr = Address.new(config: config) + expect(@addr.send(:dump)[:config]).to eq config + end - expect(@subscription.send(:dump)[:magazine_ids]).to eq Set[@magazine.id] + it 'keeps raw Number attributes as a Number' do + config = 100 + @addr = Address.new(config: config) + expect(@addr.send(:dump)[:config]).to eq config end - describe 'boolean attributes' do - it 'dumps it as boolean' do - address.deliverable = true - expect(address.send(:dump)[:deliverable]).to eql true + context 'transforms booleans' do + it 'handles true' do + deliverable = true + @addr = Address.new(deliverable: deliverable) + expect(@addr.send(:dump)[:deliverable]).to eq 't' + end + + it 'handles false' do + deliverable = false + @addr = Address.new(deliverable: deliverable) + expect(@addr.send(:dump)[:deliverable]).to eq 'f' + end + + it 'handles t' do + deliverable = 't' + @addr = Address.new(deliverable: deliverable) + expect(@addr.send(:dump)[:deliverable]).to eq 't' + end + + it 'handles f' do + deliverable = 'f' + @addr = Address.new(deliverable: deliverable) + expect(@addr.send(:dump)[:deliverable]).to eq 'f' + end + end + + context 'when dumps datetime attribute' do + it 'loads time in local time zone if config.application_timezone == :local', application_timezone: :local do + time = Time.now + user = User.create(last_logged_in_at: time) + user = User.find(user.id) + expect(user.last_logged_in_at).to be_a(DateTime) + # we can't compare objects directly because lose precision of milliseconds in conversions + expect(user.last_logged_in_at.to_s).to eq time.to_datetime.to_s + end + + it 'loads time in specified time zone if config.application_timezone == time zone name', application_timezone: 'Hawaii' do + time = '2017-06-20 08:00:00 +0300'.to_time + user = User.create(last_logged_in_at: time) + user = User.find(user.id) + expect(user.last_logged_in_at).to eq '2017-06-19 19:00:00 -1000'.to_datetime # Hawaii UTC-10 end - it 'raises exception if value is not boolean' do - address.deliverable = "true" - expect { address.send(:dump)[:deliverable] }.to raise_error(ArgumentError) + it 'loads time in UTC if config.application_timezone = :utc', application_timezone: :utc do + time = '2017-06-20 08:00:00 +0300'.to_time + user = User.create(last_logged_in_at: time) + user = User.find(user.id) + expect(user.last_logged_in_at).to eq '2017-06-20 05:00:00 +0000'.to_datetime end + + it 'can be used as sort key' do + klass = new_class do + range :expired_at, :datetime + end + + models = (1..100).map { klass.create(expired_at: Time.now) } + loaded_models = models.map do |m| + klass.find(m.id, range_key: klass.dump_field(m.expired_at, klass.attributes[:expired_at])) + end + + expect do + loaded_models.map do |m| + klass.find(m.id, range_key: klass.dump_field(m.expired_at, klass.attributes[:expired_at])) + end + end.not_to raise_error + end + end + + it 'dumps date attributes' do + address = Address.create(registered_on: '2017-06-18'.to_date) + expect(Address.find(address.id).registered_on).to eq '2017-06-18'.to_date + + # check internal format - days since 1970-01-01 + expect(Address.find(address.id).send(:dump)[:registered_on]) + .to eq ('2017-06-18'.to_date - Date.new(1970, 1, 1)).to_i + end + + it 'dumps integer attributes' do + @subscription = Subscription.create(length: 10) + expect(@subscription.send(:dump)[:length]).to eq 10 + end + + it 'dumps set attributes' do + @subscription = Subscription.create(length: 10) + @magazine = @subscription.magazine.create + + expect(@subscription.send(:dump)[:magazine_ids]).to eq Set[@magazine.hash_key] end it 'handles nil attributes properly' do @@ -186,7 +344,7 @@ end it 'dumps and undump a serialized field' do - address.options = (hash = {:x => [1, 2], "foobar" => 3.14}) + address.options = (hash = {:x => [1, 2], 'foobar' => 3.14}) expect(Address.undump(address.send(:dump))[:options]).to eq hash end @@ -214,6 +372,13 @@ }) end + it 'dumps and undumps a date' do + date = '2017-06-18'.to_date + expect( + Address.undump(Address.new(registered_on: date).send(:dump))[:registered_on] + ).to eq date + end + it 'supports empty containers in `serialized` fields' do u = User.create(name: 'Philip') u.favorite_colors = Set.new @@ -223,6 +388,26 @@ expect(u.favorite_colors).to eq Set.new end + it 'supports array being empty' do + user = User.create(todo_list: []) + expect(User.find(user.id).todo_list).to eq [] + end + + it 'saves empty set as nil' do + tweet = Tweet.create(group: 'one', tags: []) + expect(Tweet.find_by_tweet_id(tweet.tweet_id).tags).to eq nil + end + + it 'saves empty string as nil' do + user = User.create(name: '') + expect(User.find(user.id).name).to eq nil + end + + it 'saves attributes with nil value' do + user = User.create(name: nil) + expect(User.find(user.id).name).to eq nil + end + it 'supports container types being nil' do u = User.create(name: 'Philip') u.todo_list = nil @@ -239,6 +424,213 @@ end end + describe "Boolean field" do + context "stored in string format" do + let(:klass) do + new_class do + field :active, :boolean + end + end + + it "saves false as 'f'" do + obj = klass.create(active: false) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:active]).to eq "f" + end + + it "saves 'f' as 'f'" do + obj = klass.create(active: "f") + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:active]).to eq "f" + end + + it "saves true as 't'" do + obj = klass.create(active: true) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:active]).to eq "t" + end + + it "saves 't' as 't'" do + obj = klass.create(active: 't') + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:active]).to eq "t" + end + end + + context "stored in boolean format" do + let(:klass) do + new_class do + field :active, :boolean, store_as_native_boolean: true + end + end + + it "saves false as false" do + obj = klass.create(active: false) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:active]).to eq false + end + + it "saves true as true" do + obj = klass.create(active: true) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:active]).to eq true + end + + it "saves and loads boolean field correctly" do + obj = klass.create(active: true) + expect(klass.find(obj.hash_key).active).to eq true + + obj = klass.create(active: false) + expect(klass.find(obj.hash_key).active).to eq false + end + end + end + + describe "Datetime field" do + context "Stored in :number format" do + let(:klass) do + new_class do + field :sent_at, :datetime + end + end + + it "saves time as :number" do + time = Time.now + obj = klass.create(sent_at: time) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:sent_at]).to eq BigDecimal("%d.%09d" % [time.to_i, time.nsec]) + end + + it "saves date as :number" do + date = Date.today + obj = klass.create(sent_at: date) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:sent_at]).to eq BigDecimal("%d.%09d" % [date.to_time.to_i, date.to_time.nsec]) + end + end + + context "Stored in :string format" do + let(:klass) do + new_class do + field :sent_at, :datetime, { store_as_string: true } + end + end + + it "saves time as a :string" do + time = Time.now + obj = klass.create(sent_at: time) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:sent_at]).to eq time.iso8601 + end + + it "saves date as :string" do + date = Date.today + obj = klass.create(sent_at: date) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:sent_at]).to eq date.to_time.iso8601 + end + + it 'saves as :string if global option :store_date_time_as_string is true' do + klass2 = new_class do + field :sent_at, :datetime + end + + store_datetime_as_string = Dynamoid.config.store_datetime_as_string + Dynamoid.config.store_datetime_as_string = true + + time = Time.now + obj = klass2.create(sent_at: time) + attributes = Dynamoid.adapter.get_item(klass2.table_name, obj.hash_key) + expect(attributes[:sent_at]).to eq time.iso8601 + + Dynamoid.config.store_datetime_as_string = store_datetime_as_string + end + + it 'prioritize field option over global one' do + store_datetime_as_string = Dynamoid.config.store_datetime_as_string + Dynamoid.config.store_datetime_as_string = false + + time = Time.now + obj = klass.create(sent_at: time) + attributes = Dynamoid.adapter.get_item(klass.table_name, obj.hash_key) + expect(attributes[:sent_at]).to eq time.iso8601 + + Dynamoid.config.store_datetime_as_string = store_datetime_as_string + end + end + end + + describe 'Date field' do + context 'stored in :string format' do + it 'stores in ISO 8601 format' do + klass = new_class do + field :signed_up_on, :date, store_as_string: true + end + + model = klass.create(signed_up_on: '25-09-2017'.to_date) + expect(klass.find(model.id).signed_up_on).to eq('25-09-2017'.to_date) + + attributes = Dynamoid.adapter.get_item(klass.table_name, model.id) + expect(attributes[:signed_up_on]).to eq '2017-09-25' + end + + it 'stores in string format when global option :store_date_as_string is true' do + klass = new_class do + field :signed_up_on, :date + end + + store_date_as_string = Dynamoid.config.store_date_as_string + Dynamoid.config.store_date_as_string = true + + model = klass.create(signed_up_on: '25-09-2017'.to_date) + attributes = Dynamoid.adapter.get_item(klass.table_name, model.id) + expect(attributes[:signed_up_on]).to eq '2017-09-25' + + Dynamoid.config.store_date_as_string = store_date_as_string + end + + it 'prioritize field option over global one' do + klass = new_class do + field :signed_up_on, :date, store_as_string: true + end + + store_date_as_string = Dynamoid.config.store_date_as_string + Dynamoid.config.store_date_as_string = false + + model = klass.create(signed_up_on: '25-09-2017'.to_date) + attributes = Dynamoid.adapter.get_item(klass.table_name, model.id) + expect(attributes[:signed_up_on]).to eq '2017-09-25' + + Dynamoid.config.store_date_as_string = store_date_as_string + end + end + end + + describe "Set field" do + let(:klass) do + new_class do + field :string_set, :set + field :integer_set, :set, { of: :integer } + field :number_set, :set, { of: :number } + end + end + + it "stored a string set" do + obj = klass.create(string_set: Set.new(['a','b'])) + expect(obj.reload[:string_set]).to eq(Set.new(['a','b'])) + end + + it "stored an integer set" do + obj = klass.create(integer_set: Set.new([1,2])) + expect(obj.reload[:integer_set]).to eq(Set.new([1,2])) + end + + it "stored a number set" do + obj = klass.create(number_set: Set.new([1,2])) + expect(obj.reload[:number_set]).to eq(Set.new([BigDecimal(1),BigDecimal(2)])) + end + end + it 'raises on an invalid boolean value' do expect do address.deliverable = true @@ -255,10 +647,10 @@ it 'loads attributes from a hash' do @time = DateTime.now - @hash = {:name => 'Josh', :created_at => @time.to_f} + @hash = {name: 'Josh', created_at: BigDecimal("%d.%09d" % [@time.to_i, @time.nsec])} expect(User.undump(@hash)[:name]).to eq 'Josh' - User.undump(@hash)[:created_at].to_f == @time.to_f + expect(User.undump(@hash)[:created_at]).to eq @time end it 'runs the before_create callback only once' do @@ -280,15 +672,15 @@ end it 'works with a HashWithIndifferentAccess' do - hash = ActiveSupport::HashWithIndifferentAccess.new("city" => "Atlanta") + hash = ActiveSupport::HashWithIndifferentAccess.new('city' => 'Atlanta') expect{Address.create(hash)}.to_not raise_error end context 'create' do { - Tweet => ['with range', { :tweet_id => 1, :group => 'abc' }], - Message => ['without range', { :message_id => 1, :text => 'foo', :time => DateTime.now }] + Tweet => ['with range', { tweet_id: 1, group: 'abc' }], + Message => ['without range', { message_id: 1, text: 'foo', time: DateTime.now }] }.each_pair do |clazz, fields| it "checks for existence of an existing object #{fields[0]}" do t1 = clazz.new(fields[1]) @@ -306,7 +698,7 @@ let(:clazz) do Class.new do include Dynamoid::Document - table :name => :addresses + table name: :addresses field :city field :options, :serialized @@ -316,7 +708,7 @@ it 'raises when undumping a column with an unknown field type' do expect do - clazz.new(:deliverable => true) #undump is called here + clazz.new(deliverable: true) # undump is called here end.to raise_error(ArgumentError) end @@ -329,32 +721,44 @@ end end + describe 'save' do + it 'creates table if it does not exist' do + klass = Class.new do + include Dynamoid::Document + table name: :foo_bars + end + + expect { klass.create }.not_to raise_error(Aws::DynamoDB::Errors::ResourceNotFoundException) + expect(klass.create.id).to be_present + end + end + context 'update' do before :each do - @tweet = Tweet.create(:tweet_id => 1, :group => 'abc', :count => 5, :tags => Set.new(['db', 'sql']), :user_name => 'john') + @tweet = Tweet.create(tweet_id: 1, group: 'abc', count: 5, tags: Set.new(['db', 'sql']), user_name: 'john') end it 'runs before_update callbacks when doing #update' do expect_any_instance_of(CamelCase).to receive(:doing_before_update).once.and_return(true) - CamelCase.create(:color => 'blue').update do |t| - t.set(:color => 'red') + CamelCase.create(color: 'blue').update do |t| + t.set(color: 'red') end end it 'runs after_update callbacks when doing #update' do expect_any_instance_of(CamelCase).to receive(:doing_after_update).once.and_return(true) - CamelCase.create(:color => 'blue').update do |t| - t.set(:color => 'red') + CamelCase.create(color: 'blue').update do |t| + t.set(color: 'red') end end it 'support add/delete operation on a field' do @tweet.update do |t| - t.add(:count => 3) - t.delete(:tags => Set.new(['db'])) + t.add(count: 3) + t.delete(tags: Set.new(['db'])) end expect(@tweet.count).to eq(8) @@ -362,23 +766,23 @@ end it 'checks the conditions on update' do - result = @tweet.update(:if => { :count => 5 }) do |t| - t.add(:count => 3) + result = @tweet.update(if: { count: 5 }) do |t| + t.add(count: 3) end expect(result).to be_truthy expect(@tweet.count).to eq(8) - result = @tweet.update(:if => { :count => 5 }) do |t| - t.add(:count => 3) + result = @tweet.update(if: { count: 5 }) do |t| + t.add(count: 3) end expect(result).to be_falsey expect(@tweet.count).to eq(8) expect do - @tweet.update!(:if => { :count => 5 }) do |t| - t.add(:count => 3) + @tweet.update!(if: { count: 5 }) do |t| + t.add(count: 3) end end.to raise_error(Dynamoid::Errors::StaleObjectError) end @@ -386,10 +790,10 @@ it 'prevents concurrent saves to tables with a lock_version' do address.save! a2 = Address.find(address.id) - a2.update! { |a| a.set(:city => "Chicago") } + a2.update! { |a| a.set(city: 'Chicago') } expect do - address.city = "Seattle" + address.city = 'Seattle' address.save! end.to raise_error(Dynamoid::Errors::StaleObjectError) end @@ -399,7 +803,7 @@ context 'delete' do it 'deletes model with datetime range key' do expect do - msg = Message.create!(:message_id => 1, :time => DateTime.now, :text => "Hell yeah") + msg = Message.create!(message_id: 1, time: DateTime.now, text: 'Hell yeah') msg.destroy end.to_not raise_error end @@ -430,7 +834,7 @@ a1.save! a2.destroy(:skip_lock_check => true) - expect(Address.find(address.id)).to eql nil + expect { Address.find(address.id) }.to raise_exception(Dynamoid::Errors::RecordNotFound) end it 'uses the correct lock_version even if it is modified' do @@ -444,6 +848,7 @@ end context 'single table inheritance' do + let(:vehicle) { Vehicle.create } let(:car) { Car.create(power_locks: false) } let(:sub) { NuclearSubmarine.create(torpedoes: 5) } @@ -461,6 +866,69 @@ expect(v).to include(s) } end + + it 'does not load parent item when quering the child table' do + vehicle && car + + expect(Car.all).to contain_exactly(car) + expect(Car.all).not_to include(vehicle) + end + + it 'does not load items of sibling class' do + car && sub + + expect(Car.all).to contain_exactly(car) + expect(Car.all).not_to include(sub) + end + end + + describe ':raw datatype persistence' do + subject { Address.new() } + + it 'it persists raw Hash and reads the same back' do + config = {acres: 5, trees: {cyprus: 30, poplar: 10, joshua: 1}, horses: ['Lucky', 'Dummy'], lake: 1, tennis_court: 1} + subject.config = config + subject.save! + subject.reload + expect(subject.config).to eq config + end + + it 'it persists raw Array and reads the same back' do + config = ['windows', 'doors', 'roof'] + subject.config = config + subject.save! + subject.reload + expect(subject.config).to eq config + end + + it 'it persists raw Number and reads the same back' do + config = 100 + subject.config = config + subject.save! + subject.reload + expect(subject.config).to eq config + end + + it 'it persists raw String and reads the same back' do + config = 'Configy' + subject.config = config + subject.save! + subject.reload + expect(subject.config).to eq config + end + + it 'it persists raw value, then reads back, then deletes the value by setting to nil, persists and reads the nil back' do + config = 'To become nil' + subject.config = config + subject.save! + subject.reload + expect(subject.config).to eq config + + subject.config = nil + subject.save! + subject.reload + expect(subject.config).to be_nil + end end describe 'class-type fields' do @@ -487,6 +955,7 @@ def self.name; 'Doc'; end end it 'is findable as a string' do + pending 'casting to declared type is not supported yet' expect(doc_class.where(price: '5.0').first).to eq subject end end @@ -513,6 +982,7 @@ def self.name; 'Doc'; end end it 'is findable as a string' do + pending 'casting to declared type is not supported yet' expect(doc_class.where(price: '5.0').first).to eq subject end @@ -542,10 +1012,182 @@ def self.name; 'Doc'; end end it 'is findable with number semantics' do + pending 'casting to declared type is not supported yet' # With the primary key, we're forcing a Query rather than a Scan because of https://github.com/Dynamoid/Dynamoid/issues/6 primary_key = subject.id expect(doc_class.where(id: primary_key).where('price.gt' => 4).first).to_not be_nil end end end + + describe '.import' do + before do + Address.create_table + User.create_table + Tweet.create_table + end + + it 'creates multiple documents' do + expect { + Address.import([{city: 'Chicago'}, {city: 'New York'}]) + }.to change { Address.count }.by(2) + end + + it 'returns created documents' do + addresses = Address.import([{city: 'Chicago'}, {city: 'New York'}]) + expect(addresses[0].city).to eq('Chicago') + expect(addresses[1].city).to eq('New York') + end + + it 'does not validate documents' do + klass = Class.new do + include Dynamoid::Document + field :city + validates :city, presence: true + + def self.name; 'Address'; end + end + + addresses = klass.import([{city: nil}, {city: 'Chicago'}]) + expect(addresses[0].persisted?).to be true + expect(addresses[1].persisted?).to be true + end + + it 'does not run callbacks' do + klass = Class.new do + include Dynamoid::Document + field :city + validates :city, presence: true + + def self.name; 'Address'; end + + before_save { raise 'before save callback called' } + end + + expect { klass.import([{city: 'Chicago'}]) }.not_to raise_error + end + + it 'makes batch operation' do + expect(Dynamoid.adapter).to receive(:batch_write_item).and_call_original + Address.import([{city: 'Chicago'}, {city: 'New York'}]) + end + + it 'supports empty containers in `serialized` fields' do + users = User.import([name: 'Philip', favorite_colors: Set.new]) + + user = User.find(users[0].id) + expect(user.favorite_colors).to eq Set.new + end + + it 'supports array being empty' do + users = User.import([{todo_list: []}]) + + user = User.find(users[0].id) + expect(user.todo_list).to eq [] + end + + it 'saves empty set as nil' do + tweets = Tweet.import([{group: 'one', tags: []}]) + + tweet = Tweet.find_by_tweet_id(tweets[0].tweet_id) + expect(tweet.tags).to eq nil + end + + it 'saves empty string as nil' do + users = User.import([{name: ''}]) + + user = User.find(users[0].id) + expect(user.name).to eq nil + end + + it 'saves attributes with nil value' do + users = User.import([{name: nil}]) + + user = User.find(users[0].id) + expect(user.name).to eq nil + end + + it 'supports container types being nil' do + users = User.import([{name: 'Philip', todo_list: nil}]) + + user = User.find(users[0].id) + expect(user.todo_list).to eq nil + end + + context 'backoff is specified' do + let(:backoff_strategy) do + ->(_) { -> { @counter += 1 } } + end + + before do + @old_backoff = Dynamoid.config.backoff + @old_backoff_strategies = Dynamoid.config.backoff_strategies.dup + + @counter = 0 + Dynamoid.config.backoff_strategies[:simple] = backoff_strategy + Dynamoid.config.backoff = { simple: nil } + end + + after do + Dynamoid.config.backoff = @old_backoff + Dynamoid.config.backoff_strategies = @old_backoff_strategies + end + + it 'creates multiple documents' do + expect { + Address.import([{city: 'Chicago'}, {city: 'New York'}]) + }.to change { Address.count }.by(2) + end + + it 'uses specified backoff when some items are not processed' do + # dynamodb-local ignores provisioned throughput settings + # so we cannot emulate unprocessed items - let's stub + + klass = new_class + table_name = klass.table_name + items = (1 .. 3).map(&:to_s).map { |id| { id: id } } + + responses = [ + double('response 1', unprocessed_items: { table_name => [ + double(put_request: double(item: { id: '3' })) + ]}), + double('response 2', unprocessed_items: { table_name => [ + double(put_request: double(item: { id: '3' })) + ]}), + double('response 3', unprocessed_items: nil) + ] + allow(Dynamoid.adapter.client).to receive(:batch_write_item).and_return(*responses) + + klass.import(items) + expect(@counter).to eq 2 + end + + it 'uses new backoff after successful call without unprocessed items' do + # dynamodb-local ignores provisioned throughput settings + # so we cannot emulate unprocessed items - let's stub + + klass = new_class + table_name = klass.table_name + # batch_write_item processes up to 15 items at once + # so we emulate 4 calls with items + items = (1 .. 50).map(&:to_s).map { |id| { id: id } } + + responses = [ + double('response 1', unprocessed_items: { table_name => [ + double(put_request: double(item: { id: '25' })) + ]}), + double('response 3', unprocessed_items: nil), + double('response 2', unprocessed_items: { table_name => [ + double(put_request: double(item: { id: '25' })) + ]}), + double('response 3', unprocessed_items: nil) + ] + allow(Dynamoid.adapter.client).to receive(:batch_write_item).and_return(*responses) + + expect(backoff_strategy).to receive(:call).exactly(2).times.and_call_original + klass.import(items) + expect(@counter).to eq 2 + end + end + end end diff --git a/spec/dynamoid/tasks/database_spec.rb b/spec/dynamoid/tasks/database_spec.rb new file mode 100644 index 00000000..6ca6506a --- /dev/null +++ b/spec/dynamoid/tasks/database_spec.rb @@ -0,0 +1,46 @@ +require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper') + +describe Dynamoid::Tasks::Database do + + describe '#ping' do + context 'when the database is reachable' do + it 'should be able to ping (connect to) DynamoDB' do + expect{ Dynamoid::Tasks::Database.ping }.not_to raise_exception + end + end + end + + describe '#create_tables' do + before(:each) do + Dynamoid.adapter.clear_cache! + # depending on test execution order, Dynamoid.included_models gets polluted + # so find everything that is capable of having a table_name + @models = Dynamoid.included_models.select{ |m| not m.base_class.try(:name).blank? }.uniq(&:table_name) + # depending on test execution order, there are some tables hanging about + # that are not in Dynamoid's table namespace and don't get auto cleaned. + # We need this gone. + existing_tables = @models.map{ |m| m.table_name } & Dynamoid.adapter.list_tables + existing_tables.each{ |t| Dynamoid.adapter.delete_table t } + end + + context "when the tables don't already exist" do + it 'should create tables' do + expect(Dynamoid.adapter.list_tables).not_to include( *@models.map{ |m| m.table_name } ) + results = Dynamoid::Tasks::Database.create_tables + expect(Dynamoid.adapter.list_tables).to include( *@models.map{ |m| m.table_name } ) + expect(results[:created]).to include( *@models.map{ |m| m.table_name } ) + end + end + + context 'when the tables already exist' do + it 'should not attempt to re-create the table' do + User.create_table + expect(Dynamoid.adapter.list_tables).to include( User.table_name ) + results = Dynamoid::Tasks::Database.create_tables + expect(results[:existing]).to include( User.table_name ) + expect(results[:created]).not_to include( User.table_name ) + end + end + end + +end diff --git a/spec/dynamoid/validations_spec.rb b/spec/dynamoid/validations_spec.rb index fb113c2f..08331057 100644 --- a/spec/dynamoid/validations_spec.rb +++ b/spec/dynamoid/validations_spec.rb @@ -22,15 +22,79 @@ def self.name expect(doc.errors).to be_empty end + it 'validates presence of boolean field' do + doc_class.field :flag, :boolean + doc_class.validates_presence_of :flag + doc = doc_class.new + expect(doc.save).to be_falsey + doc.flag = false + expect(doc.save).to_not be_falsey + expect(doc.errors).to be_empty + end + it 'raises document not found' do doc_class.field :name doc_class.validates_presence_of :name doc = doc_class.new - expect { doc.save! }.to raise_error(Dynamoid::Errors::DocumentNotValid) + expect { doc.save! }.to raise_error(Dynamoid::Errors::DocumentNotValid) do |error| + expect(error.document).to eq doc + end expect { doc_class.create! }.to raise_error(Dynamoid::Errors::DocumentNotValid) - doc = doc_class.create!(:name => 'test') + doc = doc_class.create!(name: 'test') expect(doc.errors).to be_empty end + + it 'does not validate when saves if `validate` option is false' do + klass = Class.new do + include Dynamoid::Document + table name: :documents + + field :name + validates :name, presence: true + end + + model = klass.new + model.save(validate: false) + expect(model).to be_persisted + end + + it 'returns true if model is valid' do + klass = Class.new do + include Dynamoid::Document + table name: :documents + + field :name + validates :name, presence: true + end + + expect(klass.new(name: 'some-name').save).to eq(true) + end + + it 'returns false if model is invalid' do + klass = Class.new do + include Dynamoid::Document + table name: :documents + + field :name + validates :name, presence: true + + def self.name; 'Document'; end + end + + expect(klass.new(name: nil).save).to eq(false) + end + + describe 'save!' do + it 'returns self' do + klass = Class.new do + include Dynamoid::Document + table name: :documents + end + + model = klass.new + expect(model.save!).to eq(model) + end + end end diff --git a/spec/dynamoid_spec.rb b/spec/dynamoid_spec.rb index 6cbf2ff6..7f7993eb 100644 --- a/spec/dynamoid_spec.rb +++ b/spec/dynamoid_spec.rb @@ -1,9 +1,11 @@ -require File.expand_path(File.dirname(__FILE__) + '/spec_helper') +require 'spec_helper' -describe "Dynamoid" do +describe Dynamoid do + it 'has a version number' do + expect(Dynamoid::VERSION).not_to be nil + end - it "doesn't puke when asked for the assocations of a new record" do + it 'does not puke when asked for the assocations of a new record' do expect(User.new.books).to eq([]) end - end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7a07bd51..a247bbed 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,33 +1,35 @@ require 'coveralls' Coveralls.wear! -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) $LOAD_PATH.unshift(File.dirname(__FILE__)) -MODELS = File.join(File.dirname(__FILE__), "app/models") - require 'rspec' require 'dynamoid' require 'pry' require 'aws-sdk-resources' +require 'byebug' if ENV['DEBUG'] + +require 'dynamodb_local' ENV['ACCESS_KEY'] ||= 'abcd' ENV['SECRET_KEY'] ||= '1234' -Aws.config.update({ - region: 'us-west-2', - credentials: Aws::Credentials.new(ENV['ACCESS_KEY'], ENV['SECRET_KEY']) - }) +Aws.config.update( + region: 'us-west-2', + credentials: Aws::Credentials.new(ENV['ACCESS_KEY'], ENV['SECRET_KEY'])) Dynamoid.configure do |config| config.endpoint = 'http://127.0.0.1:8000' config.namespace = 'dynamoid_tests' config.warn_on_scan = false + config.sync_retry_wait_seconds = 0 + config.sync_retry_max_times = 3 end Dynamoid.logger.level = Logger::FATAL -MODELS = File.join(File.dirname(__FILE__), "app/models") +MODELS = File.join(File.dirname(__FILE__), 'app/models') # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. @@ -35,18 +37,25 @@ Dir["#{File.dirname(__FILE__)}/app/field_types/*.rb"].each {|f| require f} -Dir[ File.join(MODELS, "*.rb") ].sort.each { |file| require file } +Dir[ File.join(MODELS, '*.rb') ].sort.each { |file| require file } RSpec.configure do |config| config.order = :random config.raise_errors_for_deprecations! - config.alias_it_should_behave_like_to :configured_with, "configured with" + config.alias_it_should_behave_like_to :configured_with, 'configured with' + + config.include NewClassHelper config.before(:each) do - Dynamoid.adapter.list_tables.each do |table| - Dynamoid.adapter.delete_table(table) if table =~ /^#{Dynamoid::Config.namespace}/ - end - Dynamoid.adapter.tables.clear + DynamoDBLocal.delete_all_specified_tables! end -end + config.around :each, :application_timezone do |example| + application_timezone_old = Dynamoid::Config.application_timezone + Dynamoid::Config.application_timezone = example.metadata[:application_timezone] + + example.run + + Dynamoid::Config.application_timezone = application_timezone_old + end +end diff --git a/spec/support/new_class_helper.rb b/spec/support/new_class_helper.rb new file mode 100644 index 00000000..f44e9bc4 --- /dev/null +++ b/spec/support/new_class_helper.rb @@ -0,0 +1,10 @@ +module NewClassHelper + def new_class(table_name: nil, &blk) + klass = Class.new do + include Dynamoid::Document + table name: (table_name || :documents) + end + klass.class_eval(&blk) if block_given? + klass + end +end