esse-sequel 0.0.1.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 556f57525adf41b371e1e117bf9a31d4c72967ec400a46bcd2de4d6b155b0536
4
+ data.tar.gz: 0ae837cc171d0065ec04fec57130f82613932de578c90a5c4fd98da982c98582
5
+ SHA512:
6
+ metadata.gz: 46fd592b16c0f972389d6116fba050d5396ed26e2485964c18f09b1de23af37d739ae71e331daca1f1c2702e196191f90f0bd2b90e2517c56bde3b9c1944461b
7
+ data.tar.gz: 40a434be03f7339f8f39096777dd6e2d068b948b32c101cef1ec614af5ccc9eb7ccf063554a42de7cf4fe33502b20f3ef3e6b7863e0fcd8dca277b1dbfa203d0
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ inherit_mode:
2
+ merge:
3
+ - Exclude
4
+
5
+ plugins:
6
+ - rubocop-performance
7
+ - rubocop-rspec
8
+
9
+ require:
10
+ - standard/cop/block_single_line_braces
11
+
12
+ inherit_gem:
13
+ standard: config/base.yml
14
+
15
+ AllCops:
16
+ TargetRubyVersion: 2.6
17
+ SuggestExtensions: false
18
+ Exclude:
19
+ - "db/**/*"
20
+ - "tmp/**/*"
21
+ - "vendor/**/*"
22
+ NewCops: enable
23
+
24
+ RSpec/MultipleExpectations:
25
+ Enabled: false
26
+
27
+ RSpec/ExampleLength:
28
+ Enabled: false
29
+
30
+ RSpec/MultipleMemoizedHelpers:
31
+ Enabled: false
32
+
33
+ RSpec/MessageSpies:
34
+ Enabled: false
35
+
36
+ RSpec/StubbedMock:
37
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.0.1.beta1
8
+ The first release of the esse-sequel plugin
9
+ * Added: Initial implementation of the plugin
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem "esse", "~> 0.3.4"
8
+ gem "sqlite3"
9
+ gem "esse-rspec"
10
+ gem "logger"
11
+ gem "elasticsearch", "~> 7.17", ">= 7.17.10"
12
+
13
+ # Specify your gem's dependencies in esse-sequel.gemspec
14
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Marcos G. Zimmermann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # Esse Sequel Plugin
2
+
3
+ This gem is a [esse](https://github.com/marcosgz/esse) plugin for the Sequel ORM. It provides a set of methods to simplify implementation of Sequel models as datasource of esse indexes.
4
+
5
+ ## Documentation
6
+
7
+ Full guides, usage examples, and API reference are published at **[gems.marcosz.com.br/esse-sequel](https://gems.marcosz.com.br/esse-sequel/)** — part of the [marcosgz Ruby gem catalogue](https://gems.marcosz.com.br).
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'esse-sequel'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install esse-sequel
24
+
25
+ ## Usage
26
+
27
+ Add the `:sequel` plugin and configure the `collection` with the Sequel model you want to use.
28
+
29
+ ```ruby
30
+ class UsersIndex < Esse::Index
31
+ plugin :sequel
32
+
33
+ repository :user do
34
+ collection ::User
35
+ document # ...
36
+ end
37
+ end
38
+ ```
39
+
40
+ Using multiple repositories is also possible:
41
+
42
+ ```ruby
43
+ class UsersIndex < Esse::Index
44
+ plugin :sequel
45
+
46
+ repository :account do
47
+ collection ::Account
48
+ document # ...
49
+ end
50
+
51
+ repository :admin do
52
+ collection ::User.where(admin: true)
53
+ document # ...
54
+ end
55
+ end
56
+ ```
57
+
58
+ ### Collection Scope
59
+ It's also possible to specify custom scopes to the repository collection to be used to import data to the index:
60
+
61
+ ```ruby
62
+ class UsersIndex < Esse::Index
63
+ plugin :sequel
64
+
65
+ repository :user do
66
+ collection ::User do
67
+ scope :active, -> { where(active: true) }
68
+ scope :role, ->(role) { where(role: role) }
69
+ end
70
+ document # ...
71
+ end
72
+ end
73
+
74
+ # Import data using the scopes
75
+ # > UsersIndex.import(context: { active: true, role: 'admin' })
76
+ #
77
+ # Streaming data using the scopes
78
+ # > UsersIndex.documents(active: true, role: 'admin').first
79
+ ```
80
+
81
+ ## Collection Batch Context
82
+
83
+ Assume that you have a collection of orders and you want to also include the customer data that lives in a external system. To avoid making a request for each order, you can use the `batch_context` to fetch the data in batches and make it available in the document context.
84
+
85
+ ```ruby
86
+ class OrdersIndex < Esse::Index
87
+ plugin :sequel
88
+
89
+ repository :order do
90
+ collection ::Order do
91
+ batch_context :customers do |orders, **_existing_context|
92
+ # The return value will be available in the document context
93
+ # { customers: <value returned from this block> }
94
+ ExternalSystem::Customer.find_all_by_ids(orders.map(&:customer_id)).index_by(&:id) # => { 1 => <Customer>, 2 => <Customer> }
95
+ end
96
+ end
97
+ document do |order, customers: {}, **_|
98
+ customer = customers[order.customer_id]
99
+ {
100
+ id: order.id,
101
+ customer: {
102
+ id: customer&.id,
103
+ name: customer&.name
104
+ }
105
+ }
106
+ end
107
+ end
108
+ end
109
+ ```
110
+
111
+ For Sequel associations, you can define the repository collection by eager loading the associations as usual:
112
+
113
+ ```ruby
114
+
115
+ class OrdersIndex < Esse::Index
116
+ plugin :sequel
117
+
118
+ repository :order do
119
+ collection ::Order.eager(:customer)
120
+ document do |order, **_|
121
+ {
122
+ id: order.id,
123
+ customer: {
124
+ id: order.customer&.id,
125
+ name: order.customer&.name
126
+ }
127
+ }
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Data Streaming Options
134
+
135
+ By default the collection supports 3 streaming options:
136
+ * `batch_size`: the number of documents to be streamed in each batch. Default is 1000;
137
+ * `start`: the primary key value to start from, inclusive of the value;
138
+ * `finish`: the primary key value to end at, inclusive of the value;
139
+
140
+ This is useful when you want to import simultaneous data. You can make one process import all records between 1 and 10,000, and another from 10,000 and beyond
141
+
142
+ ```ruby
143
+ UsersIndex.import(context: { start: 1, finish: 10000, batch_size: 500 })
144
+ ```
145
+
146
+ The default value of `batch_size` can be also defined in the `collection` configuration:
147
+
148
+ ```ruby
149
+ class UsersIndex < Esse::Index
150
+ plugin :sequel
151
+
152
+ repository :user do
153
+ collection ::User, batch_size: 500
154
+ document # ...
155
+ end
156
+ end
157
+ ```
158
+
159
+ ### Database Server (Sharding)
160
+
161
+ If the Sequel database is configured with multiple servers, the collection queries can be routed to one of them using the `connect_with` option or the `connected_to` method:
162
+
163
+ ```ruby
164
+ DB = Sequel.connect(ENV["DATABASE_URL"], servers: { read_only: { host: "replica" } })
165
+
166
+ class UsersIndex < Esse::Index
167
+ plugin :sequel
168
+
169
+ repository :user do
170
+ collection ::User, connect_with: { server: :read_only }
171
+ # or
172
+ collection ::User do
173
+ connected_to server: :read_only
174
+ end
175
+ document # ...
176
+ end
177
+ end
178
+ ```
179
+
180
+ The collection dataset runs on the given server. When the [server_block](https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/server_block_rb.html) extension is loaded (`DB.extension :server_block`), the whole batch iteration, including `batch_context` queries, runs inside `DB.with_server`.
181
+
182
+ ### Indexing Callbacks
183
+
184
+ The `index_callback` callback can be used to automatically index or delete documents after commit on create/update/destroy events. Include the `Esse::Sequel::Model` module in the model to enable it.
185
+
186
+ ```ruby
187
+ class UsersIndex < Esse::Index
188
+ plugin :sequel
189
+
190
+ repository :user, const: true do
191
+ collection ::User
192
+ document # ...
193
+ end
194
+ end
195
+
196
+ class User < Sequel::Model
197
+ include Esse::Sequel::Model
198
+ many_to_one :organization
199
+
200
+ # Using a index and repository as argument. Note that the index name is used instead of the
201
+ # of the constant name. it's so because index and model depends on each other should result in
202
+ # circular dependencies issues.
203
+ index_callback 'users_index:user'
204
+ # Using a block to direct a different object to be indexed
205
+ index_callback('organizations') { organization } # The `_index` suffix and repo name is optional on the index name
206
+ end
207
+ ```
208
+
209
+ Callbacks run in the `after_create`, `after_update` and `after_destroy` model hooks and are deferred with `DB.after_commit`, so documents are only indexed after the transaction commits, and are not indexed at all when it is rolled back. Models that override these hooks must call `super`.
210
+
211
+ When a document uses a routing key, the update callback also deletes the document from its previous routing when the routing value changes. This requires the previous column values, so load the [dirty](https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/Dirty.html) plugin in those models:
212
+
213
+ ```ruby
214
+ class City < Sequel::Model
215
+ plugin :dirty
216
+ include Esse::Sequel::Model
217
+
218
+ index_callback 'geos_index:city'
219
+ end
220
+ ```
221
+
222
+ Callbacks can also be disabled/enabled globally:
223
+
224
+ ```ruby
225
+ Esse::Sequel::Hooks.disable!
226
+ Esse::Sequel::Hooks.enable!
227
+ Esse::Sequel::Hooks.without_indexing do
228
+ 10.times { User.create! }
229
+ end
230
+ ```
231
+
232
+ or by some specific list of index or index's repository
233
+
234
+ ```ruby
235
+ Esse::Sequel::Hooks.disable!(UsersIndex.repo)
236
+ Esse::Sequel::Hooks.enable!(UsersIndex.repo)
237
+ Esse::Sequel::Hooks.without_indexing(AccountsIndex, UsersIndex.repo) do
238
+ 10.times { User.create! }
239
+ end
240
+ ```
241
+
242
+ or by the model that the callback is configured
243
+
244
+ ```ruby
245
+ User.without_indexing do
246
+ 10.times { User.create! }
247
+ end
248
+ User.without_indexing(AccountsIndex) do
249
+ 10.times { User.create! }
250
+ end
251
+ ```
252
+
253
+ ## Development
254
+
255
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
256
+
257
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
258
+
259
+ ## Contributing
260
+
261
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcosgz/esse-sequel.
262
+
263
+ ## License
264
+
265
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/docs/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # esse-sequel
2
+
3
+ Sequel ORM integration for [Esse](../../esse/docs/README.md). The API mirrors [esse-active_record](../../esse-active_record/docs/README.md) — if you know one, you know the other.
4
+
5
+ ## Contents
6
+
7
+ - [Usage guide](usage.md)
8
+ - [API reference](api.md)
9
+
10
+ ## What you get
11
+
12
+ - `collection Model` DSL for repositories.
13
+ - `scope`, `batch_context`, `connected_to` inside collections.
14
+ - `index_callback` and `update_lazy_attribute_callback` on Sequel models.
15
+ - `Esse::Sequel::Hooks` for enabling/disabling indexing globally or per-repo.
16
+
17
+ ## Quick start
18
+
19
+ ```ruby
20
+ # Gemfile
21
+ gem 'esse', '>= 0.3.0'
22
+ gem 'esse-sequel'
23
+ gem 'sequel', '>= 5.0'
24
+ ```
25
+
26
+ ```ruby
27
+ class UsersIndex < Esse::Index
28
+ plugin :sequel
29
+
30
+ repository :user do
31
+ collection ::User do
32
+ scope :active, -> { where(active: true) }
33
+ end
34
+
35
+ document { |u, **| { _id: u.id, name: u.name } }
36
+ end
37
+ end
38
+
39
+ class User < Sequel::Model
40
+ include Esse::Sequel::Model
41
+ index_callback 'users_index:user'
42
+ end
43
+ ```
44
+
45
+ Every `User.create`, `update`, and `destroy` now syncs to Elasticsearch.
46
+
47
+ ## Version
48
+
49
+ - Version: **0.0.1.beta1**
50
+ - Ruby: `>= 2.4.0`
51
+ - Depends on: `esse >= 0.3.0`, `esse-hooks`, `sequel >= 5.0`
52
+
53
+ ## License
54
+
55
+ MIT.
data/docs/api.md ADDED
@@ -0,0 +1,119 @@
1
+ # API Reference
2
+
3
+ ## `Esse::Plugins::Sequel`
4
+
5
+ Registered with:
6
+
7
+ ```ruby
8
+ plugin :sequel
9
+ ```
10
+
11
+ ### Repository class methods
12
+
13
+ #### `collection(model, **opts, &block)`
14
+
15
+ Accepts a `Sequel::Model` class or a `Sequel::Dataset`. Produces an `Esse::Sequel::Collection` subclass.
16
+
17
+ | Argument | Description |
18
+ |----------|-------------|
19
+ | `model` | Sequel model class or dataset |
20
+ | `batch_size:` | Records per batch (default `1000`) |
21
+ | `connect_with:` | `{server: :name}` to run the queries on a Sequel server (shard) |
22
+ | `&block` | Evaluated in the collection class scope (`scope`, `batch_context`, `connected_to`) |
23
+
24
+ #### `dataset(**params)`
25
+
26
+ Returns the filtered `Sequel::Dataset` after applying scopes.
27
+
28
+ ---
29
+
30
+ ## `Esse::Sequel::Collection`
31
+
32
+ Base class. Same interface as `Esse::ActiveRecord::Collection`. Batches are fetched with keyset pagination over the model primary key.
33
+
34
+ ### Class-level DSL
35
+
36
+ - `scope(name, proc = nil, override: false, &block)` — define a named scope.
37
+ - `batch_context(name, proc = nil, override: false, &block)` — define a batch enrichment fetcher.
38
+ - `connected_to(server:)` — run the queries on the given Sequel server. Uses `DB.with_server` for the whole iteration when the `server_block` extension is loaded.
39
+
40
+ ### Instance methods
41
+
42
+ | Method | Description |
43
+ |--------|-------------|
44
+ | `each { |rows, **ctx| }` | Iterate batches with scopes + contexts applied |
45
+ | `each_batch_ids { |ids| }` | Iterate ID-only batches |
46
+ | `count` / `size` | Total row count |
47
+ | `dataset(**kwargs)` | Filtered `Sequel::Dataset` |
48
+
49
+ Constructor:
50
+
51
+ ```ruby
52
+ CollectionClass.new(start: 1, finish: 10_000, batch_size: 500, **scope_args)
53
+ ```
54
+
55
+ ---
56
+
57
+ ## `Esse::Sequel::Model`
58
+
59
+ Mixin for Sequel models. `include` it to enable callbacks.
60
+
61
+ ### `index_callback(reference, on: [...], with: nil, **opts, &block)`
62
+
63
+ Registers a callback that runs after the transaction commits (`DB.after_commit` from the model `after_create`/`after_update`/`after_destroy` hooks).
64
+
65
+ | Option | Description |
66
+ |--------|-------------|
67
+ | `reference` | `'index_name:repo_name'` (or class form) |
68
+ | `on:` | Subset of `[:create, :update, :destroy]`, default all |
69
+ | `with:` | `:update` for partial updates |
70
+ | `if:` / `unless:` | Conditional predicates |
71
+ | `&block` | Optional — return the object to index |
72
+
73
+ Raises if the same repo already has a callback registered.
74
+
75
+ ### `update_lazy_attribute_callback(reference, attribute, on: [...], **opts, &block)`
76
+
77
+ Registers a callback that calls `repo.update_documents_attribute(attribute, ids, opts)`.
78
+
79
+ ### `without_indexing(*repos, &block)`
80
+
81
+ Temporarily disables callbacks for this model class.
82
+
83
+ ### `esse_callbacks`
84
+
85
+ Returns a frozen hash of registered callbacks per reference.
86
+
87
+ ---
88
+
89
+ ## `Esse::Sequel::Hooks`
90
+
91
+ Includes `Esse::Hooks[store_key: :esse_sequel_hooks]`. See [esse-hooks](../../esse-hooks/docs/README.md) for the full API. Methods include:
92
+
93
+ - `disable!` / `enable!` (global or per-repo).
94
+ - `with_indexing` / `without_indexing` (scoped).
95
+ - `with_indexing_for_model` / `without_indexing_for_model`.
96
+ - `enabled?` / `disabled?` / `enabled_for_model?`.
97
+ - `resolve_index_repository(reference)`.
98
+
99
+ ---
100
+
101
+ ## Built-in callbacks
102
+
103
+ Registered in `Esse::Sequel::Callbacks`. All inherit from `Esse::Sequel::Callback`.
104
+
105
+ | Callback | Behavior |
106
+ |----------|----------|
107
+ | `IndexingOnCreate` | `repo.index(document)` on create |
108
+ | `IndexingOnUpdate` | `repo.update(document)` or `repo.index(document)` (depending on `with:`). Handles routing changes by deleting the previous document at the old routing (requires the `dirty` plugin). |
109
+ | `IndexingOnDestroy` | `repo.delete(document)` on destroy. Silently handles `NotFoundError`. |
110
+ | `UpdateLazyAttribute` | `repo.update_documents_attribute(attribute, ids, opts)` |
111
+
112
+ ---
113
+
114
+ ## Deprecated methods
115
+
116
+ | Deprecated | Use instead |
117
+ |------------|-------------|
118
+ | `index_callbacks` | `index_callback` |
119
+ | `esse_index_repos` | `esse_callbacks` |