mariadb_temporal_tables 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/mariadb_temporal_tables/concerns/application_versioning.rb +101 -0
- data/lib/mariadb_temporal_tables/concerns/combined_versioning.rb +41 -0
- data/lib/mariadb_temporal_tables/concerns/system_versioning.rb +212 -0
- data/lib/mariadb_temporal_tables/railtie.rb +12 -0
- data/lib/tasks/gen_migration_application.rake +44 -0
- data/lib/tasks/gen_migration_system.rake +40 -0
- data/lib/tasks/helpers/migration_generator.rb +38 -0
- data/lib/tasks/helpers/parser.rb +79 -0
- data/lib/tasks/migration_templates/application_versioning.rb.erb +16 -0
- data/lib/tasks/migration_templates/system_versioning.rb.erb +20 -0
- metadata +11 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1e568e3267a15a75752db985165b1707ea771dfed1501dacf95b5560ffdf1aa
|
4
|
+
data.tar.gz: e3b25c22dcc408ab54ccfd0835f4ea3bf78824c11173bc07fa0cec668be349c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: de4e1cdb8a056a3d544a3f5f523f4b157c140d85a194fb96951a7c3046da4fb57dcc4b256981cae09480cacde75638f8d569d710ac877269e7748b6f52c00a9c
|
7
|
+
data.tar.gz: ab3b4704b53e7236a0c1ed81a9551daa6c31544f17b28a03d7dda84955006f3ae82e9e5301660dfae0f535f6f9b701a8e746442d9ef30d8dfb43752f04a78e96
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module MariaDBTemporalTables
|
4
|
+
|
5
|
+
# <tt>ActiveSupport::Concern</tt> that adds methods to the associated model that utilize MariaDB application-time periods
|
6
|
+
#
|
7
|
+
# <tt>application_versioning_options</tt> can be called in the model to set options for this concern
|
8
|
+
# See ApplicationVersioning#application_versioning_options
|
9
|
+
module ApplicationVersioning
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
application_versioning_options # Initialize with default options
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
attr_reader :application_versioning_start_column_name, :application_versioning_end_column_name
|
18
|
+
|
19
|
+
# Sets options for application versioning
|
20
|
+
# @param [Hash] options the options to use for application versioning
|
21
|
+
# @option options [String] :start_column_name the name of the column that indicates start of validity of application versioning
|
22
|
+
# @option options [String] :end_column_name the name of the column that indicates end of validity of application versioning
|
23
|
+
# @option options [Symbol, Array<Symbol>] :primary_key primary key to be set as the model primary key (can be single or composite key)
|
24
|
+
def application_versioning_options(options = {})
|
25
|
+
@application_versioning_start_column_name = options[:start_column_name] || "valid_start"
|
26
|
+
@application_versioning_end_column_name = options[:end_column_name] || "valid_end"
|
27
|
+
|
28
|
+
self.primary_key = options[:primary_key] || [:id, @application_versioning_end_column_name]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Gets all records that were valid at the given time
|
32
|
+
# @param [Time, DateTime, Date, String] valid_at used to get records valid at this time
|
33
|
+
def all_valid_at(valid_at)
|
34
|
+
parsed_date = parse_date_or_time(valid_at)
|
35
|
+
query = "SELECT * FROM #{table_name} WHERE ? BETWEEN #{@application_versioning_start_column_name} AND #{@application_versioning_end_column_name}"
|
36
|
+
return find_by_sql [query, parsed_date]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Gets all records that were valid at the given time ordered by the given order attributes
|
40
|
+
# @param [Time, DateTime, Date, String] valid_at used to get records valid at this time
|
41
|
+
# @param [String] order order to be used on the SQL query ("ASC" or "DESC")
|
42
|
+
# @param [Array<String>, Array<Symbol>] order_attributes list of attributes to order by
|
43
|
+
# @return [Array<ActiveRecord::Base>] array of active record objects of the current model ordered
|
44
|
+
def order_valid_at(valid_at, order = "ASC", *order_attributes)
|
45
|
+
parsed_date = parse_date_or_time(valid_at)
|
46
|
+
order_attributes_s = order_attributes.join(", ")
|
47
|
+
query = "SELECT * FROM #{table_name} WHERE ? BETWEEN #{@application_versioning_start_column_name} AND #{@application_versioning_end_column_name} ORDER BY #{order_attributes_s} #{order}"
|
48
|
+
return find_by_sql [query, parsed_date]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Gets all records that were valid at the given time filtered by the given where attributes
|
52
|
+
# @param [Time, DateTime, Date, String] valid_at used to get records valid at this time
|
53
|
+
# @param [Hash] where_attributes key-value hash to be used to generate where clause (where key='value' for each)
|
54
|
+
# @return [Array<ActiveRecord::Base>] array of active record objects of the current model filtered by attributes
|
55
|
+
def where_valid_at(valid_at, where_attributes)
|
56
|
+
parsed_date = parse_date_or_time(valid_at)
|
57
|
+
|
58
|
+
where_attributes_s = "WHERE "
|
59
|
+
first = true
|
60
|
+
|
61
|
+
where_attributes.each do |attr|
|
62
|
+
unless first
|
63
|
+
where_attributes_s += " AND "
|
64
|
+
end
|
65
|
+
first = false
|
66
|
+
|
67
|
+
where_attributes_s += "#{attr[0]} = '#{attr[1]}'"
|
68
|
+
end
|
69
|
+
|
70
|
+
query = "SELECT * FROM #{table_name} #{where_attributes_s} AND ? BETWEEN #{@application_versioning_start_column_name} AND #{@application_versioning_end_column_name}"
|
71
|
+
return find_by_sql [query, parsed_date]
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_date_or_time(date_or_time)
|
75
|
+
column_type = columns_hash[@application_versioning_end_column_name].type
|
76
|
+
|
77
|
+
case column_type
|
78
|
+
when :datetime
|
79
|
+
if date_or_time.is_a? DateTime
|
80
|
+
return date_or_time
|
81
|
+
end
|
82
|
+
|
83
|
+
return DateTime.parse(date_or_time)
|
84
|
+
when :timestamp
|
85
|
+
if date_or_time.is_a? Time
|
86
|
+
return date_or_time
|
87
|
+
end
|
88
|
+
|
89
|
+
return Time.parse(date_or_time)
|
90
|
+
else
|
91
|
+
if date_or_time.is_a? Date
|
92
|
+
return date_or_time
|
93
|
+
end
|
94
|
+
|
95
|
+
return Date.parse(date_or_time)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module MariaDBTemporalTables
|
4
|
+
|
5
|
+
# <tt>ActiveSupport::Concern</tt> that combines the utility of SystemVersioning and ApplicationVersioning
|
6
|
+
#
|
7
|
+
# <tt>system_versioning_options</tt> and <tt>application_versioning_options</tt> can be called in the model to set options for this concern
|
8
|
+
module CombinedVersioning
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include SystemVersioning
|
11
|
+
include ApplicationVersioning
|
12
|
+
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
|
16
|
+
# Gets all record as of time (system versioning) and valid at time (application versioning)
|
17
|
+
# @param [Time, DateTime, Date, String] valid_at used to get records valid at this time
|
18
|
+
# @param [Time, String] as_of_time used to get records as of this time
|
19
|
+
def all_valid_at_as_of(valid_at, as_of_time)
|
20
|
+
parsed_time = parse_time(as_of_time)
|
21
|
+
parsed_date = parse_date_or_time(valid_at)
|
22
|
+
query = "SELECT * FROM #{table_name} FOR SYSTEM_TIME AS OF TIMESTAMP? WHERE ? BETWEEN #{application_versioning_start_column_name} AND #{application_versioning_end_column_name}"
|
23
|
+
return find_by_sql [query, parsed_time, parsed_date]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Gets all record as of time (system versioning) and valid at time (application versioning) ordered by the given order attributes
|
27
|
+
# @param [Time, DateTime, Date, String] valid_at used to get records valid at this time
|
28
|
+
# @param [Time, String] as_of_time used to get records as of this time
|
29
|
+
# @param [Array<String>, Array<Symbol>] order_attributes list of attributes to order by
|
30
|
+
# @return [Array<ActiveRecord::Base>] array of active record objects of the current model ordered
|
31
|
+
def order_valid_at_as_of(valid_at, as_of_time, order="ASC", *order_attributes)
|
32
|
+
parsed_time = parse_time(as_of_time)
|
33
|
+
parsed_date = parse_date_or_time(valid_at)
|
34
|
+
order_attributes_s = order_attributes.join(", ")
|
35
|
+
query = "SELECT * FROM #{table_name} FOR SYSTEM_TIME AS OF TIMESTAMP? WHERE ? BETWEEN #{application_versioning_start_column_name} AND #{application_versioning_end_column_name} ORDER BY #{order_attributes_s} #{order}"
|
36
|
+
return find_by_sql [query, parsed_time, parsed_date]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module MariaDBTemporalTables
|
4
|
+
|
5
|
+
# <tt>ActiveSupport::Concern</tt> that adds methods to the associated model that utilize MariaDB system versioning
|
6
|
+
#
|
7
|
+
# <tt>system_versioning_options</tt> can be called in the model to set options for this concern
|
8
|
+
# See SystemVersioning#system_versioning_options
|
9
|
+
module SystemVersioning
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
system_versioning_options # Initialize with default options
|
14
|
+
before_save :add_author, :add_change_list
|
15
|
+
|
16
|
+
# Get all the previous versions of this record, including the current version
|
17
|
+
# @param [String] order order to be used on the SQL query ("ASC" or "DESC")
|
18
|
+
# @return [Array<ActiveRecord::Base>] array of active record objects of the current model
|
19
|
+
def versions(order = "ASC")
|
20
|
+
where_clause = self.class.generate_where_clause_for_id(self.class.try(:primary_keys), self.id)
|
21
|
+
query = "SELECT * FROM #{self.class.table_name} FOR SYSTEM_TIME ALL #{where_clause} ORDER BY #{self.class.system_versioning_end_column_name} #{order}"
|
22
|
+
self.class.find_by_sql [query]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Revert the current object to a specific version of an object with given id and at the time of end_value
|
26
|
+
# @param id [Integer, String, Array<String>] id of the object to revert to
|
27
|
+
# @param [Time, String] as_of_time time at which to revert to
|
28
|
+
# @return [Boolean] true when successful
|
29
|
+
def revert(id, as_of_time)
|
30
|
+
parsed_time = self.class.parse_time(as_of_time)
|
31
|
+
where_clause = self.class.generate_where_clause_for_id(self.class.try(:primary_keys), id)
|
32
|
+
query = "SELECT * FROM #{self.class.table_name} FOR SYSTEM_TIME AS OF TIMESTAMP? #{where_clause} LIMIT 1"
|
33
|
+
query_result = self.class.find_by_sql([query, parsed_time])
|
34
|
+
|
35
|
+
unless query_result
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
revert_object = query_result[0]
|
40
|
+
attributes = revert_object.attributes.except(self.class.exclude_revert)
|
41
|
+
return self.update(attributes)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def add_author
|
47
|
+
if self.class.column_names.include? "author_id"
|
48
|
+
author = Thread.current[:mariadb_temporal_tables_current_author]
|
49
|
+
if author
|
50
|
+
self.author_id = author.id
|
51
|
+
else
|
52
|
+
Warning.warn("Could not find author to associate with version, make sure to call system_versioning_set_author in your controller")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_change_list
|
58
|
+
if self.class.column_names.include? "change_list"
|
59
|
+
change_list = get_change_list
|
60
|
+
self.change_list = change_list
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_change_list
|
65
|
+
change_list = ""
|
66
|
+
self.changes.each do |attr_name, change|
|
67
|
+
if self.class.exclude_change_list.include? attr_name
|
68
|
+
next
|
69
|
+
end
|
70
|
+
change_text = ""
|
71
|
+
old_value = change[0]
|
72
|
+
new_value = change[1]
|
73
|
+
|
74
|
+
if is_nil_or_empty(old_value) && !is_nil_or_empty(new_value)
|
75
|
+
change_text = "Added #{attr_name}: #{new_value}"
|
76
|
+
elsif !is_nil_or_empty(old_value) && is_nil_or_empty(new_value)
|
77
|
+
change_text = "Removed #{attr_name}"
|
78
|
+
elsif !is_nil_or_empty(old_value) && !is_nil_or_empty(new_value) && old_value != new_value
|
79
|
+
change_text = "Updated #{attr_name}: #{old_value} -> #{new_value}"
|
80
|
+
else
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
change_list += change_text + "\n"
|
85
|
+
end
|
86
|
+
return change_list
|
87
|
+
end
|
88
|
+
|
89
|
+
def is_nil_or_empty(value)
|
90
|
+
if value.is_a? String
|
91
|
+
return value.nil? || value.empty?
|
92
|
+
end
|
93
|
+
return value.nil?
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class_methods do
|
98
|
+
attr_reader :system_versioning_start_column_name, :system_versioning_end_column_name, :exclude_revert, :exclude_change_list
|
99
|
+
|
100
|
+
# Sets options for system versioning
|
101
|
+
# @param [Hash] options the options to use for system versioning
|
102
|
+
# @option options [String] :start_column_name the name of the column that indicates start of validity of system versioning
|
103
|
+
# @option options [String] :end_column_name the name of the column that indicates end of validity of system versioning
|
104
|
+
# @option options [Array<String>] :exclude_revert list of column names that should be excluded when reverting a record
|
105
|
+
# @option options [Array<String>] :exclude_change_list list of column names that should be excluded when generating the change list
|
106
|
+
# @option options [Symbol, Array<Symbol>] :primary_key primary key to be set as the model primary key (can be single or composite key)
|
107
|
+
def system_versioning_options(options = {})
|
108
|
+
@system_versioning_start_column_name = options[:start_column_name] || "transaction_start"
|
109
|
+
@system_versioning_end_column_name = options[:end_column_name] || "transaction_end"
|
110
|
+
|
111
|
+
default_exclude = %w[id author_id change_list]
|
112
|
+
@exclude_revert = (options[:exclude_revert] || []) + default_exclude + [@system_versioning_start_column_name, @system_versioning_end_column_name]
|
113
|
+
@exclude_change_list = (options[:exclude_change_list] || []) + default_exclude + [@system_versioning_start_column_name, @system_versioning_end_column_name]
|
114
|
+
|
115
|
+
self.primary_key = options[:primary_key] || :id
|
116
|
+
end
|
117
|
+
|
118
|
+
# Gets all records as of the given time
|
119
|
+
# @param [Time, String] as_of_time used to get records as of this time
|
120
|
+
def all_as_of(as_of_time)
|
121
|
+
parsed_time = parse_time(as_of_time)
|
122
|
+
query = "SELECT * FROM #{table_name} FOR SYSTEM_TIME AS OF TIMESTAMP?"
|
123
|
+
return find_by_sql [query, parsed_time]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Gets all records as of the given time ordered by the given order attributes
|
127
|
+
# @param [Time, String] as_of_time used to get records as of this time
|
128
|
+
# @param [String] order order to be used on the SQL query ("ASC" or "DESC")
|
129
|
+
# @param [Array<String>, Array<Symbol>] order_attributes list of attributes to order by
|
130
|
+
# @return [Array<ActiveRecord::Base>] array of active record objects of the current model ordered
|
131
|
+
def order_as_of(as_of_time, order = "ASC", *order_attributes)
|
132
|
+
parsed_time = parse_time(as_of_time)
|
133
|
+
order_attributes_s = order_attributes.join(", ")
|
134
|
+
query = "SELECT * FROM #{table_name} FOR SYSTEM_TIME AS OF TIMESTAMP? ORDER BY #{order_attributes_s} #{order}"
|
135
|
+
return find_by_sql [query, parsed_time]
|
136
|
+
end
|
137
|
+
|
138
|
+
# Gets all records as of the given time filtered by the given where attributes
|
139
|
+
# @param [Time, String] as_of_time used to get records as of this time
|
140
|
+
# @param [Hash] where_attributes key-value hash to be used to generate where clause (where key='value' for each)
|
141
|
+
# @return [Array<ActiveRecord::Base>] array of active record objects of the current model filtered by attributes
|
142
|
+
def where_as_of(as_of_time, where_attributes)
|
143
|
+
parsed_time = parse_time(as_of_time)
|
144
|
+
|
145
|
+
where_attributes_s = "WHERE "
|
146
|
+
first = true
|
147
|
+
|
148
|
+
where_attributes.each do |attr|
|
149
|
+
unless first
|
150
|
+
where_attributes_s += " AND "
|
151
|
+
end
|
152
|
+
first = false
|
153
|
+
|
154
|
+
where_attributes_s += "#{attr[0]} = '#{attr[1]}'"
|
155
|
+
end
|
156
|
+
|
157
|
+
query = "SELECT * FROM #{table_name} FOR SYSTEM_TIME AS OF TIMESTAMP? #{where_attributes_s}"
|
158
|
+
return find_by_sql [query, parsed_time]
|
159
|
+
end
|
160
|
+
|
161
|
+
# Gets the single records as of the given time with the given id
|
162
|
+
# @param [Time, String] as_of_time used to get record as of this time
|
163
|
+
# @param [Integer, String, Array<String>] id id of the object to find
|
164
|
+
# @return [ActiveRecord::Base] active record object of the found object
|
165
|
+
def find_as_of(as_of_time, id)
|
166
|
+
parsed_time = parse_time(as_of_time)
|
167
|
+
where_clause = generate_where_clause_for_id(try(:primary_keys), id)
|
168
|
+
query = "SELECT * FROM #{table_name} FOR SYSTEM_TIME AS OF TIMESTAMP? #{where_clause} LIMIT 1"
|
169
|
+
return find_by_sql([query, parsed_time])[0]
|
170
|
+
end
|
171
|
+
|
172
|
+
# Gets the number of versions that an author has created. Any version where author_id is equal to the given id
|
173
|
+
# @param [Integer, String] author_id id of author
|
174
|
+
# @return [Integer] number of versions author has created
|
175
|
+
def versions_count_for_author(author_id)
|
176
|
+
query = "SELECT COUNT(*) AS count FROM #{table_name} FOR SYSTEM_TIME ALL WHERE author_id=?"
|
177
|
+
find_by_sql([query, author_id])[0].count
|
178
|
+
end
|
179
|
+
|
180
|
+
def parse_time(time)
|
181
|
+
if time.is_a? Time
|
182
|
+
return time
|
183
|
+
end
|
184
|
+
|
185
|
+
Time.parse(time)
|
186
|
+
end
|
187
|
+
|
188
|
+
# In case a composite key is used, this generates where clause considering composite keys
|
189
|
+
def generate_where_clause_for_id(primary_keys, id)
|
190
|
+
if primary_keys
|
191
|
+
|
192
|
+
unless id.is_a? Array # Composite id could be '1,other_key' or ["1","other_key"]
|
193
|
+
id = id.split(",")
|
194
|
+
end
|
195
|
+
|
196
|
+
where_clause = "WHERE "
|
197
|
+
(0...primary_keys.length).each do |i|
|
198
|
+
if i != 0
|
199
|
+
where_clause += " AND "
|
200
|
+
end
|
201
|
+
where_clause += "#{primary_keys[i]}='#{id[i]}'"
|
202
|
+
end
|
203
|
+
where_clause
|
204
|
+
else
|
205
|
+
"WHERE id = '#{id}'"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative "helpers/parser"
|
2
|
+
require_relative "helpers/migration_generator"
|
3
|
+
|
4
|
+
namespace :mariadb_temporal_tables do
|
5
|
+
include Parser
|
6
|
+
|
7
|
+
desc "Generates migration for adding application versioning to a table"
|
8
|
+
task :gen_migration_application do
|
9
|
+
|
10
|
+
options = parse_options("mariadb_temporal_tables:gen_migration_application")
|
11
|
+
table_name = options[:table_name]
|
12
|
+
|
13
|
+
if table_name.nil?
|
14
|
+
raise "Missing table parameter to create migration on. Use option --table=table_name to choose a table"
|
15
|
+
end
|
16
|
+
|
17
|
+
replace_primary_key = options[:replace_primary_key].nil? ? true : options[:replace_primary_key]
|
18
|
+
add_columns = options[:add_columns].nil? ? true : options[:add_columns]
|
19
|
+
column_type = options[:column_type] || "DATE"
|
20
|
+
start_column_name = options[:start_column_name] || "valid_start"
|
21
|
+
end_column_name = options[:end_column_name] || "valid_end"
|
22
|
+
|
23
|
+
unless %w[DATE TIMESTAMP DATETIME].include?(column_type)
|
24
|
+
raise "Unsupported column_type provided: #{column_type}"
|
25
|
+
end
|
26
|
+
|
27
|
+
migration_name = "add_application_versioning_to_#{table_name.downcase}"
|
28
|
+
puts "Generating migration #{migration_name}"
|
29
|
+
|
30
|
+
generator = MigrationGenerator.new
|
31
|
+
generator.generate_migration(migration_name,
|
32
|
+
"application_versioning.rb.erb",
|
33
|
+
{ :replace_primary_key => replace_primary_key,
|
34
|
+
:add_columns => add_columns,
|
35
|
+
:column_type => column_type,
|
36
|
+
:start_column_name => start_column_name,
|
37
|
+
:end_column_name => end_column_name,
|
38
|
+
:table_name => table_name
|
39
|
+
})
|
40
|
+
|
41
|
+
puts "Migration generated successfully"
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative "helpers/parser"
|
2
|
+
require_relative "helpers/migration_generator"
|
3
|
+
|
4
|
+
namespace :mariadb_temporal_tables do
|
5
|
+
include Parser
|
6
|
+
|
7
|
+
desc "Generates migration for adding system versioning to a table"
|
8
|
+
task :gen_migration_system do
|
9
|
+
|
10
|
+
options = parse_options("mariadb_temporal_tables:gen_migration_system")
|
11
|
+
table_name = options[:table_name]
|
12
|
+
|
13
|
+
if table_name.nil?
|
14
|
+
raise "Missing table parameter to create migration on. Use option --table=table_name to choose a table"
|
15
|
+
end
|
16
|
+
|
17
|
+
include_change_list = options[:include_change_list].nil? ? false : options[:include_change_list]
|
18
|
+
include_author_reference = options[:include_author_reference].nil? ? false : options[:include_author_reference]
|
19
|
+
author_table_name = options[:author_table_name] || "users"
|
20
|
+
start_column_name = options[:start_column_name] || "transaction_start"
|
21
|
+
end_column_name = options[:end_column_name] || "transaction_end"
|
22
|
+
|
23
|
+
migration_name = "add_system_versioning_to_#{table_name.downcase}"
|
24
|
+
puts "Generating migration #{migration_name}"
|
25
|
+
|
26
|
+
generator = MigrationGenerator.new
|
27
|
+
generator.generate_migration(migration_name,
|
28
|
+
"system_versioning.rb.erb",
|
29
|
+
{ :include_change_list => include_change_list,
|
30
|
+
:include_author_reference => include_author_reference,
|
31
|
+
:author_table_name => author_table_name,
|
32
|
+
:start_column_name => start_column_name,
|
33
|
+
:end_column_name => end_column_name,
|
34
|
+
:table_name => table_name
|
35
|
+
})
|
36
|
+
|
37
|
+
puts "Migration generated successfully"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/active_record"
|
3
|
+
|
4
|
+
# Implementation based on how paper_trail gem achieves similar result
|
5
|
+
# https://github.com/paper-trail-gem/paper_trail/blob/master/lib/generators/paper_trail/migration_generator.rb
|
6
|
+
class MigrationGenerator < ::Rails::Generators::Base
|
7
|
+
include ::Rails::Generators::Migration
|
8
|
+
|
9
|
+
# Tells Migrations where to look for template files
|
10
|
+
source_root File.expand_path("../migration_templates", __dir__)
|
11
|
+
|
12
|
+
# Needs to be reimplemented or throws unimplemented error
|
13
|
+
def self.next_migration_number(dirname)
|
14
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate_migration(migration_name, template_file, template_options)
|
18
|
+
migration_directory = File.expand_path("db/migrate")
|
19
|
+
|
20
|
+
if self.class.migration_exists?(migration_directory, migration_name)
|
21
|
+
raise "Can't generate migration #{migration_name} because it already exists."
|
22
|
+
end
|
23
|
+
|
24
|
+
@template_options = template_options
|
25
|
+
migration_template(template_file,
|
26
|
+
"db/migrate/#{migration_name}.rb",
|
27
|
+
{})
|
28
|
+
end
|
29
|
+
|
30
|
+
def migration_version
|
31
|
+
format(
|
32
|
+
"[%d.%d]",
|
33
|
+
::ActiveRecord::VERSION::MAJOR,
|
34
|
+
::ActiveRecord::VERSION::MINOR
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
# Usage of names arguments in Rake as shown here: http://www.mikeball.us/blog/rake-option-parser/
|
4
|
+
module Parser
|
5
|
+
def parse_options(command)
|
6
|
+
options = {}
|
7
|
+
option_parser = OptionParser.new
|
8
|
+
option_parser.banner = "Usage: rake #{command} [options]"
|
9
|
+
|
10
|
+
case command
|
11
|
+
when "mariadb_temporal_tables:gen_migration_application"
|
12
|
+
setup_parser_for_gen_migration_application(option_parser, options)
|
13
|
+
when "mariadb_temporal_tables:gen_migration_system"
|
14
|
+
setup_parser_for_gen_migration_system(option_parser, options)
|
15
|
+
else
|
16
|
+
raise "Unable to parse options for unknown command: #{command}"
|
17
|
+
end
|
18
|
+
|
19
|
+
args = option_parser.order!(ARGV) {}
|
20
|
+
option_parser.parse!(args)
|
21
|
+
options
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def setup_parser_for_gen_migration_application(parser, options)
|
27
|
+
parser.on("--table=TABLE","Table to generate migration for") do |value|
|
28
|
+
options[:table_name] = value
|
29
|
+
end
|
30
|
+
|
31
|
+
parser.on("--add_columns=ADD","Whether columns should be added to table or not") do |value|
|
32
|
+
options[:add_columns] = value == "true"
|
33
|
+
end
|
34
|
+
|
35
|
+
parser.on("--start_column_name=NAME","Name of column that represents start of period") do |value|
|
36
|
+
options[:start_column_name] = value
|
37
|
+
end
|
38
|
+
|
39
|
+
parser.on("--end_column_name=NAME","Name of column that represents end of period") do |value|
|
40
|
+
options[:end_column_name] = value
|
41
|
+
end
|
42
|
+
|
43
|
+
parser.on("--replace_primary_key=REPLACE","Whether end column should be added to primary key or not") do |value|
|
44
|
+
options[:replace_primary_key] = value == "true"
|
45
|
+
end
|
46
|
+
|
47
|
+
parser.on("--column_type=TYPE","Type to use for column to be added") do |value|
|
48
|
+
options[:column_type] = value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_parser_for_gen_migration_system(parser, options)
|
53
|
+
parser.on("--table=TABLE","Table to generate migration for") do |value|
|
54
|
+
options[:table_name] = value
|
55
|
+
end
|
56
|
+
|
57
|
+
parser.on("--include_change_list=INC","Whether change_list column should be added or not") do |value|
|
58
|
+
options[:include_change_list] = value == "true"
|
59
|
+
end
|
60
|
+
|
61
|
+
parser.on("--include_author_reference=INC","Whether reference of author id should be added or not") do |value|
|
62
|
+
options[:include_author_reference] = value == "true"
|
63
|
+
end
|
64
|
+
|
65
|
+
parser.on("--author_table=TABLE","Table to reference author id from") do |value|
|
66
|
+
options[:author_table_name] = value
|
67
|
+
end
|
68
|
+
|
69
|
+
parser.on("--start_column_name=NAME","Name of column that represents start of period") do |value|
|
70
|
+
options[:start_column_name] = value
|
71
|
+
end
|
72
|
+
|
73
|
+
parser.on("--end_column_name=NAME","Name of column that represents end of period") do |value|
|
74
|
+
options[:end_column_name] = value
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
execute <<-SQL
|
4
|
+
ALTER TABLE <%= @template_options[:table_name] %>
|
5
|
+
<%- if @template_options[:add_columns] -%>
|
6
|
+
ADD COLUMN <%= @template_options[:start_column_name] %> <%= @template_options[:column_type] %>,
|
7
|
+
ADD COLUMN <%= @template_options[:end_column_name] %> <%= @template_options[:column_type] %>,
|
8
|
+
<%- end -%>
|
9
|
+
ADD PERIOD FOR p(<%= @template_options[:start_column_name] %>,<%= @template_options[:end_column_name] %>),
|
10
|
+
<%- if @template_options[:replace_primary_key] -%>
|
11
|
+
DROP PRIMARY KEY,
|
12
|
+
ADD PRIMARY KEY(id, <%= @template_options[:end_column_name] %>)
|
13
|
+
<%- end -%>
|
14
|
+
SQL
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
|
3
|
+
def change
|
4
|
+
<%- if @template_options[:include_change_list] -%>
|
5
|
+
add_column :<%= @template_options[:table_name] %>, :change_list, :text
|
6
|
+
<%- end -%>
|
7
|
+
<%- if @template_options[:include_author_reference] -%>
|
8
|
+
add_column :<%= @template_options[:table_name] %>, :author_id, :bigint
|
9
|
+
add_foreign_key :<%= @template_options[:table_name] %>, :<%= @template_options[:author_table_name] %>, column: :author_id, primary_key: :id
|
10
|
+
<%- end -%>
|
11
|
+
execute <<-SQL
|
12
|
+
ALTER TABLE <%= @template_options[:table_name] %>
|
13
|
+
ADD COLUMN <%= @template_options[:start_column_name] %> TIMESTAMP(6) GENERATED ALWAYS AS ROW START,
|
14
|
+
ADD COLUMN <%= @template_options[:end_column_name] %> TIMESTAMP(6) GENERATED ALWAYS AS ROW END,
|
15
|
+
ADD PERIOD FOR SYSTEM_TIME(<%= @template_options[:start_column_name] %>, <%= @template_options[:end_column_name] %>),
|
16
|
+
ADD SYSTEM VERSIONING;
|
17
|
+
SQL
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mariadb_temporal_tables
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Youssef Henna
|
@@ -74,6 +74,16 @@ extensions: []
|
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
76
|
- lib/mariadb_temporal_tables.rb
|
77
|
+
- lib/mariadb_temporal_tables/concerns/application_versioning.rb
|
78
|
+
- lib/mariadb_temporal_tables/concerns/combined_versioning.rb
|
79
|
+
- lib/mariadb_temporal_tables/concerns/system_versioning.rb
|
80
|
+
- lib/mariadb_temporal_tables/railtie.rb
|
81
|
+
- lib/tasks/gen_migration_application.rake
|
82
|
+
- lib/tasks/gen_migration_system.rake
|
83
|
+
- lib/tasks/helpers/migration_generator.rb
|
84
|
+
- lib/tasks/helpers/parser.rb
|
85
|
+
- lib/tasks/migration_templates/application_versioning.rb.erb
|
86
|
+
- lib/tasks/migration_templates/system_versioning.rb.erb
|
77
87
|
homepage: https://github.com/YoussefHenna/mariadb_temporal_tables
|
78
88
|
licenses:
|
79
89
|
- MIT
|