riverqueue-activerecord 0.11.0 → 0.12.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8316b332b5bc9bb1eae57d81751a31ef42342da5ef9a1914b3bceccb222041ac
4
- data.tar.gz: 49784779614e66d985af52516698d1766980ec43af019c62f20a8cd8459d1a69
3
+ metadata.gz: fe1889a91869f37c4e65eb7d68f94ca9413e1c27c35b6388bfc032cdc7042efd
4
+ data.tar.gz: 538826dffc04cf44bddb4a45deec552f99f3c71d3059d42453929a05383c6995
5
5
  SHA512:
6
- metadata.gz: d3b77f7ad4752bad4401ccaad39bf66513ed84e9aa498bfe8f3f8552fd7f7c6a63a60ed4b2b8cdb3ac33ea8367698c61c35a7f790ed43d9f6e67e1729cd8ede0
7
- data.tar.gz: 39992e6b530ed49a19deb622e2e689c0a221902fc23ef1ec8c7ca7bc54bbe9eb79e0bb2094aede12eafce8fe08497e248139260e2b9410e06eb43d85ae33bfbb
6
+ metadata.gz: ea37012be253b41ee43e17f43837ec35809171c6668bd367faf9716c930e1321521206757636013e135dbfd3f13e677b8ec0076f245be2327ddded4bfa9c09c8
7
+ data.tar.gz: dcf1f5c99e8ebef5f2900f702b3714b26d1de04ad63a7b4e938c9e499ecd1ac8f51ad8a6d43b2dd942d58b34ca223dc4714ed3901aba27ada01c586b5e75e20c
data/lib/driver.rb CHANGED
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "securerandom"
2
4
 
3
5
  module River::Driver
4
- # Provides an ActiveRecord driver for River that supports both PostgreSQL
6
+ # Provides an ActiveRecord driver for River supporting PostgreSQL, YugabyteDB,
5
7
  # and SQLite.
6
8
  #
7
9
  # Used in conjunction with a River client like:
@@ -10,7 +12,101 @@ module River::Driver
10
12
  # client = River::Client.new(River::Driver::ActiveRecord.new)
11
13
  #
12
14
  class ActiveRecord
13
- SQLITE_CONFLICT_WHERE = <<~SQL.chomp
15
+ include River::Driver::Runtime
16
+
17
+ # Connection class whose pool and transaction context this driver uses.
18
+ attr_reader :connection_class
19
+
20
+ # Uses an established PostgreSQL or SQLite connection. The class must be
21
+ # ActiveRecord::Base or an abstract Active Record class. Routing follows its
22
+ # current role/shard; configure consumers explicitly for each database.
23
+ def initialize(connection_class: ::ActiveRecord::Base)
24
+ unless connection_class.is_a?(Class) && connection_class <= ::ActiveRecord::Base
25
+ raise ArgumentError, "connection_class must be an Active Record class"
26
+ end
27
+
28
+ unless connection_class == ::ActiveRecord::Base || connection_class.abstract_class?
29
+ raise ArgumentError, "connection_class must be abstract"
30
+ end
31
+
32
+ @connection_class = connection_class
33
+ @postgres_capabilities_cache = PostgresCapabilities::Cache.new
34
+ @is_sqlite = connection_class.connection.adapter_name.downcase.include?("sqlite")
35
+ # Do not inherit application scopes, callbacks, or schema caches. Each
36
+ # driver owns its model, but all operations use the selected class's pool.
37
+ @job_model = Class.new(::ActiveRecord::Base) do
38
+ self.table_name = "river_job"
39
+ define_singleton_method(:connection_pool) { connection_class.connection_pool }
40
+
41
+ def self.dangerous_attribute_method?(method_name)
42
+ return false if method_name == "errors"
43
+
44
+ super
45
+ end
46
+
47
+ def errors = {}
48
+ end
49
+ end
50
+
51
+ def job_get_by_id(id)
52
+ if @is_sqlite
53
+ row = sqlite_job_rows("WHERE id = ? LIMIT 1", [id]).first
54
+ row ? sqlite_to_job_row_from_raw(row) : nil
55
+ else
56
+ row = @job_model.find_by(id: id)
57
+ row ? to_job_row_from_model(row) : nil
58
+ end
59
+ end
60
+
61
+ def job_insert(insert_params)
62
+ job_insert_many([insert_params]).first
63
+ end
64
+
65
+ def job_insert_many(insert_params_many)
66
+ return [] if insert_params_many.empty?
67
+
68
+ return sqlite_job_insert_many(insert_params_many) if @is_sqlite
69
+
70
+ transaction do
71
+ results = postgres_job_insert_many(insert_params_many)
72
+ postgres_notify_insert(insert_params_many)
73
+ results
74
+ end
75
+ end
76
+
77
+ def job_list(params = :all)
78
+ return super unless params == :all
79
+
80
+ runtime_job_rows("ORDER BY id")
81
+ end
82
+
83
+ def rollback_exception
84
+ ::ActiveRecord::Rollback
85
+ end
86
+
87
+ # Backend used by River::Migrator.
88
+ def migration_backend = @is_sqlite ? :sqlite : :postgresql
89
+
90
+ # Pins a raw connection for migration SQL and refreshes cached model columns.
91
+ def migration_connection
92
+ @connection_class.connection_pool.with_connection do |connection|
93
+ raise River::Error, "migrations cannot run inside an application transaction" if connection.transaction_open?
94
+
95
+ begin
96
+ yield connection.raw_connection
97
+ ensure
98
+ @job_model.reset_column_information
99
+ end
100
+ end
101
+ end
102
+
103
+ # Runs the block in a new Active Record transaction or savepoint and returns
104
+ # the block's result.
105
+ def transaction(&)
106
+ @connection_class.transaction(requires_new: true, &)
107
+ end
108
+
109
+ SQLITE_CONFLICT_WHERE = <<~SQL.chomp.freeze
14
110
  unique_key IS NOT NULL
15
111
  AND unique_states IS NOT NULL
16
112
  AND CASE state
@@ -25,123 +121,169 @@ module River::Driver
25
121
  ELSE 0
26
122
  END >= 1
27
123
  SQL
28
- private_constant :SQLITE_CONFLICT_WHERE
29
124
 
30
125
  # SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
31
126
  # through json() so this driver can read both the current JSONB format and
32
127
  # the text JSON used by River migrations through version 006. Cast times to
33
128
  # text so ActiveRecord doesn't interpret timezone-less SQLite timestamps in
34
- # the process timezone.
35
- SQLITE_JOB_COLUMNS = <<~SQL.chomp
129
+ # the process timezone. Flag 10 validates JSONB contents, not just its header,
130
+ # so damaged blobs reach the row decoder without aborting the SQL query.
131
+ SQLITE_JOB_COLUMNS = <<~SQL.chomp.freeze
36
132
  id,
37
- json(args) AS args,
133
+ CASE WHEN json_valid(args, 10) THEN json(args) ELSE args END AS args,
38
134
  attempt,
39
135
  CAST(attempted_at AS text) AS attempted_at,
40
- json(attempted_by) AS attempted_by,
136
+ CASE WHEN json_valid(attempted_by, 10) THEN json(attempted_by) ELSE attempted_by END AS attempted_by,
41
137
  CAST(created_at AS text) AS created_at,
42
- json(errors) AS errors,
138
+ CASE WHEN json_valid(errors, 10) THEN json(errors) ELSE errors END AS errors,
43
139
  CAST(finalized_at AS text) AS finalized_at,
44
140
  kind,
45
141
  max_attempts,
46
- json(metadata) AS metadata,
142
+ CASE WHEN json_valid(metadata, 10) THEN json(metadata) ELSE metadata END AS metadata,
47
143
  priority,
48
144
  queue,
49
145
  state,
50
146
  CAST(scheduled_at AS text) AS scheduled_at,
51
- json(tags) AS tags,
147
+ CASE WHEN json_valid(tags, 10) THEN json(tags) ELSE tags END AS tags,
52
148
  unique_key,
53
149
  unique_states
54
150
  SQL
55
- private_constant :SQLITE_JOB_COLUMNS
56
-
57
- SQLITE_UNIQUE_NONCE_KEY = "river:unique_nonce"
58
- private_constant :SQLITE_UNIQUE_NONCE_KEY
59
-
60
- def initialize
61
- @is_sqlite = ::ActiveRecord::Base.connection.adapter_name.downcase.include?("sqlite")
62
-
63
- # It's Ruby, so we can only define a model after ActiveRecord's established a
64
- # connection because it's all dynamic.
65
- if !River::Driver::ActiveRecord.const_defined?(:RiverJob)
66
- River::Driver::ActiveRecord.const_set(:RiverJob, Class.new(::ActiveRecord::Base) do
67
- self.table_name = "river_job"
68
-
69
- # Unfortunately, Rails errors if you have a column called `errors` and
70
- # provides no way to remap names (beyond ignoring a column, which we
71
- # really don't want). This patch is in place so we can hydrate this
72
- # model at all without ActiveRecord self-immolating.
73
- def self.dangerous_attribute_method?(method_name)
74
- return false if method_name == "errors"
75
- super
76
- end
77
-
78
- # See comment above, but since we force allowed `errors` as an
79
- # attribute name, ActiveRecord would otherwise fail to save a row as
80
- # it checked for its own `errors` hash and finding no values.
81
- def errors = {}
82
- end)
151
+
152
+ UNIQUE_INSERT_METADATA_KEY = "river:unique_nonce"
153
+
154
+ private_constant :SQLITE_CONFLICT_WHERE, :SQLITE_JOB_COLUMNS, :UNIQUE_INSERT_METADATA_KEY
155
+
156
+ private def format_time(time)
157
+ time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
158
+ end
159
+
160
+ private def postgres_insert_params_to_hash(insert_params, nonce)
161
+ metadata = insert_params.metadata || {}
162
+ metadata = metadata.merge(UNIQUE_INSERT_METADATA_KEY => nonce) if nonce
163
+ {
164
+ args: JSON.parse(insert_params.encoded_args),
165
+ kind: insert_params.kind,
166
+ max_attempts: insert_params.max_attempts,
167
+ metadata: metadata,
168
+ priority: insert_params.priority,
169
+ queue: insert_params.queue,
170
+ scheduled_at: insert_params.scheduled_at,
171
+ state: insert_params.state,
172
+ tags: insert_params.tags || [],
173
+ unique_key: insert_params.unique_key,
174
+ unique_states: insert_params.unique_states
175
+ }
176
+ end
177
+
178
+ private def postgres_job_insert_many(insert_params_many)
179
+ capabilities = postgres_capabilities
180
+ nonce = SecureRandom.hex(8) if capabilities.unique_insert_mode == :metadata_nonce
181
+ res = @job_model.upsert_all(
182
+ insert_params_many.map { |param| postgres_insert_params_to_hash(param, nonce) },
183
+ on_duplicate: Arel.sql("kind = EXCLUDED.kind"),
184
+ returning: Arel.sql("*, #{capabilities.unique_insert_sql} AS unique_skipped_as_duplicate"),
185
+
186
+ # It'd be nice to specify this as `(kind, unique_key) WHERE unique_key
187
+ # IS NOT NULL` like we do elsewhere, but in its pure ingenuity, fucking
188
+ # ActiveRecord tries to look up a unique index instead of letting
189
+ # Postgres handle that, and of course it doesn't support a `WHERE`
190
+ # clause. The workaround is to target the index name instead of columns.
191
+ unique_by: "river_job_unique_idx"
192
+ )
193
+ postgres_to_insert_results(res, nonce)
194
+ end
195
+
196
+ private def postgres_to_insert_results(res, nonce)
197
+ res.rows.map do |row|
198
+ job, duplicate = postgres_to_job_row_from_raw(row, res.columns, res.column_types)
199
+ [job, nonce ? job.metadata[UNIQUE_INSERT_METADATA_KEY] != nonce : duplicate]
83
200
  end
84
201
  end
85
202
 
86
- def job_get_by_id(id)
87
- if @is_sqlite
88
- row = sqlite_job_rows("WHERE id = ? LIMIT 1", [id]).first
89
- row ? sqlite_to_job_row_from_raw(row) : nil
90
- else
91
- data_set = RiverJob.where(id: id)
92
- data_set.first ? to_job_row_from_model(data_set.first) : nil
203
+ private def postgres_to_job_row_from_model(river_job)
204
+ # Read attributes directly because Active Record shadows the errors column.
205
+ runtime_decode_postgres_job(river_job.attributes)
206
+ end
207
+
208
+ # Upserts bypass model casting; normalize their values before shared decoding.
209
+ private def postgres_to_job_row_from_raw(row, columns, column_types)
210
+ river_job = {}
211
+
212
+ row.each_with_index do |val, i|
213
+ river_job[columns[i]] = column_types[i].deserialize(val)
93
214
  end
215
+
216
+ [runtime_decode_postgres_job(river_job), river_job["unique_skipped_as_duplicate"]]
94
217
  end
95
218
 
96
- def job_insert(insert_params)
97
- job_insert_many([insert_params]).first
219
+ private def runtime_connection_pool
220
+ @connection_class.connection_pool
98
221
  end
99
222
 
100
- def job_insert_many(insert_params_many)
101
- @is_sqlite ? sqlite_job_insert_many(insert_params_many) : postgres_job_insert_many(insert_params_many)
223
+ private def runtime_execute(sql)
224
+ @connection_class.connection.execute(sql)
102
225
  end
103
226
 
104
- def job_list
227
+ private def runtime_job_list_without_params
228
+ job_list(:all)
229
+ end
230
+
231
+ private def runtime_job_rows(suffix)
105
232
  if @is_sqlite
106
- sqlite_job_rows("ORDER BY id").map { |row| sqlite_to_job_row_from_raw(row) }
233
+ sqlite_job_rows(suffix).map { |row| sqlite_to_job_row_from_raw(row) }
107
234
  else
108
- RiverJob.order(:id).all.map { |job| to_job_row_from_model(job) }
235
+ @job_model.find_by_sql("SELECT * FROM river_job #{suffix}").map { |row| to_job_row_from_model(row) }
109
236
  end
110
237
  end
111
238
 
112
- def rollback_exception
113
- ::ActiveRecord::Rollback
239
+ private def runtime_postgres?
240
+ !@is_sqlite
114
241
  end
115
242
 
116
- def transaction(&)
117
- ::ActiveRecord::Base.transaction(requires_new: true, &)
243
+ private def runtime_query_rows(sql)
244
+ if @is_sqlite
245
+ @connection_class.connection.raw_connection.execute(sql).to_a
246
+ else
247
+ @connection_class.connection.select_all(sql).to_a
248
+ end
118
249
  end
119
250
 
120
- private def postgres_job_insert_many(insert_params_many)
121
- res = RiverJob.upsert_all(
122
- insert_params_many.map { |param| postgres_insert_params_to_hash(param) },
123
- on_duplicate: Arel.sql("kind = EXCLUDED.kind"),
124
- returning: Arel.sql("*, (xmax != 0) AS unique_skipped_as_duplicate"),
251
+ private def runtime_quote(value)
252
+ @connection_class.connection.quote(value)
253
+ end
125
254
 
126
- # It'd be nice to specify this as `(kind, unique_key) WHERE unique_key
127
- # IS NOT NULL` like we do elsewhere, but in its pure ingenuity, fucking
128
- # ActiveRecord tries to look up a unique index instead of letting
129
- # Postgres handle that, and of course it doesn't support a `WHERE`
130
- # clause. The workaround is to target the index name instead of columns.
131
- unique_by: "river_job_unique_idx"
132
- )
133
- postgres_to_insert_results(res)
255
+ private def runtime_unique_violation_class
256
+ ::ActiveRecord::RecordNotUnique
257
+ end
258
+
259
+ private def runtime_value(row, key)
260
+ row[key.to_s] || row[key]
261
+ end
262
+
263
+ private def sqlite_insert_params_to_hash(insert_params, nonce)
264
+ {
265
+ args: JSON.parse(insert_params.encoded_args),
266
+ kind: insert_params.kind,
267
+ max_attempts: insert_params.max_attempts,
268
+ metadata: {UNIQUE_INSERT_METADATA_KEY => nonce},
269
+ priority: insert_params.priority,
270
+ queue: insert_params.queue,
271
+ scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
272
+ state: insert_params.state,
273
+ tags: insert_params.tags || [],
274
+ unique_key: insert_params.unique_key&.unpack1("H*"),
275
+ unique_states: insert_params.unique_states&.to_i(2)
276
+ }.tap { |values| values[:metadata] = (insert_params.metadata || {}).merge(values[:metadata]) }
134
277
  end
135
278
 
136
279
  # River's current SQLite driver uses json_each to make a batch a single,
137
280
  # atomic statement. The JSON columns are converted to SQLite JSONB here,
138
281
  # matching migration 007 and newer River databases.
139
282
  private def sqlite_job_insert_many(insert_params_many)
140
- return [] if insert_params_many.empty?
141
-
142
- ::ActiveRecord::Base.transaction(requires_new: true) do
143
- nonce = SecureRandom.hex(8)
144
- jobs = insert_params_many.map { |param| sqlite_insert_params_to_hash(param, nonce) }
283
+ nonces = sqlite_insert_nonces(insert_params_many)
284
+ @connection_class.transaction(requires_new: true) do
285
+ jobs = insert_params_many.zip(nonces).map { |param, nonce| sqlite_insert_params_to_hash(param, nonce) }
286
+ inserted_nonces = nonces.to_h { |nonce| [nonce, true] }
145
287
 
146
288
  sql = <<~SQL
147
289
  INSERT INTO river_job (
@@ -181,194 +323,19 @@ module River::Driver
181
323
  RETURNING #{SQLITE_JOB_COLUMNS}
182
324
  SQL
183
325
 
184
- rows = ::ActiveRecord::Base.connection.raw_connection.execute(sql, [JSON.dump(jobs)])
326
+ rows = @connection_class.connection.raw_connection.execute(sql, [JSON.generate(jobs)])
185
327
  sqlite_notify_insert(insert_params_many)
186
328
 
187
329
  rows.map do |row|
188
330
  metadata = JSON.parse(row["metadata"])
189
- [sqlite_to_job_row_from_raw(row), metadata[SQLITE_UNIQUE_NONCE_KEY] != nonce]
331
+ [sqlite_to_job_row_from_raw(row), !inserted_nonces.key?(metadata[UNIQUE_INSERT_METADATA_KEY])]
190
332
  end
191
333
  end
192
334
  end
193
335
 
194
- private def postgres_insert_params_to_hash(insert_params)
195
- {
196
- args: JSON.parse(insert_params.encoded_args),
197
- kind: insert_params.kind,
198
- max_attempts: insert_params.max_attempts,
199
- priority: insert_params.priority,
200
- queue: insert_params.queue,
201
- state: insert_params.state,
202
- scheduled_at: insert_params.scheduled_at,
203
- tags: insert_params.tags || [],
204
- unique_key: insert_params.unique_key,
205
- unique_states: insert_params.unique_states
206
- }
207
- end
208
-
209
- private def sqlite_insert_params_to_hash(insert_params, nonce)
210
- {
211
- args: JSON.parse(insert_params.encoded_args),
212
- kind: insert_params.kind,
213
- max_attempts: insert_params.max_attempts,
214
- metadata: {SQLITE_UNIQUE_NONCE_KEY => nonce},
215
- priority: insert_params.priority,
216
- queue: insert_params.queue,
217
- scheduled_at: insert_params.scheduled_at ? format_time(insert_params.scheduled_at) : nil,
218
- state: insert_params.state,
219
- tags: insert_params.tags || [],
220
- unique_key: insert_params.unique_key&.unpack1("H*"),
221
- unique_states: insert_params.unique_states&.to_i(2)
222
- }
223
- end
224
-
225
- private def to_job_row_from_model(river_job)
226
- @is_sqlite ? sqlite_to_job_row_from_model(river_job) : postgres_to_job_row_from_model(river_job)
227
- end
228
-
229
- private def postgres_to_job_row_from_model(river_job)
230
- # needs to be accessed through values because `errors` is shadowed by both
231
- # ActiveRecord and the patch above
232
- errors = river_job.attributes["errors"]
233
-
234
- River::JobRow.new(
235
- id: river_job.id,
236
- args: river_job.args,
237
- attempt: river_job.attempt,
238
- attempted_at: river_job.attempted_at&.getutc,
239
- attempted_by: river_job.attempted_by,
240
- created_at: river_job.created_at.getutc,
241
- errors: errors&.map { |e|
242
- deserialized_error = JSON.parse(e, symbolize_names: true)
243
-
244
- River::AttemptError.new(
245
- at: Time.parse(deserialized_error[:at]),
246
- attempt: deserialized_error[:attempt],
247
- error: deserialized_error[:error],
248
- trace: deserialized_error[:trace]
249
- )
250
- },
251
- finalized_at: river_job.finalized_at&.getutc,
252
- kind: river_job.kind,
253
- max_attempts: river_job.max_attempts,
254
- metadata: river_job.metadata,
255
- priority: river_job.priority,
256
- queue: river_job.queue,
257
- scheduled_at: river_job.scheduled_at.getutc,
258
- state: river_job.state,
259
- tags: river_job.tags,
260
- unique_key: river_job.unique_key,
261
- unique_states: river_job.unique_states
262
- )
263
- end
264
-
265
- private def sqlite_to_job_row_from_model(river_job)
266
- row = sqlite_job_rows("WHERE id = ? LIMIT 1", [river_job.id]).first
267
- sqlite_to_job_row_from_raw(row)
268
- end
269
-
270
- private def sqlite_to_job_row_from_raw(row)
271
- errors = row["errors"] ? JSON.parse(row["errors"]) : []
272
-
273
- River::JobRow.new(
274
- id: row["id"],
275
- args: JSON.parse(row["args"]),
276
- attempt: row["attempt"],
277
- attempted_at: parse_sqlite_time(row["attempted_at"]),
278
- attempted_by: row["attempted_by"] ? JSON.parse(row["attempted_by"]) : nil,
279
- created_at: parse_sqlite_time(row["created_at"]),
280
- errors: errors.map { |e|
281
- River::AttemptError.new(
282
- at: Time.parse(e["at"]),
283
- attempt: e["attempt"],
284
- error: e["error"],
285
- trace: e["trace"]
286
- )
287
- },
288
- finalized_at: parse_sqlite_time(row["finalized_at"]),
289
- kind: row["kind"],
290
- max_attempts: row["max_attempts"],
291
- metadata: JSON.parse(row["metadata"]),
292
- priority: row["priority"],
293
- queue: row["queue"],
294
- scheduled_at: parse_sqlite_time(row["scheduled_at"]),
295
- state: row["state"],
296
- tags: JSON.parse(row["tags"]),
297
- unique_key: row["unique_key"]&.to_s,
298
- unique_states: row["unique_states"] ? ::River::UniqueBitmask.to_states(row["unique_states"]) : nil
299
- )
300
- end
301
-
302
- private def postgres_to_insert_results(res)
303
- res.rows.map do |row|
304
- postgres_to_job_row_from_raw(row, res.columns, res.column_types)
305
- end
306
- end
307
-
308
- # This is really awful, but some of ActiveRecord's methods (e.g. `.create`)
309
- # return a model, and others (e.g. `.upsert`) return raw values, and
310
- # therefore this second version from unmarshaling a job row exists. I
311
- # searched long and hard for a way to have the former type of method return
312
- # raw or the latter type of method return a model, but was unable to find
313
- # anything.
314
- private def postgres_to_job_row_from_raw(row, columns, column_types)
315
- river_job = {}
316
-
317
- row.each_with_index do |val, i|
318
- river_job[columns[i]] = column_types[i].deserialize(val)
319
- end
320
-
321
- errors = river_job["errors"]&.map do |e|
322
- deserialized_error = JSON.parse(e)
323
-
324
- River::AttemptError.new(
325
- at: Time.parse(deserialized_error["at"]),
326
- attempt: deserialized_error["attempt"],
327
- error: deserialized_error["error"],
328
- trace: deserialized_error["trace"]
329
- )
330
- end
331
-
332
- [
333
- River::JobRow.new(
334
- id: river_job["id"],
335
- args: river_job["args"],
336
- attempt: river_job["attempt"],
337
- attempted_at: river_job["attempted_at"]&.getutc,
338
- attempted_by: river_job["attempted_by"],
339
- created_at: river_job["created_at"].getutc,
340
- errors: errors,
341
- finalized_at: river_job["finalized_at"]&.getutc,
342
- kind: river_job["kind"],
343
- max_attempts: river_job["max_attempts"],
344
- metadata: river_job["metadata"],
345
- priority: river_job["priority"],
346
- queue: river_job["queue"],
347
- scheduled_at: river_job["scheduled_at"].getutc,
348
- state: river_job["state"],
349
- tags: river_job["tags"],
350
- unique_key: river_job["unique_key"],
351
- unique_states: ::River::UniqueBitmask.to_states(river_job["unique_states"]&.to_i(2))
352
- ),
353
- river_job["unique_skipped_as_duplicate"]
354
- ]
355
- end
356
-
357
- private def format_time(time)
358
- time.getutc.round(3).strftime("%Y-%m-%d %H:%M:%S.%3N")
359
- end
360
-
361
- private def parse_sqlite_time(value)
362
- return nil unless value
363
-
364
- value = value.to_s
365
- value += " UTC" unless value.match?(/(?:Z|[+-]\d{2}:?\d{2})\z/)
366
- Time.parse(value).utc
367
- end
368
-
369
336
  private def sqlite_job_rows(suffix, binds = [])
370
337
  sql = "SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}"
371
- ::ActiveRecord::Base.connection.raw_connection.execute(sql, binds)
338
+ @connection_class.connection.raw_connection.execute(sql, binds)
372
339
  end
373
340
 
374
341
  private def sqlite_notify_insert(insert_params_many)
@@ -379,10 +346,10 @@ module River::Driver
379
346
  return if queues.empty?
380
347
 
381
348
  notifications = queues.map do |queue|
382
- {payload: JSON.dump({queue: queue}), topic: "insert"}
349
+ {payload: JSON.generate({queue: queue}), topic: "river_insert"}
383
350
  end
384
351
 
385
- ::ActiveRecord::Base.connection.raw_connection.execute(<<~SQL, [JSON.dump(notifications)])
352
+ @connection_class.connection.raw_connection.execute(<<~SQL, [JSON.generate(notifications)])
386
353
  INSERT INTO river_notification (payload, topic)
387
354
  SELECT
388
355
  json_extract(value, '$.payload'),
@@ -390,5 +357,18 @@ module River::Driver
390
357
  FROM json_each(cast(? AS blob))
391
358
  SQL
392
359
  end
360
+
361
+ private def sqlite_to_job_row_from_model(river_job)
362
+ row = sqlite_job_rows("WHERE id = ? LIMIT 1", [river_job.id]).first
363
+ sqlite_to_job_row_from_raw(row)
364
+ end
365
+
366
+ private def sqlite_to_job_row_from_raw(row)
367
+ runtime_decode_sqlite_job(row)
368
+ end
369
+
370
+ private def to_job_row_from_model(river_job)
371
+ @is_sqlite ? sqlite_to_job_row_from_model(river_job) : postgres_to_job_row_from_model(river_job)
372
+ end
393
373
  end
394
374
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record"
4
+ require "riverqueue"
2
5
 
3
6
  require_relative "driver"
4
7
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riverqueue-activerecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Gentry
@@ -50,8 +50,22 @@ dependencies:
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '1000'
53
- description: ActiveRecord PostgreSQL and SQLite driver for the River Ruby gem. Use
54
- in conjunction with the riverqueue gem to insert jobs that are worked in Go.
53
+ - !ruby/object:Gem::Dependency
54
+ name: riverqueue
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - '='
58
+ - !ruby/object:Gem::Version
59
+ version: 0.12.0
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '='
65
+ - !ruby/object:Gem::Version
66
+ version: 0.12.0
67
+ description: ActiveRecord PostgreSQL and SQLite driver for inserting and working River
68
+ jobs in Ruby.
55
69
  email: brandur@brandur.org
56
70
  executables: []
57
71
  extensions: []
@@ -61,7 +75,7 @@ files:
61
75
  - lib/riverqueue-activerecord.rb
62
76
  homepage: https://riverqueue.com
63
77
  licenses:
64
- - LGPL-3.0-or-later
78
+ - MPL-2.0
65
79
  metadata: {}
66
80
  rdoc_options: []
67
81
  require_paths:
@@ -70,7 +84,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
84
  requirements:
71
85
  - - ">="
72
86
  - !ruby/object:Gem::Version
73
- version: '0'
87
+ version: '3.2'
74
88
  required_rubygems_version: !ruby/object:Gem::Requirement
75
89
  requirements:
76
90
  - - ">="