snaptime 0.0.1
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 +7 -0
- data/.gitignore +50 -0
- data/.releaser_config +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +118 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/lib/snaptime.rb +121 -0
- data/lib/snaptime/ar_hooks.rb +38 -0
- data/lib/snaptime/base_ar_mixin.rb +15 -0
- data/lib/snaptime/exceptions.rb +15 -0
- data/lib/snaptime/harvester.rb +195 -0
- data/lib/snaptime/migration_helpers.rb +54 -0
- data/lib/snaptime/railtie.rb +23 -0
- data/lib/snaptime/record_cloner.rb +72 -0
- data/lib/snaptime/relations.rb +26 -0
- data/lib/snaptime/relations_builder.rb +32 -0
- data/lib/snaptime/versioned.rb +49 -0
- data/lib/snaptime/versioned/scopes.rb +64 -0
- data/lib/snaptime/virtual_models/snaptime.rb +52 -0
- data/snaptime.gemspec +51 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 105979aad445d0e6f274857977310cd72e6b43c0
|
4
|
+
data.tar.gz: 8230ef5d4b9f5454000d5f9297ea9243fc3e0b09
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c8f2cd32556bc1cabf50bdde6b47fc26112864e01e6f12a0a6d0d86e3e8cb73c5597c4be5ad92c21957ce19267df89e57c0d8f856757012a5cc7d353a1732dd
|
7
|
+
data.tar.gz: 15ebe2cee56f8c14281124e00a2bb0c712083f9fa46e1c44bf48909e3661e2c7c92f55d8422f0ca20c0985c6f174b48b847e27a7b02f009f71f75b03087e1a27
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/.releaser_config
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
snaptime (0.0.1)
|
5
|
+
activerecord
|
6
|
+
activesupport
|
7
|
+
request_store
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (5.2.3)
|
13
|
+
activesupport (= 5.2.3)
|
14
|
+
activerecord (5.2.3)
|
15
|
+
activemodel (= 5.2.3)
|
16
|
+
activesupport (= 5.2.3)
|
17
|
+
arel (>= 9.0)
|
18
|
+
activesupport (5.2.3)
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
20
|
+
i18n (>= 0.7, < 2)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
arel (9.0.0)
|
24
|
+
ast (2.4.0)
|
25
|
+
benchmark-ips (2.7.2)
|
26
|
+
concurrent-ruby (1.1.5)
|
27
|
+
i18n (1.6.0)
|
28
|
+
concurrent-ruby (~> 1.0)
|
29
|
+
minitest (5.11.3)
|
30
|
+
mysql2 (0.5.2)
|
31
|
+
parallel (1.17.0)
|
32
|
+
parser (2.6.2.0)
|
33
|
+
ast (~> 2.4.0)
|
34
|
+
powerpack (0.1.2)
|
35
|
+
rack (2.0.7)
|
36
|
+
rainbow (2.2.2)
|
37
|
+
rake
|
38
|
+
rake (12.3.2)
|
39
|
+
request_store (1.4.1)
|
40
|
+
rack (>= 1.4)
|
41
|
+
rubocop (0.51.0)
|
42
|
+
parallel (~> 1.10)
|
43
|
+
parser (>= 2.3.3.1, < 3.0)
|
44
|
+
powerpack (~> 0.1)
|
45
|
+
rainbow (>= 2.2.2, < 3.0)
|
46
|
+
ruby-progressbar (~> 1.7)
|
47
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
48
|
+
ruby-progressbar (1.10.0)
|
49
|
+
thread_safe (0.3.6)
|
50
|
+
tzinfo (1.2.5)
|
51
|
+
thread_safe (~> 0.1)
|
52
|
+
unicode-display_width (1.5.0)
|
53
|
+
|
54
|
+
PLATFORMS
|
55
|
+
ruby
|
56
|
+
|
57
|
+
DEPENDENCIES
|
58
|
+
benchmark-ips
|
59
|
+
bundler (~> 2.0)
|
60
|
+
minitest
|
61
|
+
mysql2
|
62
|
+
rake
|
63
|
+
rubocop (= 0.51.0)
|
64
|
+
snaptime!
|
65
|
+
|
66
|
+
BUNDLED WITH
|
67
|
+
2.0.1
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Sitrox
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# snaptime
|
2
|
+
|
3
|
+
**This Gem is still in an early stage of development. Please not use this in production yet.**
|
4
|
+
|
5
|
+
Snaptime lets you versionize Active Record models using single table versioning.
|
6
|
+
Records are identified by a `natural_id` and their validity is controlled via
|
7
|
+
`valid_from` and `valid_to`. Snaptime also supports associations between
|
8
|
+
versioned models as well as associations to and from unversioned models.
|
9
|
+
|
10
|
+
## Reasons for snaptime
|
11
|
+
|
12
|
+
* Most of Snaptime's operation is transparent, so you won't even notice that
|
13
|
+
a certain model is versioned if you don't explicitely want to.
|
14
|
+
|
15
|
+
* Associations between, to and from versioned models are automatically handeled.
|
16
|
+
This lets Snaptime version entire, complex hierarchies.
|
17
|
+
|
18
|
+
* It is very lightweight.
|
19
|
+
|
20
|
+
* It is non-intrusive. It does not overwrite a single Rails method without
|
21
|
+
you explicitly telling it to do so.
|
22
|
+
|
23
|
+
* It can be easily extended to support other database adapters.
|
24
|
+
|
25
|
+
## Basic concept
|
26
|
+
|
27
|
+
Snaptime can be enabled on a per-model basis. Using dedicated migration methods,
|
28
|
+
each versioned table is complemented with the following fields:
|
29
|
+
|
30
|
+
- `natural_id`
|
31
|
+
|
32
|
+
This is the ID that does not change between versions. This is also the ID
|
33
|
+
that is referenced when pointing to a versioned model.
|
34
|
+
|
35
|
+
- `valid_from`, `valid_to`
|
36
|
+
|
37
|
+
These are UTC timestamps in milliseconds that specify a specific version's
|
38
|
+
validity. The field `valid_from` always specifies the point in time at which
|
39
|
+
a certain version has been created, while `valid_to` says when the version
|
40
|
+
got outdated. This can happen by deleting the record (which does not actually
|
41
|
+
delete it but just sets `valid_to`) or when a new version arises. There can't
|
42
|
+
be any gaps between the validity fields of a version string.
|
43
|
+
|
44
|
+
Snaptime works by hooking into your versioned models at *creation*, *update* and
|
45
|
+
*deletion*:
|
46
|
+
|
47
|
+
- At creation, it automatically generates a new `natural_id` and sets
|
48
|
+
`valid_from` to the current time.
|
49
|
+
|
50
|
+
- At update, `valid_from` is again set to the current time and the record is
|
51
|
+
updated as usual. But before the record gets updated, Snaptime creates a copy
|
52
|
+
of the record's current state and sets `valid_from` and `valid_to`
|
53
|
+
accordingly. The copy is created in-db (using `insert ... select`) and does
|
54
|
+
not call any application side logic.
|
55
|
+
|
56
|
+
- At deletion, all it does is setting `valid_to` of the current record to the
|
57
|
+
current time again.
|
58
|
+
|
59
|
+
What this means is that the original record always stays the newest one. This
|
60
|
+
has many advantages, as the record itself can be updated as usual and if another
|
61
|
+
model would ever point at the `id` instead the `natural_id`, it would always
|
62
|
+
point to the newest one.
|
63
|
+
|
64
|
+
## Basic setup
|
65
|
+
|
66
|
+
### Gemfile
|
67
|
+
|
68
|
+
Add the following to your application's Gemfile:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
gem :snaptime
|
72
|
+
```
|
73
|
+
|
74
|
+
You can also specify a fixed version:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
gem :snaptime, '~> 1.0.0'
|
78
|
+
```
|
79
|
+
|
80
|
+
### Setup task
|
81
|
+
|
82
|
+
Snaptime needs to generate a `natural_id` next to your `id`. In some adapters,
|
83
|
+
this can be done using database sequences, while other databases require a
|
84
|
+
custom setup (i.e. MySQL let's you workaround this by adding a dedicated
|
85
|
+
sequence table as well as a custom procedure).
|
86
|
+
|
87
|
+
Using the following rake task, Snaptime automatically generates the required
|
88
|
+
migrations. Note that this migration only performs a basic setup and does not
|
89
|
+
enable any of your models to be versioned. This happens using separate
|
90
|
+
migrations as described in the following chapters.
|
91
|
+
|
92
|
+
```bash
|
93
|
+
rake snaptime:setup -- <adapter-name>
|
94
|
+
```
|
95
|
+
|
96
|
+
Replace `<adapter-name>` with the name of your database adapter. Spell it
|
97
|
+
exactly as the `adapter` setting of your database configuration reads. If the
|
98
|
+
specific adapter does not require any setup, you will get a respective message.
|
99
|
+
|
100
|
+
## Versionizing a model
|
101
|
+
|
102
|
+
### Performing the database migrations
|
103
|
+
|
104
|
+
TODO
|
105
|
+
|
106
|
+
### Include the versionize module
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class YourModel < ActiveRecord::Base
|
110
|
+
include Snaptime::Versioned
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
## Caveats
|
115
|
+
|
116
|
+
- Do not use `def self.default_scope` but `default_scope do` in versioned models
|
117
|
+
if you need to extend the default scope.
|
118
|
+
- `.unscoped` also removes the default scope added by snaptime.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
task :gemspec do
|
2
|
+
gemspec = Gem::Specification.new do |spec|
|
3
|
+
spec.name = 'snaptime'
|
4
|
+
spec.version = IO.read('VERSION').chomp
|
5
|
+
spec.authors = ['Sitrox']
|
6
|
+
spec.summary = %(
|
7
|
+
Multi-threaded job backend with database queuing for ruby.
|
8
|
+
)
|
9
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
10
|
+
spec.executables = []
|
11
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
12
|
+
spec.require_paths = ['lib']
|
13
|
+
|
14
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
15
|
+
spec.add_development_dependency 'rake'
|
16
|
+
spec.add_development_dependency 'rubocop', '0.51.0'
|
17
|
+
spec.add_development_dependency 'minitest'
|
18
|
+
spec.add_development_dependency 'mysql2'
|
19
|
+
spec.add_development_dependency 'benchmark-ips'
|
20
|
+
spec.add_dependency 'activesupport'
|
21
|
+
spec.add_dependency 'activerecord'
|
22
|
+
spec.add_dependency 'request_store'
|
23
|
+
end
|
24
|
+
|
25
|
+
File.open('snaptime.gemspec', 'w') { |f| f.write(gemspec.to_ruby.strip) }
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new do |t|
|
31
|
+
t.pattern = 'test/snaptime/**/*_test.rb'
|
32
|
+
t.verbose = false
|
33
|
+
t.libs << 'test/lib'
|
34
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/snaptime.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'snaptime/migration_helpers'
|
2
|
+
require 'snaptime/record_cloner'
|
3
|
+
require 'snaptime/harvester'
|
4
|
+
require 'snaptime/versioned/scopes'
|
5
|
+
require 'snaptime/exceptions'
|
6
|
+
require 'snaptime/relations_builder'
|
7
|
+
require 'snaptime/railtie'
|
8
|
+
require 'snaptime/ar_hooks'
|
9
|
+
require 'snaptime/base_ar_mixin'
|
10
|
+
require 'snaptime/relations'
|
11
|
+
require 'snaptime/virtual_models/snaptime'
|
12
|
+
require 'snaptime/versioned'
|
13
|
+
|
14
|
+
module Snptime
|
15
|
+
SNAPTIME_REQUEST_STORE_KEY = :snaptime_snaptime
|
16
|
+
CURRENT_NOW_REQUEST_STORE_KEY = :snaptime_current_now
|
17
|
+
CLONED_RECORDS_STORE_KEY = :snaptime_cloned_records
|
18
|
+
RECORD_CLONING_SWITCH_REQUEST_STORE_KEY = :snaptime_record_cloning
|
19
|
+
SMALLEST_TIME_UNIT = 0.001
|
20
|
+
|
21
|
+
@consolidation_fields = ActiveSupport::OrderedHash.new
|
22
|
+
|
23
|
+
def self.register_consolidation_field(name, aggregate_with: nil, default: Arel.sql('null'))
|
24
|
+
@consolidation_fields[name] = { aggregate_with: aggregate_with, default: default }
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.consolidation_fields
|
28
|
+
@consolidation_fields
|
29
|
+
end
|
30
|
+
|
31
|
+
@model_class = Snaptime::VirtualModels::Snaptime
|
32
|
+
|
33
|
+
def self.model_class=(model_class)
|
34
|
+
@model_class = model_class
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.model_class
|
38
|
+
@model_class
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.with_snaptime(snaptime, &_block)
|
42
|
+
previous_snaptime = RequestStore.store[SNAPTIME_REQUEST_STORE_KEY]
|
43
|
+
|
44
|
+
RequestStore.store[SNAPTIME_REQUEST_STORE_KEY] = snaptime
|
45
|
+
|
46
|
+
begin
|
47
|
+
yield
|
48
|
+
ensure
|
49
|
+
RequestStore.store[SNAPTIME_REQUEST_STORE_KEY] = previous_snaptime
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.without_record_cloning(&_block)
|
54
|
+
previous_setting = RequestStore.store[RECORD_CLONING_SWITCH_REQUEST_STORE_KEY]
|
55
|
+
|
56
|
+
RequestStore.store[RECORD_CLONING_SWITCH_REQUEST_STORE_KEY] = false
|
57
|
+
|
58
|
+
begin
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
RequestStore.store[RECORD_CLONING_SWITCH_REQUEST_STORE_KEY] = previous_setting
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.record_cloning_enabled?
|
66
|
+
RequestStore.store[RECORD_CLONING_SWITCH_REQUEST_STORE_KEY] != false
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.snaptime
|
70
|
+
RequestStore.store[SNAPTIME_REQUEST_STORE_KEY]
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.current_now
|
74
|
+
RequestStore.store[CURRENT_NOW_REQUEST_STORE_KEY] ||= Time.now.utc
|
75
|
+
end
|
76
|
+
|
77
|
+
# Override the "current now" used for creating new versions. Only use this
|
78
|
+
# method for testing purposes and make sure you use `reset_current_now` if
|
79
|
+
# necessary. Use `with_fake_current_now` whenever possible.
|
80
|
+
def self.fake_current_now(time)
|
81
|
+
RequestStore.store[CURRENT_NOW_REQUEST_STORE_KEY] ||= time.utc
|
82
|
+
end
|
83
|
+
|
84
|
+
# Override the "current now" used for creating new versions. Only use this
|
85
|
+
# method for testing purposes.
|
86
|
+
def self.with_fake_current_now(time, &_block)
|
87
|
+
fake_current_now time
|
88
|
+
|
89
|
+
begin
|
90
|
+
yield
|
91
|
+
ensure
|
92
|
+
reset_current_now
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.reset_current_now
|
97
|
+
RequestStore.store[CURRENT_NOW_REQUEST_STORE_KEY] = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.record_cloned_in_current_tx(record)
|
101
|
+
RequestStore.store[CLONED_RECORDS_STORE_KEY] ||= {}
|
102
|
+
RequestStore.store[CLONED_RECORDS_STORE_KEY][record.class.table_name] ||= {}
|
103
|
+
RequestStore.store[CLONED_RECORDS_STORE_KEY][record.class.table_name][record.send(record.class.primary_key)] = true
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.record_cloned_in_current_tx?(record)
|
107
|
+
RequestStore.store
|
108
|
+
.try(:[], CLONED_RECORDS_STORE_KEY)
|
109
|
+
.try(:[], record.class.table_name)
|
110
|
+
.try(:[], record.send(record.class.primary_key)) || false
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.reset_records_cloned_in_current_tx
|
114
|
+
RequestStore.store[CLONED_RECORDS_STORE_KEY] = nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.after_commit_or_rollback
|
118
|
+
reset_current_now
|
119
|
+
reset_records_cloned_in_current_tx
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module ArHooks
|
3
|
+
def self.before_create(record)
|
4
|
+
if record.natural_id.nil?
|
5
|
+
record.valid_from = Snaptime.current_now
|
6
|
+
ActiveRecord::Base.uncached do
|
7
|
+
record.natural_id = record.class.connection.next_sequence_value(record.class.sequence_name)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.after_create(record)
|
13
|
+
Snaptime.record_cloned_in_current_tx(record)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.before_update(record)
|
17
|
+
return unless Snaptime.record_cloning_enabled?
|
18
|
+
|
19
|
+
if record.valid_to.nil? && record.changed? && !Snaptime.record_cloned_in_current_tx?(record)
|
20
|
+
record.valid_from = Snaptime.current_now
|
21
|
+
|
22
|
+
Snaptime::RecordCloner.clone_record!(
|
23
|
+
record,
|
24
|
+
override_attributes: { valid_to: record.valid_from - SMALLEST_TIME_UNIT },
|
25
|
+
return_record: false
|
26
|
+
)
|
27
|
+
|
28
|
+
Snaptime.record_cloned_in_current_tx(record)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.destroy(record)
|
33
|
+
record.deleted = true
|
34
|
+
record.version_is_minor = true if record.respond_to?(:version_is_minor=)
|
35
|
+
record.save!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module Exceptions
|
3
|
+
class DeleteMethodsAreNotAvailable < StandardError
|
4
|
+
def initialize
|
5
|
+
super('Versionized records only support the `destroy` methods.')
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class AssociationTargetNotVersioned < StandardError
|
10
|
+
def initialize(target_class)
|
11
|
+
super("Association target #{target_class.inspect} does not appear to be versioned.")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module Harvester
|
3
|
+
def self.harvest_for(record)
|
4
|
+
# ---------------------------------------------------------------
|
5
|
+
# Build individual selects for each combination of table,
|
6
|
+
# key fields and values.
|
7
|
+
# ---------------------------------------------------------------
|
8
|
+
selects = []
|
9
|
+
|
10
|
+
queries = snaptime_queries(record.class, [record.natural_id], nil)
|
11
|
+
|
12
|
+
queries.each do |klass, keys_and_values|
|
13
|
+
keys_and_values.each do |key, values|
|
14
|
+
selects << select_for(klass, key, values)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# ---------------------------------------------------------------
|
19
|
+
# Build master select that unions all of the above queries
|
20
|
+
# TODO: Probably add a separate option for coalesce to avoid
|
21
|
+
# doubling the code for max and min.
|
22
|
+
# ---------------------------------------------------------------
|
23
|
+
rel = Snaptime.model_class
|
24
|
+
rel = rel.from(
|
25
|
+
"#{union_selects(*selects)} inner_snaptimes"
|
26
|
+
)
|
27
|
+
|
28
|
+
# TODO: Move to DB-specific adapter
|
29
|
+
rel = rel.select(Arel.sql("LISTAGG(record_lookups, ';') WITHIN GROUP(ORDER BY record_lookups)").as('record_lookups'))
|
30
|
+
|
31
|
+
Snaptime.consolidation_fields.each do |field, options|
|
32
|
+
if options[:aggregate_with].nil?
|
33
|
+
rel = rel.select(field)
|
34
|
+
elsif options[:aggregate_with] == :max
|
35
|
+
rel = rel.select(Arel.sql(field.to_s).maximum.as(field.to_s))
|
36
|
+
elsif options[:aggregate_with] == :max_coalesce_0
|
37
|
+
rel = rel.select(
|
38
|
+
Arel.sql(
|
39
|
+
Arel::Nodes::NamedFunction.new('coalesce', [Arel.sql(field.to_s), Arel.sql(0.to_s)]).to_sql
|
40
|
+
).maximum.as(field.to_s)
|
41
|
+
)
|
42
|
+
elsif options[:aggregate_with] == :min
|
43
|
+
rel = rel.select(Arel.sql(field.to_s).minimum.as(field.to_s))
|
44
|
+
elsif options[:aggregate_with] == :min_coalesce_0
|
45
|
+
rel = rel.select(
|
46
|
+
Arel.sql(
|
47
|
+
Arel::Nodes::NamedFunction.new('coalesce', [Arel.sql(field.to_s), Arel.sql(0.to_s)]).to_sql
|
48
|
+
).minimum.as(field.to_s)
|
49
|
+
)
|
50
|
+
elsif options[:aggregate_with] == :sum
|
51
|
+
rel = rel.select(Arel.sql(field.to_s).sum.as(field.to_s))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
grouping_fields = Snaptime.consolidation_fields.select { |_k, v| v[:aggregate_with].nil? }.keys.collect(&:to_s)
|
56
|
+
# rel = rel.order('inner_snaptimes.valid_from desc')
|
57
|
+
rel = rel.order(Arel::Table.new(:inner_snaptimes)[:valid_from].desc)
|
58
|
+
rel = rel.group(*grouping_fields)
|
59
|
+
|
60
|
+
# ---------------------------------------------------------------
|
61
|
+
# Wrap master select in another select so that outer orders,
|
62
|
+
# wheres and counts work out-of-the-box.
|
63
|
+
# ---------------------------------------------------------------
|
64
|
+
all_keys = [:record_lookups] + Snaptime.consolidation_fields.keys
|
65
|
+
|
66
|
+
all_fields = all_keys.collect do |key|
|
67
|
+
Arel::Table.new(:snaptimes)[key]
|
68
|
+
end
|
69
|
+
|
70
|
+
outer_rel = Snaptime.model_class.select(all_fields).from("(#{rel.to_sql}) snaptimes")
|
71
|
+
|
72
|
+
return outer_rel
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.select_for(klass, key, values)
|
76
|
+
table = klass.arel_table
|
77
|
+
|
78
|
+
select = Arel::SelectManager.new(ActiveRecord::Base)
|
79
|
+
select.from table
|
80
|
+
|
81
|
+
concat = Arel::Nodes::NamedFunction.new('concat', [
|
82
|
+
Arel.sql(klass.connection.quote("#{klass.name},")),
|
83
|
+
table[klass.primary_key]
|
84
|
+
])
|
85
|
+
select.project(
|
86
|
+
concat.as('record_lookups')
|
87
|
+
)
|
88
|
+
|
89
|
+
Snaptime.consolidation_fields.each do |field_key, options|
|
90
|
+
if klass.column_names.include?(field_key.to_s)
|
91
|
+
select.project(table[field_key.to_s])
|
92
|
+
else
|
93
|
+
unless options[:default].is_a?(Arel::Nodes::SqlLiteral)
|
94
|
+
fail 'Option :default must be an Arel::Nodes::SqlLiteral.'
|
95
|
+
end
|
96
|
+
select.project(options[:default].as(field_key.to_s))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
select.where(table[key].in(values))
|
101
|
+
|
102
|
+
return select
|
103
|
+
end
|
104
|
+
|
105
|
+
private_class_method :select_for
|
106
|
+
|
107
|
+
def self.snaptime_queries(klass, natural_ids, association, visited_natural_ids_by_assoc = {})
|
108
|
+
queries = {}
|
109
|
+
|
110
|
+
# ---------------------------------------------------------------
|
111
|
+
# Abort if all natural IDs have already been processed for the
|
112
|
+
# given association.
|
113
|
+
# ---------------------------------------------------------------
|
114
|
+
visited_natural_ids_by_assoc[association] ||= []
|
115
|
+
return {} if (natural_ids - visited_natural_ids_by_assoc[association]).empty?
|
116
|
+
visited_natural_ids_by_assoc[association] += natural_ids
|
117
|
+
|
118
|
+
my_natural_ids = natural_ids
|
119
|
+
|
120
|
+
if association.nil? || association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
121
|
+
# Either:
|
122
|
+
# I'm the root of the query, return all valid_from from my versions,
|
123
|
+
# which are identified by my natural_id.
|
124
|
+
#
|
125
|
+
# Or:
|
126
|
+
# I'm target of a belongs_to, I'm getting all natural_ids that my
|
127
|
+
# peer is pointing to with his foreign key. So I will return all
|
128
|
+
# valid_from that belong to these natural_ids.
|
129
|
+
queries[klass] ||= {}
|
130
|
+
queries[klass][:natural_id] ||= []
|
131
|
+
queries[klass][:natural_id] += natural_ids
|
132
|
+
elsif association.is_a?(ActiveRecord::Reflection::HasOneReflection) || association.is_a?(ActiveRecord::Reflection::HasManyReflection)
|
133
|
+
# I'm target of a has_one / has_many, I'm getting my peer's
|
134
|
+
# natural_id and have to return all valid_from of records that are
|
135
|
+
# pointing to it.
|
136
|
+
queries[klass] ||= {}
|
137
|
+
queries[klass][association.foreign_key] ||= []
|
138
|
+
queries[klass][association.foreign_key] += natural_ids
|
139
|
+
|
140
|
+
# My own natural_ids for going further are the one's of the records that
|
141
|
+
# are pointing to my peer.
|
142
|
+
my_natural_ids = klass.unscoped.select(:natural_id).where(association.foreign_key => natural_ids).collect(&:natural_id)
|
143
|
+
end
|
144
|
+
|
145
|
+
klass.versioned_associations.each do |_name, nested_association|
|
146
|
+
if nested_association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
147
|
+
# I have an association that I am pointing to. I'll have to supply
|
148
|
+
# them with all the natural_ids I'm pointing to.
|
149
|
+
|
150
|
+
table = Arel::Table.new(klass.table_name)
|
151
|
+
|
152
|
+
natural_fks = klass
|
153
|
+
.unscoped
|
154
|
+
.select(nested_association.foreign_key)
|
155
|
+
.where(natural_id: my_natural_ids)
|
156
|
+
.where(table[nested_association.foreign_key].not_eq(nil))
|
157
|
+
.collect(&nested_association.foreign_key.to_sym)
|
158
|
+
|
159
|
+
queries = merge_queries(
|
160
|
+
queries,
|
161
|
+
snaptime_queries(nested_association.klass, natural_fks, nested_association, visited_natural_ids_by_assoc)
|
162
|
+
)
|
163
|
+
elsif nested_association.is_a?(ActiveRecord::Reflection::HasOneReflection) || nested_association.is_a?(ActiveRecord::Reflection::HasManyReflection)
|
164
|
+
# I have an association that points to me. I'll have to supply my
|
165
|
+
# natural_ids of interest.
|
166
|
+
queries = merge_queries(
|
167
|
+
queries,
|
168
|
+
snaptime_queries(nested_association.klass, my_natural_ids, nested_association, visited_natural_ids_by_assoc)
|
169
|
+
)
|
170
|
+
else
|
171
|
+
fail "Unsupported relation type #{nested_association.class}."
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
return queries
|
176
|
+
end
|
177
|
+
|
178
|
+
private_class_method :snaptime_queries
|
179
|
+
|
180
|
+
def self.merge_queries(a, b)
|
181
|
+
a.deep_merge b do |_key, val_a, val_b|
|
182
|
+
(val_a + val_b).uniq
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
private_class_method :merge_queries
|
187
|
+
|
188
|
+
def self.union_selects(*selects)
|
189
|
+
stmt = selects.collect(&:to_sql).collect { |sql| "(#{sql})" }.join("\nUNION\n")
|
190
|
+
return "(#{stmt})"
|
191
|
+
end
|
192
|
+
|
193
|
+
private_class_method :union_selects
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module MigrationHelpers
|
3
|
+
def self.load
|
4
|
+
ActiveRecord::ConnectionAdapters::Table.class_eval do
|
5
|
+
include SchemaStatements::Table
|
6
|
+
end
|
7
|
+
|
8
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
|
9
|
+
include SchemaStatements::TopLevel
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module SchemaStatements
|
14
|
+
module Table
|
15
|
+
def versionize
|
16
|
+
column :natural_id, :integer
|
17
|
+
column :valid_from, :timestamp, precision: 3
|
18
|
+
column :valid_to, :timestamp, precision: 3
|
19
|
+
column :deleted, :boolean, null: false, default: 0
|
20
|
+
|
21
|
+
index :natural_id
|
22
|
+
index :valid_from
|
23
|
+
index :valid_to
|
24
|
+
|
25
|
+
index %i(natural_id valid_to), unique: true
|
26
|
+
|
27
|
+
@base.execute %(
|
28
|
+
ALTER TABLE "#{name.to_s.upcase}"
|
29
|
+
ADD CONSTRAINT "#{name.to_s.upcase}_VCVD" CHECK (
|
30
|
+
VALID_TO IS NULL OR VALID_FROM <= VALID_TO
|
31
|
+
)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def unversionize
|
36
|
+
remove :natural_id
|
37
|
+
remove :valid_from
|
38
|
+
remove :valid_to
|
39
|
+
remove :deleted
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module TopLevel
|
44
|
+
def versionize_table(table_name)
|
45
|
+
change_table table_name, &:versionize
|
46
|
+
end
|
47
|
+
|
48
|
+
def unversionize_table(table_name)
|
49
|
+
change_table table_name, &:unversionize
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Snaptime
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
railtie_name :snaptime
|
4
|
+
|
5
|
+
initializer :snaptime do
|
6
|
+
ActiveSupport.on_load :active_record do
|
7
|
+
Snaptime::MigrationHelpers.load
|
8
|
+
end
|
9
|
+
|
10
|
+
ActiveRecord::Base.send :include, Snaptime::BaseArMixin
|
11
|
+
|
12
|
+
ActiveRecord::Base.send :after_commit do
|
13
|
+
Snaptime.after_commit_or_rollback
|
14
|
+
end
|
15
|
+
|
16
|
+
ActiveRecord::Base.send :after_rollback do
|
17
|
+
Snaptime.after_commit_or_rollback
|
18
|
+
end
|
19
|
+
|
20
|
+
Snaptime.register_consolidation_field(:valid_from)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Snaptime
|
2
|
+
class RecordCloner
|
3
|
+
def self.clone_record!(record, override_attributes: {}, return_record: true)
|
4
|
+
override_attribute_keys = override_attributes.keys
|
5
|
+
|
6
|
+
table = record.class.arel_table
|
7
|
+
|
8
|
+
cloned_column_names = record.class.column_names - [record.class.primary_key] - override_attribute_keys.collect(&:to_s)
|
9
|
+
|
10
|
+
# ---------------------------------------------------------------
|
11
|
+
# Prepare select statement
|
12
|
+
# ---------------------------------------------------------------
|
13
|
+
select = Arel::SelectManager.new(record.class)
|
14
|
+
select.from table
|
15
|
+
|
16
|
+
# Add primary key
|
17
|
+
clone_id = next_id_for(record)
|
18
|
+
select.project clone_id
|
19
|
+
|
20
|
+
# Project custom column values
|
21
|
+
override_attribute_keys.each do |key|
|
22
|
+
select.project record.class.connection.quote(override_attributes[key])
|
23
|
+
end
|
24
|
+
|
25
|
+
# Project remaining (cloned) column values
|
26
|
+
cloned_column_names.each do |col|
|
27
|
+
select.project table[col.to_sym]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Where statement for selecting the original record
|
31
|
+
select.where(table[record.class.primary_key.to_sym].eq(record.send(record.class.primary_key)))
|
32
|
+
|
33
|
+
# ---------------------------------------------------------------
|
34
|
+
# Prepare insert statement
|
35
|
+
# ---------------------------------------------------------------
|
36
|
+
insert = Arel::InsertManager.new
|
37
|
+
insert.into table
|
38
|
+
insert.select select
|
39
|
+
|
40
|
+
# Add primary key column name
|
41
|
+
insert.columns << table[record.class.primary_key.to_sym]
|
42
|
+
|
43
|
+
# Add override column names
|
44
|
+
override_attribute_keys.each do |attr|
|
45
|
+
insert.columns << table[attr.to_sym]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add remaining (cloned) column names
|
49
|
+
cloned_column_names.each do |c|
|
50
|
+
insert.columns << table[c.to_sym]
|
51
|
+
end
|
52
|
+
|
53
|
+
# ---------------------------------------------------------------
|
54
|
+
# Execute statement
|
55
|
+
# ---------------------------------------------------------------
|
56
|
+
record.class.connection.execute(insert.to_sql)
|
57
|
+
|
58
|
+
if return_record
|
59
|
+
return record.class.find(clone_id)
|
60
|
+
else
|
61
|
+
return nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.next_id_for(record)
|
66
|
+
# See https://github.com/rsim/oracle-enhanced/issues/1733
|
67
|
+
ActiveRecord::Base.uncached do
|
68
|
+
record.class.connection.next_sequence_value(record.class.sequence_name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# rubocop: disable Style/PredicateName
|
2
|
+
|
3
|
+
module Snaptime
|
4
|
+
module Relations
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :versioned_associations
|
9
|
+
self.versioned_associations = {}.freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def has_one_versioned(name, scope = nil, options = {})
|
14
|
+
RelationsBuilder.build_versioned_relation(self, :has_one, name, scope, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_many_versioned(name, scope = nil, options = {}, &extension)
|
18
|
+
RelationsBuilder.build_versioned_relation(self, :has_many, name, scope, options, &extension)
|
19
|
+
end
|
20
|
+
|
21
|
+
def belongs_to_versioned(name, scope = nil, options = {})
|
22
|
+
RelationsBuilder.build_versioned_relation(self, :belongs_to, name, scope, options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# rubocop: disable Metrics/ParameterLists
|
2
|
+
|
3
|
+
module Snaptime
|
4
|
+
module RelationsBuilder
|
5
|
+
def self.build_versioned_relation(klass, macro, name, scope = nil, options = {}, &extension)
|
6
|
+
if scope.is_a?(Hash)
|
7
|
+
options = scope
|
8
|
+
scope = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
options[:primary_key] ||= (klass.versioned? ? :natural_id : klass.primary_key)
|
12
|
+
|
13
|
+
versioned_scope = proc do
|
14
|
+
rel = spawn.unscope(where: %i(valid_from valid_to))
|
15
|
+
rel = rel.merge(scope) unless scope.nil?
|
16
|
+
rel._at_explicit_snaptime(Snaptime.snaptime)
|
17
|
+
end
|
18
|
+
|
19
|
+
klass.send(macro, name, versioned_scope, options, &extension)
|
20
|
+
|
21
|
+
reflection = klass.reflect_on_association(name)
|
22
|
+
|
23
|
+
unless reflection.klass.versioned?
|
24
|
+
fail Exceptions::AssociationTargetNotVersioned, reflection.klass
|
25
|
+
end
|
26
|
+
|
27
|
+
klass.versioned_associations = klass.versioned_associations.merge(
|
28
|
+
name => reflection
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module Versioned
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
include Scopes
|
6
|
+
include Relations
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def delete(*_args)
|
10
|
+
fail Exceptions::DeleteMethodsAreNotAvailable
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete_all(*_args)
|
14
|
+
fail Exceptions::DeleteMethodsAreNotAvailable
|
15
|
+
end
|
16
|
+
|
17
|
+
def versioned?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def _run_create_callbacks(*args, &block)
|
23
|
+
super do
|
24
|
+
ArHooks.before_create(self)
|
25
|
+
yield
|
26
|
+
ArHooks.after_create(self)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# To make sure our before_update always runs after all other before_update
|
31
|
+
# methods, we override {_run_update_callbacks}. This prevents cases where an
|
32
|
+
# after_update callback changes the record after it has already been
|
33
|
+
# detected as no-changed. In this case, no shadow clone would be created.
|
34
|
+
def _run_update_callbacks(*args, &block)
|
35
|
+
super do
|
36
|
+
ArHooks.before_update(self)
|
37
|
+
yield
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroy
|
42
|
+
ArHooks.destroy(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete
|
46
|
+
fail Exceptions::DeleteMethodsAreNotAvailable
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module Versioned
|
3
|
+
module Scopes
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
default_scope do
|
8
|
+
current_version
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def current_version
|
14
|
+
snaptime = Snaptime.snaptime
|
15
|
+
_at_explicit_snaptime(snaptime)
|
16
|
+
end
|
17
|
+
|
18
|
+
def _at_explicit_snaptime(snaptime = nil)
|
19
|
+
if snaptime.nil?
|
20
|
+
where(valid_to: nil, deleted: false)
|
21
|
+
else
|
22
|
+
where(
|
23
|
+
arel_table[:valid_from].lteq(snaptime).and(
|
24
|
+
arel_table[:valid_to].eq(nil).or(
|
25
|
+
arel_table[:valid_to].gteq(snaptime)
|
26
|
+
)
|
27
|
+
).and(
|
28
|
+
arel_table[:deleted].eq(false)
|
29
|
+
)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def at_snaptime
|
35
|
+
_at_explicit_snaptime Snaptime.snaptime
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def snaptimes
|
40
|
+
Harvester.harvest_for(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_snaptime(snaptime = nil)
|
44
|
+
Snaptime.with_snaptime(snaptime) do
|
45
|
+
yield at_snaptime
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def at_snaptime
|
50
|
+
_at_explicit_snaptime Snaptime.snaptime
|
51
|
+
end
|
52
|
+
|
53
|
+
def all_versions
|
54
|
+
self.class.unscoped.where('natural_id = ?', natural_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def _at_explicit_snaptime(snaptime = nil)
|
60
|
+
all_versions._at_explicit_snaptime(snaptime).first
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Snaptime
|
2
|
+
module VirtualModels
|
3
|
+
class Snaptime < ActiveRecord::Base
|
4
|
+
def self.load_schema
|
5
|
+
columns_hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.columns
|
9
|
+
[
|
10
|
+
ActiveRecord::ConnectionAdapters::Column.new('valid_from', nil, ActiveRecord::Base.connection.send(:lookup_cast_type, :timestamp))
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.columns_hash
|
15
|
+
Hash[columns.map { |c| [c.name, c] }]
|
16
|
+
end
|
17
|
+
|
18
|
+
self.primary_key = :valid_from
|
19
|
+
|
20
|
+
def record_lookups
|
21
|
+
read_attribute(:record_lookups).split(';')
|
22
|
+
end
|
23
|
+
|
24
|
+
def records
|
25
|
+
to_fetch = {}
|
26
|
+
|
27
|
+
record_lookups.collect do |identifier|
|
28
|
+
klass_name, id = identifier.split(',')
|
29
|
+
|
30
|
+
to_fetch[klass_name] ||= Set.new
|
31
|
+
to_fetch[klass_name] << id
|
32
|
+
end
|
33
|
+
|
34
|
+
records = []
|
35
|
+
|
36
|
+
to_fetch.each do |klass_name, ids|
|
37
|
+
records += klass_name.constantize.unscoped.find(ids.to_a)
|
38
|
+
end
|
39
|
+
|
40
|
+
return records
|
41
|
+
end
|
42
|
+
|
43
|
+
def model_names
|
44
|
+
read_attribute(:model_names).split(',')
|
45
|
+
end
|
46
|
+
|
47
|
+
def models
|
48
|
+
model_names.collect(&:constantize)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/snaptime.gemspec
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# stub: snaptime 0.0.1 ruby lib
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "snaptime".freeze
|
6
|
+
s.version = "0.0.1"
|
7
|
+
|
8
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
9
|
+
s.require_paths = ["lib".freeze]
|
10
|
+
s.authors = ["Sitrox".freeze]
|
11
|
+
s.date = "2019-04-03"
|
12
|
+
s.files = [".gitignore".freeze, ".releaser_config".freeze, "Gemfile".freeze, "Gemfile.lock".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "lib/snaptime.rb".freeze, "lib/snaptime/ar_hooks.rb".freeze, "lib/snaptime/base_ar_mixin.rb".freeze, "lib/snaptime/exceptions.rb".freeze, "lib/snaptime/harvester.rb".freeze, "lib/snaptime/migration_helpers.rb".freeze, "lib/snaptime/railtie.rb".freeze, "lib/snaptime/record_cloner.rb".freeze, "lib/snaptime/relations.rb".freeze, "lib/snaptime/relations_builder.rb".freeze, "lib/snaptime/versioned.rb".freeze, "lib/snaptime/versioned/scopes.rb".freeze, "lib/snaptime/virtual_models/snaptime.rb".freeze, "snaptime.gemspec".freeze]
|
13
|
+
s.rubygems_version = "2.5.2.3".freeze
|
14
|
+
s.summary = "Multi-threaded job backend with database queuing for ruby.".freeze
|
15
|
+
|
16
|
+
if s.respond_to? :specification_version then
|
17
|
+
s.specification_version = 4
|
18
|
+
|
19
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
20
|
+
s.add_development_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
21
|
+
s.add_development_dependency(%q<rake>.freeze, [">= 0"])
|
22
|
+
s.add_development_dependency(%q<rubocop>.freeze, ["= 0.51.0"])
|
23
|
+
s.add_development_dependency(%q<minitest>.freeze, [">= 0"])
|
24
|
+
s.add_development_dependency(%q<mysql2>.freeze, [">= 0"])
|
25
|
+
s.add_development_dependency(%q<benchmark-ips>.freeze, [">= 0"])
|
26
|
+
s.add_runtime_dependency(%q<activesupport>.freeze, [">= 0"])
|
27
|
+
s.add_runtime_dependency(%q<activerecord>.freeze, [">= 0"])
|
28
|
+
s.add_runtime_dependency(%q<request_store>.freeze, [">= 0"])
|
29
|
+
else
|
30
|
+
s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
31
|
+
s.add_dependency(%q<rake>.freeze, [">= 0"])
|
32
|
+
s.add_dependency(%q<rubocop>.freeze, ["= 0.51.0"])
|
33
|
+
s.add_dependency(%q<minitest>.freeze, [">= 0"])
|
34
|
+
s.add_dependency(%q<mysql2>.freeze, [">= 0"])
|
35
|
+
s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
|
36
|
+
s.add_dependency(%q<activesupport>.freeze, [">= 0"])
|
37
|
+
s.add_dependency(%q<activerecord>.freeze, [">= 0"])
|
38
|
+
s.add_dependency(%q<request_store>.freeze, [">= 0"])
|
39
|
+
end
|
40
|
+
else
|
41
|
+
s.add_dependency(%q<bundler>.freeze, ["~> 2.0"])
|
42
|
+
s.add_dependency(%q<rake>.freeze, [">= 0"])
|
43
|
+
s.add_dependency(%q<rubocop>.freeze, ["= 0.51.0"])
|
44
|
+
s.add_dependency(%q<minitest>.freeze, [">= 0"])
|
45
|
+
s.add_dependency(%q<mysql2>.freeze, [">= 0"])
|
46
|
+
s.add_dependency(%q<benchmark-ips>.freeze, [">= 0"])
|
47
|
+
s.add_dependency(%q<activesupport>.freeze, [">= 0"])
|
48
|
+
s.add_dependency(%q<activerecord>.freeze, [">= 0"])
|
49
|
+
s.add_dependency(%q<request_store>.freeze, [">= 0"])
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snaptime
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sitrox
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubocop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.51.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.51.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: mysql2
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: benchmark-ips
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activesupport
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: activerecord
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: request_store
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
executables: []
|
142
|
+
extensions: []
|
143
|
+
extra_rdoc_files: []
|
144
|
+
files:
|
145
|
+
- ".gitignore"
|
146
|
+
- ".releaser_config"
|
147
|
+
- Gemfile
|
148
|
+
- Gemfile.lock
|
149
|
+
- LICENSE
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- VERSION
|
153
|
+
- lib/snaptime.rb
|
154
|
+
- lib/snaptime/ar_hooks.rb
|
155
|
+
- lib/snaptime/base_ar_mixin.rb
|
156
|
+
- lib/snaptime/exceptions.rb
|
157
|
+
- lib/snaptime/harvester.rb
|
158
|
+
- lib/snaptime/migration_helpers.rb
|
159
|
+
- lib/snaptime/railtie.rb
|
160
|
+
- lib/snaptime/record_cloner.rb
|
161
|
+
- lib/snaptime/relations.rb
|
162
|
+
- lib/snaptime/relations_builder.rb
|
163
|
+
- lib/snaptime/versioned.rb
|
164
|
+
- lib/snaptime/versioned/scopes.rb
|
165
|
+
- lib/snaptime/virtual_models/snaptime.rb
|
166
|
+
- snaptime.gemspec
|
167
|
+
homepage:
|
168
|
+
licenses: []
|
169
|
+
metadata: {}
|
170
|
+
post_install_message:
|
171
|
+
rdoc_options: []
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
requirements: []
|
185
|
+
rubyforge_project:
|
186
|
+
rubygems_version: 2.5.2.3
|
187
|
+
signing_key:
|
188
|
+
specification_version: 4
|
189
|
+
summary: Multi-threaded job backend with database queuing for ruby.
|
190
|
+
test_files: []
|