deimos-ruby 1.18.2 → 1.19.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 +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +48 -0
- data/lib/deimos/active_record_consume/batch_consumption.rb +113 -17
- data/lib/deimos/active_record_consumer.rb +14 -0
- data/lib/deimos/exceptions.rb +5 -0
- data/lib/deimos/version.rb +1 -1
- data/lib/generators/deimos/bulk_import_id/templates/migration.rb.tt +7 -0
- data/lib/generators/deimos/bulk_import_id_generator.rb +51 -0
- data/spec/active_record_batch_consumer_mysql_spec.rb +244 -0
- data/spec/active_record_batch_consumer_spec.rb +20 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e73c9ad8e132353f906ffd5ffe91820e8ad9d5225dbf091c7889752c8d699703
|
4
|
+
data.tar.gz: ac5d117e07aa5881bfd484ce74bd1715cb1f483004b2a0609b2b9e45a1a42937
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1762491acddff361c0290837911eaf862697c9c1a1f2cb054636fdbc097290efb5babe43c9b6e85217a4c43892cf34f899011e5c22485b568bed1a690fd2fcc3
|
7
|
+
data.tar.gz: 56ce518a0b9d8bb62ebb8e799fa51f95ccf5f275e4e3231d7b01084d8a35c73c1d08998035694dde72ff530e6b72c001b6c99ab44ebde82357aee0d456ba6575
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## UNRELEASED
|
9
9
|
|
10
|
+
- Feature: When consuming data in batch mode, this adds ability to save data to multiple tables with `association_list`.
|
11
|
+
|
10
12
|
# 1.18.2 - 2022-11-14
|
11
13
|
|
12
14
|
- Fixes bug related to wait between runs when no records are fetched by updating poll_info
|
data/README.md
CHANGED
@@ -353,6 +353,54 @@ class MyBatchConsumer < Deimos::Consumer
|
|
353
353
|
end
|
354
354
|
end
|
355
355
|
```
|
356
|
+
#### Saving data to Multiple Database tables
|
357
|
+
|
358
|
+
> This feature is implemented and tested with MySQL database ONLY.
|
359
|
+
|
360
|
+
Sometimes, the Kafka message needs to be saved to multiple database tables. For example, if a `User` topic provides you metadata and profile image for users, we might want to save it to multiple tables: `User` and `Image`.
|
361
|
+
|
362
|
+
- The `association_list` configuration allows you to achieve this use case.
|
363
|
+
- The optional `bulk_import_id_column` config allows you to specify column_name on `record_class` which can be used to retrieve IDs after save. Defaults to `bulk_import_id`
|
364
|
+
|
365
|
+
You must override the `build_records` and `bulk_import_columns` methods on your ActiveRecord class for this feature to work.
|
366
|
+
- `build_records` - This method is required to set the value of the `bulk_import_id` column and map Kafka messages to ActiveRecord model objects.
|
367
|
+
- `columns(klass)` - Should return an array of column names that should be used by ActiveRecord klass during SQL insert operation.
|
368
|
+
- `key_columns(messages, klass)` - Should return an array of column name(s) that makes a row unique.
|
369
|
+
```ruby
|
370
|
+
class MyBatchConsumer < Deimos::ActiveRecordConsumer
|
371
|
+
|
372
|
+
record_class User
|
373
|
+
association_list :images
|
374
|
+
|
375
|
+
def build_records(messages)
|
376
|
+
# Initialise bulk_import_id and build ActiveRecord objects out of Kafka message attributes
|
377
|
+
messages.each do |m|
|
378
|
+
u = User.new(first_name: m.first_name, bulk_import_id: SecureRandom.uuid)
|
379
|
+
i = Image.new(attr1: m.image_url)
|
380
|
+
u.images << i
|
381
|
+
u
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
def key_columns(_records, klass)
|
386
|
+
case klass
|
387
|
+
when User
|
388
|
+
super
|
389
|
+
when Image
|
390
|
+
["image_url", "image_name"]
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def columns(klass)
|
395
|
+
case klass
|
396
|
+
when User
|
397
|
+
super
|
398
|
+
when Image
|
399
|
+
klass.columns.map(&:name) - [:created_at, :updated_at, :id]
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
```
|
356
404
|
|
357
405
|
# Rails Integration
|
358
406
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'deimos/active_record_consume/batch_slicer'
|
4
4
|
require 'deimos/utils/deadlock_retry'
|
5
5
|
require 'deimos/message'
|
6
|
+
require 'deimos/exceptions'
|
6
7
|
|
7
8
|
module Deimos
|
8
9
|
module ActiveRecordConsume
|
@@ -86,38 +87,63 @@ module Deimos
|
|
86
87
|
# records to either be updated or inserted.
|
87
88
|
# @return [void]
|
88
89
|
def upsert_records(messages)
|
89
|
-
key_cols = key_columns(messages)
|
90
|
-
|
91
|
-
# Create payloads with payload + key attributes
|
92
|
-
upserts = messages.map do |m|
|
93
|
-
attrs = if self.method(:record_attributes).parameters.size == 2
|
94
|
-
record_attributes(m.payload, m.key)
|
95
|
-
else
|
96
|
-
record_attributes(m.payload)
|
97
|
-
end
|
98
|
-
|
99
|
-
attrs&.merge(record_key(m.key))
|
100
|
-
end
|
90
|
+
key_cols = key_columns(messages, nil)
|
101
91
|
|
92
|
+
# Create ActiveRecord Models with payload + key attributes
|
93
|
+
upserts = build_records(messages)
|
102
94
|
# If overridden record_attributes indicated no record, skip
|
103
95
|
upserts.compact!
|
96
|
+
# apply ActiveRecord validations and fetch valid Records
|
97
|
+
valid_upserts = filter_records(upserts)
|
98
|
+
|
99
|
+
return if valid_upserts.empty?
|
100
|
+
|
101
|
+
save_records_to_database(@klass, key_cols, valid_upserts)
|
102
|
+
import_associations(valid_upserts) unless @association_list.blank?
|
103
|
+
end
|
104
|
+
|
105
|
+
def save_records_to_database(record_class, key_cols, records)
|
106
|
+
columns = columns(record_class)
|
104
107
|
|
105
108
|
options = if key_cols.empty?
|
106
109
|
{} # Can't upsert with no key, just do regular insert
|
107
|
-
elsif
|
110
|
+
elsif mysql_adapter?
|
108
111
|
{
|
109
|
-
on_duplicate_key_update:
|
112
|
+
on_duplicate_key_update: columns
|
110
113
|
}
|
111
114
|
else
|
112
115
|
{
|
113
116
|
on_duplicate_key_update: {
|
114
117
|
conflict_target: key_cols,
|
115
|
-
columns:
|
118
|
+
columns: columns
|
116
119
|
}
|
117
120
|
}
|
118
121
|
end
|
122
|
+
record_class.import!(columns, records, options)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Imports associated objects and import them to database table
|
126
|
+
# The base table is expected to contain bulk_import_id column for indexing associated objects with id
|
127
|
+
# @association_list configured on the consumer helps identify the ones required to be saved.
|
128
|
+
def import_associations(entities)
|
129
|
+
_validate_associations(entities)
|
130
|
+
_fill_primary_key_on_entities(entities)
|
131
|
+
|
132
|
+
# Select associations from config parameter association_list and
|
133
|
+
# fill id to associated_objects foreign_key column
|
134
|
+
@klass.reflect_on_all_associations.select { |assoc| @association_list.include?(assoc.name) }.
|
135
|
+
each do |assoc|
|
136
|
+
sub_records = entities.map { |entity|
|
137
|
+
# Get associated `has_one` or `has_many` records for each entity
|
138
|
+
sub_records = Array(entity.send(assoc.name))
|
139
|
+
# Set IDS from master to each of the records in `has_one` or `has_many` relation
|
140
|
+
sub_records.each { |d| d.send("#{assoc.send(:foreign_key)}=", entity.id) }
|
141
|
+
sub_records
|
142
|
+
}.flatten
|
119
143
|
|
120
|
-
|
144
|
+
columns = key_columns(nil, assoc.klass)
|
145
|
+
save_records_to_database(assoc.klass, columns, sub_records) if sub_records.any?
|
146
|
+
end
|
121
147
|
end
|
122
148
|
|
123
149
|
# Delete any records with a tombstone.
|
@@ -144,16 +170,31 @@ module Deimos
|
|
144
170
|
|
145
171
|
# Get the set of attribute names that uniquely identify messages in the
|
146
172
|
# batch. Requires at least one record.
|
173
|
+
# The parameters are mutually exclusive. records is used by default implementation.
|
147
174
|
# @param records [Array<Message>] Non-empty list of messages.
|
175
|
+
# @param klass [ActiveRecord::Class] Class Name can be used to fetch columns
|
148
176
|
# @return [Array<String>] List of attribute names.
|
149
177
|
# @raise If records is empty.
|
150
|
-
def key_columns(records)
|
178
|
+
def key_columns(records, klass)
|
179
|
+
raise 'Must implement key_columns method for associations!' unless klass.nil?
|
180
|
+
|
151
181
|
raise 'Cannot determine key from empty batch' if records.empty?
|
152
182
|
|
153
183
|
first_key = records.first.key
|
154
184
|
record_key(first_key).keys
|
155
185
|
end
|
156
186
|
|
187
|
+
# Get the list of database table column names that should be saved to the database
|
188
|
+
# @param record_class [Class] ActiveRecord class associated to the Entity Object
|
189
|
+
# @return Array[String] list of table columns
|
190
|
+
def columns(record_class)
|
191
|
+
# In-memory records contain created_at and updated_at as nil
|
192
|
+
# which messes up ActiveRecord-Import bulk_import.
|
193
|
+
# It is necessary to ignore timestamp columns when using ActiveRecord objects
|
194
|
+
ignored_columns = %w(created_at updated_at)
|
195
|
+
record_class.columns.map(&:name) - ignored_columns
|
196
|
+
end
|
197
|
+
|
157
198
|
# Compact a batch of messages, taking only the last message for each
|
158
199
|
# unique key.
|
159
200
|
# @param batch [Array<Message>] Batch of messages.
|
@@ -163,6 +204,61 @@ module Deimos
|
|
163
204
|
|
164
205
|
batch.reverse.uniq(&:key).reverse!
|
165
206
|
end
|
207
|
+
|
208
|
+
# Turns Kafka payload into ActiveRecord Objects by mapping relevant fields
|
209
|
+
# Override this method to build object and associations with message payload
|
210
|
+
# @param messages [Array<Deimos::Message>] the array of deimos messages in batch mode
|
211
|
+
# @return [Array<ActiveRecord>] Array of ActiveRecord objects
|
212
|
+
def build_records(messages)
|
213
|
+
messages.map do |m|
|
214
|
+
attrs = if self.method(:record_attributes).parameters.size == 2
|
215
|
+
record_attributes(m.payload, m.key)
|
216
|
+
else
|
217
|
+
record_attributes(m.payload)
|
218
|
+
end
|
219
|
+
|
220
|
+
attrs = attrs&.merge(record_key(m.key))
|
221
|
+
@klass.new(attrs) unless attrs.nil?
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Filters list of Active Records by applying active record validations.
|
226
|
+
# Tip: Add validates_associated in ActiveRecord model to validate associated models
|
227
|
+
# Optionally inherit this method and apply more filters in the application code
|
228
|
+
# The default implementation throws ActiveRecord::RecordInvalid by default
|
229
|
+
# @param records Array<ActiveRecord> - List of active records which will be subjected to model validations
|
230
|
+
# @return valid Array<ActiveRecord> - Subset of records that passed the model validations
|
231
|
+
def filter_records(records)
|
232
|
+
records.each(&:validate!)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Returns true if MySQL Adapter is currently used
|
236
|
+
def mysql_adapter?
|
237
|
+
ActiveRecord::Base.connection.adapter_name.downcase =~ /mysql/
|
238
|
+
end
|
239
|
+
|
240
|
+
# Checks whether the entities has necessary columns for `association_list` to work
|
241
|
+
# @return void
|
242
|
+
def _validate_associations(entities)
|
243
|
+
raise Deimos::MissingImplementationError unless mysql_adapter?
|
244
|
+
|
245
|
+
return if entities.first.respond_to?(@bulk_import_id_column)
|
246
|
+
|
247
|
+
raise "Create bulk_import_id on #{entities.first.class} and set it in `build_records` for associations." \
|
248
|
+
' Run rails g deimos:bulk_import_id:setup to create the migration.'
|
249
|
+
end
|
250
|
+
|
251
|
+
# Fills Primary Key ID on in-memory objects.
|
252
|
+
# Uses @bulk_import_id_column on in-memory records to fetch saved records in database.
|
253
|
+
# @return void
|
254
|
+
def _fill_primary_key_on_entities(entities)
|
255
|
+
table_by_bulk_import_id = @klass.
|
256
|
+
where(@bulk_import_id_column => entities.map { |e| e[@bulk_import_id_column] }).
|
257
|
+
select(:id, @bulk_import_id_column).
|
258
|
+
index_by { |e| e[@bulk_import_id_column] }
|
259
|
+
# update IDs in upsert entity
|
260
|
+
entities.each { |entity| entity.id = table_by_bulk_import_id[entity[@bulk_import_id_column]].id }
|
261
|
+
end
|
166
262
|
end
|
167
263
|
end
|
168
264
|
end
|
@@ -30,6 +30,18 @@ module Deimos
|
|
30
30
|
config[:record_class] = klass
|
31
31
|
end
|
32
32
|
|
33
|
+
# @param associations [List<String>] Optional list of associations that the consumer
|
34
|
+
# should save in addition to @klass
|
35
|
+
# @return [void]
|
36
|
+
def association_list(associations)
|
37
|
+
config[:association_list] = Array(associations)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param
|
41
|
+
def bulk_import_id_column(name)
|
42
|
+
config[:bulk_import_id_column] = name
|
43
|
+
end
|
44
|
+
|
33
45
|
# @param val [Boolean] Turn pre-compaction of the batch on or off. If true,
|
34
46
|
# only the last message for each unique key in a batch is processed.
|
35
47
|
# @return [void]
|
@@ -41,6 +53,8 @@ module Deimos
|
|
41
53
|
# Setup
|
42
54
|
def initialize
|
43
55
|
@klass = self.class.config[:record_class]
|
56
|
+
@association_list = self.class.config[:association_list]
|
57
|
+
@bulk_import_id_column = self.class.config[:bulk_import_id_column] || :bulk_import_id
|
44
58
|
@converter = ActiveRecordConsume::SchemaModelConverter.new(self.class.decoder, @klass)
|
45
59
|
|
46
60
|
if self.class.config[:key_schema]
|
data/lib/deimos/version.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/active_record/migration'
|
5
|
+
require 'rails/version'
|
6
|
+
|
7
|
+
# Generates a migration for bulk import ID in consumer.
|
8
|
+
module Deimos
|
9
|
+
module Generators
|
10
|
+
# Generator for ActiveRecord model and migration.
|
11
|
+
class BulkImportIdGenerator < Rails::Generators::Base
|
12
|
+
include Rails::Generators::Migration
|
13
|
+
include ActiveRecord::Generators::Migration
|
14
|
+
|
15
|
+
namespace 'deimos:bulk_import_id:setup'
|
16
|
+
|
17
|
+
argument :table_name, desc: 'The table to add bulk import column.', required: true
|
18
|
+
argument :column_name, desc: 'The bulk import ID column name.', default: 'bulk_import_id'
|
19
|
+
|
20
|
+
source_root File.expand_path('bulk_import_id/templates', __dir__)
|
21
|
+
desc 'Add column migration to the given table and name'
|
22
|
+
|
23
|
+
no_commands do
|
24
|
+
# @return [String]
|
25
|
+
def db_migrate_path
|
26
|
+
if defined?(Rails.application) && Rails.application
|
27
|
+
paths = Rails.application.config.paths['db/migrate']
|
28
|
+
paths.respond_to?(:to_ary) ? paths.to_ary.first : paths.to_a.first
|
29
|
+
else
|
30
|
+
'db/migrate'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String]
|
35
|
+
def migration_version
|
36
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
37
|
+
rescue StandardError
|
38
|
+
''
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# For a given table_name and column_name, create a migration to add the column
|
43
|
+
# column_name defaults to bulk_import_id
|
44
|
+
def generate
|
45
|
+
Rails.logger.info("Arguments: #{table_name},#{column_name}")
|
46
|
+
migration_template('migration.rb',
|
47
|
+
"#{db_migrate_path}/add_#{column_name}_column_to_#{table_name}.rb")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordBatchConsumerTest
|
4
|
+
describe Deimos::ActiveRecordConsumer,
|
5
|
+
'Batch Consumer with MySQL handling associations',
|
6
|
+
:integration,
|
7
|
+
db_config: DbConfigs::DB_OPTIONS.second do
|
8
|
+
include_context('with DB')
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
ActiveRecord::Base.connection.create_table(:widgets, force: true) do |t|
|
12
|
+
t.string(:test_id)
|
13
|
+
t.string(:part_one)
|
14
|
+
t.string(:part_two)
|
15
|
+
t.integer(:some_int)
|
16
|
+
t.boolean(:deleted, default: false)
|
17
|
+
t.timestamps
|
18
|
+
|
19
|
+
t.index(%i(part_one part_two), unique: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
# create one-to-one association -- Details
|
23
|
+
ActiveRecord::Base.connection.create_table(:details, force: true) do |t|
|
24
|
+
t.string(:title)
|
25
|
+
t.belongs_to(:widget)
|
26
|
+
|
27
|
+
t.index(%i(title), unique: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create one-to-many association Locales
|
31
|
+
ActiveRecord::Base.connection.create_table(:locales, force: true) do |t|
|
32
|
+
t.string(:title)
|
33
|
+
t.string(:language)
|
34
|
+
t.belongs_to(:widget)
|
35
|
+
|
36
|
+
t.index(%i(title language), unique: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
class Detail < ActiveRecord::Base
|
40
|
+
validates :title, presence: true
|
41
|
+
end
|
42
|
+
|
43
|
+
class Locale < ActiveRecord::Base
|
44
|
+
validates :title, presence: true
|
45
|
+
validates :language, presence: true
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sample model
|
49
|
+
class Widget < ActiveRecord::Base
|
50
|
+
has_one :detail
|
51
|
+
has_many :locales
|
52
|
+
validates :test_id, presence: true
|
53
|
+
|
54
|
+
default_scope -> { where(deleted: false) }
|
55
|
+
end
|
56
|
+
|
57
|
+
Widget.reset_column_information
|
58
|
+
Detail.reset_column_information
|
59
|
+
Locale.reset_column_information
|
60
|
+
end
|
61
|
+
|
62
|
+
after(:all) do
|
63
|
+
ActiveRecord::Base.connection.drop_table(:widgets)
|
64
|
+
ActiveRecord::Base.connection.drop_table(:details)
|
65
|
+
ActiveRecord::Base.connection.drop_table(:locales)
|
66
|
+
end
|
67
|
+
|
68
|
+
before(:each) do
|
69
|
+
ActiveRecord::Base.connection.truncate_tables(%i(widgets details locales))
|
70
|
+
end
|
71
|
+
|
72
|
+
prepend_before(:each) do
|
73
|
+
stub_const('MyBatchConsumer', consumer_class)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Helper to publish a list of messages and call the consumer
|
77
|
+
def publish_batch(messages)
|
78
|
+
keys = messages.map { |m| m[:key] }
|
79
|
+
payloads = messages.map { |m| m[:payload] }
|
80
|
+
|
81
|
+
test_consume_batch(MyBatchConsumer, payloads, keys: keys, call_original: true)
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when association_list configured in consumer without model changes' do
|
85
|
+
let(:consumer_class) do
|
86
|
+
Class.new(described_class) do
|
87
|
+
schema 'MySchema'
|
88
|
+
namespace 'com.my-namespace'
|
89
|
+
key_config plain: true
|
90
|
+
record_class Widget
|
91
|
+
association_list :detail
|
92
|
+
|
93
|
+
def build_records(messages)
|
94
|
+
messages.map do |m|
|
95
|
+
payload = m.payload
|
96
|
+
w = Widget.new(test_id: payload['test_id'], some_int: payload['some_int'])
|
97
|
+
d = Detail.new(title: payload['title'])
|
98
|
+
w.detail = d
|
99
|
+
w
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'should raise error when bulk_import_id is not found' do
|
106
|
+
stub_const('MyBatchConsumer', consumer_class)
|
107
|
+
expect {
|
108
|
+
publish_batch([{ key: 2,
|
109
|
+
payload: { test_id: 'xyz', some_int: 5, title: 'Widget Title' } }])
|
110
|
+
}.to raise_error('Create bulk_import_id on ActiveRecordBatchConsumerTest::Widget'\
|
111
|
+
' and set it in `build_records` for associations. Run rails g deimos:bulk_import_id:setup'\
|
112
|
+
' to create the migration.')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with one-to-one relation in association_list and custom bulk_import_id' do
|
117
|
+
let(:consumer_class) do
|
118
|
+
Class.new(described_class) do
|
119
|
+
schema 'MySchema'
|
120
|
+
namespace 'com.my-namespace'
|
121
|
+
key_config plain: true
|
122
|
+
record_class Widget
|
123
|
+
association_list :detail
|
124
|
+
bulk_import_id_column :custom_id
|
125
|
+
|
126
|
+
def build_records(messages)
|
127
|
+
messages.map do |m|
|
128
|
+
payload = m.payload
|
129
|
+
w = Widget.new(test_id: payload['test_id'],
|
130
|
+
some_int: payload['some_int'],
|
131
|
+
custom_id: SecureRandom.uuid)
|
132
|
+
d = Detail.new(title: payload['title'])
|
133
|
+
w.detail = d
|
134
|
+
w
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def key_columns(messages, klass)
|
139
|
+
case klass.to_s
|
140
|
+
when Widget.to_s
|
141
|
+
super
|
142
|
+
when Detail.to_s
|
143
|
+
%w(title widget_id)
|
144
|
+
else
|
145
|
+
[]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def columns(record_class)
|
150
|
+
all_cols = record_class.columns.map(&:name)
|
151
|
+
|
152
|
+
case record_class.to_s
|
153
|
+
when Widget.to_s
|
154
|
+
super
|
155
|
+
when Detail.to_s
|
156
|
+
all_cols - ['id']
|
157
|
+
else
|
158
|
+
[]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
before(:all) do
|
165
|
+
ActiveRecord::Base.connection.add_column(:widgets, :custom_id, :string, if_not_exists: true)
|
166
|
+
Widget.reset_column_information
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should save item to widget and associated detail' do
|
170
|
+
stub_const('MyBatchConsumer', consumer_class)
|
171
|
+
publish_batch([{ key: 2,
|
172
|
+
payload: { test_id: 'xyz', some_int: 5, title: 'Widget Title' } }])
|
173
|
+
expect(Widget.count).to eq(1)
|
174
|
+
expect(Detail.count).to eq(1)
|
175
|
+
expect(Widget.first.id).to eq(Detail.first.widget_id)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'with one-to-many relationship in association_list and default bulk_import_id' do
|
180
|
+
let(:consumer_class) do
|
181
|
+
Class.new(described_class) do
|
182
|
+
schema 'MySchema'
|
183
|
+
namespace 'com.my-namespace'
|
184
|
+
key_config plain: true
|
185
|
+
record_class Widget
|
186
|
+
association_list :locales
|
187
|
+
|
188
|
+
def build_records(messages)
|
189
|
+
messages.map do |m|
|
190
|
+
payload = m.payload
|
191
|
+
w = Widget.new(test_id: payload['test_id'],
|
192
|
+
some_int: payload['some_int'],
|
193
|
+
bulk_import_id: SecureRandom.uuid)
|
194
|
+
w.locales << Locale.new(title: payload['title'], language: 'en')
|
195
|
+
w.locales << Locale.new(title: payload['title'], language: 'fr')
|
196
|
+
w
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def key_columns(messages, klass)
|
201
|
+
case klass.to_s
|
202
|
+
when Widget.to_s
|
203
|
+
super
|
204
|
+
when Detail.to_s
|
205
|
+
%w(title widget_id)
|
206
|
+
when Locale.to_s
|
207
|
+
%w(title language)
|
208
|
+
else
|
209
|
+
[]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def columns(record_class)
|
214
|
+
all_cols = record_class.columns.map(&:name)
|
215
|
+
|
216
|
+
case record_class.to_s
|
217
|
+
when Widget.to_s
|
218
|
+
super
|
219
|
+
when Detail.to_s, Locale.to_s
|
220
|
+
all_cols - ['id']
|
221
|
+
else
|
222
|
+
[]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
before(:all) do
|
229
|
+
ActiveRecord::Base.connection.add_column(:widgets, :bulk_import_id, :string, if_not_exists: true)
|
230
|
+
Widget.reset_column_information
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should save item to widget and associated details' do
|
234
|
+
stub_const('MyBatchConsumer', consumer_class)
|
235
|
+
publish_batch([{ key: 2,
|
236
|
+
payload: { test_id: 'xyz', some_int: 5, title: 'Widget Title' } }])
|
237
|
+
expect(Widget.count).to eq(1)
|
238
|
+
expect(Locale.count).to eq(2)
|
239
|
+
expect(Widget.first.id).to eq(Locale.first.widget_id)
|
240
|
+
expect(Widget.first.id).to eq(Locale.second.widget_id)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -487,5 +487,25 @@ module ActiveRecordBatchConsumerTest
|
|
487
487
|
to match_array([have_attributes(id: 2, test_id: 'abc123')])
|
488
488
|
end
|
489
489
|
end
|
490
|
+
|
491
|
+
describe 'association_list feature for SQLite database' do
|
492
|
+
let(:consumer_class) do
|
493
|
+
Class.new(described_class) do
|
494
|
+
schema 'MySchema'
|
495
|
+
namespace 'com.my-namespace'
|
496
|
+
key_config plain: true
|
497
|
+
record_class Widget
|
498
|
+
association_list :locales
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
it 'should throw NotImplemented error' do
|
503
|
+
stub_const('MyBatchConsumer', consumer_class)
|
504
|
+
expect {
|
505
|
+
publish_batch([{ key: 2, payload: { test_id: 'xyz', some_int: 5, title: 'Widget Title' } }])
|
506
|
+
}.to raise_error(Deimos::MissingImplementationError)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
490
510
|
end
|
491
511
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deimos-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.19.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Orner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: avro_turf
|
@@ -446,6 +446,7 @@ files:
|
|
446
446
|
- lib/deimos/consume/batch_consumption.rb
|
447
447
|
- lib/deimos/consume/message_consumption.rb
|
448
448
|
- lib/deimos/consumer.rb
|
449
|
+
- lib/deimos/exceptions.rb
|
449
450
|
- lib/deimos/instrumentation.rb
|
450
451
|
- lib/deimos/kafka_message.rb
|
451
452
|
- lib/deimos/kafka_source.rb
|
@@ -488,6 +489,8 @@ files:
|
|
488
489
|
- lib/generators/deimos/active_record/templates/migration.rb.tt
|
489
490
|
- lib/generators/deimos/active_record/templates/model.rb.tt
|
490
491
|
- lib/generators/deimos/active_record_generator.rb
|
492
|
+
- lib/generators/deimos/bulk_import_id/templates/migration.rb.tt
|
493
|
+
- lib/generators/deimos/bulk_import_id_generator.rb
|
491
494
|
- lib/generators/deimos/db_backend/templates/migration
|
492
495
|
- lib/generators/deimos/db_backend/templates/rails3_migration
|
493
496
|
- lib/generators/deimos/db_backend_generator.rb
|
@@ -505,6 +508,7 @@ files:
|
|
505
508
|
- sig/avro.rbs
|
506
509
|
- sig/defs.rbs
|
507
510
|
- sig/fig_tree.rbs
|
511
|
+
- spec/active_record_batch_consumer_mysql_spec.rb
|
508
512
|
- spec/active_record_batch_consumer_spec.rb
|
509
513
|
- spec/active_record_consume/batch_slicer_spec.rb
|
510
514
|
- spec/active_record_consume/schema_model_converter_spec.rb
|
@@ -620,15 +624,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
620
624
|
version: '0'
|
621
625
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
622
626
|
requirements:
|
623
|
-
- - "
|
627
|
+
- - ">"
|
624
628
|
- !ruby/object:Gem::Version
|
625
|
-
version:
|
629
|
+
version: 1.3.1
|
626
630
|
requirements: []
|
627
631
|
rubygems_version: 3.3.20
|
628
632
|
signing_key:
|
629
633
|
specification_version: 4
|
630
634
|
summary: Kafka libraries for Ruby.
|
631
635
|
test_files:
|
636
|
+
- spec/active_record_batch_consumer_mysql_spec.rb
|
632
637
|
- spec/active_record_batch_consumer_spec.rb
|
633
638
|
- spec/active_record_consume/batch_slicer_spec.rb
|
634
639
|
- spec/active_record_consume/schema_model_converter_spec.rb
|