partition_gardener 0.3.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/CHANGELOG.md +36 -0
- data/LICENSE.md +21 -0
- data/README.md +203 -0
- data/SECURITY.md +27 -0
- data/docs/THIRD_PARTY_LICENSE_MANIFEST.tsv +60 -0
- data/docs/application_contract.md +82 -0
- data/docs/audit_reference.md +125 -0
- data/docs/background_job.md +161 -0
- data/docs/cli.md +71 -0
- data/docs/configuration.md +261 -0
- data/docs/cutover.md +180 -0
- data/docs/decision_flow.md +154 -0
- data/docs/host_testing.md +91 -0
- data/docs/monitoring.md +110 -0
- data/docs/naming.md +71 -0
- data/docs/operations.md +131 -0
- data/docs/partition_landscape.md +323 -0
- data/docs/pg_party_recipe.md +44 -0
- data/docs/related_postgres_tooling.md +195 -0
- data/docs/retention.md +65 -0
- data/docs/schemas/partition_garden.schema.json +114 -0
- data/docs/schemas/plan_report.schema.json +82 -0
- data/docs/tooling_split.md +79 -0
- data/exe/partition_gardener +6 -0
- data/lib/partition_gardener/active_record_run_record_store.rb +1 -0
- data/lib/partition_gardener/advisory_lock.rb +48 -0
- data/lib/partition_gardener/archive_retention.rb +88 -0
- data/lib/partition_gardener/audit.rb +93 -0
- data/lib/partition_gardener/blank.rb +13 -0
- data/lib/partition_gardener/cli.rb +190 -0
- data/lib/partition_gardener/config_document.rb +240 -0
- data/lib/partition_gardener/configuration.rb +102 -0
- data/lib/partition_gardener/connection.rb +260 -0
- data/lib/partition_gardener/date_bucket.rb +117 -0
- data/lib/partition_gardener/date_calendar.rb +65 -0
- data/lib/partition_gardener/date_range_maintenance.rb +297 -0
- data/lib/partition_gardener/default_partition.rb +54 -0
- data/lib/partition_gardener/executor.rb +324 -0
- data/lib/partition_gardener/gap_detection.rb +59 -0
- data/lib/partition_gardener/hash_routing.rb +70 -0
- data/lib/partition_gardener/layout/calendar_year.rb +28 -0
- data/lib/partition_gardener/layout/integer_window.rb +28 -0
- data/lib/partition_gardener/layout/sliding_window.rb +28 -0
- data/lib/partition_gardener/layout/three_area.rb +27 -0
- data/lib/partition_gardener/layout/zone_segments.rb +62 -0
- data/lib/partition_gardener/lock_not_acquired.rb +3 -0
- data/lib/partition_gardener/maintenance_backend.rb +73 -0
- data/lib/partition_gardener/memory_run_record_store.rb +19 -0
- data/lib/partition_gardener/migration/hot_switch_concern.rb +445 -0
- data/lib/partition_gardener/missing_conflict_index.rb +3 -0
- data/lib/partition_gardener/naming.rb +29 -0
- data/lib/partition_gardener/orphaned_rebalance_staging.rb +3 -0
- data/lib/partition_gardener/pg_connection.rb +94 -0
- data/lib/partition_gardener/plan.rb +49 -0
- data/lib/partition_gardener/plan_applier.rb +289 -0
- data/lib/partition_gardener/plan_diff.rb +51 -0
- data/lib/partition_gardener/plan_report.rb +95 -0
- data/lib/partition_gardener/planner.rb +21 -0
- data/lib/partition_gardener/predicate.rb +85 -0
- data/lib/partition_gardener/premake_monthly_maintenance.rb +44 -0
- data/lib/partition_gardener/rails.rb +12 -0
- data/lib/partition_gardener/registry.rb +84 -0
- data/lib/partition_gardener/run_failed.rb +10 -0
- data/lib/partition_gardener/run_metrics.rb +65 -0
- data/lib/partition_gardener/run_record.rb +76 -0
- data/lib/partition_gardener/sql_run_record_store.rb +106 -0
- data/lib/partition_gardener/stdlib_extensions.rb +15 -0
- data/lib/partition_gardener/strategy/composite.rb +27 -0
- data/lib/partition_gardener/strategy/cursor_columns.rb +18 -0
- data/lib/partition_gardener/strategy/date_range.rb +303 -0
- data/lib/partition_gardener/strategy/hash_branches.rb +161 -0
- data/lib/partition_gardener/strategy/integer_range.rb +261 -0
- data/lib/partition_gardener/strategy/list_split.rb +125 -0
- data/lib/partition_gardener/strategy/requires_default_partition.rb +19 -0
- data/lib/partition_gardener/strategy.rb +26 -0
- data/lib/partition_gardener/templates.rb +373 -0
- data/lib/partition_gardener/unmoved_rows_remaining.rb +15 -0
- data/lib/partition_gardener/version.rb +3 -0
- data/lib/partition_gardener.rb +215 -0
- data/sig/partition_gardener.rbs +19 -0
- metadata +367 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module Connection
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def connection
|
|
6
|
+
PartitionGardener.configuration.connection
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def schema_name
|
|
10
|
+
PartitionGardener.configuration.schema_name
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def quoted_table(name)
|
|
14
|
+
connection.quote_table_name(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def table_is_partitioned?(table_name)
|
|
18
|
+
return false unless connection.table_exists?(table_name)
|
|
19
|
+
|
|
20
|
+
sql = <<~SQL
|
|
21
|
+
SELECT COUNT(*) AS count
|
|
22
|
+
FROM pg_partitioned_table pt
|
|
23
|
+
JOIN pg_class c ON c.oid = pt.partrelid
|
|
24
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
25
|
+
WHERE n.nspname = #{connection.quote(schema_name)}
|
|
26
|
+
AND c.relname = #{connection.quote(table_name)}
|
|
27
|
+
SQL
|
|
28
|
+
connection.execute(sql).first["count"].to_i.positive?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def partition_exists?(partition_name)
|
|
32
|
+
connection.table_exists?(partition_name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def partition_attached?(table_name, partition_name)
|
|
36
|
+
sql = <<~SQL
|
|
37
|
+
SELECT COUNT(*) AS count
|
|
38
|
+
FROM pg_catalog.pg_inherits i
|
|
39
|
+
JOIN pg_class parent ON parent.oid = i.inhparent
|
|
40
|
+
JOIN pg_namespace parent_ns ON parent_ns.oid = parent.relnamespace
|
|
41
|
+
JOIN pg_class child ON child.oid = i.inhrelid
|
|
42
|
+
JOIN pg_namespace child_ns ON child_ns.oid = child.relnamespace
|
|
43
|
+
WHERE parent_ns.nspname = #{connection.quote(schema_name)}
|
|
44
|
+
AND child_ns.nspname = #{connection.quote(schema_name)}
|
|
45
|
+
AND parent.relname = #{connection.quote(table_name)}
|
|
46
|
+
AND child.relname = #{connection.quote(partition_name)}
|
|
47
|
+
SQL
|
|
48
|
+
|
|
49
|
+
connection.execute(sql).first["count"].to_i.positive?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def current_partition_lower_bound(table_name, partition_name)
|
|
53
|
+
sql = <<~SQL
|
|
54
|
+
SELECT pg_get_expr(child.relpartbound, child.oid) AS bound_expression
|
|
55
|
+
FROM pg_catalog.pg_inherits inheritance
|
|
56
|
+
JOIN pg_class parent ON parent.oid = inheritance.inhparent
|
|
57
|
+
JOIN pg_namespace parent_namespace ON parent_namespace.oid = parent.relnamespace
|
|
58
|
+
JOIN pg_class child ON child.oid = inheritance.inhrelid
|
|
59
|
+
JOIN pg_namespace child_namespace ON child_namespace.oid = child.relnamespace
|
|
60
|
+
WHERE parent_namespace.nspname = #{connection.quote(schema_name)}
|
|
61
|
+
AND child_namespace.nspname = #{connection.quote(schema_name)}
|
|
62
|
+
AND parent.relname = #{connection.quote(table_name)}
|
|
63
|
+
AND child.relname = #{connection.quote(partition_name)}
|
|
64
|
+
SQL
|
|
65
|
+
|
|
66
|
+
bound_expression = connection.execute(sql).first&.fetch("bound_expression", nil)
|
|
67
|
+
return if Blank.blank?(bound_expression)
|
|
68
|
+
|
|
69
|
+
match = bound_expression.match(/FROM \('([^']+)'\)/)
|
|
70
|
+
match ? Date.parse(match[1]) : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def count_rows_in_partition_table(partition_name)
|
|
74
|
+
sql = <<~SQL
|
|
75
|
+
SELECT COUNT(*) AS count
|
|
76
|
+
FROM #{quoted_table(partition_name)}
|
|
77
|
+
SQL
|
|
78
|
+
connection.execute(sql).first["count"].to_i
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def count_rows_in_partition(partition_name, where_condition)
|
|
82
|
+
sql = <<~SQL
|
|
83
|
+
SELECT COUNT(*) AS count
|
|
84
|
+
FROM #{quoted_table(partition_name)}
|
|
85
|
+
WHERE #{where_condition}
|
|
86
|
+
SQL
|
|
87
|
+
connection.execute(sql).first["count"].to_i
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def attached_partitions(table_name)
|
|
91
|
+
attached_partitions_cache[table_name] ||= fetch_attached_partitions(table_name)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def clear_attached_partitions_cache!
|
|
95
|
+
Thread.current[:partition_gardener_attached_partitions] = nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def fetch_attached_partitions(table_name)
|
|
99
|
+
sql = <<~SQL
|
|
100
|
+
SELECT child.relname AS partition_name,
|
|
101
|
+
pg_get_expr(child.relpartbound, child.oid) AS bound_expression
|
|
102
|
+
FROM pg_catalog.pg_inherits inheritance
|
|
103
|
+
JOIN pg_class parent ON parent.oid = inheritance.inhparent
|
|
104
|
+
JOIN pg_namespace parent_namespace ON parent_namespace.oid = parent.relnamespace
|
|
105
|
+
JOIN pg_class child ON child.oid = inheritance.inhrelid
|
|
106
|
+
JOIN pg_namespace child_namespace ON child_namespace.oid = child.relnamespace
|
|
107
|
+
WHERE parent_namespace.nspname = #{connection.quote(schema_name)}
|
|
108
|
+
AND child_namespace.nspname = #{connection.quote(schema_name)}
|
|
109
|
+
AND parent.relname = #{connection.quote(table_name)}
|
|
110
|
+
ORDER BY child.relname
|
|
111
|
+
SQL
|
|
112
|
+
|
|
113
|
+
connection.execute(sql).filter_map do |row|
|
|
114
|
+
parse_attached_partition(row["partition_name"], row["bound_expression"])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def attached_partitions_cache
|
|
119
|
+
Thread.current[:partition_gardener_attached_partitions] ||= {}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private :attached_partitions_cache, :fetch_attached_partitions
|
|
123
|
+
|
|
124
|
+
def parse_attached_partition(partition_name, bound_expression)
|
|
125
|
+
return if Blank.blank?(bound_expression)
|
|
126
|
+
|
|
127
|
+
if bound_expression == "DEFAULT"
|
|
128
|
+
return AttachedPartition.new(
|
|
129
|
+
name: partition_name,
|
|
130
|
+
range_start: nil,
|
|
131
|
+
range_end: nil,
|
|
132
|
+
default: true,
|
|
133
|
+
list_values: nil
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
hash_match = bound_expression.match(/modulus (\d+), remainder (\d+)/)
|
|
138
|
+
if hash_match
|
|
139
|
+
return AttachedPartition.new(
|
|
140
|
+
name: partition_name,
|
|
141
|
+
range_start: {modulus: hash_match[1].to_i, remainder: hash_match[2].to_i},
|
|
142
|
+
range_end: nil,
|
|
143
|
+
default: false,
|
|
144
|
+
list_values: nil
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
list_match = bound_expression.match(/^IN \((.+)\)$/)
|
|
149
|
+
if list_match
|
|
150
|
+
values = list_match[1].split(",").map { |value| parse_list_value(value.strip) }
|
|
151
|
+
return AttachedPartition.new(
|
|
152
|
+
name: partition_name,
|
|
153
|
+
range_start: values.first,
|
|
154
|
+
range_end: nil,
|
|
155
|
+
default: false,
|
|
156
|
+
list_values: values
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
from_match = bound_expression.match(/FROM \('([^']+)'\)/) || bound_expression.match(/FROM \((\d+)\)/)
|
|
161
|
+
to_match = bound_expression.match(/TO \('([^']+)'\)/) || bound_expression.match(/TO \((\d+)\)/)
|
|
162
|
+
range_end = if bound_expression.include?("MAXVALUE")
|
|
163
|
+
:max
|
|
164
|
+
elsif to_match
|
|
165
|
+
parse_bound_value(to_match[1])
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
AttachedPartition.new(
|
|
169
|
+
name: partition_name,
|
|
170
|
+
range_start: from_match ? parse_bound_value(from_match[1]) : nil,
|
|
171
|
+
range_end: range_end,
|
|
172
|
+
default: false,
|
|
173
|
+
list_values: nil
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_list_value(value)
|
|
178
|
+
return nil if value.upcase == "NULL"
|
|
179
|
+
|
|
180
|
+
value.delete_prefix("'").delete_suffix("'")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def parse_bound_value(value)
|
|
184
|
+
return Date.parse(value) if value.include?("-")
|
|
185
|
+
|
|
186
|
+
value.to_i
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
AttachedPartition = Data.define(:name, :range_start, :range_end, :default, :list_values) do
|
|
190
|
+
def signature
|
|
191
|
+
[name, range_start, range_end, default, list_values]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def get_distinct_partition_identifiers(partition_name, partition_key_column, extract_partition_identifier)
|
|
196
|
+
if partition_key_column.include?("::")
|
|
197
|
+
base_column = partition_key_column.split("::").first.strip
|
|
198
|
+
sql = <<~SQL
|
|
199
|
+
SELECT DISTINCT #{partition_key_column} AS partition_key_value
|
|
200
|
+
FROM #{quoted_table(partition_name)}
|
|
201
|
+
WHERE #{connection.quote_column_name(base_column)} IS NOT NULL
|
|
202
|
+
ORDER BY #{partition_key_column}
|
|
203
|
+
SQL
|
|
204
|
+
else
|
|
205
|
+
sql = <<~SQL
|
|
206
|
+
SELECT DISTINCT #{connection.quote_column_name(partition_key_column)} AS partition_key_value
|
|
207
|
+
FROM #{quoted_table(partition_name)}
|
|
208
|
+
WHERE #{connection.quote_column_name(partition_key_column)} IS NOT NULL
|
|
209
|
+
ORDER BY #{connection.quote_column_name(partition_key_column)}
|
|
210
|
+
SQL
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
connection.execute(sql).to_a.filter_map do |row|
|
|
214
|
+
value = row["partition_key_value"]
|
|
215
|
+
extract_partition_identifier.call(value) if Blank.present?(value)
|
|
216
|
+
end.uniq
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def unique_index_column_sets(table_name)
|
|
220
|
+
sql = <<~SQL
|
|
221
|
+
SELECT i.indisprimary AS is_primary,
|
|
222
|
+
array_agg(a.attname ORDER BY key_ord.ordinality) AS column_names
|
|
223
|
+
FROM pg_index i
|
|
224
|
+
JOIN pg_class table_class ON table_class.oid = i.indrelid
|
|
225
|
+
JOIN pg_namespace table_namespace ON table_namespace.oid = table_class.relnamespace
|
|
226
|
+
JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS key_ord(attnum, ordinality) ON true
|
|
227
|
+
JOIN pg_attribute a ON a.attrelid = table_class.oid AND a.attnum = key_ord.attnum
|
|
228
|
+
WHERE table_namespace.nspname = #{connection.quote(schema_name)}
|
|
229
|
+
AND table_class.relname = #{connection.quote(table_name)}
|
|
230
|
+
AND (i.indisunique OR i.indisprimary)
|
|
231
|
+
GROUP BY i.indexrelid, i.indisprimary
|
|
232
|
+
SQL
|
|
233
|
+
|
|
234
|
+
connection.execute(sql).map do |row|
|
|
235
|
+
columns = row["column_names"]
|
|
236
|
+
columns = columns.gsub(/[{}]/, "").split(",") if columns.is_a?(String)
|
|
237
|
+
columns
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def unique_index_covers?(table_name, conflict_key)
|
|
242
|
+
conflict_columns = conflict_key.map(&:to_s)
|
|
243
|
+
unique_index_column_sets(table_name).any? do |index_columns|
|
|
244
|
+
index_columns.first(conflict_columns.length) == conflict_columns
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def partman_parent_configured?(table_name)
|
|
249
|
+
qualified_name = "#{schema_name}.#{table_name}"
|
|
250
|
+
sql = <<~SQL
|
|
251
|
+
SELECT 1
|
|
252
|
+
FROM partman.part_config
|
|
253
|
+
WHERE parent_table = #{connection.quote(qualified_name)}
|
|
254
|
+
LIMIT 1
|
|
255
|
+
SQL
|
|
256
|
+
|
|
257
|
+
connection.execute(sql).any?
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module DateBucket
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def normalize(bucket)
|
|
6
|
+
bucket.to_sym
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def active_key(bucket)
|
|
10
|
+
case normalize(bucket)
|
|
11
|
+
when :day then :active_days
|
|
12
|
+
when :week then :active_weeks
|
|
13
|
+
when :quarter then :active_quarters
|
|
14
|
+
when :year then :active_years
|
|
15
|
+
else :active_months
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def default_active_span(bucket)
|
|
20
|
+
case normalize(bucket)
|
|
21
|
+
when :day then 90
|
|
22
|
+
when :week then 52
|
|
23
|
+
when :quarter then 8
|
|
24
|
+
when :year then 2
|
|
25
|
+
else 12
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def date_trunc_unit(bucket)
|
|
30
|
+
case normalize(bucket)
|
|
31
|
+
when :day then "day"
|
|
32
|
+
when :week then "week"
|
|
33
|
+
when :quarter then "quarter"
|
|
34
|
+
when :year then "year"
|
|
35
|
+
else "month"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def beginning_of_bucket(date, bucket)
|
|
40
|
+
case normalize(bucket)
|
|
41
|
+
when :day then DateCalendar.beginning_of_day(date)
|
|
42
|
+
when :week then DateCalendar.beginning_of_week(date)
|
|
43
|
+
when :quarter then DateCalendar.beginning_of_quarter(date)
|
|
44
|
+
when :year then DateCalendar.beginning_of_year(date)
|
|
45
|
+
else DateCalendar.beginning_of_month(date)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def end_of_bucket(date, bucket)
|
|
50
|
+
beginning_of_bucket(next_bucket(date, bucket), bucket)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def next_bucket(date, bucket)
|
|
54
|
+
case normalize(bucket)
|
|
55
|
+
when :day then DateCalendar.add_days(date, 1)
|
|
56
|
+
when :week then DateCalendar.add_weeks(date, 1)
|
|
57
|
+
when :quarter then DateCalendar.add_quarters(date, 1)
|
|
58
|
+
when :year then DateCalendar.next_year(date)
|
|
59
|
+
else DateCalendar.next_month(date)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add_buckets(date, count, bucket)
|
|
64
|
+
case normalize(bucket)
|
|
65
|
+
when :day then DateCalendar.add_days(date, count)
|
|
66
|
+
when :week then DateCalendar.add_weeks(date, count)
|
|
67
|
+
when :quarter then DateCalendar.add_quarters(date, count)
|
|
68
|
+
when :year then DateCalendar.add_years(date, count)
|
|
69
|
+
else DateCalendar.add_months(date, count)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def partition_name_suffix(identifier, bucket)
|
|
74
|
+
case normalize(bucket)
|
|
75
|
+
when :day then identifier.strftime("%Y_%m_%d")
|
|
76
|
+
when :week then identifier.strftime("%G_W%V")
|
|
77
|
+
when :quarter
|
|
78
|
+
quarter = ((identifier.month - 1) / 3) + 1
|
|
79
|
+
format("%d_Q%d", identifier.year, quarter)
|
|
80
|
+
when :year then identifier.year.to_s
|
|
81
|
+
else identifier.strftime("%Y_%m")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def partition_name(table_name, identifier, bucket)
|
|
86
|
+
"#{table_name}_#{partition_name_suffix(identifier, bucket)}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def archive_bucket_from_partition_name(table_name, partition_name, bucket)
|
|
90
|
+
case normalize(bucket)
|
|
91
|
+
when :day
|
|
92
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})_(\d{2})_(\d{2})$/)
|
|
93
|
+
return Date.new(match[1].to_i, match[2].to_i, match[3].to_i) if match
|
|
94
|
+
when :week
|
|
95
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})_W(\d{2})$/)
|
|
96
|
+
return DateCalendar.beginning_of_week(Date.strptime("#{match[1]}-W#{match[2]}-1", "%G-W%V-%u")) if match
|
|
97
|
+
when :quarter
|
|
98
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})_Q([1-4])$/)
|
|
99
|
+
return Date.new(match[1].to_i, ((match[2].to_i - 1) * 3) + 1, 1) if match
|
|
100
|
+
when :year
|
|
101
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})$/)
|
|
102
|
+
return Date.new(match[1].to_i, 1, 1) if match
|
|
103
|
+
else
|
|
104
|
+
match = partition_name.match(/^#{Regexp.escape(table_name)}_(\d{4})_(\d{2})$/)
|
|
105
|
+
return Date.new(match[1].to_i, match[2].to_i, 1) if match
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def partition_definition_clause(date, bucket)
|
|
112
|
+
start_range = beginning_of_bucket(date, bucket)
|
|
113
|
+
end_range = end_of_bucket(date, bucket)
|
|
114
|
+
"FROM ('#{start_range}') TO ('#{end_range}')"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module PartitionGardener
|
|
2
|
+
module DateCalendar
|
|
3
|
+
module_function
|
|
4
|
+
|
|
5
|
+
def to_date(value)
|
|
6
|
+
value.respond_to?(:to_date) ? value.to_date : Date.parse(value.to_s)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def beginning_of_month(date)
|
|
10
|
+
value = to_date(date)
|
|
11
|
+
Date.new(value.year, value.month, 1)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def beginning_of_year(date)
|
|
15
|
+
value = to_date(date)
|
|
16
|
+
Date.new(value.year, 1, 1)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def beginning_of_day(date)
|
|
20
|
+
to_date(date)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def beginning_of_week(date)
|
|
24
|
+
value = to_date(date)
|
|
25
|
+
value - ((value.wday + 6) % 7)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def beginning_of_quarter(date)
|
|
29
|
+
value = to_date(date)
|
|
30
|
+
quarter_month = ((value.month - 1) / 3) * 3 + 1
|
|
31
|
+
Date.new(value.year, quarter_month, 1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_days(date, count)
|
|
35
|
+
to_date(date) + count
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_weeks(date, count)
|
|
39
|
+
add_days(date, count * 7)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def add_quarters(date, count)
|
|
43
|
+
add_months(date, count * 3)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def next_month(date)
|
|
47
|
+
add_months(beginning_of_month(date), 1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def next_year(date)
|
|
51
|
+
Date.new(to_date(date).year + 1, 1, 1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_years(date, count)
|
|
55
|
+
value = to_date(date)
|
|
56
|
+
Date.new(value.year + count, value.month, value.day)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_months(date, count)
|
|
60
|
+
base = beginning_of_month(date)
|
|
61
|
+
month_index = (base.year * 12) + (base.month - 1) + count
|
|
62
|
+
Date.new(month_index / 12, (month_index % 12) + 1, 1)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|