Class: DataShifter::Shift

Inherits:
Object
  • Object
show all
Includes:
Axn
Defined in:
lib/data_shifter/shift.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.allow_external_requests(hosts) ⇒ Object

Allow these hosts (or regexes) for HTTP during dry run only. Combines with DataShifter.config.allow_external_requests. Has no effect in commit mode — HTTP is unrestricted when dry_run is false. Example: allow_external_requests ["api.readonly.example.com", %r\\.internal\\.internal\.company\z]



136
137
138
# File 'lib/data_shifter/shift.rb', line 136

def allow_external_requests(hosts)
  self._allow_external_requests = Array(hosts)
end

.description(text = nil) ⇒ Object



86
87
88
89
90
91
92
# File 'lib/data_shifter/shift.rb', line 86

def description(text = nil)
  if text.nil?
    _description
  else
    self._description = text.to_s.presence
  end
end

.progress(enabled = nil) ⇒ Object

Per-shift override for progress bar visibility. Overrides DataShifter.config.progress_enabled. Thin alias over the progress_enabled override accessor generated by DataShifter.overrides. progress false # set override (coerced to a boolean) progress # read the raw override (nil when unset; does NOT fall back to config)



119
120
121
122
123
124
125
126
# File 'lib/data_shifter/shift.rb', line 119

def progress(enabled = nil)
  if enabled.nil?
    raw = progress_enabled_override
    Axn::Configurable::UNSET.equal?(raw) ? nil : raw
  else
    progress_enabled(!!enabled)
  end
end

.run!Object

Raises:

  • (StandardError)


175
176
177
178
179
180
# File 'lib/data_shifter/shift.rb', line 175

def run!
  dry_run = Internal::Env.dry_run?
  result = call(dry_run:)
  raise result.exception if result.exception
  raise StandardError, result.error unless result.ok?
end

.suppress_repeated_logs(enabled) ⇒ Object

Enable/disable log deduplication for this shift. Overrides DataShifter.config.suppress_repeated_logs. Thin alias over the suppress_repeated_logs override accessor generated by DataShifter.overrides (which would otherwise read the resolved value when called with no args); we always treat a call with an argument as a set, coercing to a boolean. Example: suppress_repeated_logs false



145
146
147
# File 'lib/data_shifter/shift.rb', line 145

def suppress_repeated_logs(enabled)
  super(!!enabled)
end

.task(label = nil, axn = nil, **axn_kwargs, &block) ⇒ Object

Define a task to run instead of collection/process_record. Multiple tasks run in sequence; labels appear in errors and summary.

Block form (arbitrary code, runtime/instance state available):

task "Fix user A" do
User.find(123).update!(...)
end

Axn class form (sugar for one helper axn; kwargs are static, evaluated at class-load time, and forwarded to .call! so a failure is never silently swallowed). Use the block form when you need runtime values:

task "Recalculate totals", RecalculateTotals, company_id: 123
# => RecalculateTotals.call!(company_id: 123)


162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/data_shifter/shift.rb', line 162

def task(label = nil, axn = nil, **axn_kwargs, &block)
  if axn
    raise ArgumentError, "task accepts either an Axn class or a block, not both" if block
    raise ArgumentError, "task expected an Axn class but got #{axn.inspect}" unless axn.is_a?(Class) && axn.include?(Axn)

    block = -> { axn.call!(**axn_kwargs) }
  elsif block.nil?
    raise ArgumentError, "task requires a block or an Axn class"
  end

  self._task_blocks = (_task_blocks || []).dup + [{ label: label.presence, block: }]
end

.task_name(value = nil) ⇒ Object



94
95
96
97
98
99
100
# File 'lib/data_shifter/shift.rb', line 94

def task_name(value = nil)
  if value.nil?
    _task_name
  else
    self._task_name = value.to_s.presence
  end
end

.throttle(interval, per: 1) ⇒ Object



128
129
130
131
# File 'lib/data_shifter/shift.rb', line 128

def throttle(interval, per: 1)
  self._throttle_interval = interval
  self._throttle_per = per
end

.transaction(mode) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/data_shifter/shift.rb', line 102

def transaction(mode)
  case mode
  when :per_record
    self._transaction_mode = :per_record
  when :none, false
    self._transaction_mode = :none
  when :single, true
    self._transaction_mode = :single
  else
    raise ArgumentError, "Invalid transaction mode: #{mode.inspect}. Expected :single, :per_record, :none, true, or false."
  end
end

Instance Method Details

#callObject

--- Public API (intentionally exposed to subclasses) ---



185
186
187
188
189
190
191
# File 'lib/data_shifter/shift.rb', line 185

def call
  if self.class._task_blocks.any?
    _run_task_blocks
  else
    _for_each_record_in(collection) { |record| process_record(record) }
  end
end

#dry_run?Boolean

Returns:

  • (Boolean)


222
# File 'lib/data_shifter/shift.rb', line 222

def dry_run? = dry_run

#find_exactly!(model, ids) ⇒ Object



193
194
195
196
197
198
199
200
201
202
# File 'lib/data_shifter/shift.rb', line 193

def find_exactly!(model, ids)
  ids = Array(ids).compact.uniq
  return model.none if ids.empty?

  records_by_id = model.where(id: ids).index_by(&:id)
  missing = ids.reject { |id| records_by_id.key?(id) }
  raise "Expected #{model.name} with ids #{ids.inspect}, but missing: #{missing.inspect}" if missing.any?

  ids.map { |id| records_by_id[id] }
end

#inline_csv(**csv_opts) ⇒ Object

Parse CSV colocated with the shift, after a __END__ marker, so small data sets can live alongside the code. Returns the data rows as an array (CSV::Row objects when headers: true, the default — so row["id"] works); extra options forward straight to CSV.parse. Typically used as the collection:

def collection = inline_csv
def process_record(row) = User.find(row["id"]).update!(...)
__END__
id,...

Lazily requires csv (a bundled gem on Ruby 3.4+); raises with a hint to add it to the Gemfile if unavailable.



216
217
218
219
220
# File 'lib/data_shifter/shift.rb', line 216

def inline_csv(**csv_opts)
  _require_csv!
  parsed = CSV.parse(_inline_data_body, headers: true, **csv_opts)
  parsed.is_a?(CSV::Table) ? parsed.each.to_a : parsed
end

#log(message) ⇒ Object



231
232
233
# File 'lib/data_shifter/shift.rb', line 231

def log(message)
  puts Internal::Colors.dim(message)
end

#skip!(reason = nil) ⇒ Object

Raises:

  • (SkipRecord)


224
225
226
227
228
229
# File 'lib/data_shifter/shift.rb', line 224

def skip!(reason = nil)
  @stats[:skipped] += 1
  key = reason.to_s.presence || "(no reason given)"
  @skip_reasons[key] += 1
  raise SkipRecord
end