active_version 1.1.0 → 1.3.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +4 -2
- data/lib/active_version/adapters/sequel/versioning.rb +2 -1
- data/lib/active_version/audits/has_audits/change_filters.rb +8 -4
- data/lib/active_version/audits/has_audits.rb +10 -12
- data/lib/active_version/query.rb +0 -8
- data/lib/active_version/revisions/has_revisions/revision_manipulation.rb +37 -11
- data/lib/active_version/revisions/has_revisions/revision_queries.rb +7 -2
- data/lib/active_version/version.rb +1 -1
- data/lib/active_version/version_registry.rb +5 -3
- data/lib/active_version.rb +28 -4
- metadata +4 -46
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f1b2d07a134e66e4e4330b9d195b32d831d3a2ce0599c324bed86157eb2e3fc
|
|
4
|
+
data.tar.gz: f612e466b59f335183016fc5d9d601005a4b66b07efa6276b54c1260bbd08736
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '068727dace361f83992e6b6e92b25e2dcb50cc5128e5cbb80114cfd4d48fa7f4f406f0fe216de0518becbe75f9186ab18637211452ce2a98b66825ea513172f7'
|
|
7
|
+
data.tar.gz: 6d09e28cbb0b651a4aaa89fb879a48e978bb9d9fcde7d3c312226ca256963ed119b9aba9e016eaddbe6a0a613d2e10140c5474f280d545f318fdb80282007930
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2026-04-15)
|
|
4
|
+
|
|
5
|
+
- Add `polyrun` `~> 1.2.0` development dependency; `bin/polyrun`, `bin/rspec`, `polyrun.yml`, and `config/polyrun_coverage.yml` for parallel RSpec and merged coverage.
|
|
6
|
+
- Add `script/create_postgres_shard_databases.sh` for CI and local Postgres shard databases; document in `POLYRUN.md`; set job-level `DATABASE_URL` in GitHub Actions test workflow.
|
|
7
|
+
- Add internal library logger.
|
|
8
|
+
- Fix audits, Sequel foreign keys, revision debounce, and versions walk edge cases.
|
|
9
|
+
- Adjust RSpec and RuboCop test configuration; update spec coverage.
|
|
10
|
+
|
|
11
|
+
## 1.2.0 (2026-03-27)
|
|
12
|
+
|
|
13
|
+
- Fixed SQL generation for audit creation to avoid duplicate `created_at` and `updated_at` columns (or other possible duplicate columns)
|
|
14
|
+
|
|
3
15
|
## 1.1.0 (2026-03-27)
|
|
4
16
|
|
|
5
17
|
- Added `audit_auditable_optional` configuration option to control polymorphic `belongs_to :auditable` optionality
|
data/README.md
CHANGED
|
@@ -447,8 +447,8 @@ The report includes per-record overhead vs ActiveRecord baseline (`p5`, `mean`,
|
|
|
447
447
|
The report also separates ActiveRecord and Sequel benchmark groups to avoid cross-ORM baseline mixing.
|
|
448
448
|
|
|
449
449
|
```bash
|
|
450
|
-
# Normal test run (benchmarks excluded)
|
|
451
|
-
|
|
450
|
+
# Normal test run (benchmarks excluded) — parallel OS processes via Polyrun (same as CI)
|
|
451
|
+
./bin/polyrun
|
|
452
452
|
|
|
453
453
|
# Explicit benchmark run
|
|
454
454
|
usr/bin/benchmark.rb
|
|
@@ -507,6 +507,8 @@ Environment knobs:
|
|
|
507
507
|
|
|
508
508
|
Bug reports and pull requests are welcome on GitHub at https://github.com/amkisko/active_version.rb.
|
|
509
509
|
|
|
510
|
+
Development dependencies include [polyrun](https://rubygems.org/gems/polyrun) **`~> 1.2.0`** (declared in `active_version.gemspec`) for **parallel RSpec** (`./bin/polyrun`), coverage, and CI report formats—see [POLYRUN.md](POLYRUN.md).
|
|
511
|
+
|
|
510
512
|
## License
|
|
511
513
|
|
|
512
514
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require "active_support/core_ext/string/inflections"
|
|
2
3
|
|
|
3
4
|
module ActiveVersion
|
|
4
5
|
module Adapters
|
|
@@ -51,7 +52,7 @@ module ActiveVersion
|
|
|
51
52
|
|
|
52
53
|
module InstanceMethods
|
|
53
54
|
def active_version_foreign_key
|
|
54
|
-
self.class.active_version_config[:foreign_key] || :"#{model_name.
|
|
55
|
+
self.class.active_version_config[:foreign_key] || :"#{model_name.singularize}_id"
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def active_version_tracked_columns
|
|
@@ -104,11 +104,15 @@ module ActiveVersion
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def prepare_sql_values(changes)
|
|
107
|
-
changes.each_with_object({}) do |(k, v),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
h = changes.each_with_object({}) do |(k, v), acc|
|
|
108
|
+
acc[k] = v.last if v.is_a?(Array)
|
|
109
|
+
acc[k] = v unless v.is_a?(Array)
|
|
110
|
+
acc[k] = JSON.generate(acc[k]) if acc[k].is_a?(Hash) || acc[k].is_a?(Array)
|
|
111
111
|
end
|
|
112
|
+
# Arel INSERT uses keys as column names; stringify so :action / mirror :attrs match
|
|
113
|
+
# string keys from column_mapper and so symbol + string for the same column cannot
|
|
114
|
+
# both appear (PG::DuplicateColumn).
|
|
115
|
+
h.transform_keys(&:to_s)
|
|
112
116
|
end
|
|
113
117
|
end
|
|
114
118
|
end
|
|
@@ -22,7 +22,13 @@ module ActiveVersion
|
|
|
22
22
|
false
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Class attributes (must be declared before the custom .audit_class reader below —
|
|
26
|
+
# otherwise class_attribute :audit_class overwrites the resolver and leaves it dead code.)
|
|
27
|
+
class_attribute :audit_associated_with, instance_writer: false
|
|
28
|
+
class_attribute :audited_options, instance_writer: false
|
|
29
|
+
class_attribute :audit_class, instance_writer: false
|
|
30
|
+
|
|
31
|
+
# Get audit class name (overrides class_attribute reader with resolution logic)
|
|
26
32
|
def self.audit_class
|
|
27
33
|
return @audit_class if @audit_class
|
|
28
34
|
|
|
@@ -53,11 +59,6 @@ module ActiveVersion
|
|
|
53
59
|
end
|
|
54
60
|
end
|
|
55
61
|
|
|
56
|
-
# Class attributes
|
|
57
|
-
class_attribute :audit_associated_with, instance_writer: false
|
|
58
|
-
class_attribute :audited_options, instance_writer: false
|
|
59
|
-
class_attribute :audit_class, instance_writer: false
|
|
60
|
-
|
|
61
62
|
# Instance attributes
|
|
62
63
|
attr_accessor :audit_version, :audit_comment, :audit_context
|
|
63
64
|
|
|
@@ -121,13 +122,10 @@ module ActiveVersion
|
|
|
121
122
|
def revisions(from_version = 1)
|
|
122
123
|
return [] unless audits.from_version(from_version).exists?
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
targeted_audits = all_audits.select do |audit|
|
|
127
|
-
audit.read_attribute(version_column).to_i >= from_version
|
|
128
|
-
end
|
|
125
|
+
prior_audits = audits.to_version(from_version - 1).ascending.to_a
|
|
126
|
+
targeted_audits = audits.from_version(from_version).ascending.to_a
|
|
129
127
|
|
|
130
|
-
previous_attributes = audit_class.reconstruct_attributes(
|
|
128
|
+
previous_attributes = audit_class.reconstruct_attributes(prior_audits)
|
|
131
129
|
|
|
132
130
|
targeted_audits.map do |audit|
|
|
133
131
|
previous_attributes.merge!(audit.new_attributes)
|
data/lib/active_version/query.rb
CHANGED
|
@@ -92,14 +92,6 @@ module ActiveVersion
|
|
|
92
92
|
version_column = ActiveVersion.column_mapper.column_for(record.class, :revisions, :version)
|
|
93
93
|
query.order(version_column => :asc)
|
|
94
94
|
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def log_debug(message)
|
|
99
|
-
if defined?(Rails) && Rails.respond_to?(:logger)
|
|
100
|
-
Rails.logger&.debug("[ActiveVersion::Query] #{message}")
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
95
|
end
|
|
104
96
|
end
|
|
105
97
|
end
|
|
@@ -17,10 +17,13 @@ module ActiveVersion
|
|
|
17
17
|
|
|
18
18
|
# Check debounce time - merge with previous revision if within window
|
|
19
19
|
debounce_time = opts[:debounce_time] || ActiveVersion.config.debounce_time
|
|
20
|
-
if !batch_capture_active && debounce_time
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
if !batch_capture_active && debounce_time
|
|
21
|
+
last_for_merge = last_revision_for_debounce_merge(debounce_time, timestamp)
|
|
22
|
+
if last_for_merge
|
|
23
|
+
merge_with_previous_revision!(timestamp, only_attrs, except_attrs, use_old_values, last_revision: last_for_merge)
|
|
24
|
+
version_column = revision_version_column
|
|
25
|
+
return revisions_scope.order(version_column => :desc).first
|
|
26
|
+
end
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
new_version = if batch_capture_active
|
|
@@ -113,7 +116,7 @@ module ActiveVersion
|
|
|
113
116
|
|
|
114
117
|
# Use create! instead of build + save to get better error messages
|
|
115
118
|
begin
|
|
116
|
-
revision =
|
|
119
|
+
revision = create_revision_record_with_version_retry!(revision_attrs, version_column_sym)
|
|
117
120
|
rescue ActiveRecord::RecordInvalid => e
|
|
118
121
|
ActiveVersion::Instrumentation.instrument_revision_write_failed(self, error: e)
|
|
119
122
|
error_msg = "Failed to create revision: #{e.class}"
|
|
@@ -251,7 +254,7 @@ module ActiveVersion
|
|
|
251
254
|
end
|
|
252
255
|
|
|
253
256
|
# Create the revision using the association (it will set foreign_key automatically)
|
|
254
|
-
revision =
|
|
257
|
+
revision = create_revision_record_with_version_retry!(revision_attrs, version_column_sym)
|
|
255
258
|
# Ensure it was created and persisted
|
|
256
259
|
raise "Failed to create revision" unless revision.persisted?
|
|
257
260
|
# Reload the post to clear all caches and see the new revision
|
|
@@ -350,19 +353,26 @@ module ActiveVersion
|
|
|
350
353
|
end
|
|
351
354
|
|
|
352
355
|
def should_merge_with_previous?(debounce_time, timestamp)
|
|
353
|
-
|
|
356
|
+
!last_revision_for_debounce_merge(debounce_time, timestamp).nil?
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Latest revision row if +timestamp+ falls within +debounce_time+ seconds after that revision; one query.
|
|
360
|
+
def last_revision_for_debounce_merge(debounce_time, timestamp)
|
|
361
|
+
return nil unless debounce_time
|
|
354
362
|
|
|
355
363
|
version_column = ActiveVersion.column_mapper.column_for(self.class, :revisions, :version)
|
|
356
364
|
last_revision = revisions_scope.order(version_column => :desc).first
|
|
357
|
-
return
|
|
365
|
+
return nil unless last_revision
|
|
358
366
|
|
|
359
367
|
time_diff = timestamp.to_f - last_revision.created_at.to_f
|
|
360
|
-
time_diff
|
|
368
|
+
return nil if time_diff > debounce_time
|
|
369
|
+
|
|
370
|
+
last_revision
|
|
361
371
|
end
|
|
362
372
|
|
|
363
|
-
def merge_with_previous_revision!(timestamp, only_attrs, except_attrs, use_old_values)
|
|
373
|
+
def merge_with_previous_revision!(timestamp, only_attrs, except_attrs, use_old_values, last_revision: nil)
|
|
364
374
|
version_column = ActiveVersion.column_mapper.column_for(self.class, :revisions, :version)
|
|
365
|
-
last_revision
|
|
375
|
+
last_revision ||= revisions_scope.order(version_column => :desc).first
|
|
366
376
|
return unless last_revision
|
|
367
377
|
|
|
368
378
|
# Update last revision with current or persisted attributes (filtered by only/except)
|
|
@@ -416,6 +426,22 @@ module ActiveVersion
|
|
|
416
426
|
revisions.reset
|
|
417
427
|
end
|
|
418
428
|
|
|
429
|
+
# One retry on unique (version) violation, matching audit_writer behavior when a DB unique index exists.
|
|
430
|
+
def create_revision_record_with_version_retry!(revision_attrs, version_column_sym)
|
|
431
|
+
attempt = 0
|
|
432
|
+
begin
|
|
433
|
+
attempt += 1
|
|
434
|
+
revisions.create!(revision_attrs)
|
|
435
|
+
rescue ActiveRecord::RecordNotUnique => e
|
|
436
|
+
raise e if attempt >= 2
|
|
437
|
+
|
|
438
|
+
max_version = revisions_scope.maximum(version_column_sym) || 0
|
|
439
|
+
revision_attrs[version_column_sym] = max_version + 1
|
|
440
|
+
retry
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
private :create_revision_record_with_version_retry!
|
|
444
|
+
|
|
419
445
|
def refreshable_column_names
|
|
420
446
|
@refreshable_column_names ||=
|
|
421
447
|
self.class.columns
|
|
@@ -91,7 +91,9 @@ module ActiveVersion
|
|
|
91
91
|
# Enumerate all versions (lazy enumerator returning revision instances)
|
|
92
92
|
def versions(reverse: false, include_self: false)
|
|
93
93
|
version_column = revision_version_column
|
|
94
|
-
|
|
94
|
+
ordered_rows = revisions_scope.order(version_column => (reverse ? :desc : :asc)).to_a
|
|
95
|
+
entries_by_version = ordered_rows.index_by { |row| row.public_send(version_column) }
|
|
96
|
+
version_list = ordered_rows.map { |row| row.public_send(version_column) }
|
|
95
97
|
|
|
96
98
|
# If include_self, prepare current state as a revision instance
|
|
97
99
|
current_self = nil
|
|
@@ -104,7 +106,10 @@ module ActiveVersion
|
|
|
104
106
|
|
|
105
107
|
Enumerator.new do |yielder|
|
|
106
108
|
version_list.each do |v|
|
|
107
|
-
|
|
109
|
+
revision_entry = entries_by_version[v]
|
|
110
|
+
next unless revision_entry
|
|
111
|
+
|
|
112
|
+
revision = build_revision_dup(revision_entry)
|
|
108
113
|
yielder << revision if revision
|
|
109
114
|
end
|
|
110
115
|
if include_self
|
|
@@ -17,9 +17,11 @@ module ActiveVersion
|
|
|
17
17
|
|
|
18
18
|
# Detect option conflicts
|
|
19
19
|
if existing[:options] != options
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
ActiveVersion.logger&.warn(
|
|
21
|
+
"[ActiveVersion] Re-registering #{model_class.name} with :#{version_type} " \
|
|
22
|
+
"with different options. Previous: #{existing[:options].inspect}, " \
|
|
23
|
+
"New: #{options.inspect}. This may indicate a configuration issue."
|
|
24
|
+
)
|
|
23
25
|
else
|
|
24
26
|
# Same options - likely a double include, but not necessarily a problem
|
|
25
27
|
# Log at debug level if needed
|
data/lib/active_version.rb
CHANGED
|
@@ -163,7 +163,7 @@ module ActiveVersion
|
|
|
163
163
|
elsif Fiber.current.respond_to?(:[])
|
|
164
164
|
Fiber.current[key]
|
|
165
165
|
else
|
|
166
|
-
|
|
166
|
+
thread_current_for_fallback_store[key]
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
169
|
|
|
@@ -173,7 +173,7 @@ module ActiveVersion
|
|
|
173
173
|
elsif Fiber.current.respond_to?(:[]=)
|
|
174
174
|
Fiber.current[key] = value
|
|
175
175
|
else
|
|
176
|
-
|
|
176
|
+
thread_current_for_fallback_store[key] = value
|
|
177
177
|
end
|
|
178
178
|
end
|
|
179
179
|
|
|
@@ -184,13 +184,18 @@ module ActiveVersion
|
|
|
184
184
|
def self.store_keys
|
|
185
185
|
if config.execution_scope == :thread
|
|
186
186
|
Thread.current.thread_variables
|
|
187
|
-
elsif
|
|
188
|
-
|
|
187
|
+
elsif thread_current_for_fallback_store.respond_to?(:keys)
|
|
188
|
+
thread_current_for_fallback_store.keys
|
|
189
189
|
else
|
|
190
190
|
[]
|
|
191
191
|
end
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
def self.thread_current_for_fallback_store
|
|
195
|
+
Thread.current
|
|
196
|
+
end
|
|
197
|
+
private_class_method :thread_current_for_fallback_store
|
|
198
|
+
|
|
194
199
|
def self.clear_scoped_keys!(pattern)
|
|
195
200
|
store_keys.grep(pattern).each { |key| store_delete(key) }
|
|
196
201
|
end
|
|
@@ -322,6 +327,25 @@ module ActiveVersion
|
|
|
322
327
|
def self.with_connection(model_class, version_type, &block)
|
|
323
328
|
yield(Runtime.adapter.connection_for(model_class, version_type))
|
|
324
329
|
end
|
|
330
|
+
|
|
331
|
+
# Library warnings (defaults to stderr). Set to +nil+ to silence; use +Logger.new(File::NULL)+ in tests.
|
|
332
|
+
class << self
|
|
333
|
+
attr_writer :logger
|
|
334
|
+
|
|
335
|
+
def logger
|
|
336
|
+
return @logger if defined?(@logger)
|
|
337
|
+
|
|
338
|
+
@logger = default_logger
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def default_logger
|
|
342
|
+
l = Logger.new($stderr)
|
|
343
|
+
l.level = Logger::WARN
|
|
344
|
+
l.formatter = proc { |_, _, _, msg| "#{msg}\n" }
|
|
345
|
+
l
|
|
346
|
+
end
|
|
347
|
+
private :default_logger
|
|
348
|
+
end
|
|
325
349
|
end
|
|
326
350
|
|
|
327
351
|
# Load Rails integration if available
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_version
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrei Makarov
|
|
@@ -136,61 +136,19 @@ dependencies:
|
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
137
|
version: '0.4'
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
|
-
name:
|
|
139
|
+
name: polyrun
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
141
141
|
requirements:
|
|
142
142
|
- - "~>"
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version:
|
|
144
|
+
version: 1.2.0
|
|
145
145
|
type: :development
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
149
|
- - "~>"
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version:
|
|
152
|
-
- !ruby/object:Gem::Dependency
|
|
153
|
-
name: simplecov-console
|
|
154
|
-
requirement: !ruby/object:Gem::Requirement
|
|
155
|
-
requirements:
|
|
156
|
-
- - "~>"
|
|
157
|
-
- !ruby/object:Gem::Version
|
|
158
|
-
version: '0.9'
|
|
159
|
-
type: :development
|
|
160
|
-
prerelease: false
|
|
161
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
-
requirements:
|
|
163
|
-
- - "~>"
|
|
164
|
-
- !ruby/object:Gem::Version
|
|
165
|
-
version: '0.9'
|
|
166
|
-
- !ruby/object:Gem::Dependency
|
|
167
|
-
name: rspec_junit_formatter
|
|
168
|
-
requirement: !ruby/object:Gem::Requirement
|
|
169
|
-
requirements:
|
|
170
|
-
- - "~>"
|
|
171
|
-
- !ruby/object:Gem::Version
|
|
172
|
-
version: '0.6'
|
|
173
|
-
type: :development
|
|
174
|
-
prerelease: false
|
|
175
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
-
requirements:
|
|
177
|
-
- - "~>"
|
|
178
|
-
- !ruby/object:Gem::Version
|
|
179
|
-
version: '0.6'
|
|
180
|
-
- !ruby/object:Gem::Dependency
|
|
181
|
-
name: simplecov-cobertura
|
|
182
|
-
requirement: !ruby/object:Gem::Requirement
|
|
183
|
-
requirements:
|
|
184
|
-
- - "~>"
|
|
185
|
-
- !ruby/object:Gem::Version
|
|
186
|
-
version: '3'
|
|
187
|
-
type: :development
|
|
188
|
-
prerelease: false
|
|
189
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
190
|
-
requirements:
|
|
191
|
-
- - "~>"
|
|
192
|
-
- !ruby/object:Gem::Version
|
|
193
|
-
version: '3'
|
|
151
|
+
version: 1.2.0
|
|
194
152
|
- !ruby/object:Gem::Dependency
|
|
195
153
|
name: standard
|
|
196
154
|
requirement: !ruby/object:Gem::Requirement
|