pgslice 0.7.0 → 0.7.1
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 +5 -0
- data/README.md +7 -3
- data/lib/pgslice/cli/add_partitions.rb +17 -17
- data/lib/pgslice/cli/analyze.rb +3 -1
- data/lib/pgslice/cli/fill.rb +5 -5
- data/lib/pgslice/cli/prep.rb +20 -23
- data/lib/pgslice/cli/unprep.rb +7 -2
- data/lib/pgslice/cli.rb +3 -5
- data/lib/pgslice/table.rb +46 -22
- data/lib/pgslice/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b8084c5bbabb6b85b6e90f03d1895d95c4fa9130dcf68fbb0c0929b43132bf25
|
4
|
+
data.tar.gz: 6313d7ff50a64695fac17e31be5e3939f7982db3d0662629ba0841f4c5a71518
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82751357b5799a2ee7f3e36cff221a9d0a8a41c08bfc3cae1309e50a1af9d6fbd5392ee9ecd657051f03fc59440276d6d481266cf560da034a3012d2a79dee62
|
7
|
+
data.tar.gz: '06320641649d6a8caa68c2fdba355f0bc76755b13f37ae0e1afba53115f0a0e7df1c073e718308e591a7c91938283505730eaf638fb4959bacfdbbb8851bf956'
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -14,7 +14,9 @@ pgslice is a command line tool. To install, run:
|
|
14
14
|
gem install pgslice
|
15
15
|
```
|
16
16
|
|
17
|
-
This will give you the `pgslice` command.
|
17
|
+
This will give you the `pgslice` command. If installation fails, you may need to install [dependencies](#dependencies).
|
18
|
+
|
19
|
+
You can also install it with [Homebrew](#homebrew) or [Docker](#docker).
|
18
20
|
|
19
21
|
## Steps
|
20
22
|
|
@@ -299,12 +301,14 @@ You can also use pgslice to reduce the size of a table without partitioning by c
|
|
299
301
|
```sh
|
300
302
|
pgslice prep <table> --no-partition
|
301
303
|
pgslice fill <table> --where "id > 1000" # use any conditions
|
304
|
+
pgslice analyze <table>
|
302
305
|
pgslice swap <table>
|
306
|
+
pgslice fill <table> --where "id > 1000" --swapped
|
303
307
|
```
|
304
308
|
|
305
309
|
## Triggers
|
306
310
|
|
307
|
-
Triggers aren’t copied from the original table. You can set up triggers on the intermediate table if needed.
|
311
|
+
Triggers aren’t copied from the original table. You can set up triggers on the intermediate table if needed.
|
308
312
|
|
309
313
|
## Data Protection
|
310
314
|
|
@@ -317,7 +321,7 @@ Always make sure your [connection is secure](https://ankane.org/postgres-sslmode
|
|
317
321
|
With Homebrew, you can use:
|
318
322
|
|
319
323
|
```sh
|
320
|
-
brew install
|
324
|
+
brew install pgslice
|
321
325
|
```
|
322
326
|
|
323
327
|
### Docker
|
@@ -63,14 +63,14 @@ module PgSlice
|
|
63
63
|
added_partitions << partition
|
64
64
|
|
65
65
|
if declarative
|
66
|
-
queries <<
|
67
|
-
CREATE TABLE #{quote_table(partition)} PARTITION OF #{quote_table(table)} FOR VALUES FROM (#{sql_date(day, cast, false)}) TO (#{sql_date(advance_date(day, period, 1), cast, false)})#{tablespace_str};
|
66
|
+
queries << <<~SQL
|
67
|
+
CREATE TABLE #{quote_table(partition)} PARTITION OF #{quote_table(table)} FOR VALUES FROM (#{sql_date(day, cast, false)}) TO (#{sql_date(advance_date(day, period, 1), cast, false)})#{tablespace_str};
|
68
68
|
SQL
|
69
69
|
else
|
70
|
-
queries <<
|
71
|
-
CREATE TABLE #{quote_table(partition)}
|
72
|
-
|
73
|
-
|
70
|
+
queries << <<~SQL
|
71
|
+
CREATE TABLE #{quote_table(partition)}
|
72
|
+
(CHECK (#{quote_ident(field)} >= #{sql_date(day, cast)} AND #{quote_ident(field)} < #{sql_date(advance_date(day, period, 1), cast)}))
|
73
|
+
INHERITS (#{quote_table(table)})#{tablespace_str};
|
74
74
|
SQL
|
75
75
|
end
|
76
76
|
|
@@ -115,17 +115,17 @@ CREATE TABLE #{quote_table(partition)}
|
|
115
115
|
trigger_defs = current_defs + future_defs + past_defs.reverse
|
116
116
|
|
117
117
|
if trigger_defs.any?
|
118
|
-
queries <<
|
119
|
-
CREATE OR REPLACE FUNCTION #{quote_ident(trigger_name)}()
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
118
|
+
queries << <<~SQL
|
119
|
+
CREATE OR REPLACE FUNCTION #{quote_ident(trigger_name)}()
|
120
|
+
RETURNS trigger AS $$
|
121
|
+
BEGIN
|
122
|
+
IF #{trigger_defs.join("\n ELSIF ")}
|
123
|
+
ELSE
|
124
|
+
RAISE EXCEPTION 'Date out of range. Ensure partitions are created.';
|
125
|
+
END IF;
|
126
|
+
RETURN NULL;
|
127
|
+
END;
|
128
|
+
$$ LANGUAGE plpgsql;
|
129
129
|
SQL
|
130
130
|
end
|
131
131
|
end
|
data/lib/pgslice/cli/analyze.rb
CHANGED
@@ -5,8 +5,10 @@ module PgSlice
|
|
5
5
|
def analyze(table)
|
6
6
|
table = create_table(table)
|
7
7
|
parent_table = options[:swapped] ? table : table.intermediate_table
|
8
|
+
assert_table(parent_table)
|
8
9
|
|
9
|
-
|
10
|
+
_, _, _, _, declarative, _ = parent_table.fetch_settings(table.trigger_name)
|
11
|
+
analyze_list = declarative ? [parent_table] : (parent_table.partitions + [parent_table])
|
10
12
|
run_queries_without_transaction(analyze_list.map { |t| "ANALYZE VERBOSE #{quote_table(t)};" })
|
11
13
|
end
|
12
14
|
end
|
data/lib/pgslice/cli/fill.rb
CHANGED
@@ -82,11 +82,11 @@ module PgSlice
|
|
82
82
|
where << " AND #{options[:where]}"
|
83
83
|
end
|
84
84
|
|
85
|
-
query =
|
86
|
-
/* #{i} of #{batch_count} */
|
87
|
-
INSERT INTO #{quote_table(dest_table)} (#{fields})
|
88
|
-
|
89
|
-
|
85
|
+
query = <<~SQL
|
86
|
+
/* #{i} of #{batch_count} */
|
87
|
+
INSERT INTO #{quote_table(dest_table)} (#{fields})
|
88
|
+
SELECT #{fields} FROM #{quote_table(source_table)}
|
89
|
+
WHERE #{where}
|
90
90
|
SQL
|
91
91
|
|
92
92
|
run_query(query)
|
data/lib/pgslice/cli/prep.rb
CHANGED
@@ -33,15 +33,12 @@ module PgSlice
|
|
33
33
|
declarative = version > 1
|
34
34
|
|
35
35
|
if declarative && options[:partition]
|
36
|
-
including = ["DEFAULTS", "CONSTRAINTS", "STORAGE", "COMMENTS", "STATISTICS"]
|
37
|
-
if server_version_num >= 120000
|
38
|
-
including << "GENERATED"
|
39
|
-
end
|
36
|
+
including = ["DEFAULTS", "CONSTRAINTS", "STORAGE", "COMMENTS", "STATISTICS", "GENERATED"]
|
40
37
|
if server_version_num >= 140000
|
41
38
|
including << "COMPRESSION"
|
42
39
|
end
|
43
|
-
queries <<
|
44
|
-
CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} #{including.map { |v| "INCLUDING #{v}" }.join(" ")}) PARTITION BY RANGE (#{quote_ident(column)});
|
40
|
+
queries << <<~SQL
|
41
|
+
CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} #{including.map { |v| "INCLUDING #{v}" }.join(" ")}) PARTITION BY RANGE (#{quote_ident(column)});
|
45
42
|
SQL
|
46
43
|
|
47
44
|
if version == 3
|
@@ -57,12 +54,12 @@ CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} #{in
|
|
57
54
|
|
58
55
|
# add comment
|
59
56
|
cast = table.column_cast(column)
|
60
|
-
queries <<
|
61
|
-
COMMENT ON TABLE #{quote_table(intermediate_table)} IS 'column:#{column},period:#{period},cast:#{cast},version:#{version}';
|
57
|
+
queries << <<~SQL
|
58
|
+
COMMENT ON TABLE #{quote_table(intermediate_table)} IS 'column:#{column},period:#{period},cast:#{cast},version:#{version}';
|
62
59
|
SQL
|
63
60
|
else
|
64
|
-
queries <<
|
65
|
-
CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} INCLUDING ALL);
|
61
|
+
queries << <<~SQL
|
62
|
+
CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} INCLUDING ALL);
|
66
63
|
SQL
|
67
64
|
|
68
65
|
table.foreign_keys.each do |fk_def|
|
@@ -71,24 +68,24 @@ CREATE TABLE #{quote_table(intermediate_table)} (LIKE #{quote_table(table)} INCL
|
|
71
68
|
end
|
72
69
|
|
73
70
|
if options[:partition] && !declarative
|
74
|
-
queries <<
|
75
|
-
CREATE FUNCTION #{quote_ident(trigger_name)}()
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
71
|
+
queries << <<~SQL
|
72
|
+
CREATE FUNCTION #{quote_ident(trigger_name)}()
|
73
|
+
RETURNS trigger AS $$
|
74
|
+
BEGIN
|
75
|
+
RAISE EXCEPTION 'Create partitions first.';
|
76
|
+
END;
|
77
|
+
$$ LANGUAGE plpgsql;
|
81
78
|
SQL
|
82
79
|
|
83
|
-
queries <<
|
84
|
-
CREATE TRIGGER #{quote_ident(trigger_name)}
|
85
|
-
|
86
|
-
|
80
|
+
queries << <<~SQL
|
81
|
+
CREATE TRIGGER #{quote_ident(trigger_name)}
|
82
|
+
BEFORE INSERT ON #{quote_table(intermediate_table)}
|
83
|
+
FOR EACH ROW EXECUTE PROCEDURE #{quote_ident(trigger_name)}();
|
87
84
|
SQL
|
88
85
|
|
89
86
|
cast = table.column_cast(column)
|
90
|
-
queries <<
|
91
|
-
COMMENT ON TRIGGER #{quote_ident(trigger_name)} ON #{quote_table(intermediate_table)} IS 'column:#{column},period:#{period},cast:#{cast}';
|
87
|
+
queries << <<~SQL
|
88
|
+
COMMENT ON TRIGGER #{quote_ident(trigger_name)} ON #{quote_table(intermediate_table)} IS 'column:#{column},period:#{period},cast:#{cast}';
|
92
89
|
SQL
|
93
90
|
end
|
94
91
|
|
data/lib/pgslice/cli/unprep.rb
CHANGED
@@ -9,9 +9,14 @@ module PgSlice
|
|
9
9
|
assert_table(intermediate_table)
|
10
10
|
|
11
11
|
queries = [
|
12
|
-
"DROP TABLE #{quote_table(intermediate_table)} CASCADE;"
|
13
|
-
"DROP FUNCTION IF EXISTS #{quote_ident(trigger_name)}();"
|
12
|
+
"DROP TABLE #{quote_table(intermediate_table)} CASCADE;"
|
14
13
|
]
|
14
|
+
|
15
|
+
_, _, _, _, declarative, _ = intermediate_table.fetch_settings(table.trigger_name)
|
16
|
+
unless declarative
|
17
|
+
queries << "DROP FUNCTION IF EXISTS #{quote_ident(trigger_name)}();"
|
18
|
+
end
|
19
|
+
|
15
20
|
run_queries(queries)
|
16
21
|
end
|
17
22
|
end
|
data/lib/pgslice/cli.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
module PgSlice
|
2
2
|
class CLI < Thor
|
3
3
|
class << self
|
4
|
-
attr_accessor :instance
|
4
|
+
attr_accessor :instance, :exit_on_failure
|
5
|
+
alias_method :exit_on_failure?, :exit_on_failure
|
5
6
|
end
|
7
|
+
self.exit_on_failure = true
|
6
8
|
|
7
9
|
include Helpers
|
8
10
|
|
@@ -13,10 +15,6 @@ module PgSlice
|
|
13
15
|
|
14
16
|
map %w[--version -v] => :version
|
15
17
|
|
16
|
-
def self.exit_on_failure?
|
17
|
-
ENV["PGSLICE_ENV"] != "test"
|
18
|
-
end
|
19
|
-
|
20
18
|
def initialize(*args)
|
21
19
|
PgSlice::CLI.instance = self
|
22
20
|
$stdout.sync = true
|
data/lib/pgslice/table.rb
CHANGED
@@ -12,16 +12,24 @@ module PgSlice
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def exists?
|
15
|
-
|
15
|
+
query = <<~SQL
|
16
|
+
SELECT COUNT(*) FROM pg_catalog.pg_tables
|
17
|
+
WHERE schemaname = $1 AND tablename = $2
|
18
|
+
SQL
|
19
|
+
execute(query, [schema, name]).first["count"].to_i > 0
|
16
20
|
end
|
17
21
|
|
18
22
|
def columns
|
19
|
-
|
23
|
+
query = <<~SQL
|
24
|
+
SELECT column_name FROM information_schema.columns
|
25
|
+
WHERE table_schema = $1 AND table_name = $2 AND is_generated = 'NEVER'
|
26
|
+
SQL
|
27
|
+
execute(query, [schema, name]).map { |r| r["column_name"] }
|
20
28
|
end
|
21
29
|
|
22
30
|
# http://www.dbforums.com/showthread.php?1667561-How-to-list-sequences-and-the-columns-by-SQL
|
23
31
|
def sequences
|
24
|
-
query =
|
32
|
+
query = <<~SQL
|
25
33
|
SELECT
|
26
34
|
a.attname AS related_column,
|
27
35
|
n.nspname AS sequence_schema,
|
@@ -41,13 +49,17 @@ module PgSlice
|
|
41
49
|
end
|
42
50
|
|
43
51
|
def foreign_keys
|
44
|
-
|
52
|
+
query = <<~SQL
|
53
|
+
SELECT pg_get_constraintdef(oid) FROM pg_constraint
|
54
|
+
WHERE conrelid = $1::regclass AND contype ='f'
|
55
|
+
SQL
|
56
|
+
execute(query, [quote_table]).map { |r| r["pg_get_constraintdef"] }
|
45
57
|
end
|
46
58
|
|
47
59
|
# https://stackoverflow.com/a/20537829
|
48
60
|
# TODO can simplify with array_position in Postgres 9.5+
|
49
61
|
def primary_key
|
50
|
-
query =
|
62
|
+
query = <<~SQL
|
51
63
|
SELECT
|
52
64
|
pg_attribute.attname,
|
53
65
|
format_type(pg_attribute.atttypid, pg_attribute.atttypmod),
|
@@ -69,7 +81,11 @@ module PgSlice
|
|
69
81
|
end
|
70
82
|
|
71
83
|
def index_defs
|
72
|
-
|
84
|
+
query = <<~SQL
|
85
|
+
SELECT pg_get_indexdef(indexrelid) FROM pg_index
|
86
|
+
WHERE indrelid = $1::regclass AND indisprimary = 'f'
|
87
|
+
SQL
|
88
|
+
execute(query, [quote_table]).map { |r| r["pg_get_indexdef"] }
|
73
89
|
end
|
74
90
|
|
75
91
|
def quote_table
|
@@ -89,17 +105,25 @@ module PgSlice
|
|
89
105
|
end
|
90
106
|
|
91
107
|
def column_cast(column)
|
92
|
-
|
108
|
+
query = <<~SQL
|
109
|
+
SELECT data_type FROM information_schema.columns
|
110
|
+
WHERE table_schema = $1 AND table_name = $2 AND column_name = $3
|
111
|
+
SQL
|
112
|
+
data_type = execute(query, [schema, name, column])[0]["data_type"]
|
93
113
|
data_type == "timestamp with time zone" ? "timestamptz" : "date"
|
94
114
|
end
|
95
115
|
|
96
116
|
def max_id(primary_key, below: nil, where: nil)
|
97
117
|
query = "SELECT MAX(#{quote_ident(primary_key)}) FROM #{quote_table}"
|
98
118
|
conditions = []
|
99
|
-
|
119
|
+
params = []
|
120
|
+
if below
|
121
|
+
conditions << "#{quote_ident(primary_key)} <= $1"
|
122
|
+
params << below
|
123
|
+
end
|
100
124
|
conditions << where if where
|
101
125
|
query << " WHERE #{conditions.join(" AND ")}" if conditions.any?
|
102
|
-
execute(query)[0]["max"].to_i
|
126
|
+
execute(query, params)[0]["max"].to_i
|
103
127
|
end
|
104
128
|
|
105
129
|
def min_id(primary_key, column, cast, starting_time, where)
|
@@ -113,15 +137,15 @@ module PgSlice
|
|
113
137
|
|
114
138
|
# ensure this returns partitions in the correct order
|
115
139
|
def partitions
|
116
|
-
query =
|
140
|
+
query = <<~SQL
|
117
141
|
SELECT
|
118
|
-
nmsp_child.nspname
|
119
|
-
child.relname
|
142
|
+
nmsp_child.nspname AS schema,
|
143
|
+
child.relname AS name
|
120
144
|
FROM pg_inherits
|
121
|
-
JOIN pg_class parent
|
122
|
-
JOIN pg_class child
|
123
|
-
JOIN pg_namespace nmsp_parent
|
124
|
-
JOIN pg_namespace nmsp_child
|
145
|
+
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
|
146
|
+
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
|
147
|
+
JOIN pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace
|
148
|
+
JOIN pg_namespace nmsp_child ON nmsp_child.oid = child.relnamespace
|
125
149
|
WHERE
|
126
150
|
nmsp_parent.nspname = $1 AND
|
127
151
|
parent.relname = $2
|
@@ -131,11 +155,15 @@ module PgSlice
|
|
131
155
|
end
|
132
156
|
|
133
157
|
def fetch_comment
|
134
|
-
execute("SELECT obj_description(
|
158
|
+
execute("SELECT obj_description($1::regclass) AS comment", [quote_table])[0]
|
135
159
|
end
|
136
160
|
|
137
161
|
def fetch_trigger(trigger_name)
|
138
|
-
|
162
|
+
query = <<~SQL
|
163
|
+
SELECT obj_description(oid, 'pg_trigger') AS comment FROM pg_trigger
|
164
|
+
WHERE tgname = $1 AND tgrelid = $2::regclass
|
165
|
+
SQL
|
166
|
+
execute(query, [trigger_name, quote_table])[0]
|
139
167
|
end
|
140
168
|
|
141
169
|
# legacy
|
@@ -186,10 +214,6 @@ module PgSlice
|
|
186
214
|
PG::Connection.quote_ident(value)
|
187
215
|
end
|
188
216
|
|
189
|
-
def regclass
|
190
|
-
"#{escape_literal(quote_table)}::regclass"
|
191
|
-
end
|
192
|
-
|
193
217
|
def sql_date(time, cast, add_cast = true)
|
194
218
|
if cast == "timestamptz"
|
195
219
|
fmt = "%Y-%m-%d %H:%M:%S UTC"
|
data/lib/pgslice/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pgslice
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
77
|
- !ruby/object:Gem::Version
|
78
78
|
version: '0'
|
79
79
|
requirements: []
|
80
|
-
rubygems_version: 3.6.
|
80
|
+
rubygems_version: 3.6.9
|
81
81
|
specification_version: 4
|
82
82
|
summary: Postgres partitioning as easy as pie
|
83
83
|
test_files: []
|