pgslice 0.4.2 → 0.4.7
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/CHANGELOG.md +48 -22
- data/LICENSE.txt +1 -1
- data/README.md +99 -126
- data/exe/pgslice +4 -7
- data/lib/pgslice.rb +18 -740
- data/lib/pgslice/cli.rb +32 -0
- data/lib/pgslice/cli/add_partitions.rb +133 -0
- data/lib/pgslice/cli/analyze.rb +13 -0
- data/lib/pgslice/cli/fill.rb +103 -0
- data/lib/pgslice/cli/prep.rb +97 -0
- data/lib/pgslice/cli/swap.rb +28 -0
- data/lib/pgslice/cli/unprep.rb +18 -0
- data/lib/pgslice/cli/unswap.rb +25 -0
- data/lib/pgslice/helpers.rb +180 -0
- data/lib/pgslice/table.rb +199 -0
- data/lib/pgslice/version.rb +1 -1
- metadata +18 -9
@@ -0,0 +1,180 @@
|
|
1
|
+
module PgSlice
|
2
|
+
module Helpers
|
3
|
+
SQL_FORMAT = {
|
4
|
+
day: "YYYYMMDD",
|
5
|
+
month: "YYYYMM",
|
6
|
+
year: "YYYY"
|
7
|
+
}
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
# output
|
12
|
+
|
13
|
+
def log(message = nil)
|
14
|
+
error message
|
15
|
+
end
|
16
|
+
|
17
|
+
def log_sql(message = nil)
|
18
|
+
say message
|
19
|
+
end
|
20
|
+
|
21
|
+
def abort(message)
|
22
|
+
raise Thor::Error, message
|
23
|
+
end
|
24
|
+
|
25
|
+
# database connection
|
26
|
+
|
27
|
+
def connection
|
28
|
+
@connection ||= begin
|
29
|
+
url = options[:url] || ENV["PGSLICE_URL"]
|
30
|
+
abort "Set PGSLICE_URL or use the --url option" unless url
|
31
|
+
|
32
|
+
uri = URI.parse(url)
|
33
|
+
params = CGI.parse(uri.query.to_s)
|
34
|
+
# remove schema
|
35
|
+
@schema = Array(params.delete("schema") || "public")[0]
|
36
|
+
uri.query = URI.encode_www_form(params)
|
37
|
+
|
38
|
+
ENV["PGCONNECT_TIMEOUT"] ||= "1"
|
39
|
+
conn = PG::Connection.new(uri.to_s)
|
40
|
+
conn.set_notice_processor do |message|
|
41
|
+
say message
|
42
|
+
end
|
43
|
+
conn
|
44
|
+
end
|
45
|
+
rescue PG::ConnectionBad => e
|
46
|
+
abort e.message
|
47
|
+
rescue URI::InvalidURIError
|
48
|
+
abort "Invalid url"
|
49
|
+
end
|
50
|
+
|
51
|
+
def schema
|
52
|
+
connection # ensure called first
|
53
|
+
@schema
|
54
|
+
end
|
55
|
+
|
56
|
+
def execute(query, params = [])
|
57
|
+
connection.exec_params(query, params).to_a
|
58
|
+
end
|
59
|
+
|
60
|
+
def run_queries(queries)
|
61
|
+
connection.transaction do
|
62
|
+
execute("SET LOCAL client_min_messages TO warning") unless options[:dry_run]
|
63
|
+
log_sql "BEGIN;"
|
64
|
+
log_sql
|
65
|
+
run_queries_without_transaction(queries)
|
66
|
+
log_sql "COMMIT;"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def run_query(query)
|
71
|
+
log_sql query
|
72
|
+
unless options[:dry_run]
|
73
|
+
begin
|
74
|
+
execute(query)
|
75
|
+
rescue PG::ServerError => e
|
76
|
+
abort("#{e.class.name}: #{e.message}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
log_sql
|
80
|
+
end
|
81
|
+
|
82
|
+
def run_queries_without_transaction(queries)
|
83
|
+
queries.each do |query|
|
84
|
+
run_query(query)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def server_version_num
|
89
|
+
execute("SHOW server_version_num")[0]["server_version_num"].to_i
|
90
|
+
end
|
91
|
+
|
92
|
+
# helpers
|
93
|
+
|
94
|
+
def sql_date(time, cast, add_cast = true)
|
95
|
+
if cast == "timestamptz"
|
96
|
+
fmt = "%Y-%m-%d %H:%M:%S UTC"
|
97
|
+
else
|
98
|
+
fmt = "%Y-%m-%d"
|
99
|
+
end
|
100
|
+
str = "'#{time.strftime(fmt)}'"
|
101
|
+
add_cast ? "#{str}::#{cast}" : str
|
102
|
+
end
|
103
|
+
|
104
|
+
def name_format(period)
|
105
|
+
case period.to_sym
|
106
|
+
when :day
|
107
|
+
"%Y%m%d"
|
108
|
+
when :month
|
109
|
+
"%Y%m"
|
110
|
+
else
|
111
|
+
"%Y"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def partition_date(partition, name_format)
|
116
|
+
DateTime.strptime(partition.name.split("_").last, name_format)
|
117
|
+
end
|
118
|
+
|
119
|
+
def round_date(date, period)
|
120
|
+
date = date.to_date
|
121
|
+
case period.to_sym
|
122
|
+
when :day
|
123
|
+
date
|
124
|
+
when :month
|
125
|
+
Date.new(date.year, date.month)
|
126
|
+
else
|
127
|
+
Date.new(date.year)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def assert_table(table)
|
132
|
+
abort "Table not found: #{table}" unless table.exists?
|
133
|
+
end
|
134
|
+
|
135
|
+
def assert_no_table(table)
|
136
|
+
abort "Table already exists: #{table}" if table.exists?
|
137
|
+
end
|
138
|
+
|
139
|
+
def advance_date(date, period, count = 1)
|
140
|
+
date = date.to_date
|
141
|
+
case period.to_sym
|
142
|
+
when :day
|
143
|
+
date.next_day(count)
|
144
|
+
when :month
|
145
|
+
date.next_month(count)
|
146
|
+
else
|
147
|
+
date.next_year(count)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def quote_ident(value)
|
152
|
+
PG::Connection.quote_ident(value)
|
153
|
+
end
|
154
|
+
|
155
|
+
def quote_table(table)
|
156
|
+
table.quote_table
|
157
|
+
end
|
158
|
+
|
159
|
+
def quote_no_schema(table)
|
160
|
+
quote_ident(table.name)
|
161
|
+
end
|
162
|
+
|
163
|
+
def create_table(name)
|
164
|
+
if name.include?(".")
|
165
|
+
schema, name = name.split(".", 2)
|
166
|
+
else
|
167
|
+
schema = self.schema
|
168
|
+
end
|
169
|
+
Table.new(schema, name)
|
170
|
+
end
|
171
|
+
|
172
|
+
def make_index_def(index_def, table)
|
173
|
+
index_def.sub(/ ON \S+ USING /, " ON #{quote_table(table)} USING ").sub(/ INDEX .+ ON /, " INDEX ON ") + ";"
|
174
|
+
end
|
175
|
+
|
176
|
+
def make_fk_def(fk_def, table)
|
177
|
+
"ALTER TABLE #{quote_table(table)} ADD #{fk_def};"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module PgSlice
|
2
|
+
class Table
|
3
|
+
attr_reader :schema, :name
|
4
|
+
|
5
|
+
def initialize(schema, name)
|
6
|
+
@schema = schema
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
[schema, name].join(".")
|
12
|
+
end
|
13
|
+
|
14
|
+
def exists?
|
15
|
+
execute("SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE schemaname = $1 AND tablename = $2", [schema, name]).first["count"].to_i > 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def columns
|
19
|
+
execute("SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2", [schema, name]).map{ |r| r["column_name"] }
|
20
|
+
end
|
21
|
+
|
22
|
+
# http://www.dbforums.com/showthread.php?1667561-How-to-list-sequences-and-the-columns-by-SQL
|
23
|
+
def sequences
|
24
|
+
query = <<-SQL
|
25
|
+
SELECT
|
26
|
+
a.attname AS related_column,
|
27
|
+
n.nspname AS sequence_schema,
|
28
|
+
s.relname AS sequence_name
|
29
|
+
FROM pg_class s
|
30
|
+
INNER JOIN pg_depend d ON d.objid = s.oid
|
31
|
+
INNER JOIN pg_class t ON d.objid = s.oid AND d.refobjid = t.oid
|
32
|
+
INNER JOIN pg_attribute a ON (d.refobjid, d.refobjsubid) = (a.attrelid, a.attnum)
|
33
|
+
INNER JOIN pg_namespace n ON n.oid = s.relnamespace
|
34
|
+
INNER JOIN pg_namespace nt ON nt.oid = t.relnamespace
|
35
|
+
WHERE s.relkind = 'S'
|
36
|
+
AND nt.nspname = $1
|
37
|
+
AND t.relname = $2
|
38
|
+
ORDER BY s.relname ASC
|
39
|
+
SQL
|
40
|
+
execute(query, [schema, name])
|
41
|
+
end
|
42
|
+
|
43
|
+
def foreign_keys
|
44
|
+
execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = #{regclass} AND contype ='f'").map { |r| r["pg_get_constraintdef"] }
|
45
|
+
end
|
46
|
+
|
47
|
+
# https://stackoverflow.com/a/20537829
|
48
|
+
# TODO can simplify with array_position in Postgres 9.5+
|
49
|
+
def primary_key
|
50
|
+
query = <<-SQL
|
51
|
+
SELECT
|
52
|
+
pg_attribute.attname,
|
53
|
+
format_type(pg_attribute.atttypid, pg_attribute.atttypmod),
|
54
|
+
pg_attribute.attnum,
|
55
|
+
pg_index.indkey
|
56
|
+
FROM
|
57
|
+
pg_index, pg_class, pg_attribute, pg_namespace
|
58
|
+
WHERE
|
59
|
+
nspname = $1 AND
|
60
|
+
relname = $2 AND
|
61
|
+
indrelid = pg_class.oid AND
|
62
|
+
pg_class.relnamespace = pg_namespace.oid AND
|
63
|
+
pg_attribute.attrelid = pg_class.oid AND
|
64
|
+
pg_attribute.attnum = any(pg_index.indkey) AND
|
65
|
+
indisprimary
|
66
|
+
SQL
|
67
|
+
rows = execute(query, [schema, name])
|
68
|
+
rows.sort_by { |r| r["indkey"].split(" ").index(r["attnum"]) }.map { |r| r["attname"] }
|
69
|
+
end
|
70
|
+
|
71
|
+
def index_defs
|
72
|
+
execute("SELECT pg_get_indexdef(indexrelid) FROM pg_index WHERE indrelid = #{regclass} AND indisprimary = 'f'").map { |r| r["pg_get_indexdef"] }
|
73
|
+
end
|
74
|
+
|
75
|
+
def quote_table
|
76
|
+
[quote_ident(schema), quote_ident(name)].join(".")
|
77
|
+
end
|
78
|
+
|
79
|
+
def intermediate_table
|
80
|
+
self.class.new(schema, "#{name}_intermediate")
|
81
|
+
end
|
82
|
+
|
83
|
+
def retired_table
|
84
|
+
self.class.new(schema, "#{name}_retired")
|
85
|
+
end
|
86
|
+
|
87
|
+
def trigger_name
|
88
|
+
"#{name}_insert_trigger"
|
89
|
+
end
|
90
|
+
|
91
|
+
def column_cast(column)
|
92
|
+
data_type = execute("SELECT data_type FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3", [schema, name, column])[0]["data_type"]
|
93
|
+
data_type == "timestamp with time zone" ? "timestamptz" : "date"
|
94
|
+
end
|
95
|
+
|
96
|
+
def max_id(primary_key, below: nil, where: nil)
|
97
|
+
query = "SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_table}"
|
98
|
+
conditions = []
|
99
|
+
conditions << "#{quote_ident(primary_key)} <= #{below}" if below
|
100
|
+
conditions << where if where
|
101
|
+
query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
|
102
|
+
execute(query)[0]["max"].to_i
|
103
|
+
end
|
104
|
+
|
105
|
+
def min_id(primary_key, column, cast, starting_time, where)
|
106
|
+
query = "SELECT MIN(#{quote_ident(primary_key)}) FROM #{quote_table}"
|
107
|
+
conditions = []
|
108
|
+
conditions << "#{quote_ident(column)} >= #{sql_date(starting_time, cast)}" if starting_time
|
109
|
+
conditions << where if where
|
110
|
+
query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
|
111
|
+
(execute(query)[0]["min"] || 1).to_i
|
112
|
+
end
|
113
|
+
|
114
|
+
# ensure this returns partitions in the correct order
|
115
|
+
def partitions
|
116
|
+
query = <<-SQL
|
117
|
+
SELECT
|
118
|
+
nmsp_child.nspname AS schema,
|
119
|
+
child.relname AS name
|
120
|
+
FROM pg_inherits
|
121
|
+
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
|
122
|
+
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
|
123
|
+
JOIN pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace
|
124
|
+
JOIN pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace
|
125
|
+
WHERE
|
126
|
+
nmsp_parent.nspname = $1 AND
|
127
|
+
parent.relname = $2
|
128
|
+
ORDER BY child.relname ASC
|
129
|
+
SQL
|
130
|
+
execute(query, [schema, name]).map { |r| Table.new(r["schema"], r["name"]) }
|
131
|
+
end
|
132
|
+
|
133
|
+
def fetch_comment
|
134
|
+
execute("SELECT obj_description(#{regclass}) AS comment")[0]
|
135
|
+
end
|
136
|
+
|
137
|
+
def fetch_trigger(trigger_name)
|
138
|
+
execute("SELECT obj_description(oid, 'pg_trigger') AS comment FROM pg_trigger WHERE tgname = $1 AND tgrelid = #{regclass}", [trigger_name])[0]
|
139
|
+
end
|
140
|
+
|
141
|
+
# legacy
|
142
|
+
def fetch_settings(trigger_name)
|
143
|
+
needs_comment = false
|
144
|
+
trigger_comment = fetch_trigger(trigger_name)
|
145
|
+
comment = trigger_comment || fetch_comment
|
146
|
+
if comment
|
147
|
+
field, period, cast, version = comment["comment"].split(",").map { |v| v.split(":").last } rescue []
|
148
|
+
version = version.to_i if version
|
149
|
+
end
|
150
|
+
|
151
|
+
unless period
|
152
|
+
needs_comment = true
|
153
|
+
function_def = execute("SELECT pg_get_functiondef(oid) FROM pg_proc WHERE proname = $1", [trigger_name])[0]
|
154
|
+
return [] unless function_def
|
155
|
+
function_def = function_def["pg_get_functiondef"]
|
156
|
+
sql_format = Helpers::SQL_FORMAT.find { |_, f| function_def.include?("'#{f}'") }
|
157
|
+
return [] unless sql_format
|
158
|
+
period = sql_format[0]
|
159
|
+
field = /to_char\(NEW\.(\w+),/.match(function_def)[1]
|
160
|
+
end
|
161
|
+
|
162
|
+
# backwards compatibility with 0.2.3 and earlier (pre-timestamptz support)
|
163
|
+
unless cast
|
164
|
+
cast = "date"
|
165
|
+
# update comment to explicitly define cast
|
166
|
+
needs_comment = true
|
167
|
+
end
|
168
|
+
|
169
|
+
version ||= trigger_comment ? 1 : 2
|
170
|
+
declarative = version > 1
|
171
|
+
|
172
|
+
[period, field, cast, needs_comment, declarative, version]
|
173
|
+
end
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def execute(*args)
|
178
|
+
PgSlice::CLI.instance.send(:execute, *args)
|
179
|
+
end
|
180
|
+
|
181
|
+
def quote_ident(value)
|
182
|
+
PG::Connection.quote_ident(value)
|
183
|
+
end
|
184
|
+
|
185
|
+
def regclass
|
186
|
+
"'#{quote_table}'::regclass"
|
187
|
+
end
|
188
|
+
|
189
|
+
def sql_date(time, cast, add_cast = true)
|
190
|
+
if cast == "timestamptz"
|
191
|
+
fmt = "%Y-%m-%d %H:%M:%S UTC"
|
192
|
+
else
|
193
|
+
fmt = "%Y-%m-%d"
|
194
|
+
end
|
195
|
+
str = "'#{time.strftime(fmt)}'"
|
196
|
+
add_cast ? "#{str}::#{cast}" : str
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/pgslice/version.rb
CHANGED
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pgslice
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: thor
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: '0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: pg
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.18.2
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 0.18.2
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -92,6 +92,16 @@ files:
|
|
92
92
|
- README.md
|
93
93
|
- exe/pgslice
|
94
94
|
- lib/pgslice.rb
|
95
|
+
- lib/pgslice/cli.rb
|
96
|
+
- lib/pgslice/cli/add_partitions.rb
|
97
|
+
- lib/pgslice/cli/analyze.rb
|
98
|
+
- lib/pgslice/cli/fill.rb
|
99
|
+
- lib/pgslice/cli/prep.rb
|
100
|
+
- lib/pgslice/cli/swap.rb
|
101
|
+
- lib/pgslice/cli/unprep.rb
|
102
|
+
- lib/pgslice/cli/unswap.rb
|
103
|
+
- lib/pgslice/helpers.rb
|
104
|
+
- lib/pgslice/table.rb
|
95
105
|
- lib/pgslice/version.rb
|
96
106
|
homepage: https://github.com/ankane/pgslice
|
97
107
|
licenses:
|
@@ -112,8 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
122
|
- !ruby/object:Gem::Version
|
113
123
|
version: '0'
|
114
124
|
requirements: []
|
115
|
-
|
116
|
-
rubygems_version: 2.7.7
|
125
|
+
rubygems_version: 3.1.2
|
117
126
|
signing_key:
|
118
127
|
specification_version: 4
|
119
128
|
summary: Postgres partitioning as easy as pie
|