diff --git a/CHANGELOG.md b/CHANGELOG.md index 98d70c7..f27c8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * Raise ActiveRecord error if Job failed to be queued +* Renamed `Disqualified.server_options` to `Disqualified.config` +* Renamed `Disqualified.configure_server` to `Disqualified.configure` +* Renamed configuration options ## v0.3.0 - 2023-04-16 diff --git a/app/models/disqualified/internal.rb b/app/models/disqualified/internal.rb new file mode 100644 index 0000000..f190ef6 --- /dev/null +++ b/app/models/disqualified/internal.rb @@ -0,0 +1,5 @@ +# typed: strict + +class Disqualified::Internal < Disqualified::BaseRecord + self.table_name = "disqualified_internals" +end diff --git a/app/models/disqualified/record.rb b/app/models/disqualified/record.rb index 6700585..b26320c 100644 --- a/app/models/disqualified/record.rb +++ b/app/models/disqualified/record.rb @@ -5,6 +5,8 @@ class Disqualified::Record < Disqualified::BaseRecord self.table_name = "disqualified_jobs" + serialize :metadata, JSON + scope :runnable, -> { where(finished_at: nil, run_at: (..Time.now), locked_by: nil) } sig do @@ -72,6 +74,12 @@ def run! sig { void } def finish + Kernel.catch(:abort) do + Disqualified.config.plugins.sorted_plugins.each do |plugin| + plugin.before_finish(record: self) + end + end + update!(locked_by: nil, locked_at: nil, finished_at: Time.now) end diff --git a/lib/disqualified.rb b/lib/disqualified.rb index fdba524..19ed8d1 100644 --- a/lib/disqualified.rb +++ b/lib/disqualified.rb @@ -4,6 +4,7 @@ module Disqualified end require "optparse" +require "tsort" require "concurrent" require "rails" @@ -12,9 +13,13 @@ module Disqualified require_relative "disqualified/error" require_relative "disqualified/logging" +require_relative "disqualified/configuration" require_relative "disqualified/engine" require_relative "disqualified/job" require_relative "disqualified/main" require_relative "disqualified/pool" -require_relative "disqualified/server_configuration" +require_relative "disqualified/options" +require_relative "disqualified/plugin" +require_relative "disqualified/plugin_registry" +require_relative "disqualified/unique" require_relative "disqualified/version" diff --git a/lib/disqualified/cli.rb b/lib/disqualified/cli.rb index 4799931..a78beb1 100644 --- a/lib/disqualified/cli.rb +++ b/lib/disqualified/cli.rb @@ -12,7 +12,7 @@ def self.run class ServerEngine < Rails::Engine config.before_initialize do - Disqualified.server_options = Disqualified::ServerConfiguration.new + Disqualified.config = Disqualified::Configuration.new end end @@ -27,11 +27,11 @@ def run option_parser.parse(@original_argv) - server_options = T.must(Disqualified.server_options) - delay_range = server_options.delay_range - error_hooks = server_options.error_hooks - logger = server_options.logger - pool_size = server_options.pool_size + config = Disqualified.config + delay_range = config.delay_range + error_hooks = config.execution_error_hooks + logger = config.logger + pool_size = config.pool_size # standard:disable Style/StringLiterals logger.info { ' ____ _ ___ _____ __' } @@ -41,7 +41,7 @@ def run logger.info { '/_____/_/____/\__, /\__,_/\__,_/_/_/_/ /_/\___/\__,_/' } logger.info { ' /_/' + "v#{Disqualified::VERSION}".rjust(32, " ") } # standard:enable Style/StringLiterals - logger.info { Disqualified.server_options.to_s } + logger.info { Disqualified.config.to_s } pool = Disqualified::Pool.new(delay_range:, pool_size:, error_hooks:, logger:) do |args| args => {promise_index:} @@ -64,18 +64,18 @@ def option_parser option_parser = OptionParser.new do |opts| opts.banner = "Usage: #{File.basename($0)} [OPTIONS]" - server_options = T.must(Disqualified.server_options) + config = Disqualified.config - opts.on("--delay-low SECONDS", Numeric, "Default: #{server_options.delay_low}") do |value| - server_options.delay_low = value + opts.on("--poll-low SECONDS", Numeric, "Default: #{config.poll_low}") do |value| + config.poll_low = value end - opts.on("--delay-high SECONDS", Numeric, "Default: #{server_options.delay_high}") do |value| - server_options.delay_high = value + opts.on("--poll-high SECONDS", Numeric, "Default: #{config.poll_high}") do |value| + config.poll_high = value end - opts.on("--pool COUNT", Integer, "Default: #{server_options.pool_size}") do |value| - server_options.pool_size = value + opts.on("--pool COUNT", Integer, "Default: #{config.pool_size}") do |value| + config.pool_size = value end opts.on("-h", "--help", "Prints this help") do diff --git a/lib/disqualified/configuration.rb b/lib/disqualified/configuration.rb new file mode 100644 index 0000000..e727eb3 --- /dev/null +++ b/lib/disqualified/configuration.rb @@ -0,0 +1,72 @@ +# typed: strict + +module Disqualified + extend T::Sig + + class << self + extend T::Sig + + sig { params(config: Disqualified::Configuration).returns(Disqualified::Configuration) } + attr_writer :config + + sig { returns(Disqualified::Configuration) } + def config + @config ||= T.let(Disqualified::Configuration.new, T.nilable(Disqualified::Configuration)) + end + + sig { params(block: T.proc.params(arg0: Disqualified::Configuration).void).void } + def configure(&block) + block.call(config) + end + end +end + +class Disqualified::Configuration + extend T::Sig + + sig { void } + def initialize + @poll_high = T.let(5.0, Numeric) + @poll_low = T.let(1.0, Numeric) + @logger = T.let(Rails.logger, T.untyped) + @pool_size = T.let(5, Integer) + @pwd = T.let(Dir.pwd, String) + @execution_error_hooks = T.let([], T::Array[Disqualified::Logging::ERROR_HOOK_TYPE]) + @plugins = T.let(Disqualified::PluginRegistry.new, Disqualified::PluginRegistry) + + plugins.register(Disqualified::Unique::Plugin.new) + end + + sig { returns(Numeric) } + attr_accessor :poll_high + sig { returns(Numeric) } + attr_accessor :poll_low + sig { returns(T::Array[Disqualified::Logging::ERROR_HOOK_TYPE]) } + attr_accessor :execution_error_hooks + sig { returns(T.untyped) } + attr_accessor :logger + sig { returns(Integer) } + attr_accessor :pool_size + sig { returns(String) } + attr_accessor :pwd + sig { returns(Disqualified::PluginRegistry) } + attr_accessor :plugins + + private :execution_error_hooks= + private :plugins= + + sig { returns(T::Range[Float]) } + def delay_range + poll_low.to_f..poll_high.to_f + end + + sig { params(block: Disqualified::Logging::ERROR_HOOK_TYPE).void } + def on_execution_error(&block) + execution_error_hooks.push(block) + end + + sig { returns(String) } + def to_s + "{ delay: #{delay_range}, pool_size: #{pool_size}, error_hooks_size: #{execution_error_hooks.size} }" + end +end diff --git a/lib/disqualified/error.rb b/lib/disqualified/error.rb index 51008d2..9d6bc88 100644 --- a/lib/disqualified/error.rb +++ b/lib/disqualified/error.rb @@ -10,5 +10,8 @@ class JobAlreadyFinished < DisqualifiedError class JobNotClaimed < DisqualifiedError end + + class DuplicateSetting < DisqualifiedError + end end end diff --git a/lib/disqualified/job.rb b/lib/disqualified/job.rb index 6f3bfe0..051ba61 100644 --- a/lib/disqualified/job.rb +++ b/lib/disqualified/job.rb @@ -6,13 +6,44 @@ module Disqualified::Job module ClassMethods extend T::Sig + sig { returns(Disqualified::Options) } + private def job_options + @job_options ||= T.let(Disqualified::Options.new, T.nilable(Disqualified::Options)) + end + + sig { params(till: Symbol, including: Symbol).void } + private def unique(till = :until_executed, including: :arguments) + if job_options.key?("unique") + Kernel.raise Disqualified::Error::DuplicateSetting, "`unique` called more than once" + end + + job_options["unique"] = { + "till" => till, + "including" => including, + "handler" => T.unsafe(self).name + } + end + sig { params(the_time: T.any(Time, Date, ActiveSupport::TimeWithZone), args: T.untyped).void } def perform_at(the_time, *args) + metadata = {} + before_queue_completed = T.let(false, T::Boolean) + + Kernel.catch(:abort) do + Disqualified.config.plugins.sorted_plugins.each do |plugin| + plugin.before_queue(metadata:, job_options:, arguments: args) + end + before_queue_completed = true + end + + return unless before_queue_completed + Disqualified::Record.create!( handler: T.unsafe(self).name, arguments: JSON.dump(args), queue: "default", - run_at: the_time + run_at: the_time, + metadata: ) end diff --git a/lib/disqualified/options.rb b/lib/disqualified/options.rb new file mode 100644 index 0000000..c984aff --- /dev/null +++ b/lib/disqualified/options.rb @@ -0,0 +1,34 @@ +# typed: strict + +class Disqualified::Options + extend T::Sig + + Scalar = T.type_alias { T.any(Integer, String, T::Boolean) } + FlatHash = T.type_alias { T::Hash[String, Scalar] } + Value = T.type_alias { T.nilable(T.any(Scalar, FlatHash)) } + + sig { void } + def initialize + @data = T.let({}, T::Hash[String, Value]) + end + + sig { params(key: String).returns(Value) } + def [](key) + @data[key] + end + + sig { params(key: String, value: Value).returns(Value) } + def []=(key, value) + @data[key] = value + end + + sig { params(key: String).returns(T::Boolean) } + def key?(key) + @data.key?(key) + end + + sig { returns(T::Hash[String, Value]) } + private def to_h + @data + end +end diff --git a/lib/disqualified/plugin.rb b/lib/disqualified/plugin.rb new file mode 100644 index 0000000..c3a8516 --- /dev/null +++ b/lib/disqualified/plugin.rb @@ -0,0 +1,39 @@ +# typed: strict + +module Disqualified::Plugin + extend T::Sig + extend T::Helpers + abstract! + + sig { abstract.returns(String) } + def name + end + + sig { abstract.returns(String) } + def job_config_namespace + end + + sig { abstract.returns(String) } + def metadata_namespace + end + + sig { overridable.void } + def on_registry + end + + sig do + overridable + .params( + metadata: T.untyped, + job_options: Disqualified::Options, + arguments: T.untyped + ) + .void + end + def before_queue(metadata:, job_options:, arguments:) + end + + sig { overridable.params(record: Disqualified::Record).void } + def before_finish(record:) + end +end diff --git a/lib/disqualified/plugin_registry.rb b/lib/disqualified/plugin_registry.rb new file mode 100644 index 0000000..2ddc470 --- /dev/null +++ b/lib/disqualified/plugin_registry.rb @@ -0,0 +1,60 @@ +# typed: strict + +class Disqualified::PluginRegistry + include TSort + extend T::Sig + + sig { void } + def initialize + @ordering = T.let({}, T::Hash[String, T::Array[String]]) + @registered = T.let({}, T::Hash[String, Disqualified::Plugin]) + end + + ONE_PLUS_PLUGINS = T.type_alias { T.any(String, T::Array[String]) } + + sig { params(plugin: Disqualified::Plugin, after: ONE_PLUS_PLUGINS, before: ONE_PLUS_PLUGINS).void } + def register(plugin, after: [], before: []) + @registered[plugin.name] = plugin + order(plugin.name, after:, before:) + end + + sig { params(plugin_name: String, after: ONE_PLUS_PLUGINS, before: ONE_PLUS_PLUGINS).void } + def order(plugin_name, after: [], before: []) + @ordering[plugin_name] ||= [] + @ordering[plugin_name].push(*after) + [].push(*before).each do |dependant| + @ordering[dependant] ||= [] + T.must(@ordering[dependant]).push(plugin_name) + end + end + + sig { params(block: T.proc.params(arg0: String).void).void } + def tsort_each_node(&block) + @ordering.each do |key, _| + next unless @registered.key?(key) + block.call(key) + end + end + + sig { params(node: String, block: T.proc.params(arg0: String).void).void } + def tsort_each_child(node, &block) + @ordering.fetch(node).each do |dependency| + next unless @registered.key?(dependency) + block.call(dependency) + end + end + + sig { returns(T::Array[String]) } + def sorted + tsort + end + + sig { returns(T::Array[Disqualified::Plugin]) } + def sorted_plugins + tsort.map do |plugin_name| + @registered.fetch(plugin_name) + end + end + + private :tsort +end diff --git a/lib/disqualified/server_configuration.rb b/lib/disqualified/server_configuration.rb deleted file mode 100644 index 9c4d7fb..0000000 --- a/lib/disqualified/server_configuration.rb +++ /dev/null @@ -1,63 +0,0 @@ -# typed: strict - -module Disqualified - extend T::Sig - - class << self - extend T::Sig - - sig { returns(T.nilable(Disqualified::ServerConfiguration)) } - attr_accessor :server_options - - sig { params(block: T.proc.params(arg0: Disqualified::ServerConfiguration).void).void } - def configure_server(&block) - if server_options - block.call(T.must(server_options)) - end - end - end -end - -class Disqualified::ServerConfiguration - extend T::Sig - - sig { void } - def initialize - @delay_high = T.let(5.0, Numeric) - @delay_low = T.let(1.0, Numeric) - @logger = T.let(Rails.logger, T.untyped) - @pool_size = T.let(5, Integer) - @pwd = T.let(Dir.pwd, String) - @error_hooks = T.let([], T::Array[Disqualified::Logging::ERROR_HOOK_TYPE]) - end - - sig { returns(Numeric) } - attr_accessor :delay_high - sig { returns(Numeric) } - attr_accessor :delay_low - sig { returns(T::Array[Disqualified::Logging::ERROR_HOOK_TYPE]) } - attr_accessor :error_hooks - sig { returns(T.untyped) } - attr_accessor :logger - sig { returns(Integer) } - attr_accessor :pool_size - sig { returns(String) } - attr_accessor :pwd - - private :error_hooks= - - sig { returns(T::Range[Float]) } - def delay_range - delay_low.to_f..delay_high.to_f - end - - sig { params(block: Disqualified::Logging::ERROR_HOOK_TYPE).void } - def on_error(&block) - error_hooks.push(block) - end - - sig { returns(String) } - def to_s - "{ delay: #{delay_range}, pool_size: #{pool_size}, error_hooks_size: #{error_hooks.size} }" - end -end diff --git a/lib/disqualified/unique.rb b/lib/disqualified/unique.rb new file mode 100644 index 0000000..ab56e30 --- /dev/null +++ b/lib/disqualified/unique.rb @@ -0,0 +1,96 @@ +# typed: strict + +class Disqualified::Unique + extend T::Sig + + RECORD_METADATA_KEY = "UNIQUE_JOB_KEY" + RECORD_METADATA_VALUE = "UNIQUE_JOB_VALUE" + + INCL_ARGUMENTS = :arguments + + sig { params(till: Symbol, including: T.any(Symbol, T::Array[Symbol]), handler: String).void } + def initialize(till, including, handler) + @till = till + including = + if including.is_a?(Array) + including + else + [including] + end + @including = T.let(including, T::Array[Symbol]) + @handler = handler + end + + sig { returns(Symbol) } + attr_reader :till + + sig { returns(T::Array[Symbol]) } + attr_reader :including + + sig { returns(String) } + attr_reader :handler + + sig { returns(String) } + def key + "unique_job" + end + + sig { params(arguments: T.untyped).returns(String) } + def unique_key(arguments:) + parts = ["unique_job", handler] + if including.include?(INCL_ARGUMENTS) + parts.push(JSON.dump(arguments)) + end + parts.join("|") + end + + class Plugin + extend T::Sig + include Disqualified::Plugin + + sig { override.returns(String) } + def name = "Disqualified::Plugin::Name" + + sig { override.returns(String) } + def job_config_namespace = "" + + sig { override.returns(String) } + def metadata_namespace = "" + + sig do + override + .params( + metadata: T.untyped, + job_options: Disqualified::Options, + arguments: T::Array[T.untyped] + ) + .void + end + def before_queue(metadata:, job_options:, arguments:) + return if !job_options.key?("unique") + + args_for_unique = T.let(job_options["unique"], T.untyped).values_at("till", "including", "handler") + unique = Disqualified::Unique.new(*args_for_unique) + unique_key = unique.unique_key(arguments:) + random = SecureRandom.hex + unique_record = Disqualified::Internal.create_or_find_by(unique_key:) do |record| + record.key = unique.key + record.value = random + end + + metadata[Disqualified::Unique::RECORD_METADATA_KEY] = unique_key + metadata[Disqualified::Unique::RECORD_METADATA_VALUE] = random + + throw :abort if unique_record.value != random + end + + sig { override.params(record: Disqualified::Record).void } + def before_finish(record:) + return unless record.metadata&.key?(Disqualified::Unique::RECORD_METADATA_KEY) + + unique_key = record.metadata.fetch(Disqualified::Unique::RECORD_METADATA_KEY) + value = record.metadata.fetch(Disqualified::Unique::RECORD_METADATA_VALUE) + Disqualified::Internal.where(unique_key:, value:).delete_all + end + end +end diff --git a/lib/generators/disqualified/install/install_generator.rb b/lib/generators/disqualified/install/install_generator.rb index 3bf1dd3..fbd4805 100644 --- a/lib/generators/disqualified/install/install_generator.rb +++ b/lib/generators/disqualified/install/install_generator.rb @@ -1,8 +1,15 @@ class Disqualified::InstallGenerator < Rails::Generators::Base source_root File.expand_path("templates", __dir__) - def copy_migration_file - basename = "20220703062536_create_disqualified_jobs.rb" - copy_file basename, "db/migrate/#{basename}" + MIGRATION_FILES = [ + "20220703062536_create_disqualified_jobs.rb", + "20231218213817_create_disqualified_internals.rb", + "20231218232905_add_metadata_to_disqualified_jobs.rb" + ] + + def copy_migration_files + MIGRATION_FILES.each do |basename| + copy_file basename, "db/migrate/#{basename}" + end end end diff --git a/lib/generators/disqualified/install/templates/20231218213817_create_disqualified_internals.rb b/lib/generators/disqualified/install/templates/20231218213817_create_disqualified_internals.rb new file mode 100644 index 0000000..a695afe --- /dev/null +++ b/lib/generators/disqualified/install/templates/20231218213817_create_disqualified_internals.rb @@ -0,0 +1,14 @@ +class CreateDisqualifiedInternals < ActiveRecord::Migration[7.0] + def change + create_table :disqualified_internals do |t| + t.text :key, null: false + t.text :unique_key + t.text :value + + t.timestamps + + t.index :key + t.index :unique_key, unique: true + end + end +end diff --git a/lib/generators/disqualified/install/templates/20231218232905_add_metadata_to_disqualified_jobs.rb b/lib/generators/disqualified/install/templates/20231218232905_add_metadata_to_disqualified_jobs.rb new file mode 100644 index 0000000..d759f95 --- /dev/null +++ b/lib/generators/disqualified/install/templates/20231218232905_add_metadata_to_disqualified_jobs.rb @@ -0,0 +1,5 @@ +class AddMetadataToDisqualifiedJobs < ActiveRecord::Migration[7.0] + def change + add_column :disqualified_jobs, :metadata, :text + end +end diff --git a/sorbet/rbi/dsl/disqualified/internal.rbi b/sorbet/rbi/dsl/disqualified/internal.rbi new file mode 100644 index 0000000..aac729d --- /dev/null +++ b/sorbet/rbi/dsl/disqualified/internal.rbi @@ -0,0 +1,1262 @@ +# typed: true + +# DO NOT EDIT MANUALLY +# This is an autogenerated file for dynamic methods in `Disqualified::Internal`. +# Please instead update this file by running `bin/tapioca dsl Disqualified::Internal`. + + +class Disqualified::Internal + include GeneratedAttributeMethods + extend CommonRelationMethods + extend GeneratedRelationMethods + + private + + sig { returns(NilClass) } + def to_ary; end + + module CommonRelationMethods + sig do + params( + block: T.nilable(T.proc.params(record: ::Disqualified::Internal).returns(T.untyped)) + ).returns(T::Boolean) + end + def any?(&block); end + + sig { params(column_name: T.any(String, Symbol)).returns(T.any(Integer, Float, BigDecimal)) } + def average(column_name); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def build(attributes = nil, &block); end + + sig { params(operation: Symbol, column_name: T.any(String, Symbol)).returns(T.any(Integer, Float, BigDecimal)) } + def calculate(operation, column_name); end + + sig { params(column_name: T.nilable(T.any(String, Symbol))).returns(Integer) } + sig { params(column_name: NilClass, block: T.proc.params(object: ::Disqualified::Internal).void).returns(Integer) } + def count(column_name = nil, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def create(attributes = nil, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def create!(attributes = nil, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def create_or_find_by(attributes, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def create_or_find_by!(attributes, &block); end + + sig { returns(T::Array[::Disqualified::Internal]) } + def destroy_all; end + + sig { params(conditions: T.untyped).returns(T::Boolean) } + def exists?(conditions = :none); end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def fifth; end + + sig { returns(::Disqualified::Internal) } + def fifth!; end + + sig do + params( + args: T.any(String, Symbol, ::ActiveSupport::Multibyte::Chars, T::Boolean, BigDecimal, Numeric, ::ActiveRecord::Type::Binary::Data, ::ActiveRecord::Type::Time::Value, Date, Time, ::ActiveSupport::Duration, T::Class[T.anything]) + ).returns(::Disqualified::Internal) + end + sig do + params( + args: T::Array[T.any(String, Symbol, ::ActiveSupport::Multibyte::Chars, T::Boolean, BigDecimal, Numeric, ::ActiveRecord::Type::Binary::Data, ::ActiveRecord::Type::Time::Value, Date, Time, ::ActiveSupport::Duration, T::Class[T.anything])] + ).returns(T::Enumerable[::Disqualified::Internal]) + end + sig do + params( + args: NilClass, + block: T.proc.params(object: ::Disqualified::Internal).void + ).returns(T.nilable(::Disqualified::Internal)) + end + def find(args = nil, &block); end + + sig { params(args: T.untyped).returns(T.nilable(::Disqualified::Internal)) } + def find_by(*args); end + + sig { params(args: T.untyped).returns(::Disqualified::Internal) } + def find_by!(*args); end + + sig do + params( + start: T.untyped, + finish: T.untyped, + batch_size: Integer, + error_on_ignore: T.untyped, + order: Symbol, + block: T.proc.params(object: ::Disqualified::Internal).void + ).void + end + sig do + params( + start: T.untyped, + finish: T.untyped, + batch_size: Integer, + error_on_ignore: T.untyped, + order: Symbol + ).returns(T::Enumerator[::Disqualified::Internal]) + end + def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc, &block); end + + sig do + params( + start: T.untyped, + finish: T.untyped, + batch_size: Integer, + error_on_ignore: T.untyped, + order: Symbol, + block: T.proc.params(object: T::Array[::Disqualified::Internal]).void + ).void + end + sig do + params( + start: T.untyped, + finish: T.untyped, + batch_size: Integer, + error_on_ignore: T.untyped, + order: Symbol + ).returns(T::Enumerator[T::Enumerator[::Disqualified::Internal]]) + end + def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def find_or_create_by(attributes, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def find_or_create_by!(attributes, &block); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def find_or_initialize_by(attributes, &block); end + + sig { params(signed_id: T.untyped, purpose: T.untyped).returns(T.nilable(::Disqualified::Internal)) } + def find_signed(signed_id, purpose: nil); end + + sig { params(signed_id: T.untyped, purpose: T.untyped).returns(::Disqualified::Internal) } + def find_signed!(signed_id, purpose: nil); end + + sig { params(arg: T.untyped, args: T.untyped).returns(::Disqualified::Internal) } + def find_sole_by(arg, *args); end + + sig { params(limit: NilClass).returns(T.nilable(::Disqualified::Internal)) } + sig { params(limit: Integer).returns(T::Array[::Disqualified::Internal]) } + def first(limit = nil); end + + sig { returns(::Disqualified::Internal) } + def first!; end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def forty_two; end + + sig { returns(::Disqualified::Internal) } + def forty_two!; end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def fourth; end + + sig { returns(::Disqualified::Internal) } + def fourth!; end + + sig { returns(Array) } + def ids; end + + sig do + params( + of: Integer, + start: T.untyped, + finish: T.untyped, + load: T.untyped, + error_on_ignore: T.untyped, + order: Symbol, + use_ranges: T.untyped, + block: T.proc.params(object: PrivateRelation).void + ).void + end + sig do + params( + of: Integer, + start: T.untyped, + finish: T.untyped, + load: T.untyped, + error_on_ignore: T.untyped, + order: Symbol, + use_ranges: T.untyped + ).returns(::ActiveRecord::Batches::BatchEnumerator) + end + def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil, order: :asc, use_ranges: nil, &block); end + + sig { params(record: T.untyped).returns(T::Boolean) } + def include?(record); end + + sig { params(limit: NilClass).returns(T.nilable(::Disqualified::Internal)) } + sig { params(limit: Integer).returns(T::Array[::Disqualified::Internal]) } + def last(limit = nil); end + + sig { returns(::Disqualified::Internal) } + def last!; end + + sig do + params( + block: T.nilable(T.proc.params(record: ::Disqualified::Internal).returns(T.untyped)) + ).returns(T::Boolean) + end + def many?(&block); end + + sig { params(column_name: T.any(String, Symbol)).returns(T.untyped) } + def maximum(column_name); end + + sig { params(record: T.untyped).returns(T::Boolean) } + def member?(record); end + + sig { params(column_name: T.any(String, Symbol)).returns(T.untyped) } + def minimum(column_name); end + + sig do + params( + attributes: T.untyped, + block: T.nilable(T.proc.params(object: ::Disqualified::Internal).void) + ).returns(::Disqualified::Internal) + end + def new(attributes = nil, &block); end + + sig do + params( + block: T.nilable(T.proc.params(record: ::Disqualified::Internal).returns(T.untyped)) + ).returns(T::Boolean) + end + def none?(&block); end + + sig do + params( + block: T.nilable(T.proc.params(record: ::Disqualified::Internal).returns(T.untyped)) + ).returns(T::Boolean) + end + def one?(&block); end + + sig { params(column_names: T.untyped).returns(T.untyped) } + def pick(*column_names); end + + sig { params(column_names: T.untyped).returns(T.untyped) } + def pluck(*column_names); end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def second; end + + sig { returns(::Disqualified::Internal) } + def second!; end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def second_to_last; end + + sig { returns(::Disqualified::Internal) } + def second_to_last!; end + + sig { returns(::Disqualified::Internal) } + def sole; end + + sig { params(initial_value_or_column: T.untyped).returns(T.any(Integer, Float, BigDecimal)) } + sig do + type_parameters(:U) + .params( + initial_value_or_column: T.nilable(T.type_parameter(:U)), + block: T.proc.params(object: ::Disqualified::Internal).returns(T.type_parameter(:U)) + ).returns(T.type_parameter(:U)) + end + def sum(initial_value_or_column = nil, &block); end + + sig { params(limit: NilClass).returns(T.nilable(::Disqualified::Internal)) } + sig { params(limit: Integer).returns(T::Array[::Disqualified::Internal]) } + def take(limit = nil); end + + sig { returns(::Disqualified::Internal) } + def take!; end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def third; end + + sig { returns(::Disqualified::Internal) } + def third!; end + + sig { returns(T.nilable(::Disqualified::Internal)) } + def third_to_last; end + + sig { returns(::Disqualified::Internal) } + def third_to_last!; end + end + + module GeneratedAssociationRelationMethods + sig { returns(PrivateAssociationRelation) } + def all; end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def and(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def annotate(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def create_with(*args, &blk); end + + sig { params(value: T::Boolean).returns(PrivateAssociationRelation) } + def distinct(value = true); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def eager_load(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def except(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def excluding(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def extending(*args, &blk); end + + sig { params(association: Symbol).returns(T::Array[T.untyped]) } + def extract_associated(association); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def from(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelationGroupChain) } + def group(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def having(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def in_order_of(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def includes(*args, &blk); end + + sig do + params( + attributes: Hash, + returning: T.nilable(T.any(T::Array[Symbol], FalseClass)), + unique_by: T.nilable(T.any(T::Array[Symbol], Symbol)) + ).returns(ActiveRecord::Result) + end + def insert(attributes, returning: nil, unique_by: nil); end + + sig do + params( + attributes: Hash, + returning: T.nilable(T.any(T::Array[Symbol], FalseClass)) + ).returns(ActiveRecord::Result) + end + def insert!(attributes, returning: nil); end + + sig do + params( + attributes: T::Array[Hash], + returning: T.nilable(T.any(T::Array[Symbol], FalseClass)), + unique_by: T.nilable(T.any(T::Array[Symbol], Symbol)) + ).returns(ActiveRecord::Result) + end + def insert_all(attributes, returning: nil, unique_by: nil); end + + sig do + params( + attributes: T::Array[Hash], + returning: T.nilable(T.any(T::Array[Symbol], FalseClass)) + ).returns(ActiveRecord::Result) + end + def insert_all!(attributes, returning: nil); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def invert_where(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def joins(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def left_joins(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def left_outer_joins(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def limit(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def lock(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def merge(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def none(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def null_relation?(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def offset(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def only(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def optimizer_hints(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def or(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def order(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def preload(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def readonly(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def references(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def regroup(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def reorder(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def reselect(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def reverse_order(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def rewhere(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def select(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def strict_loading(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def structurally_compatible?(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def uniq!(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def unscope(*args, &blk); end + + sig do + params( + attributes: Hash, + returning: T.nilable(T.any(T::Array[Symbol], FalseClass)), + unique_by: T.nilable(T.any(T::Array[Symbol], Symbol)) + ).returns(ActiveRecord::Result) + end + def upsert(attributes, returning: nil, unique_by: nil); end + + sig do + params( + attributes: T::Array[Hash], + returning: T.nilable(T.any(T::Array[Symbol], FalseClass)), + unique_by: T.nilable(T.any(T::Array[Symbol], Symbol)) + ).returns(ActiveRecord::Result) + end + def upsert_all(attributes, returning: nil, unique_by: nil); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelationWhereChain) } + def where(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def with(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) } + def without(*args, &blk); end + end + + module GeneratedAttributeMethods + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def created_at; end + + sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) } + def created_at=(value); end + + sig { returns(T::Boolean) } + def created_at?; end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def created_at_before_last_save; end + + sig { returns(T.untyped) } + def created_at_before_type_cast; end + + sig { returns(T::Boolean) } + def created_at_came_from_user?; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def created_at_change; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def created_at_change_to_be_saved; end + + sig { params(from: ::ActiveSupport::TimeWithZone, to: ::ActiveSupport::TimeWithZone).returns(T::Boolean) } + def created_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def created_at_in_database; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def created_at_previous_change; end + + sig { params(from: ::ActiveSupport::TimeWithZone, to: ::ActiveSupport::TimeWithZone).returns(T::Boolean) } + def created_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def created_at_previously_was; end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def created_at_was; end + + sig { void } + def created_at_will_change!; end + + sig { returns(T.nilable(::Integer)) } + def id; end + + sig { params(value: ::Integer).returns(::Integer) } + def id=(value); end + + sig { returns(T::Boolean) } + def id?; end + + sig { returns(T.nilable(::Integer)) } + def id_before_last_save; end + + sig { returns(T.untyped) } + def id_before_type_cast; end + + sig { returns(T::Boolean) } + def id_came_from_user?; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def id_change; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def id_change_to_be_saved; end + + sig { params(from: ::Integer, to: ::Integer).returns(T::Boolean) } + def id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::Integer)) } + def id_in_database; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def id_previous_change; end + + sig { params(from: ::Integer, to: ::Integer).returns(T::Boolean) } + def id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::Integer)) } + def id_previously_was; end + + sig { returns(T.nilable(::Integer)) } + def id_value; end + + sig { params(value: ::Integer).returns(::Integer) } + def id_value=(value); end + + sig { returns(T::Boolean) } + def id_value?; end + + sig { returns(T.nilable(::Integer)) } + def id_value_before_last_save; end + + sig { returns(T.untyped) } + def id_value_before_type_cast; end + + sig { returns(T::Boolean) } + def id_value_came_from_user?; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def id_value_change; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def id_value_change_to_be_saved; end + + sig { params(from: ::Integer, to: ::Integer).returns(T::Boolean) } + def id_value_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::Integer)) } + def id_value_in_database; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def id_value_previous_change; end + + sig { params(from: ::Integer, to: ::Integer).returns(T::Boolean) } + def id_value_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::Integer)) } + def id_value_previously_was; end + + sig { returns(T.nilable(::Integer)) } + def id_value_was; end + + sig { void } + def id_value_will_change!; end + + sig { returns(T.nilable(::Integer)) } + def id_was; end + + sig { void } + def id_will_change!; end + + sig { returns(::String) } + def key; end + + sig { params(value: ::String).returns(::String) } + def key=(value); end + + sig { returns(T::Boolean) } + def key?; end + + sig { returns(T.nilable(::String)) } + def key_before_last_save; end + + sig { returns(T.untyped) } + def key_before_type_cast; end + + sig { returns(T::Boolean) } + def key_came_from_user?; end + + sig { returns(T.nilable([::String, ::String])) } + def key_change; end + + sig { returns(T.nilable([::String, ::String])) } + def key_change_to_be_saved; end + + sig { params(from: ::String, to: ::String).returns(T::Boolean) } + def key_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::String)) } + def key_in_database; end + + sig { returns(T.nilable([::String, ::String])) } + def key_previous_change; end + + sig { params(from: ::String, to: ::String).returns(T::Boolean) } + def key_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::String)) } + def key_previously_was; end + + sig { returns(T.nilable(::String)) } + def key_was; end + + sig { void } + def key_will_change!; end + + sig { void } + def restore_created_at!; end + + sig { void } + def restore_id!; end + + sig { void } + def restore_id_value!; end + + sig { void } + def restore_key!; end + + sig { void } + def restore_unique_key!; end + + sig { void } + def restore_updated_at!; end + + sig { void } + def restore_value!; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def saved_change_to_created_at; end + + sig { returns(T::Boolean) } + def saved_change_to_created_at?; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def saved_change_to_id; end + + sig { returns(T::Boolean) } + def saved_change_to_id?; end + + sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) } + def saved_change_to_id_value; end + + sig { returns(T::Boolean) } + def saved_change_to_id_value?; end + + sig { returns(T.nilable([::String, ::String])) } + def saved_change_to_key; end + + sig { returns(T::Boolean) } + def saved_change_to_key?; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def saved_change_to_unique_key; end + + sig { returns(T::Boolean) } + def saved_change_to_unique_key?; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def saved_change_to_updated_at; end + + sig { returns(T::Boolean) } + def saved_change_to_updated_at?; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def saved_change_to_value; end + + sig { returns(T::Boolean) } + def saved_change_to_value?; end + + sig { returns(T.nilable(::String)) } + def unique_key; end + + sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) } + def unique_key=(value); end + + sig { returns(T::Boolean) } + def unique_key?; end + + sig { returns(T.nilable(::String)) } + def unique_key_before_last_save; end + + sig { returns(T.untyped) } + def unique_key_before_type_cast; end + + sig { returns(T::Boolean) } + def unique_key_came_from_user?; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def unique_key_change; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def unique_key_change_to_be_saved; end + + sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) } + def unique_key_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::String)) } + def unique_key_in_database; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def unique_key_previous_change; end + + sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) } + def unique_key_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::String)) } + def unique_key_previously_was; end + + sig { returns(T.nilable(::String)) } + def unique_key_was; end + + sig { void } + def unique_key_will_change!; end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def updated_at; end + + sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) } + def updated_at=(value); end + + sig { returns(T::Boolean) } + def updated_at?; end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def updated_at_before_last_save; end + + sig { returns(T.untyped) } + def updated_at_before_type_cast; end + + sig { returns(T::Boolean) } + def updated_at_came_from_user?; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def updated_at_change; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def updated_at_change_to_be_saved; end + + sig { params(from: ::ActiveSupport::TimeWithZone, to: ::ActiveSupport::TimeWithZone).returns(T::Boolean) } + def updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def updated_at_in_database; end + + sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) } + def updated_at_previous_change; end + + sig { params(from: ::ActiveSupport::TimeWithZone, to: ::ActiveSupport::TimeWithZone).returns(T::Boolean) } + def updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def updated_at_previously_was; end + + sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) } + def updated_at_was; end + + sig { void } + def updated_at_will_change!; end + + sig { returns(T.nilable(::String)) } + def value; end + + sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) } + def value=(value); end + + sig { returns(T::Boolean) } + def value?; end + + sig { returns(T.nilable(::String)) } + def value_before_last_save; end + + sig { returns(T.untyped) } + def value_before_type_cast; end + + sig { returns(T::Boolean) } + def value_came_from_user?; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def value_change; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def value_change_to_be_saved; end + + sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) } + def value_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::String)) } + def value_in_database; end + + sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) } + def value_previous_change; end + + sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) } + def value_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.nilable(::String)) } + def value_previously_was; end + + sig { returns(T.nilable(::String)) } + def value_was; end + + sig { void } + def value_will_change!; end + + sig { returns(T::Boolean) } + def will_save_change_to_created_at?; end + + sig { returns(T::Boolean) } + def will_save_change_to_id?; end + + sig { returns(T::Boolean) } + def will_save_change_to_id_value?; end + + sig { returns(T::Boolean) } + def will_save_change_to_key?; end + + sig { returns(T::Boolean) } + def will_save_change_to_unique_key?; end + + sig { returns(T::Boolean) } + def will_save_change_to_updated_at?; end + + sig { returns(T::Boolean) } + def will_save_change_to_value?; end + end + + module GeneratedRelationMethods + sig { returns(PrivateRelation) } + def all; end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def and(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def annotate(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def create_with(*args, &blk); end + + sig { params(value: T::Boolean).returns(PrivateRelation) } + def distinct(value = true); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def eager_load(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def except(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def excluding(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def extending(*args, &blk); end + + sig { params(association: Symbol).returns(T::Array[T.untyped]) } + def extract_associated(association); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def from(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelationGroupChain) } + def group(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def having(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def in_order_of(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def includes(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def invert_where(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def joins(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def left_joins(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def left_outer_joins(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def limit(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def lock(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def merge(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def none(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def null_relation?(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def offset(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def only(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def optimizer_hints(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def or(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def order(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def preload(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def readonly(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def references(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def regroup(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def reorder(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def reselect(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def reverse_order(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def rewhere(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def select(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def strict_loading(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def structurally_compatible?(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def uniq!(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def unscope(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelationWhereChain) } + def where(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def with(*args, &blk); end + + sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) } + def without(*args, &blk); end + end + + class PrivateAssociationRelation < ::ActiveRecord::AssociationRelation + include CommonRelationMethods + include GeneratedAssociationRelationMethods + + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig { returns(T::Array[::Disqualified::Internal]) } + def to_a; end + + sig { returns(T::Array[::Disqualified::Internal]) } + def to_ary; end + end + + class PrivateAssociationRelationGroupChain < PrivateAssociationRelation + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) } + def average(column_name); end + + sig do + params( + operation: Symbol, + column_name: T.any(String, Symbol) + ).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) + end + def calculate(operation, column_name); end + + sig { params(column_name: T.untyped).returns(T::Hash[T.untyped, Integer]) } + def count(column_name = nil); end + + sig { params(args: T.untyped, blk: T.untyped).returns(T.self_type) } + def having(*args, &blk); end + + sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.untyped]) } + def maximum(column_name); end + + sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.untyped]) } + def minimum(column_name); end + + sig do + params( + column_name: T.nilable(T.any(String, Symbol)), + block: T.nilable(T.proc.params(record: T.untyped).returns(T.untyped)) + ).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) + end + def sum(column_name = nil, &block); end + end + + class PrivateAssociationRelationWhereChain < PrivateAssociationRelation + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig { params(args: T.untyped).returns(PrivateAssociationRelation) } + def associated(*args); end + + sig { params(args: T.untyped).returns(PrivateAssociationRelation) } + def missing(*args); end + + sig { params(opts: T.untyped, rest: T.untyped).returns(PrivateAssociationRelation) } + def not(opts, *rest); end + end + + class PrivateCollectionProxy < ::ActiveRecord::Associations::CollectionProxy + include CommonRelationMethods + include GeneratedAssociationRelationMethods + + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig do + params( + records: T.any(::Disqualified::Internal, T::Enumerable[T.any(::Disqualified::Internal, T::Enumerable[::Disqualified::Internal])]) + ).returns(PrivateCollectionProxy) + end + def <<(*records); end + + sig do + params( + records: T.any(::Disqualified::Internal, T::Enumerable[T.any(::Disqualified::Internal, T::Enumerable[::Disqualified::Internal])]) + ).returns(PrivateCollectionProxy) + end + def append(*records); end + + sig { returns(PrivateCollectionProxy) } + def clear; end + + sig do + params( + records: T.any(::Disqualified::Internal, T::Enumerable[T.any(::Disqualified::Internal, T::Enumerable[::Disqualified::Internal])]) + ).returns(PrivateCollectionProxy) + end + def concat(*records); end + + sig do + params( + records: T.any(::Disqualified::Internal, Integer, String, T::Enumerable[T.any(::Disqualified::Internal, Integer, String, T::Enumerable[::Disqualified::Internal])]) + ).returns(T::Array[::Disqualified::Internal]) + end + def delete(*records); end + + sig do + params( + records: T.any(::Disqualified::Internal, Integer, String, T::Enumerable[T.any(::Disqualified::Internal, Integer, String, T::Enumerable[::Disqualified::Internal])]) + ).returns(T::Array[::Disqualified::Internal]) + end + def destroy(*records); end + + sig { returns(T::Array[::Disqualified::Internal]) } + def load_target; end + + sig do + params( + records: T.any(::Disqualified::Internal, T::Enumerable[T.any(::Disqualified::Internal, T::Enumerable[::Disqualified::Internal])]) + ).returns(PrivateCollectionProxy) + end + def prepend(*records); end + + sig do + params( + records: T.any(::Disqualified::Internal, T::Enumerable[T.any(::Disqualified::Internal, T::Enumerable[::Disqualified::Internal])]) + ).returns(PrivateCollectionProxy) + end + def push(*records); end + + sig do + params( + other_array: T.any(::Disqualified::Internal, T::Enumerable[T.any(::Disqualified::Internal, T::Enumerable[::Disqualified::Internal])]) + ).returns(T::Array[::Disqualified::Internal]) + end + def replace(other_array); end + + sig { returns(PrivateAssociationRelation) } + def scope; end + + sig { returns(T::Array[::Disqualified::Internal]) } + def target; end + + sig { returns(T::Array[::Disqualified::Internal]) } + def to_a; end + + sig { returns(T::Array[::Disqualified::Internal]) } + def to_ary; end + end + + class PrivateRelation < ::ActiveRecord::Relation + include CommonRelationMethods + include GeneratedRelationMethods + + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig { returns(T::Array[::Disqualified::Internal]) } + def to_a; end + + sig { returns(T::Array[::Disqualified::Internal]) } + def to_ary; end + end + + class PrivateRelationGroupChain < PrivateRelation + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) } + def average(column_name); end + + sig do + params( + operation: Symbol, + column_name: T.any(String, Symbol) + ).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) + end + def calculate(operation, column_name); end + + sig { params(column_name: T.untyped).returns(T::Hash[T.untyped, Integer]) } + def count(column_name = nil); end + + sig { params(args: T.untyped, blk: T.untyped).returns(T.self_type) } + def having(*args, &blk); end + + sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.untyped]) } + def maximum(column_name); end + + sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.untyped]) } + def minimum(column_name); end + + sig do + params( + column_name: T.nilable(T.any(String, Symbol)), + block: T.nilable(T.proc.params(record: T.untyped).returns(T.untyped)) + ).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) + end + def sum(column_name = nil, &block); end + end + + class PrivateRelationWhereChain < PrivateRelation + Elem = type_member { { fixed: ::Disqualified::Internal } } + + sig { params(args: T.untyped).returns(PrivateRelation) } + def associated(*args); end + + sig { params(args: T.untyped).returns(PrivateRelation) } + def missing(*args); end + + sig { params(opts: T.untyped, rest: T.untyped).returns(PrivateRelation) } + def not(opts, *rest); end + end +end diff --git a/sorbet/rbi/dsl/disqualified/record.rbi b/sorbet/rbi/dsl/disqualified/record.rbi index 48d4dae..abfbf92 100644 --- a/sorbet/rbi/dsl/disqualified/record.rbi +++ b/sorbet/rbi/dsl/disqualified/record.rbi @@ -948,6 +948,51 @@ class Disqualified::Record sig { void } def locked_by_will_change!; end + sig { returns(T.untyped) } + def metadata; end + + sig { params(value: T.untyped).returns(T.untyped) } + def metadata=(value); end + + sig { returns(T::Boolean) } + def metadata?; end + + sig { returns(T.untyped) } + def metadata_before_last_save; end + + sig { returns(T.untyped) } + def metadata_before_type_cast; end + + sig { returns(T::Boolean) } + def metadata_came_from_user?; end + + sig { returns(T.nilable([T.untyped, T.untyped])) } + def metadata_change; end + + sig { returns(T.nilable([T.untyped, T.untyped])) } + def metadata_change_to_be_saved; end + + sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) } + def metadata_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.untyped) } + def metadata_in_database; end + + sig { returns(T.nilable([T.untyped, T.untyped])) } + def metadata_previous_change; end + + sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) } + def metadata_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end + + sig { returns(T.untyped) } + def metadata_previously_was; end + + sig { returns(T.untyped) } + def metadata_was; end + + sig { void } + def metadata_will_change!; end + sig { returns(::String) } def queue; end @@ -1020,6 +1065,9 @@ class Disqualified::Record sig { void } def restore_locked_by!; end + sig { void } + def restore_metadata!; end + sig { void } def restore_queue!; end @@ -1128,6 +1176,12 @@ class Disqualified::Record sig { returns(T::Boolean) } def saved_change_to_locked_by?; end + sig { returns(T.nilable([T.untyped, T.untyped])) } + def saved_change_to_metadata; end + + sig { returns(T::Boolean) } + def saved_change_to_metadata?; end + sig { returns(T.nilable([::String, ::String])) } def saved_change_to_queue; end @@ -1218,6 +1272,9 @@ class Disqualified::Record sig { returns(T::Boolean) } def will_save_change_to_locked_by?; end + sig { returns(T::Boolean) } + def will_save_change_to_metadata?; end + sig { returns(T::Boolean) } def will_save_change_to_queue?; end diff --git a/test/disqualified/configuration_test.rb b/test/disqualified/configuration_test.rb new file mode 100644 index 0000000..1bbe375 --- /dev/null +++ b/test/disqualified/configuration_test.rb @@ -0,0 +1,19 @@ +require "test_helper" + +class Disqualified::ConfigurationTest < ActiveSupport::TestCase + class Plugin1 + include Disqualified::Plugin + + def name = "plugin1" + + def job_config_namespace = "plugin1" + + def metadata_namespace = "plugin1" + end + + test "plugin registry" do + sc = Disqualified::Configuration.new + sc.plugins.register(Plugin1.new) + assert_includes(sc.plugins.sorted, "plugin1") + end +end diff --git a/test/disqualified/job_test.rb b/test/disqualified/job_test.rb index ee1cbff..73ce6ad 100644 --- a/test/disqualified/job_test.rb +++ b/test/disqualified/job_test.rb @@ -6,11 +6,29 @@ class Disqualified::JobTest < ActiveSupport::TestCase class OneArgJob include Disqualified::Job - def perform + def perform(args) end end - test "#perform_async" do + test ".job_options" do + klass = Class.new do + include Disqualified::Job + job_options["klass"] = "klass" + end + llass = Class.new do + include Disqualified::Job + job_options["llass"] = "llass" + end + mlass = Class.new(llass) do + job_options["mlass"] = "mlass" + end + + assert_equal({"klass" => "klass"}, klass.send(:job_options).send(:to_h)) + assert_equal({"llass" => "llass"}, llass.send(:job_options).send(:to_h)) + assert_equal({"mlass" => "mlass"}, mlass.send(:job_options).send(:to_h)) + end + + test ".perform_async" do freeze_time do assert_difference("Disqualified::Record.count", 1) do OneArgJob.perform_async("hello there") @@ -24,7 +42,7 @@ def perform end end - test "#perform_in" do + test ".perform_in" do freeze_time do assert_difference("Disqualified::Record.count", 1) do OneArgJob.perform_in(5.minutes, "hello there") @@ -38,7 +56,7 @@ def perform end end - test "#perform_at" do + test ".perform_at" do assert_difference("Disqualified::Record.count", 1) do OneArgJob.perform_at(Time.utc(1970, 1, 1), "hello there") end diff --git a/test/disqualified/plugin_registry_test.rb b/test/disqualified/plugin_registry_test.rb new file mode 100644 index 0000000..07c3583 --- /dev/null +++ b/test/disqualified/plugin_registry_test.rb @@ -0,0 +1,68 @@ +require "test_helper" + +class Disqualified::PluginRegistryTest < ActiveSupport::TestCase + class Plugin1 + include Disqualified::Plugin + + def name = "plugin1" + + def job_config_namespace = "plugin1" + + def metadata_namespace = "plugin1" + end + + class Plugin2 + include Disqualified::Plugin + + def name = "plugin2" + + def job_config_namespace = "plugin2" + + def metadata_namespace = "plugin2" + end + + class Plugin3 + include Disqualified::Plugin + + def name = "plugin3" + + def job_config_namespace = "plugin3" + + def metadata_namespace = "plugin3" + end + + class Plugin4 + include Disqualified::Plugin + + def name = "plugin4" + + def job_config_namespace = "plugin4" + + def metadata_namespace = "plugin4" + end + + test "plugins without any explcit dependencies" do + r = Disqualified::PluginRegistry.new + r.register(Plugin1.new) + r.register(Plugin2.new) + r.register(Plugin3.new) + r.register(Plugin4.new) + assert_equal(4, r.sorted.uniq.size) + end + + test "plugins with explicit dependencies during registration" do + r = Disqualified::PluginRegistry.new + r.register(Plugin1.new, after: "plugin2") + r.register(Plugin2.new, after: %w[plugin3 plugin4]) + r.register(Plugin3.new, before: "plugin4") + r.register(Plugin4.new) + assert_equal(%w[plugin3 plugin4 plugin2 plugin1], r.sorted) + end + + test "ordering nonexistant dependencies" do + r = Disqualified::PluginRegistry.new + r.register(Plugin1.new, after: "plugin5") + r.register(Plugin2.new, after: %w[plugin5 plugin1]) + assert_equal(%w[plugin1 plugin2], r.sorted) + end +end diff --git a/test/disqualified/unique_test.rb b/test/disqualified/unique_test.rb new file mode 100644 index 0000000..5464a2a --- /dev/null +++ b/test/disqualified/unique_test.rb @@ -0,0 +1,113 @@ +require "test_helper" + +class Disqualified::UniqueTest < ActiveSupport::TestCase + class UniqueJob + include Disqualified::Job + + unique(:until_executed) + + def perform = nil + end + + class Unique2Job + include Disqualified::Job + + unique(:until_executed) + + def perform = nil + end + + class UniqueArgJob + include Disqualified::Job + + unique(:until_executed) + + def perform(arg) = nil + end + + setup do + @original_config = Disqualified.config + Disqualified.config = Disqualified::Configuration.new + end + + teardown do + Disqualified.config = @original_config + end + + test "only one when scheduling" do + now = Time.now.round + + assert_difference "Disqualified::Record.count", 1 do + UniqueJob.perform_at(now + 10.minutes) + UniqueJob.perform_at(now + 15.minutes) + UniqueJob.perform_at(now + 2.minutes) + UniqueJob.perform_in(1.minute) + UniqueJob.perform_in(3.minutes) + UniqueJob.perform_async + end + + assert_equal(now + 10.minutes, Disqualified::Record.find_by!(handler: UniqueJob.name).run_at) + end + + test "only one when immediate" do + assert_difference "Disqualified::Record.count", 1 do + UniqueJob.perform_async + UniqueJob.perform_async + end + end + + test "queues after running" do + UniqueJob.perform_async + assert_difference "Disqualified::Record.count", 0 do + UniqueJob.perform_async + end + Disqualified::Record.first.run! + assert_difference "Disqualified::Record.count", 1 do + UniqueJob.perform_async + end + end + + test "looks at arguments" do + assert_difference "Disqualified::Record.count", 1 do + UniqueArgJob.perform_async(1) + UniqueArgJob.perform_async(1) + end + assert_difference "Disqualified::Record.count", 1 do + UniqueArgJob.perform_async(2) + UniqueArgJob.perform_async(2) + end + end + + test "unrelated jobs don't matter" do + assert_difference "Disqualified::Record.count", 2 do + UniqueJob.perform_async + Unique2Job.perform_async + end + end + + test "can't call multiple times" do + assert_raise(Disqualified::Error::DuplicateSetting) do + Class.new.tap do |klass| + Disqualified::UniqueTest.const_set(:TEST_A, klass) + klass.instance_exec do + include Disqualified::Job + + unique(:until_executed) + unique(:until_executed) + + def perform = nil + end + end + end + end + + test "can't be called during runtime (at least not easily)" do + klass = Class.new do + include Disqualified::Job + end + + assert_raise(NoMethodError) do + klass.unique + end + end +end diff --git a/test/dummy/config/initializers/disqualified.rb b/test/dummy/config/initializers/disqualified.rb index 409f161..45a2355 100644 --- a/test/dummy/config/initializers/disqualified.rb +++ b/test/dummy/config/initializers/disqualified.rb @@ -1,8 +1,8 @@ -Disqualified.configure_server do |config| +Disqualified.configure do |config| config.logger = Rails.logger ActiveRecord::Base.logger = Rails.logger - config.on_error do |error, context| + config.on_execution_error do |error, context| puts "🔥" * 10 pp error pp context diff --git a/test/dummy/db/migrate/20231218213817_create_disqualified_internals.rb b/test/dummy/db/migrate/20231218213817_create_disqualified_internals.rb new file mode 100644 index 0000000..a695afe --- /dev/null +++ b/test/dummy/db/migrate/20231218213817_create_disqualified_internals.rb @@ -0,0 +1,14 @@ +class CreateDisqualifiedInternals < ActiveRecord::Migration[7.0] + def change + create_table :disqualified_internals do |t| + t.text :key, null: false + t.text :unique_key + t.text :value + + t.timestamps + + t.index :key + t.index :unique_key, unique: true + end + end +end diff --git a/test/dummy/db/migrate/20231218232905_add_metadata_to_disqualified_jobs.rb b/test/dummy/db/migrate/20231218232905_add_metadata_to_disqualified_jobs.rb new file mode 100644 index 0000000..d759f95 --- /dev/null +++ b/test/dummy/db/migrate/20231218232905_add_metadata_to_disqualified_jobs.rb @@ -0,0 +1,5 @@ +class AddMetadataToDisqualifiedJobs < ActiveRecord::Migration[7.0] + def change + add_column :disqualified_jobs, :metadata, :text + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 42a5262..d23fabf 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -10,7 +10,17 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_07_03_062536) do +ActiveRecord::Schema[7.1].define(version: 2023_12_18_232905) do + create_table "disqualified_internals", force: :cascade do |t| + t.text "key", null: false + t.text "unique_key" + t.text "value" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_disqualified_internals_on_key" + t.index ["unique_key"], name: "index_disqualified_internals_on_unique_key", unique: true + end + create_table "disqualified_jobs", force: :cascade do |t| t.string "handler", null: false t.text "arguments", null: false @@ -22,6 +32,7 @@ t.datetime "finished_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "metadata" end end