dynamoid 3.8.0 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e1643f808e93716814d86080198b41cdf26991d9e75614f7d877561590b23d9
4
- data.tar.gz: 453bb6e2c52d73cc94db42ba550fc621524ecba4cc4703445a00977d013b5c9a
3
+ metadata.gz: aedb91a28972fd9357cbb260341e617c3d0d35ae05a620d5ac8312f5d3edbf96
4
+ data.tar.gz: a026d8c3d1e53f4e6800dcffe2b8350da50f70d953a19ae86dfeff0f65555c8c
5
5
  SHA512:
6
- metadata.gz: 3cc09e208f6d1be1bf953efda001cb006fa8a752830e192a418ba449807163761427dd443fac32ee3cb34cf690e613c3ff1e34a3cf0515d50f609319b71c69e7
7
- data.tar.gz: 8ded4ae6a694788719dfc152adb6890f5cd18ff38acf99e422b824ed28c2d010e54caf05016af230d3cb49e5900618ced303148ebbc6ea5dce391dc838c278b2
6
+ metadata.gz: dcb483ea21eaa16246d82603d18952a70bab15556b65ea94fad3ac9dbfe6358f00730e1825275d241a18675fdc0a2e4c3c4b0553acb41f9205e5fe350337f54c
7
+ data.tar.gz: 4e4c96ed9b90ccab85f2f09ad848455affa681c93740dcf1f7b6283797f8e47bebd1c83a70a524eb6ea44906bdd6baec07549f67a9e6bd6561b857ea8997b692
data/CHANGELOG.md CHANGED
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
+
8
9
  ### Fixed
9
10
 
10
11
  ### Added
@@ -13,6 +14,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
14
 
14
15
  ### Removed
15
16
 
17
+ ## 3.9.0
18
+ ### Fixed
19
+ * [#610](https://github.com/Dynamoid/dynamoid/pull/610) Specs in JRuby; Support for JRuby 9.4.0.0 (@pboling)
20
+ * [#624](https://github.com/Dynamoid/dynamoid/pull/624) Fixed `#increment!`/`#decrement!` methods and made them compatible with Rails counterparts
21
+ * [#626](https://github.com/Dynamoid/dynamoid/pull/626) Fixed saving empty Set and String and replacing with `nil` in `#update`, `#update!`, `.update_fields`, and `.upsert` methods
22
+ * [#628](https://github.com/Dynamoid/dynamoid/pull/628) Fixed `.import` method to mark persisted model attributes as not changed/not dirty
23
+ * [#632](https://github.com/Dynamoid/dynamoid/pull/632) Fixed `#save` called with `touch: false` option to set `updated_at` attribute even for a new record (to comply with Rails)
24
+ * [#634](https://github.com/Dynamoid/dynamoid/pull/634) Fixed model callbacks:
25
+ * changed order of `save` and `create`/`update` callbacks - `save` callbacks are outer for the `create`/`update` ones
26
+ * removed `before_initialize` and `around_initialize` callbacks - there should be only `after_initialize` one
27
+ * [#634](https://github.com/Dynamoid/dynamoid/pull/634) Fixed `#touch` method compatibility with a Rails counterpart:
28
+ * don't save other modified attributes - only timestamps
29
+ * don't perform validation and don't call `save`/`create`/`update` callbacks
30
+ * accept a list of attribute names, but not one name
31
+ * accept a `:time` option
32
+ ### Added
33
+ * [#611](https://github.com/Dynamoid/dynamoid/pull/611) Add `rubocop-md` (@pboling)
34
+ * [#612](https://github.com/Dynamoid/dynamoid/pull/612) Add `rubocop-rspec` (@pboling)
35
+ * [#613](https://github.com/Dynamoid/dynamoid/pull/613) Add `rubocop-performance` and `rubocop-rake` (@pboling)
36
+ * Added `funding_uri` set to open collective: https://opencollective.com/dynamoid
37
+ * Added `required_ruby_version` as `>= 2.3.0` (which was already the minimum supported version of Ruby)
38
+ * [#616](https://github.com/Dynamoid/dynamoid/pull/616) Upgrade `simplecov` (& remove `coveralls`) (@pboling)
39
+ * Setup GitHub actions for Code Coverage
40
+ * Setup GitHub actions for RuboCop linting
41
+ * Automate coverage feedback on Pull Requests via GitHub Actions and CodeCov
42
+ * [#618](https://github.com/Dynamoid/dynamoid/pull/618) Upgrade README Badges (@pboling)
43
+ * [#624](https://github.com/Dynamoid/dynamoid/pull/624) Added `:touch` option for `.inc` method to be more compatible with the Rails counterpart method `.update_counters`
44
+ * [#627](https://github.com/Dynamoid/dynamoid/pull/627) Made the following methods in the Dirty API public (to comply with Rails):
45
+ * `clear_changes_information`
46
+ * `changes_applied`
47
+ * `clear_attribute_changes`
48
+ * [#630](https://github.com/Dynamoid/dynamoid/pull/630) Added `Dynamoid::Adapter#execute` method to run PartiQL queries
49
+ * [#634](https://github.com/Dynamoid/dynamoid/pull/634) Added `after_touch` callback and run it in the following methods:
50
+ * `#touch`
51
+ * `#increment!`
52
+ * `#decrement!`
53
+ * [#642](https://github.com/Dynamoid/dynamoid/pull/642) Run specs on CI agains Ruby 3.2
54
+ * [#645](https://github.com/Dynamoid/dynamoid/pull/645) Added `after_find` callback
55
+ ### Changed
56
+ * [#610](https://github.com/Dynamoid/dynamoid/pull/610) Switch to [`rubocop-lts`](https://rubocop-lts.gitlab.io/) (@pboling)
57
+ ### Removed
58
+ * [#633](https://github.com/Dynamoid/dynamoid/pull/633) Change `#inspect` method to return only attributes
59
+ * [#623](https://github.com/Dynamoid/dynamoid/pull/623) Optimized performance of persisting to send only changed attributes in a request to DynamoDB
60
+
16
61
  ## 3.8.0
17
62
  ### Fixed
18
63
  * [#525](https://github.com/Dynamoid/dynamoid/pull/525) Don't mark an attribute as changed if new assigned value equals the old one (@a5-stable)
data/README.md CHANGED
@@ -1,21 +1,18 @@
1
1
  # Dynamoid
2
2
 
3
- [![Build Status](https://github.com/Dynamoid/dynamoid/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/Dynamoid/dynamoid/actions/workflows/ci.yml/badge.svg?branch=master)
4
- [![Code Climate](https://codeclimate.com/github/Dynamoid/dynamoid.svg)](https://codeclimate.com/github/Dynamoid/dynamoid)
5
- [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/dynamoid?branch=master)
6
- [![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid)
7
- [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](https://www.rubydoc.info/github/Dynamoid/dynamoid/frames)
8
- [![Inline docs](http://inch-ci.org/github/Dynamoid/Dynamoid.svg?branch=master)](http://inch-ci.org/github/Dynamoid/Dynamoid)
9
- ![GitHub](https://img.shields.io/github/license/Dynamoid/dynamoid.svg)
3
+ [![Gem Version][⛳️version-img]][⛳️gem]
4
+ [![Supported Build Status][🏘sup-wf-img]][🏘sup-wf]
5
+ [![Maintainability][⛳cclim-maint-img♻️]][⛳cclim-maint]
6
+ [![Coveralls][🏘coveralls-img]][🏘coveralls]
7
+ [![CodeCov][🖇codecov-img♻️]][🖇codecov]
8
+ [![Helpers][🖇triage-help-img]][🖇triage-help]
9
+ [![Contributors][🖐contributors-img]][🖐contributors]
10
+ [![RubyDoc.info][🚎yard-img]][🚎yard]
11
+ [![License][🖇src-license-img]][🖇src-license]
10
12
  [![GitMoji][🖐gitmoji-img]][🖐gitmoji]
11
- [![SemVer 2.0.0][🧮semver-img]][semver]
13
+ [![SemVer 2.0.0][🧮semver-img]][🧮semver]
12
14
  [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog]
13
-
14
- [🖐gitmoji]: https://gitmoji.dev
15
- [🖐gitmoji-img]: https://img.shields.io/badge/gitmoji-3.9.0-FFDD67.svg?style=flat
16
- [🧮semver-img]: https://img.shields.io/badge/semver-2.0.0-FFDD67.svg?style=flat
17
- [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
18
- [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-FFDD67.svg?style=flat
15
+ [![Sponsor Project][🖇sponsor-img]][🖇sponsor]
19
16
 
20
17
  Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
21
18
  provides similar functionality to ActiveRecord and improves on Amazon's
@@ -71,9 +68,9 @@ Create `config/initializers/aws.rb` as follows:
71
68
 
72
69
  ```ruby
73
70
  Aws.config.update({
74
- region: 'us-west-2',
71
+ region: 'us-west-2',
75
72
  credentials: Aws::Credentials.new('REPLACE_WITH_ACCESS_KEY_ID', 'REPLACE_WITH_SECRET_ACCESS_KEY'),
76
- })
73
+ })
77
74
  ```
78
75
 
79
76
  Alternatively, if you don't want Aws connection settings to be
@@ -97,16 +94,16 @@ elsewhere in your project, etc.), you may do so:
97
94
  require 'dynamoid'
98
95
 
99
96
  credentials = Aws::AssumeRoleCredentials.new(
100
- region: region,
101
- access_key_id: key,
102
- secret_access_key: secret,
103
- role_arn: role_arn,
104
- role_session_name: 'our-session'
105
- )
97
+ region: region,
98
+ access_key_id: key,
99
+ secret_access_key: secret,
100
+ role_arn: role_arn,
101
+ role_session_name: 'our-session'
102
+ )
106
103
 
107
104
  Dynamoid.configure do |config|
108
105
  config.region = 'us-west-2',
109
- config.credentials = credentials
106
+ config.credentials = credentials
110
107
  end
111
108
  ```
112
109
 
@@ -135,7 +132,7 @@ end
135
132
  Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
136
133
 
137
134
  Its compatibility is tested against following Ruby versions: 2.3, 2.4,
138
- 2.5, 2.6, 2.7 and 3.0, JRuby 9.2.x and against Rails versions: 4.2, 5.0, 5.1,
135
+ 2.5, 2.6, 2.7, 3.0, 3.1 and 3.2, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
139
136
  5.2, 6.0, 6.1 and 7.0.
140
137
 
141
138
  ## Setup
@@ -360,7 +357,6 @@ class User
360
357
  field :number, :number
361
358
  field :joined_at, :datetime
362
359
  field :hash, :serialized
363
-
364
360
  end
365
361
  ```
366
362
 
@@ -406,7 +402,7 @@ class Money
406
402
  'serialized representation as a string'
407
403
  end
408
404
 
409
- def self.dynamoid_load(serialized_str)
405
+ def self.dynamoid_load(_serialized_str)
410
406
  # parse serialized representation and return a Money instance
411
407
  Money.new(1.23)
412
408
  end
@@ -431,7 +427,7 @@ serializing. Example:
431
427
  class Money; end
432
428
 
433
429
  class MoneyAdapter
434
- def self.dynamoid_load(money_serialized_str)
430
+ def self.dynamoid_load(_money_serialized_str)
435
431
  Money.new(1.23)
436
432
  end
437
433
 
@@ -550,9 +546,18 @@ model.save(validate: false)
550
546
 
551
547
  ### Callbacks
552
548
 
553
- Dynamoid also employs ActiveModel callbacks. Right now, callbacks are
554
- defined on `save`, `update`, `destroy`, which allows you to do `before_`
555
- or `after_` any of those.
549
+ Dynamoid also employs ActiveModel callbacks. Right now the following
550
+ callbacks are supported:
551
+ - `save` (before, after, around)
552
+ - `create` (before, after, around)
553
+ - `update` (before, after, around)
554
+ - `validation` (before, after)
555
+ - `destroy` (before, after, around)
556
+ - `after_touch`
557
+ - `after_initialize`
558
+ - `after_find`
559
+
560
+ Example:
556
561
 
557
562
  ```ruby
558
563
  class User
@@ -684,14 +689,14 @@ address.save
684
689
  To create multiple documents at once:
685
690
 
686
691
  ```ruby
687
- User.create([{name: 'Josh'}, {name: 'Nick'}])
692
+ User.create([{ name: 'Josh' }, { name: 'Nick' }])
688
693
  ```
689
694
 
690
695
  There is an efficient and low-level way to create multiple documents
691
696
  (without validation and callbacks running):
692
697
 
693
698
  ```ruby
694
- users = User.import([{name: 'Josh'}, {name: 'Nick'}])
699
+ users = User.import([{ name: 'Josh' }, { name: 'Nick' }])
695
700
  ```
696
701
 
697
702
  ### Querying
@@ -796,8 +801,8 @@ for requesting documents in batches:
796
801
 
797
802
  ```ruby
798
803
  # Do some maintenance on the entire table without flooding DynamoDB
799
- Address.batch(100).each { |address| address.do_some_work; sleep(0.01) }
800
- Address.record_limit(10_000).batch(100).each { } # Batch specified as part of a chain
804
+ Address.batch(100).each { |addr| addr.do_some_work && sleep(0.01) }
805
+ Address.record_limit(10_000).batch(100).each { |addr| addr.do_some_work && sleep(0.01) } # Batch specified as part of a chain
801
806
  ```
802
807
 
803
808
  The implication of batches is that the underlying requests are done in
@@ -849,13 +854,13 @@ operators are available: `gt`, `lt`, `gte`, `lte`, `begins_with`,
849
854
  `between` as well as equality:
850
855
 
851
856
  ```ruby
852
- Address.where(latitude: 10212)
853
- Address.where('latitude.gt': 10212)
854
- Address.where('latitude.lt': 10212)
855
- Address.where('latitude.gte': 10212)
856
- Address.where('latitude.lte': 10212)
857
+ Address.where(latitude: 10_212)
858
+ Address.where('latitude.gt': 10_212)
859
+ Address.where('latitude.lt': 10_212)
860
+ Address.where('latitude.gte': 10_212)
861
+ Address.where('latitude.lte': 10_212)
857
862
  Address.where('city.begins_with': 'Lon')
858
- Address.where('latitude.between': [10212, 20000])
863
+ Address.where('latitude.between': [10_212, 20_000])
859
864
  ```
860
865
 
861
866
  You are able to filter results on the DynamoDB side and specify
@@ -863,7 +868,7 @@ conditions for non-key fields. Following additional operators are
863
868
  available: `in`, `contains`, `not_contains`, `null`, `not_null`:
864
869
 
865
870
  ```ruby
866
- Address.where('city.in': ['London', 'Edenburg', 'Birmingham'])
871
+ Address.where('city.in': %w[London Edenburg Birmingham])
867
872
  Address.where('city.contains': ['on'])
868
873
  Address.where('city.not_contains': ['ing'])
869
874
  Address.where('postcode.null': false)
@@ -965,6 +970,14 @@ Address.upsert(id, city: 'Chicago')
965
970
  Address.upsert(id, { city: 'Chicago' }, if: { deliverable: true })
966
971
  ```
967
972
 
973
+ By default, `#upsert` will update all attributes of the document if it already exists.
974
+ To idempotently create-but-not-update a record, apply the `unless_exists` condition
975
+ to its keys when you upsert.
976
+
977
+ ```ruby
978
+ Address.upsert(id, { city: 'Chicago' }, if: { unless_exists: [:id] })
979
+ ```
980
+
968
981
  ### Deleting
969
982
 
970
983
  In order to delete some items `delete_all` method should be used. Any
@@ -1046,6 +1059,21 @@ resolving the fields with a second query against the table since a query
1046
1059
  against GSI then a query on base table is still likely faster than scan
1047
1060
  on the base table*
1048
1061
 
1062
+ ### PartiQL
1063
+
1064
+ To run PartiQL statements `Dynamoid.adapter.execute` method should be
1065
+ used:
1066
+
1067
+ ```ruby
1068
+ Dynamoid.adapter.execute("UPDATE users SET name = 'Mike' WHERE id = '1'")
1069
+ ```
1070
+
1071
+ Parameters are also supported:
1072
+
1073
+ ```ruby
1074
+ Dynamoid.adapter.execute('SELECT * FROM users WHERE id = ?', ['1'])
1075
+ ```
1076
+
1049
1077
  ## Configuration
1050
1078
 
1051
1079
  Listed below are all configuration options.
@@ -1282,6 +1310,7 @@ accidentally by adding the following to your test environment setup:
1282
1310
 
1283
1311
  ```ruby
1284
1312
  raise "Tests should be run in 'test' environment only" if Rails.env != 'test'
1313
+
1285
1314
  Dynamoid.configure do |config|
1286
1315
  config.namespace = "#{Rails.application.railtie_name}_#{Rails.env}"
1287
1316
  end
@@ -1406,4 +1435,29 @@ See [LICENSE][license] for the official [Copyright Notice][copyright-notice-expl
1406
1435
 
1407
1436
  [security]: https://github.com/Dynamoid/dynamoid/blob/master/SECURITY.md
1408
1437
 
1409
- [semver]: http://semver.org/
1438
+ [⛳️gem]: https://rubygems.org/gems/dynamoid
1439
+ [⛳️version-img]: http://img.shields.io/gem/v/dynamoid.svg
1440
+ [⛳cclim-maint]: https://codeclimate.com/github/Dynamoid/dynamoid/maintainability
1441
+ [⛳cclim-maint-img♻️]: https://api.codeclimate.com/v1/badges/27fd8b6b7ff338fa4914/maintainability
1442
+ [🏘coveralls]: https://coveralls.io/github/Dynamoid/dynamoid?branch=master
1443
+ [🏘coveralls-img]: https://coveralls.io/repos/github/Dynamoid/dynamoid/badge.svg?branch=master
1444
+ [🖇codecov]: https://codecov.io/gh/Dynamoid/dynamoid
1445
+ [🖇codecov-img♻️]: https://codecov.io/gh/Dynamoid/dynamoid/branch/master/graph/badge.svg?token=84WeeoxaN9
1446
+ [🖇src-license]: https://github.com/Dynamoid/dynamoid/blob/master/LICENSE.txt
1447
+ [🖇src-license-img]: https://img.shields.io/badge/License-MIT-green.svg
1448
+ [🖐gitmoji]: https://gitmoji.dev
1449
+ [🖐gitmoji-img]: https://img.shields.io/badge/gitmoji-3.9.0-FFDD67.svg?style=flat
1450
+ [🚎yard]: https://www.rubydoc.info/gems/dynamoid
1451
+ [🚎yard-img]: https://img.shields.io/badge/yard-docs-blue.svg?style=flat
1452
+ [🧮semver]: http://semver.org/
1453
+ [🧮semver-img]: https://img.shields.io/badge/semver-2.0.0-FFDD67.svg?style=flat
1454
+ [🖐contributors]: https://github.com/Dynamoid/dynamoid/graphs/contributors
1455
+ [🖐contributors-img]: https://img.shields.io/github/contributors-anon/Dynamoid/dynamoid
1456
+ [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
1457
+ [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-FFDD67.svg?style=flat
1458
+ [🖇sponsor-img]: https://img.shields.io/opencollective/all/dynamoid
1459
+ [🖇sponsor]: https://opencollective.com/dynamoid
1460
+ [🖇triage-help]: https://www.codetriage.com/dynamoid/dynamoid
1461
+ [🖇triage-help-img]: https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg
1462
+ [🏘sup-wf]: https://github.com/Dynamoid/dynamoid/actions/workflows/ci.yml?query=branch%3Amaster
1463
+ [🏘sup-wf-img]: https://github.com/Dynamoid/dynamoid/actions/workflows/ci.yml/badge.svg?branch=master
data/SECURITY.md ADDED
@@ -0,0 +1,17 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | 3.7.x | ✅ |
8
+ | <= 3.6 | ❌ |
9
+ | 2.x | ❌ |
10
+ | 1.x | ❌ |
11
+ | 0.x | ❌ |
12
+
13
+ ## Reporting a Vulnerability
14
+
15
+ Peter Boling is responsible for the security maintenance of this gem. Please find a way
16
+ to [contact him directly](https://railsbling.com/contact) to report the issue. Include as much relevant information as
17
+ possible.
data/dynamoid.gemspec ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'dynamoid/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'dynamoid'
9
+ spec.version = Dynamoid::VERSION
10
+
11
+ # Keep in sync with README
12
+ spec.authors = [
13
+ 'Josh Symonds',
14
+ 'Logan Bowers',
15
+ 'Craig Heneveld',
16
+ 'Anatha Kumaran',
17
+ 'Jason Dew',
18
+ 'Luis Arias',
19
+ 'Stefan Neculai',
20
+ 'Philip White',
21
+ 'Peeyush Kumar',
22
+ 'Sumanth Ravipati',
23
+ 'Pascal Corpet',
24
+ 'Brian Glusman',
25
+ 'Peter Boling',
26
+ 'Andrew Konchin'
27
+ ]
28
+ spec.email = ['andry.konchin@gmail.com', 'peter.boling@gmail.com', 'brian@stellaservice.com']
29
+
30
+ 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."
31
+ spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
32
+ # Ignore not commited files
33
+ spec.files = Dir[
34
+ 'CHANGELOG.md',
35
+ 'dynamoid.gemspec',
36
+ 'lib/**/*',
37
+ 'LICENSE.txt',
38
+ 'README.md',
39
+ 'SECURITY.md'
40
+ ]
41
+ spec.homepage = 'http://github.com/Dynamoid/dynamoid'
42
+ spec.licenses = ['MIT']
43
+ spec.require_paths = ['lib']
44
+
45
+ spec.metadata['homepage_uri'] = spec.homepage
46
+ spec.metadata['source_code_uri'] = "https://github.com/Dynamoid/dynamoid/tree/v#{spec.version}"
47
+ spec.metadata['changelog_uri'] = "https://github.com/Dynamoid/dynamoid/blob/v#{spec.version}/CHANGELOG.md"
48
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/Dynamoid/dynamoid/issues'
49
+ spec.metadata['documentation_uri'] = "https://www.rubydoc.info/gems/dynamoid/#{spec.version}"
50
+ spec.metadata['funding_uri'] = 'https://opencollective.com/dynamoid'
51
+ spec.metadata['wiki_uri'] = 'https://github.com/Dynamoid/dynamoid/wiki'
52
+ spec.metadata['rubygems_mfa_required'] = 'true'
53
+
54
+ spec.add_runtime_dependency 'activemodel', '>=4'
55
+ spec.add_runtime_dependency 'aws-sdk-dynamodb', '~> 1.0'
56
+ spec.add_runtime_dependency 'concurrent-ruby', '>= 1.0'
57
+
58
+ spec.add_development_dependency 'appraisal'
59
+ spec.add_development_dependency 'bundler'
60
+ spec.add_development_dependency 'pry', '~> 0.14'
61
+ spec.add_development_dependency 'rake', '~> 13.0'
62
+ spec.add_development_dependency 'rspec', '~> 3.12'
63
+ # 'rubocop-lts' is for Ruby 2.3+, see https://rubocop-lts.gitlab.io/
64
+ spec.add_development_dependency 'rubocop-lts', '~> 10.0'
65
+ spec.add_development_dependency 'yard'
66
+ end
@@ -142,7 +142,7 @@ module Dynamoid
142
142
  end
143
143
  end
144
144
 
145
- %i[batch_get_item delete_item get_item list_tables put_item truncate batch_write_item batch_delete_item].each do |m|
145
+ %i[batch_get_item delete_item get_item list_tables put_item truncate batch_write_item batch_delete_item execute].each do |m|
146
146
  # Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
147
147
  #
148
148
  # @since 0.2.0
@@ -163,6 +163,7 @@ module Dynamoid
163
163
  # https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html
164
164
 
165
165
  return benchmark(method, *args) { adapter.send(method, *args, &block) } if adapter.respond_to?(method)
166
+
166
167
  super
167
168
  end
168
169
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ # @private
5
+ module AdapterPlugin
6
+ class AwsSdkV3
7
+ # Excecute a PartiQL query
8
+ #
9
+ # Documentation:
10
+ # - https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ExecuteStatement.html
11
+ # - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#execute_statement-instance_method
12
+ #
13
+ # NOTE: For reads result may be paginated. Only pagination with NextToken
14
+ # is implemented. Currently LastEvaluatedKey in response cannot be fed to
15
+ # ExecuteStatement to get the next page.
16
+ #
17
+ # See also:
18
+ # - https://repost.aws/questions/QUgNPbBYWiRoOlMsJv-XzrWg/how-to-use-last-evaluated-key-in-execute-statement-request
19
+ # - https://stackoverflow.com/questions/71438439/aws-dynamodb-executestatement-pagination
20
+ class ExecuteStatement
21
+ attr_reader :client, :statement, :parameters, :options
22
+
23
+ def initialize(client, statement, parameters, options)
24
+ @client = client
25
+ @statement = statement
26
+ @parameters = parameters
27
+ @options = options.symbolize_keys.slice(:consistent_read)
28
+ end
29
+
30
+ def call
31
+ request = {
32
+ statement: @statement,
33
+ parameters: @parameters,
34
+ consistent_read: @options[:consistent_read],
35
+ }
36
+
37
+ response = client.execute_statement(request)
38
+
39
+ unless response.next_token
40
+ return response_to_items(response)
41
+ end
42
+
43
+ Enumerator.new do |yielder|
44
+ yielder.yield(response_to_items(response))
45
+
46
+ while response.next_token
47
+ request[:next_token] = response.next_token
48
+ response = client.execute_statement(request)
49
+ yielder.yield(response_to_items(response))
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def response_to_items(response)
57
+ response.items.map(&:symbolize_keys)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -87,7 +87,15 @@ module Dynamoid
87
87
 
88
88
  def sanitize_attributes(attributes)
89
89
  attributes.transform_values do |v|
90
- v.is_a?(Hash) ? v.stringify_keys : v
90
+ if v.is_a?(Hash)
91
+ v.stringify_keys
92
+ elsif v.is_a?(Set) && v.empty?
93
+ nil
94
+ elsif v.is_a?(String) && v.empty?
95
+ nil
96
+ else
97
+ v
98
+ end
91
99
  end
92
100
  end
93
101
  end
@@ -21,14 +21,17 @@ module Dynamoid
21
21
  # lower to obey limits. We can assume the difference won't be
22
22
  # negative due to break statements below but choose smaller limit
23
23
  # which is why we have 2 separate if statements.
24
+ #
24
25
  # NOTE: Adjusting based on record_limit can cause many HTTP requests
25
26
  # being made. We may want to change this behavior, but it affects
26
27
  # filtering on data with potentially large gaps.
28
+ #
27
29
  # Example:
28
30
  # User.where('created_at.gte' => 1.day.ago).record_limit(1000)
29
31
  # Records 1-999 User's that fit criteria
30
32
  # Records 1000-2000 Users's that do not fit criteria
31
33
  # Record 2001 fits criteria
34
+ #
32
35
  # The underlying implementation will have 1 page for records 1-999
33
36
  # then will request with limit 1 for records 1000-2000 (making 1000
34
37
  # requests of limit 1) until hit record 2001.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'aws_sdk_v3/query'
4
4
  require_relative 'aws_sdk_v3/scan'
5
+ require_relative 'aws_sdk_v3/execute_statement'
5
6
  require_relative 'aws_sdk_v3/create_table'
6
7
  require_relative 'aws_sdk_v3/batch_get_item'
7
8
  require_relative 'aws_sdk_v3/item_updater'
@@ -393,7 +394,7 @@ module Dynamoid
393
394
  item = client.get_item(table_name: table_name,
394
395
  key: key_stanza(table, key, range_key),
395
396
  consistent_read: consistent_read)[:item]
396
- item ? result_item_to_hash(item) : nil
397
+ item ? item_to_hash(item) : nil
397
398
  end
398
399
 
399
400
  # Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values
@@ -422,7 +423,7 @@ module Dynamoid
422
423
  attribute_updates: item_updater.attribute_updates,
423
424
  expected: expected_stanza(conditions),
424
425
  return_values: 'ALL_NEW')
425
- result_item_to_hash(result[:attributes])
426
+ item_to_hash(result[:attributes])
426
427
  rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
427
428
  raise Dynamoid::Errors::ConditionalCheckFailedException, e
428
429
  end
@@ -489,7 +490,7 @@ module Dynamoid
489
490
 
490
491
  Query.new(client, table, options).call.each do |page|
491
492
  yielder.yield(
492
- page.items.map { |row| result_item_to_hash(row) },
493
+ page.items.map { |item| item_to_hash(item) },
493
494
  last_evaluated_key: page.last_evaluated_key
494
495
  )
495
496
  end
@@ -522,7 +523,7 @@ module Dynamoid
522
523
 
523
524
  Scan.new(client, table, conditions, options).call.each do |page|
524
525
  yielder.yield(
525
- page.items.map { |row| result_item_to_hash(row) },
526
+ page.items.map { |item| item_to_hash(item) },
526
527
  last_evaluated_key: page.last_evaluated_key
527
528
  )
528
529
  end
@@ -560,6 +561,28 @@ module Dynamoid
560
561
  describe_table(table_name, true).item_count
561
562
  end
562
563
 
564
+ # Run PartiQL query.
565
+ #
566
+ # Dynamoid.adapter.execute("SELECT * FROM users WHERE id = ?", ["758"])
567
+ #
568
+ # @param [String] statement PartiQL statement
569
+ # @param [Array] parameters a list of bind parameters
570
+ # @param [Hash] options
571
+ # @option [Boolean] consistent_read
572
+ # @return [[] | Array[Hash] | Enumerator::Lazy[Hash]] items when used a SELECT statement and empty Array otherwise
573
+ #
574
+ def execute(statement, parameters = [], options = {})
575
+ items = ExecuteStatement.new(client, statement, parameters, options).call
576
+
577
+ if items.is_a?(Array)
578
+ items
579
+ else
580
+ items.lazy.flat_map { |array| array }
581
+ end
582
+ rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
583
+ []
584
+ end
585
+
563
586
  protected
564
587
 
565
588
  #
@@ -605,10 +628,8 @@ module Dynamoid
605
628
  #
606
629
  # Converts a hash returned by get_item, scan, etc. into a key-value hash
607
630
  #
608
- def result_item_to_hash(item)
609
- {}.tap do |r|
610
- item.each { |k, v| r[k.to_sym] = v }
611
- end
631
+ def item_to_hash(hash)
632
+ hash.symbolize_keys
612
633
  end
613
634
 
614
635
  def sanitize_item(attributes)
@@ -11,10 +11,9 @@ module Dynamoid
11
11
  extend ActiveModel::Translation
12
12
  extend ActiveModel::Callbacks
13
13
 
14
- define_model_callbacks :create, :save, :destroy, :initialize, :update
14
+ define_model_callbacks :create, :save, :destroy, :update
15
+ define_model_callbacks :initialize, :find, :touch, only: :after
15
16
 
16
- before_create :set_created_at
17
- before_save :set_updated_at
18
17
  before_save :set_expires_field
19
18
  after_initialize :set_inheritance_field
20
19
  end