time-travel 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []