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 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
@@ -0,0 +1,3 @@
1
+ version_file: VERSION
2
+ always_from_master: true
3
+ gem_style: github
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify gem dependencies in the .gemspec file
4
+ gemspec
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 BaseArMixin
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def versioned?
7
+ false
8
+ end
9
+ end
10
+
11
+ def natural_id_or_id
12
+ self.class.versioned? ? natural_id : id
13
+ end
14
+ end
15
+ 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: []