riverqueue 0.14.0 → 0.15.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: 1ce8622d0f151131d632d673e694e8ea90ded0502ba912274c7d044076063ee9
4
- data.tar.gz: 1df7e4f2f10a3cfe36d3d08793a38b5400bf12dd32c3db3d3fd248184523d961
3
+ metadata.gz: c862e762c9679461633535e60ce0063ea7cad3fbe42d1f4b35c97350826ee3e0
4
+ data.tar.gz: 9a80c1852a2d587408fdc8e4c0c0c6b1af20bba5c60c0b777787f87248eb4cc9
5
5
  SHA512:
6
- metadata.gz: c09ee3fefb3b0d9bdb4bd969761117470a84b7bcfa1278819446471c53348356ddffea3fa424d0cf086f57b1134ec49c1d470d03314286d21d50c9d1670568a4
7
- data.tar.gz: a79a913f6f455f8a6a14e989ae72c415adb39248d97c1457b2cfd077b74ce41676e1f3bc79239d4a33d2653485074de7a8a6fd414699fec2e30b97c1fedb3d11
6
+ metadata.gz: 1b189d118fbb38fbf14f961ce645505c0a47e28421c1b0af11154c2dde5d6d057a4ddf25f88b4fc5a3d8f602c931db792281d89a73c8d0dab230fb9266aab1d4
7
+ data.tar.gz: ebf174f14f464c6be842647ddfc3a23a5ee3b33ffd84ce8c10b471849192bedcbc1e05db8e15d457ccf6cfbdfe8e3c04090882c636abb9fa2b6111ad2ffd483b
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.0] - 2026-10-30
11
+
12
+ ### Added
13
+
14
+ - Add `river migrate-get` for offline Postgres/SQLite SQL export with version selection, exclusions, and schema substitution; `river validate` to check migration state without applying changes; and `river version` / `river --version` to report active core, Pro, and Ruby runtime versions. Export and validation support `--line pro`. [PR #1560](https://github.com/riverqueue/river/pull/1560).
15
+
10
16
  ## [0.14.0] - 2026-10-26
11
17
 
12
18
  ### Added
data/docs/migrations.md CHANGED
@@ -40,6 +40,68 @@ bundle exec river migrate-down --database-url postgres://localhost/my_app --yes
40
40
  Down migrations can delete jobs and other data. Back up the database and stop
41
41
  workers first. `--target 0 --yes` removes the complete selected migration line.
42
42
 
43
+ ## Validating migration state
44
+
45
+ Use `validate` in a deployment check to require every bundled migration in the
46
+ selected line to be applied:
47
+
48
+ ```sh
49
+ bundle exec river validate --database-url postgres://localhost/my_app
50
+ bundle exec river validate --database-url postgres://localhost/my_app --line pro
51
+ ```
52
+
53
+ Validation exits with status 0 and no output when the selected line is current.
54
+ Pending migrations are listed on stderr with exit status 1. Invalid history or
55
+ connection errors also return 1. Validation reads migration history without
56
+ applying migrations or changing data; it does not inspect individual tables or
57
+ columns for schema drift. It accepts the same `DATABASE_URL`, `--schema`, and
58
+ driver selection as the migration commands. Validate main and Pro separately.
59
+
60
+ ## Version information
61
+
62
+ `version` and the top-level `--version` flag print the active core gem version
63
+ and Ruby runtime version, plus the Pro gem version when `riverqueue-pro` is
64
+ active in the bundle:
65
+
66
+ ```sh
67
+ bundle exec river version
68
+ bundle exec river --version
69
+ ```
70
+
71
+ Neither command connects to a database or loads a database adapter. The
72
+ `--version` option on `migrate-get` still selects migration versions to export.
73
+
74
+ ## Exporting SQL for other frameworks
75
+
76
+ `migrate-get` prints SQL without connecting to a database or requiring a River
77
+ driver gem. Select exactly one of `--version` or `--all`, and one of `--up` or
78
+ `--down`:
79
+
80
+ ```sh
81
+ bundle exec river migrate-get --version 3 --up > river3.up.sql
82
+ bundle exec river migrate-get --version 3 --down > river3.down.sql
83
+ bundle exec river migrate-get --all --exclude-version 1 --up > river_all.up.sql
84
+ bundle exec river migrate-get --all --exclude-version 1 --down > river_all.down.sql
85
+ ```
86
+
87
+ `--version` and `--exclude-version` accept comma-separated values and may be
88
+ repeated. Explicit versions retain the supplied order. `--all` exports up SQL
89
+ in ascending order and down SQL in descending order. Version 1 creates River's
90
+ migration-tracking table and can usually be omitted with another framework.
91
+ Preserve version boundaries and apply each migration in its own transaction.
92
+
93
+ Export defaults to Postgres and ignores `DATABASE_URL`. Use
94
+ `--database-url sqlite://` (or `sqlite3://`) to select SQLite SQL; the URL is only
95
+ a dialect hint. `--schema jobs` qualifies Postgres objects with that schema.
96
+ Without it, objects are unqualified. Schema placeholders are always substituted.
97
+
98
+ With `riverqueue-pro` installed, export Pro's bundled SQL using the same command:
99
+
100
+ ```sh
101
+ bundle exec river migrate-get --line pro --version 1 --up > river_pro_1.up.sql
102
+ bundle exec river migrate-get --line pro --version 1 --down > river_pro_1.down.sql
103
+ ```
104
+
43
105
  ## Ruby API
44
106
 
45
107
  ```ruby
data/lib/cli.rb CHANGED
@@ -5,14 +5,16 @@ require_relative "migration_cli"
5
5
  require_relative "worker_runner"
6
6
 
7
7
  module River
8
- # Shared command dispatch for migrations and dedicated worker processes.
8
+ # Shared command dispatch for migrations, version information, and workers.
9
9
  module CLI
10
10
  # Runs a River command and returns its process exit status.
11
11
  def self.run(argv, err: $stderr, out: $stdout)
12
+ return version(argv.drop(1), err: err, out: out) if %w[version --version].include?(argv.first)
12
13
  return worker(argv.drop(1), out: out) if argv.first == "worker"
13
14
 
14
15
  if argv.empty? || %w[-h --help].include?(argv.first)
15
- out.puts("Usage: river worker|migrate-up|migrate-down|migrate-status [options]")
16
+ out.puts("Usage: river worker|migrate-up|migrate-down|migrate-get|migrate-status|validate|version [options]")
17
+ out.puts("Use river --version to print gem and Ruby versions.")
16
18
  return 0
17
19
  end
18
20
 
@@ -22,6 +24,29 @@ module River
22
24
  1
23
25
  end
24
26
 
27
+ # Prints active gem versions and the Ruby runtime without loading adapters.
28
+ def self.version(argv, err:, out:)
29
+ parser = OptionParser.new do |args|
30
+ args.banner = "Usage: river version | river --version"
31
+ args.on("-h", "--help") do
32
+ out.puts(args)
33
+ return 0
34
+ end
35
+ end
36
+ args = argv.dup
37
+ parser.parse!(args)
38
+ raise ArgumentError, parser.banner unless args.empty?
39
+
40
+ out.puts("River version #{Gem.loaded_specs["riverqueue"]&.version || "(unknown)"}")
41
+ pro = Gem.loaded_specs["riverqueue-pro"]
42
+ out.puts("River Pro version #{pro.version}") if pro
43
+ out.puts("Ruby #{RUBY_VERSION}")
44
+ 0
45
+ rescue ArgumentError, OptionParser::ParseError => error
46
+ err.puts("Version failed: #{error.message}")
47
+ 1
48
+ end
49
+
25
50
  def self.worker(argv, out:)
26
51
  options = {} #: Hash[Symbol, untyped]
27
52
  parser = OptionParser.new do |args|
data/lib/migration_cli.rb CHANGED
@@ -11,14 +11,21 @@ module River
11
11
  # @type var active_record_base: untyped
12
12
  options = {line: "main"} #: Hash[Symbol, untyped]
13
13
  parser = OptionParser.new do |args|
14
- args.banner = "Usage: river migrate-up|migrate-down|migrate-status [options]"
15
- args.on("--database-url URL", "Defaults to DATABASE_URL") { |value| options[:url] = value }
14
+ args.banner = "Usage: river migrate-up|migrate-down|migrate-get|migrate-status|validate [options]"
15
+ args.on("--database-url URL", "Defaults to DATABASE_URL; migrate-get uses only the dialect (default: Postgres)") { |value| options[:url] = value }
16
16
  args.on("--line NAME", %w[main pro]) { |value| options[:line] = value }
17
17
  args.on("--schema NAME") { |value| options[:schema] = value }
18
18
  args.on("--steps N", Integer) { |value| options[:steps] = value }
19
19
  args.on("--target VERSION", Integer) { |value| options[:target] = value }
20
20
  args.on("--dry-run") { options[:dry_run] = true }
21
21
  args.on("--yes", "Confirm destructive down migrations") { options[:yes] = true }
22
+ args.separator ""
23
+ args.separator "migrate-get: export SQL without a database connection or driver gem"
24
+ args.on("--all", "Export all versions in migration order") { options[:all] = true }
25
+ args.on("--up", "Export up SQL") { options[:up] = true }
26
+ args.on("--down", "Export down SQL") { options[:down] = true }
27
+ args.on("--version N[,N...]", "Versions in the supplied order; repeatable") { |value| (options[:version] ||= []).concat(parse_versions(value, "--version")) }
28
+ args.on("--exclude-version N[,N...]", "Versions to omit; repeatable") { |value| (options[:exclude_version] ||= []).concat(parse_versions(value, "--exclude-version")) }
22
29
  args.on("-h", "--help") {
23
30
  out.puts(args)
24
31
  return 0
@@ -28,7 +35,19 @@ module River
28
35
  args = argv.dup
29
36
  parser.parse!(args)
30
37
  command = args.shift
31
- raise ArgumentError, parser.banner unless %w[migrate-up migrate-down migrate-status].include?(command) && args.empty?
38
+ raise ArgumentError, parser.banner unless %w[migrate-up migrate-down migrate-get migrate-status validate].include?(command) && args.empty?
39
+
40
+ incompatible_options = options.keys & ((command == "migrate-get") ? %i[dry_run steps target yes] : %i[all down exclude_version up version])
41
+ incompatible_options |= options.keys & %i[dry_run steps target yes] if command == "validate"
42
+ unless incompatible_options.empty?
43
+ raise ArgumentError, "#{incompatible_options.map { |key| "--#{key.to_s.tr("_", "-")}" }.join(", ")} cannot be used with #{command}"
44
+ end
45
+
46
+ if options[:line] == "pro"
47
+ require "riverqueue-pro"
48
+ end
49
+ migrator_class = (options[:line] == "pro") ? River.const_get(:Pro).const_get(:Migrator) : River::Migrator
50
+ return export(options, migrator_class, out: out) if command == "migrate-get"
32
51
 
33
52
  url = options[:url] || ENV["DATABASE_URL"]
34
53
  raise ArgumentError, "provide --database-url or DATABASE_URL" if url.nil? || url.empty?
@@ -39,11 +58,6 @@ module River
39
58
 
40
59
  adapter = detect_driver
41
60
 
42
- if options[:line] == "pro"
43
- require "riverqueue-pro"
44
- end
45
-
46
- migrator_class = (options[:line] == "pro") ? River.const_get(:Pro).const_get(:Migrator) : River::Migrator
47
61
  if adapter == "activerecord"
48
62
  require "riverqueue-activerecord"
49
63
  active_record_base = Object.const_get("ActiveRecord::Base")
@@ -56,7 +70,13 @@ module River
56
70
  end
57
71
 
58
72
  migrator = migrator_class.new(driver, schema: options[:schema])
59
- if command == "migrate-status"
73
+ if command == "validate"
74
+ pending = migrator.status.reject(&:applied)
75
+ unless pending.empty?
76
+ err.puts("unapplied migrations: #{pending.map(&:version).join(", ")}")
77
+ return 1
78
+ end
79
+ elsif command == "migrate-status"
60
80
  migrator.status.each { |migration| out.puts("#{migration.applied ? "applied" : "pending"} #{migration.version.to_s.rjust(3, "0")} #{migration.name}") }
61
81
  else
62
82
  migrations = migrator.migrate(direction: (command == "migrate-up") ? :up : :down,
@@ -86,5 +106,52 @@ module River
86
106
  driver
87
107
  end
88
108
  private_class_method :detect_driver
109
+
110
+ def self.export(options, migrator_class, out:)
111
+ versions = options.fetch(:version, [])
112
+ excluded = options.fetch(:exclude_version, [])
113
+ raise ArgumentError, "pass exactly one of --all or --version" if !!options[:all] == versions.any?
114
+ raise ArgumentError, "pass exactly one of --up or --down" if !!options[:up] == !!options[:down]
115
+
116
+ backend = case options[:url] || "postgres://"
117
+ when /\Apostgres(?:ql)?:/i then :postgresql
118
+ when /\Asqlite3?:/i then :sqlite
119
+ else raise ArgumentError, "--database-url must use postgres:, postgresql:, sqlite:, or sqlite3:"
120
+ end
121
+ schema = options[:schema]
122
+ raise ArgumentError, "SQLite does not support a migration schema option" if backend == :sqlite && schema
123
+ raise ArgumentError, "schema must be a simple SQL identifier" if schema && !schema.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
124
+
125
+ migrations = migrator_class.migrations(backend: backend, line: options[:line])
126
+ selected = if options[:all]
127
+ options[:down] ? migrations.reverse : migrations
128
+ else
129
+ versions.map do |version|
130
+ migrations.find { |migration| migration.version == version } || raise(ArgumentError, "migration #{version} does not exist")
131
+ end
132
+ end
133
+ selected = selected.reject { |migration| excluded.include?(migration.version) }
134
+ direction = options[:down] ? "down" : "up"
135
+ selected.each_with_index do |migration, index|
136
+ out.puts if index.positive?
137
+ out.puts("-- River #{options[:line]} migration #{migration.version.to_s.rjust(3, "0")} [#{direction}]")
138
+ sql = options[:down] ? migration.sql_down : migration.sql_up
139
+ out.puts(sql.gsub("/* TEMPLATE: schema */", schema ? %("#{schema}".) : "").strip)
140
+ end
141
+ 0
142
+ end
143
+ private_class_method :export
144
+
145
+ def self.parse_versions(value, flag)
146
+ raise ArgumentError, "#{flag} must contain positive integers separated by commas" unless value.match?(/\A[0-9]+(?:,[0-9]+)*\z/)
147
+
148
+ value.split(",").map do |entry|
149
+ version = Integer(entry, 10)
150
+ raise ArgumentError, "#{flag} must contain positive integers" unless version.positive?
151
+
152
+ version
153
+ end
154
+ end
155
+ private_class_method :parse_versions
89
156
  end
90
157
  end
data/lib/migrator.rb CHANGED
@@ -15,22 +15,29 @@ module River
15
15
  def initialize(driver, line: "main", migrations_path: File.expand_path("../migration", __dir__.to_s), schema: nil)
16
16
  @backend = driver.migration_backend
17
17
  @driver = driver
18
- raise ArgumentError, "unsupported migration backend" unless [:postgresql, :sqlite].include?(@backend)
19
- raise ArgumentError, "invalid migration line" unless line.match?(/\A[a-z][a-z0-9_]*\z/)
20
18
  raise ArgumentError, "SQLite does not support a migration schema option" if @backend == :sqlite && schema
21
19
 
22
20
  @line = line
21
+ @migrations = Migrator.migrations(backend: @backend, line: line, migrations_path: migrations_path)
23
22
  @mutex = Mutex.new
24
23
  @requested_schema = schema
24
+ end
25
25
 
26
- directory = File.join(migrations_path, @backend.to_s, line)
27
- @migrations = Dir.glob(File.join(directory, "*.up.sql")).sort.map do |path|
26
+ # Returns bundled SQL without a driver or database connection.
27
+ def self.migrations(backend: :postgresql, line: "main", migrations_path: File.expand_path("../migration", __dir__.to_s))
28
+ raise ArgumentError, "unsupported migration backend" unless [:postgresql, :sqlite].include?(backend)
29
+ raise ArgumentError, "invalid migration line" unless line.match?(/\A[a-z][a-z0-9_]*\z/)
30
+
31
+ directory = File.join(migrations_path, backend.to_s, line)
32
+ migrations = Dir.glob(File.join(directory, "*.up.sql")).sort.map do |path|
28
33
  version, name = File.basename(path).delete_suffix(".up.sql").split("_", 2)
29
34
  Migration.new(Integer(version.to_s, 10), name, File.read(path).freeze, File.read(path.sub(/\.up\.sql\z/, ".down.sql")).freeze)
30
35
  end.freeze
31
- unless @migrations.any? && @migrations.map(&:version) == (1..@migrations.length).to_a
36
+ unless migrations.any? && migrations.map(&:version) == (1..migrations.length).to_a
32
37
  raise ArgumentError, "migration files must contain contiguous versions starting at 1"
33
38
  end
39
+
40
+ migrations
34
41
  end
35
42
 
36
43
  # Applies pending migrations up (all by default) or down (one by default).
data/sig/migrator.rbs CHANGED
@@ -5,6 +5,8 @@ end
5
5
  module River
6
6
  module MigrationCLI
7
7
  def self.detect_driver: () -> String
8
+ def self.export: (Hash[Symbol, untyped], untyped, out: untyped) -> Integer
9
+ def self.parse_versions: (String, String) -> Array[Integer]
8
10
  def self.run: (Array[String], ?err: untyped, ?out: untyped) -> Integer
9
11
  end
10
12
  class Migrator
@@ -31,6 +33,7 @@ module River
31
33
  @schema: untyped
32
34
  attr_reader migrations: Array[Migration]
33
35
  def initialize: (untyped, ?line: String, ?migrations_path: String, ?schema: untyped) -> void
36
+ def self.migrations: (?backend: Symbol, ?line: String, ?migrations_path: String) -> Array[Migration]
34
37
  def migrate: (?direction: Symbol, ?dry_run: bool, ?steps: untyped, ?target: untyped) -> Array[Migration]
35
38
  def status: () -> Array[Status]
36
39
  private
@@ -1,3 +1,7 @@
1
+ class Gem::BasicSpecification
2
+ attr_reader version: Gem::Version
3
+ end
4
+
1
5
  module Process
2
6
  def self.exit!: (?Integer) -> bot
3
7
  end
@@ -5,6 +9,7 @@ end
5
9
  module River
6
10
  module CLI
7
11
  def self.run: (Array[String], ?err: untyped, ?out: untyped) -> Integer
12
+ def self.version: (Array[String], err: untyped, out: untyped) -> Integer
8
13
  def self.worker: (Array[String], out: untyped) -> Integer
9
14
  end
10
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riverqueue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blake Gentry