riverqueue-activerecord 0.10.1 → 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: b74ec767cc9eb7216aaad6ea31922d9c7ba2d19fd9f49700c230092001f38b6a
4
- data.tar.gz: aee6b04ec4bcfa62ab555480fc2a6c476cccb165c18392f61d697f86a03d0f1e
3
+ metadata.gz: fe1889a91869f37c4e65eb7d68f94ca9413e1c27c35b6388bfc032cdc7042efd
4
+ data.tar.gz: 538826dffc04cf44bddb4a45deec552f99f3c71d3059d42453929a05383c6995
5
5
  SHA512:
6
- metadata.gz: 9d06c2301b8d231bd75c97c64e960b7fccdc13a9969a4a3f2fee945de12c0f08c3c028f89e16f67600e422a9b1d64cb05e513bc80f25f34af45f6fff01866981
7
- data.tar.gz: 7e5489860c578f9eabab7c7c8d81ce52446b35f6512e7bf2711121c6e1981ab79ffaf52b127998ad0135c74b796e64961acdbbe74e17f7cfc71185618651967f
6
+ metadata.gz: ea37012be253b41ee43e17f43837ec35809171c6668bd367faf9716c930e1321521206757636013e135dbfd3f13e677b8ec0076f245be2327ddded4bfa9c09c8
7
+ data.tar.gz: dcf1f5c99e8ebef5f2900f702b3714b26d1de04ad63a7b4e938c9e499ecd1ac8f51ad8a6d43b2dd942d58b34ca223dc4714ed3901aba27ada01c586b5e75e20c
data/lib/driver.rb CHANGED
@@ -1,39 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
1
5
  module River::Driver
2
- # Provides a ActiveRecord driver for River.
6
+ # Provides an ActiveRecord driver for River supporting PostgreSQL, YugabyteDB,
7
+ # and SQLite.
3
8
  #
4
9
  # Used in conjunction with a River client like:
5
10
  #
6
- # DB = ActiveRecord.connect("postgres://...")
7
- # client = River::Client.new(River::Driver::ActiveRecord.new(DB))
11
+ # ActiveRecord::Base.establish_connection("postgres://...")
12
+ # client = River::Client.new(River::Driver::ActiveRecord.new)
8
13
  #
9
14
  class ActiveRecord
10
- def initialize
11
- # It's Ruby, so we can only define a model after ActiveRecord's established a
12
- # connection because it's all dynamic.
13
- if !River::Driver::ActiveRecord.const_defined?(:RiverJob)
14
- River::Driver::ActiveRecord.const_set(:RiverJob, Class.new(::ActiveRecord::Base) do
15
- self.table_name = "river_job"
16
-
17
- # Unfortunately, Rails errors if you have a column called `errors` and
18
- # provides no way to remap names (beyond ignoring a column, which we
19
- # really don't want). This patch is in place so we can hydrate this
20
- # model at all without ActiveRecord self-immolating.
21
- def self.dangerous_attribute_method?(method_name)
22
- return false if method_name == "errors"
23
- super
24
- end
25
-
26
- # See comment above, but since we force allowed `errors` as an
27
- # attribute name, ActiveRecord would otherwise fail to save a row as
28
- # it checked for its own `errors` hash and finding no values.
29
- def errors = {}
30
- end)
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 = {}
31
48
  end
32
49
  end
33
50
 
34
51
  def job_get_by_id(id)
35
- data_set = RiverJob.where(id: id)
36
- data_set.first ? to_job_row_from_model(data_set.first) : nil
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
37
59
  end
38
60
 
39
61
  def job_insert(insert_params)
@@ -41,138 +63,312 @@ module River::Driver
41
63
  end
42
64
 
43
65
  def job_insert_many(insert_params_many)
44
- res = RiverJob.upsert_all(
45
- insert_params_many.map { |param| insert_params_to_hash(param) },
46
- on_duplicate: Arel.sql("kind = EXCLUDED.kind"),
47
- returning: Arel.sql("*, (xmax != 0) AS unique_skipped_as_duplicate"),
66
+ return [] if insert_params_many.empty?
48
67
 
49
- # It'd be nice to specify this as `(kind, unique_key) WHERE unique_key
50
- # IS NOT NULL` like we do elsewhere, but in its pure ingenuity, fucking
51
- # ActiveRecord tries to look up a unique index instead of letting
52
- # Postgres handle that, and of course it doesn't support a `WHERE`
53
- # clause. The workaround is to target the index name instead of columns.
54
- unique_by: "river_job_unique_idx"
55
- )
56
- to_insert_results(res)
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
57
75
  end
58
76
 
59
- def job_list
60
- data_set = RiverJob.order(:id)
61
- data_set.all.map { |job| to_job_row_from_model(job) }
77
+ def job_list(params = :all)
78
+ return super unless params == :all
79
+
80
+ runtime_job_rows("ORDER BY id")
62
81
  end
63
82
 
64
83
  def rollback_exception
65
84
  ::ActiveRecord::Rollback
66
85
  end
67
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.
68
105
  def transaction(&)
69
- ::ActiveRecord::Base.transaction(requires_new: true, &)
106
+ @connection_class.transaction(requires_new: true, &)
107
+ end
108
+
109
+ SQLITE_CONFLICT_WHERE = <<~SQL.chomp.freeze
110
+ unique_key IS NOT NULL
111
+ AND unique_states IS NOT NULL
112
+ AND CASE state
113
+ WHEN 'available' THEN unique_states & (1 << 0)
114
+ WHEN 'cancelled' THEN unique_states & (1 << 1)
115
+ WHEN 'completed' THEN unique_states & (1 << 2)
116
+ WHEN 'discarded' THEN unique_states & (1 << 3)
117
+ WHEN 'pending' THEN unique_states & (1 << 4)
118
+ WHEN 'retryable' THEN unique_states & (1 << 5)
119
+ WHEN 'running' THEN unique_states & (1 << 6)
120
+ WHEN 'scheduled' THEN unique_states & (1 << 7)
121
+ ELSE 0
122
+ END >= 1
123
+ SQL
124
+
125
+ # SQLite 3.45+ may store JSON as binary JSONB. Always project JSON columns
126
+ # through json() so this driver can read both the current JSONB format and
127
+ # the text JSON used by River migrations through version 006. Cast times to
128
+ # text so ActiveRecord doesn't interpret timezone-less SQLite timestamps in
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
132
+ id,
133
+ CASE WHEN json_valid(args, 10) THEN json(args) ELSE args END AS args,
134
+ attempt,
135
+ CAST(attempted_at AS text) AS attempted_at,
136
+ CASE WHEN json_valid(attempted_by, 10) THEN json(attempted_by) ELSE attempted_by END AS attempted_by,
137
+ CAST(created_at AS text) AS created_at,
138
+ CASE WHEN json_valid(errors, 10) THEN json(errors) ELSE errors END AS errors,
139
+ CAST(finalized_at AS text) AS finalized_at,
140
+ kind,
141
+ max_attempts,
142
+ CASE WHEN json_valid(metadata, 10) THEN json(metadata) ELSE metadata END AS metadata,
143
+ priority,
144
+ queue,
145
+ state,
146
+ CAST(scheduled_at AS text) AS scheduled_at,
147
+ CASE WHEN json_valid(tags, 10) THEN json(tags) ELSE tags END AS tags,
148
+ unique_key,
149
+ unique_states
150
+ SQL
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")
70
158
  end
71
159
 
72
- private def insert_params_to_hash(insert_params)
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
73
163
  {
74
164
  args: JSON.parse(insert_params.encoded_args),
75
165
  kind: insert_params.kind,
76
166
  max_attempts: insert_params.max_attempts,
167
+ metadata: metadata,
77
168
  priority: insert_params.priority,
78
169
  queue: insert_params.queue,
79
- state: insert_params.state,
80
170
  scheduled_at: insert_params.scheduled_at,
171
+ state: insert_params.state,
81
172
  tags: insert_params.tags || [],
82
173
  unique_key: insert_params.unique_key,
83
174
  unique_states: insert_params.unique_states
84
175
  }
85
176
  end
86
177
 
87
- private def to_job_row_from_model(river_job)
88
- # needs to be accessed through values because `errors` is shadowed by both
89
- # ActiveRecord and the patch above
90
- errors = river_job.attributes["errors"]
91
-
92
- River::JobRow.new(
93
- id: river_job.id,
94
- args: river_job.args,
95
- attempt: river_job.attempt,
96
- attempted_at: river_job.attempted_at&.getutc,
97
- attempted_by: river_job.attempted_by,
98
- created_at: river_job.created_at.getutc,
99
- errors: errors&.map { |e|
100
- deserialized_error = JSON.parse(e, symbolize_names: true)
101
-
102
- River::AttemptError.new(
103
- at: Time.parse(deserialized_error[:at]),
104
- attempt: deserialized_error[:attempt],
105
- error: deserialized_error[:error],
106
- trace: deserialized_error[:trace]
107
- )
108
- },
109
- finalized_at: river_job.finalized_at&.getutc,
110
- kind: river_job.kind,
111
- max_attempts: river_job.max_attempts,
112
- metadata: river_job.metadata,
113
- priority: river_job.priority,
114
- queue: river_job.queue,
115
- scheduled_at: river_job.scheduled_at.getutc,
116
- state: river_job.state,
117
- tags: river_job.tags,
118
- unique_key: river_job.unique_key,
119
- unique_states: river_job.unique_states
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"
120
192
  )
193
+ postgres_to_insert_results(res, nonce)
121
194
  end
122
195
 
123
- private def to_insert_results(res)
196
+ private def postgres_to_insert_results(res, nonce)
124
197
  res.rows.map do |row|
125
- to_job_row_from_raw(row, res.columns, res.column_types)
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]
126
200
  end
127
201
  end
128
202
 
129
- # This is really awful, but some of ActiveRecord's methods (e.g. `.create`)
130
- # return a model, and others (e.g. `.upsert`) return raw values, and
131
- # therefore this second version from unmarshaling a job row exists. I
132
- # searched long and hard for a way to have the former type of method return
133
- # raw or the latter type of method return a model, but was unable to find
134
- # anything.
135
- private def to_job_row_from_raw(row, columns, column_types)
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)
136
210
  river_job = {}
137
211
 
138
212
  row.each_with_index do |val, i|
139
213
  river_job[columns[i]] = column_types[i].deserialize(val)
140
214
  end
141
215
 
142
- errors = river_job["errors"]&.map do |e|
143
- deserialized_error = JSON.parse(e)
216
+ [runtime_decode_postgres_job(river_job), river_job["unique_skipped_as_duplicate"]]
217
+ end
218
+
219
+ private def runtime_connection_pool
220
+ @connection_class.connection_pool
221
+ end
222
+
223
+ private def runtime_execute(sql)
224
+ @connection_class.connection.execute(sql)
225
+ end
226
+
227
+ private def runtime_job_list_without_params
228
+ job_list(:all)
229
+ end
230
+
231
+ private def runtime_job_rows(suffix)
232
+ if @is_sqlite
233
+ sqlite_job_rows(suffix).map { |row| sqlite_to_job_row_from_raw(row) }
234
+ else
235
+ @job_model.find_by_sql("SELECT * FROM river_job #{suffix}").map { |row| to_job_row_from_model(row) }
236
+ end
237
+ end
238
+
239
+ private def runtime_postgres?
240
+ !@is_sqlite
241
+ end
242
+
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
249
+ end
250
+
251
+ private def runtime_quote(value)
252
+ @connection_class.connection.quote(value)
253
+ end
254
+
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]) }
277
+ end
278
+
279
+ # River's current SQLite driver uses json_each to make a batch a single,
280
+ # atomic statement. The JSON columns are converted to SQLite JSONB here,
281
+ # matching migration 007 and newer River databases.
282
+ private def sqlite_job_insert_many(insert_params_many)
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] }
287
+
288
+ sql = <<~SQL
289
+ INSERT INTO river_job (
290
+ args,
291
+ created_at,
292
+ kind,
293
+ max_attempts,
294
+ metadata,
295
+ priority,
296
+ queue,
297
+ scheduled_at,
298
+ state,
299
+ tags,
300
+ unique_key,
301
+ unique_states
302
+ )
303
+ SELECT
304
+ jsonb(json_extract(value, '$.args')),
305
+ datetime('now', 'subsec'),
306
+ cast(json_extract(value, '$.kind') AS text),
307
+ cast(json_extract(value, '$.max_attempts') AS integer),
308
+ jsonb(json_extract(value, '$.metadata')),
309
+ cast(json_extract(value, '$.priority') AS integer),
310
+ cast(json_extract(value, '$.queue') AS text),
311
+ coalesce(cast(json_extract(value, '$.scheduled_at') AS text), datetime('now', 'subsec')),
312
+ cast(json_extract(value, '$.state') AS text),
313
+ jsonb(json_extract(value, '$.tags')),
314
+ CASE
315
+ WHEN length(cast(json_extract(value, '$.unique_key') AS text)) = 0 THEN NULL
316
+ ELSE unhex(cast(json_extract(value, '$.unique_key') AS text))
317
+ END,
318
+ nullif(cast(json_extract(value, '$.unique_states') AS integer), 0)
319
+ FROM json_each(cast(? AS blob))
320
+ WHERE true
321
+ ON CONFLICT (unique_key) WHERE #{SQLITE_CONFLICT_WHERE}
322
+ DO UPDATE SET kind = EXCLUDED.kind
323
+ RETURNING #{SQLITE_JOB_COLUMNS}
324
+ SQL
325
+
326
+ rows = @connection_class.connection.raw_connection.execute(sql, [JSON.generate(jobs)])
327
+ sqlite_notify_insert(insert_params_many)
328
+
329
+ rows.map do |row|
330
+ metadata = JSON.parse(row["metadata"])
331
+ [sqlite_to_job_row_from_raw(row), !inserted_nonces.key?(metadata[UNIQUE_INSERT_METADATA_KEY])]
332
+ end
333
+ end
334
+ end
335
+
336
+ private def sqlite_job_rows(suffix, binds = [])
337
+ sql = "SELECT #{SQLITE_JOB_COLUMNS} FROM river_job #{suffix}"
338
+ @connection_class.connection.raw_connection.execute(sql, binds)
339
+ end
340
+
341
+ private def sqlite_notify_insert(insert_params_many)
342
+ queues = insert_params_many
343
+ .select { |param| param.state == ::River::JOB_STATE_AVAILABLE }
344
+ .map(&:queue)
345
+ .uniq
346
+ return if queues.empty?
144
347
 
145
- River::AttemptError.new(
146
- at: Time.parse(deserialized_error["at"]),
147
- attempt: deserialized_error["attempt"],
148
- error: deserialized_error["error"],
149
- trace: deserialized_error["trace"]
150
- )
348
+ notifications = queues.map do |queue|
349
+ {payload: JSON.generate({queue: queue}), topic: "river_insert"}
151
350
  end
152
351
 
153
- [
154
- River::JobRow.new(
155
- id: river_job["id"],
156
- args: river_job["args"],
157
- attempt: river_job["attempt"],
158
- attempted_at: river_job["attempted_at"]&.getutc,
159
- attempted_by: river_job["attempted_by"],
160
- created_at: river_job["created_at"].getutc,
161
- errors: errors,
162
- finalized_at: river_job["finalized_at"]&.getutc,
163
- kind: river_job["kind"],
164
- max_attempts: river_job["max_attempts"],
165
- metadata: river_job["metadata"],
166
- priority: river_job["priority"],
167
- queue: river_job["queue"],
168
- scheduled_at: river_job["scheduled_at"].getutc,
169
- state: river_job["state"],
170
- tags: river_job["tags"],
171
- unique_key: river_job["unique_key"],
172
- unique_states: ::River::UniqueBitmask.to_states(river_job["unique_states"]&.to_i(2))
173
- ),
174
- river_job["unique_skipped_as_duplicate"]
175
- ]
352
+ @connection_class.connection.raw_connection.execute(<<~SQL, [JSON.generate(notifications)])
353
+ INSERT INTO river_notification (payload, topic)
354
+ SELECT
355
+ json_extract(value, '$.payload'),
356
+ json_extract(value, '$.topic')
357
+ FROM json_each(cast(? AS blob))
358
+ SQL
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)
176
372
  end
177
373
  end
178
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.10.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Gentry
@@ -51,27 +51,21 @@ dependencies:
51
51
  - !ruby/object:Gem::Version
52
52
  version: '1000'
53
53
  - !ruby/object:Gem::Dependency
54
- name: pg
54
+ name: riverqueue
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - ">"
58
- - !ruby/object:Gem::Version
59
- version: '0'
60
- - - "<"
57
+ - - '='
61
58
  - !ruby/object:Gem::Version
62
- version: '1000'
59
+ version: 0.12.0
63
60
  type: :runtime
64
61
  prerelease: false
65
62
  version_requirements: !ruby/object:Gem::Requirement
66
63
  requirements:
67
- - - ">"
64
+ - - '='
68
65
  - !ruby/object:Gem::Version
69
- version: '0'
70
- - - "<"
71
- - !ruby/object:Gem::Version
72
- version: '1000'
73
- description: ActiveRecord driver for the River Ruby gem. Use in conjunction with the
74
- riverqueue gem to insert jobs that are worked in Go.
66
+ version: 0.12.0
67
+ description: ActiveRecord PostgreSQL and SQLite driver for inserting and working River
68
+ jobs in Ruby.
75
69
  email: brandur@brandur.org
76
70
  executables: []
77
71
  extensions: []
@@ -81,7 +75,7 @@ files:
81
75
  - lib/riverqueue-activerecord.rb
82
76
  homepage: https://riverqueue.com
83
77
  licenses:
84
- - LGPL-3.0-or-later
78
+ - MPL-2.0
85
79
  metadata: {}
86
80
  rdoc_options: []
87
81
  require_paths:
@@ -90,7 +84,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
84
  requirements:
91
85
  - - ">="
92
86
  - !ruby/object:Gem::Version
93
- version: '0'
87
+ version: '3.2'
94
88
  required_rubygems_version: !ruby/object:Gem::Requirement
95
89
  requirements:
96
90
  - - ">="
@@ -99,5 +93,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
99
93
  requirements: []
100
94
  rubygems_version: 4.0.9
101
95
  specification_version: 4
102
- summary: ActiveRecord driver for the River Ruby gem.
96
+ summary: ActiveRecord PostgreSQL and SQLite driver for the River Ruby gem.
103
97
  test_files: []