time-travel 1.0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +293 -0
- data/Rakefile +29 -0
- data/lib/generators/time_travel/USAGE +9 -0
- data/lib/generators/time_travel/templates/time_travel_migration_existing.rb.erb +17 -0
- data/lib/generators/time_travel/templates/time_travel_migration_new.rb.erb +19 -0
- data/lib/generators/time_travel/time_travel_generator.rb +49 -0
- data/lib/tasks/create_postgres_function.rake +6 -0
- data/lib/time_travel/configuration.rb +9 -0
- data/lib/time_travel/railtie.rb +7 -0
- data/lib/time_travel/sql_function_helper.rb +19 -0
- data/lib/time_travel/timeline.rb +225 -0
- data/lib/time_travel/timeline_helper.rb +105 -0
- data/lib/time_travel/update_helper.rb +72 -0
- data/lib/time_travel/version.rb +3 -0
- data/lib/time_travel.rb +22 -0
- data/lib/time_travel_backup.rb +279 -0
- data/sql/create_column_value.sql +67 -0
- data/sql/get_json_attrs.sql +36 -0
- data/sql/update_bulk_history.sql +68 -0
- data/sql/update_history.sql +209 -0
- data/sql/update_latest.sql +94 -0
- metadata +122 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
do $$
|
2
|
+
DECLARE
|
3
|
+
routine_record text;
|
4
|
+
BEGIN
|
5
|
+
IF EXISTS ( SELECT 1 FROM Information_Schema.Routines WHERE Routine_Type ='FUNCTION' AND routine_name = 'update_history') THEN
|
6
|
+
SELECT CONCAT(routine_schema, '.', routine_name) INTO routine_record FROM Information_Schema.Routines WHERE Routine_Type ='FUNCTION' AND routine_name = 'update_history' limit 1;
|
7
|
+
EXECUTE concat('DROP FUNCTION ', routine_record);
|
8
|
+
END IF;
|
9
|
+
END
|
10
|
+
$$;
|
11
|
+
|
12
|
+
CREATE OR REPLACE FUNCTION update_history(
|
13
|
+
query text,
|
14
|
+
table_name text,
|
15
|
+
update_attrs text,
|
16
|
+
empty_obj_attrs text,
|
17
|
+
effective_from timestamp,
|
18
|
+
effective_till timestamp,
|
19
|
+
time_current timestamp,
|
20
|
+
infinite_date timestamp
|
21
|
+
)
|
22
|
+
|
23
|
+
RETURNS bigint[]
|
24
|
+
LANGUAGE 'plpgsql'
|
25
|
+
|
26
|
+
COST 100
|
27
|
+
VOLATILE
|
28
|
+
AS $BODY$
|
29
|
+
|
30
|
+
DECLARE
|
31
|
+
affected_rows bigint[];
|
32
|
+
target RECORD;
|
33
|
+
pre_post RECORD;
|
34
|
+
res jsonb;
|
35
|
+
current_id bigint;
|
36
|
+
in_between_effective_till timestamp;
|
37
|
+
original_effective_till timestamp;
|
38
|
+
original_effective_from timestamp;
|
39
|
+
update_attrs_json jsonb;
|
40
|
+
empty_obj_attrs_json jsonb;
|
41
|
+
head_found boolean;
|
42
|
+
tail_found boolean;
|
43
|
+
timeframe_attrs jsonb[] := '{}';
|
44
|
+
timeframe jsonb;
|
45
|
+
previous_timeframe jsonb;
|
46
|
+
temp_prev jsonb;
|
47
|
+
temp_curr jsonb;
|
48
|
+
begin
|
49
|
+
update_attrs_json := update_attrs::jsonb;
|
50
|
+
empty_obj_attrs_json := empty_obj_attrs::jsonb;
|
51
|
+
|
52
|
+
-- HEAD START
|
53
|
+
-- RAISE NOTICE 'head start';
|
54
|
+
FOR target IN EXECUTE concat(query, ' AND effective_from <= $1 AND effective_till > $2 order by effective_from ASC limit 1') USING effective_from, effective_from LOOP
|
55
|
+
-- RAISE NOTICE 'inside head iteration';
|
56
|
+
head_found := true;
|
57
|
+
original_effective_till := target.effective_till;
|
58
|
+
original_effective_from := target.effective_from;
|
59
|
+
|
60
|
+
-- HEAD NON-OVERLAPPING (with new) TIMEFRAME
|
61
|
+
IF target.effective_from <> effective_from THEN
|
62
|
+
target.effective_till := effective_from;
|
63
|
+
select get_json_attrs(to_jsonb(target), '{}'::jsonb) into res ;
|
64
|
+
timeframe_attrs := timeframe_attrs || res;
|
65
|
+
-- RAISE NOTICE 'new ineffective head id %',jsonb_pretty(res);
|
66
|
+
END IF;
|
67
|
+
|
68
|
+
-- HEAD AND NEW OVERLAPPING TIMEFRAME
|
69
|
+
target.effective_from := effective_from;
|
70
|
+
IF original_effective_till > effective_till THEN
|
71
|
+
target.effective_till := effective_till;
|
72
|
+
ELSE
|
73
|
+
target.effective_till := original_effective_till;
|
74
|
+
END IF;
|
75
|
+
select get_json_attrs(to_jsonb(target), update_attrs_json) into res ;
|
76
|
+
timeframe_attrs := timeframe_attrs || res;
|
77
|
+
-- RAISE NOTICE 'new in head id %', jsonb_pretty(res);
|
78
|
+
|
79
|
+
-- HEAD TAIL SAME, TAIL NON-OVERLAPPING (with new) TIMEFRAME
|
80
|
+
IF original_effective_from < effective_till AND original_effective_till > effective_till THEN
|
81
|
+
-- RAISE NOTICE 'same record head and tail';
|
82
|
+
target.effective_from := effective_till;
|
83
|
+
target.effective_till := original_effective_till;
|
84
|
+
select get_json_attrs(to_jsonb(target), '{}'::jsonb) into res ;
|
85
|
+
timeframe_attrs := timeframe_attrs || res;
|
86
|
+
-- RAISE NOTICE 'new tail id %', jsonb_pretty(res);
|
87
|
+
END IF;
|
88
|
+
|
89
|
+
-- NO TAIL, NEW NON-OVERLAPPING (with head) TIMEFRAME
|
90
|
+
EXECUTE concat(query, ' AND effective_till >= $1 limit 1') USING effective_till INTO pre_post;
|
91
|
+
-- RAISE NOTICE 'extending future pre post %', pre_post;
|
92
|
+
IF pre_post.effective_from IS NULL THEN
|
93
|
+
-- RAISE NOTICE 'extending future';
|
94
|
+
target.effective_from = original_effective_till;
|
95
|
+
target.effective_till = effective_till;
|
96
|
+
res = jsonb_build_object('effective_from', original_effective_till) || jsonb_build_object('effective_till', effective_till);
|
97
|
+
select get_json_attrs(empty_obj_attrs_json, update_attrs_json || res) into res ;
|
98
|
+
timeframe_attrs := timeframe_attrs || res;
|
99
|
+
-- RAISE NOTICE 'future in head %', jsonb_pretty(res);
|
100
|
+
END IF;
|
101
|
+
|
102
|
+
EXECUTE 'UPDATE ' || table_name|| ' SET valid_till=$1 WHERE id=$2' USING time_current, target.id;
|
103
|
+
END LOOP;
|
104
|
+
|
105
|
+
-- NO HEAD, NEW NON OVERLAPPING(with any) TIMEFRAME
|
106
|
+
-- HANGING IN PRE HISTORY
|
107
|
+
IF head_found IS NULL THEN
|
108
|
+
FOR target IN EXECUTE concat(query, ' AND effective_from > $1 order by effective_from ASC limit 1') USING effective_from LOOP
|
109
|
+
IF target.effective_from > effective_till THEN -- hanging
|
110
|
+
-- RAISE NOTICE 'hanging';
|
111
|
+
res = jsonb_build_object('effective_from', effective_from) || jsonb_build_object('effective_till', effective_till);
|
112
|
+
ELSE -- prehistoric
|
113
|
+
-- RAISE NOTICE 'pre historic';
|
114
|
+
res = jsonb_build_object('effective_from', effective_from) || jsonb_build_object('effective_till', target.effective_from);
|
115
|
+
END IF;
|
116
|
+
select get_json_attrs(empty_obj_attrs_json, update_attrs_json || res ) into res ;
|
117
|
+
timeframe_attrs := timeframe_attrs || res;
|
118
|
+
-- RAISE NOTICE 'pre historic/hanging tail %', jsonb_pretty(res);
|
119
|
+
END LOOP;
|
120
|
+
END IF;
|
121
|
+
|
122
|
+
-- BETWEEN AND NEW OVERLAPPING TIMEFRAME
|
123
|
+
-- RAISE NOTICE 'between start';
|
124
|
+
FOR target IN EXECUTE concat(query, ' AND effective_from > $1 AND effective_till < $2 order by effective_from ASC') USING effective_from, effective_till LOOP
|
125
|
+
-- RAISE NOTICE ' inside between';
|
126
|
+
select get_json_attrs(to_jsonb(target), update_attrs_json) into res ;
|
127
|
+
timeframe_attrs := timeframe_attrs || res;
|
128
|
+
|
129
|
+
in_between_effective_till := target.effective_till;
|
130
|
+
-- RAISE NOTICE 'between %', jsonb_pretty(res);
|
131
|
+
EXECUTE 'UPDATE ' || table_name|| ' SET valid_till=$1 WHERE id=$2' USING time_current, target.id;
|
132
|
+
END LOOP;
|
133
|
+
|
134
|
+
|
135
|
+
-- RAISE NOTICE 'tail start';
|
136
|
+
FOR target IN EXECUTE concat(query, ' AND effective_from > $1 AND effective_from < $2 AND effective_till >= $3 order by effective_from ASC limit 1') USING effective_from, effective_till, effective_till LOOP
|
137
|
+
-- RAISE NOTICE 'inside tail iteration';
|
138
|
+
-- TAIL AND NEW OVERLAPPING TIMEFRAME
|
139
|
+
original_effective_from := target.effective_from;
|
140
|
+
original_effective_till := target.effective_till;
|
141
|
+
|
142
|
+
tail_found := true;
|
143
|
+
target.effective_till := effective_till ;
|
144
|
+
target.effective_from := original_effective_from;
|
145
|
+
select get_json_attrs(to_jsonb(target), update_attrs_json) into res ;
|
146
|
+
timeframe_attrs := timeframe_attrs || res;
|
147
|
+
-- RAISE NOTICE 'new in tail id %', jsonb_pretty(res);
|
148
|
+
|
149
|
+
-- TAIL NON-OVERLAPPING(with new) TIMEFRAME
|
150
|
+
IF original_effective_till <> effective_till THEN
|
151
|
+
target.effective_from := effective_till;
|
152
|
+
target.effective_till := original_effective_till;
|
153
|
+
select get_json_attrs(to_jsonb(target), '{}'::jsonb) into res ;
|
154
|
+
timeframe_attrs := timeframe_attrs || res;
|
155
|
+
-- RAISE NOTICE 'new ineffective tail id %', jsonb_pretty(res);
|
156
|
+
END IF;
|
157
|
+
EXECUTE 'UPDATE ' || table_name|| ' SET valid_till=$1 WHERE id=$2' USING time_current, target.id;
|
158
|
+
END LOOP;
|
159
|
+
|
160
|
+
-- ONLY BETWEEN AND NO TAIL, NEW NON-OVERLAPPING TIMEFRAME
|
161
|
+
IF in_between_effective_till IS NOT NULL AND tail_found IS NULL THEN
|
162
|
+
-- RAISE NOTICE 'in between with no tail';
|
163
|
+
res := jsonb_build_object('effective_from', in_between_effective_till) || jsonb_build_object('effective_till', effective_till);
|
164
|
+
select get_json_attrs(empty_obj_attrs_json, update_attrs_json || res) into res ;
|
165
|
+
timeframe_attrs := timeframe_attrs || res;
|
166
|
+
-- RAISE NOTICE 'in between with no tail %', jsonb_pretty(res);
|
167
|
+
END IF;
|
168
|
+
|
169
|
+
|
170
|
+
-- HANGING ANYWHERE
|
171
|
+
IF array_length(timeframe_attrs, 1) IS NULL THEN
|
172
|
+
res := jsonb_build_object('effective_from', effective_from) || jsonb_build_object('effective_till', effective_till);
|
173
|
+
select get_json_attrs(empty_obj_attrs_json, update_attrs_json || res) into res ;
|
174
|
+
timeframe_attrs := timeframe_attrs || res;
|
175
|
+
-- RAISE NOTICE 'hanging %', jsonb_pretty(res);
|
176
|
+
END IF;
|
177
|
+
|
178
|
+
|
179
|
+
-- SQUISH RECORDS
|
180
|
+
previous_timeframe := null;
|
181
|
+
|
182
|
+
FOREACH timeframe IN ARRAY timeframe_attrs LOOP
|
183
|
+
IF previous_timeframe IS NOT NULL THEN
|
184
|
+
temp_prev = previous_timeframe - 'effective_from' - 'effective_till';
|
185
|
+
temp_curr = timeframe - 'effective_from' - 'effective_till';
|
186
|
+
|
187
|
+
IF previous_timeframe -> 'effective_till' < timeframe -> 'effective_from' THEN
|
188
|
+
previous_timeframe := previous_timeframe || jsonb_build_object('effective_till', timeframe -> 'effective_from');
|
189
|
+
END IF;
|
190
|
+
|
191
|
+
IF temp_prev @> temp_curr AND temp_prev <@ temp_curr THEN
|
192
|
+
previous_timeframe := previous_timeframe || jsonb_build_object('effective_till',timeframe -> 'effective_till');
|
193
|
+
ELSE
|
194
|
+
select create_column_value(to_json(previous_timeframe), time_current, table_name, '{}'::json, infinite_date) INTO current_id;
|
195
|
+
previous_timeframe := timeframe;
|
196
|
+
END IF;
|
197
|
+
ELSE
|
198
|
+
previous_timeframe := timeframe;
|
199
|
+
END IF;
|
200
|
+
END LOOP;
|
201
|
+
|
202
|
+
IF previous_timeframe IS NOT NULL THEN
|
203
|
+
select create_column_value(to_json(previous_timeframe), time_current, table_name, '{}'::json, infinite_date) INTO current_id;
|
204
|
+
END IF;
|
205
|
+
|
206
|
+
return affected_rows;
|
207
|
+
end
|
208
|
+
|
209
|
+
$BODY$;
|
@@ -0,0 +1,94 @@
|
|
1
|
+
do $$
|
2
|
+
DECLARE
|
3
|
+
routine_record text;
|
4
|
+
BEGIN
|
5
|
+
IF EXISTS ( SELECT 1 FROM Information_Schema.Routines WHERE Routine_Type ='FUNCTION' AND routine_name = 'update_latest') THEN
|
6
|
+
SELECT CONCAT(routine_schema, '.', routine_name) INTO routine_record FROM Information_Schema.Routines WHERE Routine_Type ='FUNCTION' AND routine_name = 'update_latest' limit 1;
|
7
|
+
EXECUTE concat('DROP FUNCTION ', routine_record);
|
8
|
+
END IF;
|
9
|
+
END
|
10
|
+
$$;
|
11
|
+
|
12
|
+
CREATE OR REPLACE FUNCTION update_latest(
|
13
|
+
query text,
|
14
|
+
table_name text,
|
15
|
+
update_attrs text,
|
16
|
+
empty_obj_attrs text,
|
17
|
+
effective_from timestamp,
|
18
|
+
effective_till timestamp,
|
19
|
+
time_current timestamp,
|
20
|
+
infinite_date timestamp
|
21
|
+
)
|
22
|
+
|
23
|
+
RETURNS void
|
24
|
+
LANGUAGE 'plpgsql'
|
25
|
+
|
26
|
+
COST 100
|
27
|
+
VOLATILE
|
28
|
+
AS $BODY$
|
29
|
+
|
30
|
+
DECLARE
|
31
|
+
affected_rows bigint[];
|
32
|
+
target RECORD;
|
33
|
+
current_record RECORD;
|
34
|
+
res jsonb;
|
35
|
+
current_id bigint;
|
36
|
+
-- original_effective_till timestamp;
|
37
|
+
-- original_effective_from timestamp;
|
38
|
+
update_attrs_json jsonb;
|
39
|
+
empty_obj_attrs_json jsonb;
|
40
|
+
-- head_found boolean;
|
41
|
+
-- tail_found boolean;
|
42
|
+
timeframe_attrs jsonb[] := '{}';
|
43
|
+
timeframe jsonb;
|
44
|
+
-- previous_timeframe jsonb;
|
45
|
+
-- temp_prev jsonb;
|
46
|
+
-- temp_curr jsonb;
|
47
|
+
begin
|
48
|
+
update_attrs_json := update_attrs::jsonb;
|
49
|
+
empty_obj_attrs_json := empty_obj_attrs::jsonb;
|
50
|
+
|
51
|
+
-- HEAD START
|
52
|
+
-- RAISE NOTICE 'head start';
|
53
|
+
FOR target IN EXECUTE concat(query, ' AND effective_from <= $1 AND effective_till = $2 order by effective_from ASC limit 1') USING effective_from, infinite_date LOOP
|
54
|
+
-- RAISE NOTICE 'inside head iteration';
|
55
|
+
-- head_found := true;
|
56
|
+
-- original_effective_till := target.effective_till;
|
57
|
+
-- original_effective_from := target.effective_from;
|
58
|
+
|
59
|
+
-- HEAD NON-OVERLAPPING (with new) TIMEFRAME
|
60
|
+
IF target.effective_from <> effective_from THEN
|
61
|
+
target.effective_till := effective_from;
|
62
|
+
select get_json_attrs(to_jsonb(target), '{}'::jsonb) into res ;
|
63
|
+
timeframe_attrs := timeframe_attrs || res;
|
64
|
+
-- RAISE NOTICE 'new ineffective head id %',jsonb_pretty(res);
|
65
|
+
END IF;
|
66
|
+
|
67
|
+
-- HEAD AND NEW OVERLAPPING TIMEFRAME
|
68
|
+
target.effective_from := effective_from;
|
69
|
+
target.effective_till := effective_till;
|
70
|
+
select get_json_attrs(to_jsonb(target), update_attrs_json) into res ;
|
71
|
+
timeframe_attrs := timeframe_attrs || res;
|
72
|
+
|
73
|
+
EXECUTE 'UPDATE ' || table_name|| ' SET valid_till=$1 WHERE id=$2' USING time_current, target.id;
|
74
|
+
END LOOP;
|
75
|
+
|
76
|
+
-- HANGING ANYWHERE
|
77
|
+
IF array_length(timeframe_attrs, 1) IS NULL THEN
|
78
|
+
EXECUTE(concat(query, ' LIMIT 1')) INTO current_record;
|
79
|
+
IF current_record IS NULL THEN
|
80
|
+
res := jsonb_build_object('effective_from', effective_from) || jsonb_build_object('effective_till', effective_till);
|
81
|
+
select get_json_attrs(empty_obj_attrs_json, update_attrs_json || res) into res ;
|
82
|
+
timeframe_attrs := timeframe_attrs || res;
|
83
|
+
ELSE
|
84
|
+
RAISE EXCEPTION 'you cannot update non latest values';
|
85
|
+
END IF;
|
86
|
+
-- RAISE NOTICE 'hanging %', jsonb_pretty(res);
|
87
|
+
END IF;
|
88
|
+
|
89
|
+
FOREACH timeframe IN ARRAY timeframe_attrs LOOP
|
90
|
+
select create_column_value(to_json(timeframe), time_current, table_name, '{}'::json, infinite_date) INTO current_id;
|
91
|
+
END LOOP;
|
92
|
+
end
|
93
|
+
|
94
|
+
$BODY$;
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: time-travel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ''
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-09-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.8'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pg
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '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'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: timecop
|
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
|
+
description: The time travel gem adds history and correction tracking to models.
|
70
|
+
email:
|
71
|
+
- ''
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- MIT-LICENSE
|
77
|
+
- README.md
|
78
|
+
- Rakefile
|
79
|
+
- lib/generators/time_travel/USAGE
|
80
|
+
- lib/generators/time_travel/templates/time_travel_migration_existing.rb.erb
|
81
|
+
- lib/generators/time_travel/templates/time_travel_migration_new.rb.erb
|
82
|
+
- lib/generators/time_travel/time_travel_generator.rb
|
83
|
+
- lib/tasks/create_postgres_function.rake
|
84
|
+
- lib/time_travel.rb
|
85
|
+
- lib/time_travel/configuration.rb
|
86
|
+
- lib/time_travel/railtie.rb
|
87
|
+
- lib/time_travel/sql_function_helper.rb
|
88
|
+
- lib/time_travel/timeline.rb
|
89
|
+
- lib/time_travel/timeline_helper.rb
|
90
|
+
- lib/time_travel/update_helper.rb
|
91
|
+
- lib/time_travel/version.rb
|
92
|
+
- lib/time_travel_backup.rb
|
93
|
+
- sql/create_column_value.sql
|
94
|
+
- sql/get_json_attrs.sql
|
95
|
+
- sql/update_bulk_history.sql
|
96
|
+
- sql/update_history.sql
|
97
|
+
- sql/update_latest.sql
|
98
|
+
homepage: https://weinvest.net
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.7.7
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: The Time travel gem adds history and correction tracking to models.
|
122
|
+
test_files: []
|