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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 325b5cd0c6ec6dfa7a98db88a06bb47ed4f8ee327c6c21dddfd897ec13f54533
4
- data.tar.gz: aabbfee0e2bced3b7e93f4d9f0db218db060779a3b75b14ac7056867901bae26
3
+ metadata.gz: b8084c5bbabb6b85b6e90f03d1895d95c4fa9130dcf68fbb0c0929b43132bf25
4
+ data.tar.gz: 6313d7ff50a64695fac17e31be5e3939f7982db3d0662629ba0841f4c5a71518
5
5
  SHA512:
6
- metadata.gz: ba7310102950041d8247779282b6b252d40d7c824bd03b07d08ed7e400534cbf44eb31161595004ff782ccdaa9eacd0d6698797068ea06109ef974eebab8439b
7
- data.tar.gz: 50faa245fb1ac68b6b685af8529277f23a46146477fd37e9ec52a6984222daa3392ca49e9d5276f03177f755ccec5c94989d4f6f25e182f3013ed9b24a8a7250
6
+ metadata.gz: 82751357b5799a2ee7f3e36cff221a9d0a8a41c08bfc3cae1309e50a1af9d6fbd5392ee9ecd657051f03fc59440276d6d481266cf560da034a3012d2a79dee62
7
+ data.tar.gz: '06320641649d6a8caa68c2fdba355f0bc76755b13f37ae0e1afba53115f0a0e7df1c073e718308e591a7c91938283505730eaf638fb4959bacfdbbb8851bf956'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.7.1 (2025-07-27)
2
+
3
+ - Fixed `analyze` analyzing partitions twice with declarative partitioning
4
+ - Removed unnecessary query for `unprep` with declarative partitioning
5
+
1
6
  ## 0.7.0 (2025-05-26)
2
7
 
3
8
  - Dropped support for Ruby < 3
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. You can also install it with [Homebrew](#homebrew) or [Docker](#docker). If installation fails, you may need to install [dependencies](#dependencies).
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. Note that Postgres doesn’t support `BEFORE / FOR EACH ROW` triggers on partitioned tables.
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 ankane/brew/pgslice
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 << <<-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};
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 << <<-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};
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 << <<-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;
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
@@ -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
- analyze_list = parent_table.partitions + [parent_table]
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
@@ -82,11 +82,11 @@ module PgSlice
82
82
  where << " AND #{options[:where]}"
83
83
  end
84
84
 
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}
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)
@@ -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 << <<-SQL
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 << <<-SQL
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 << <<-SQL
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 << <<-SQL
75
- CREATE FUNCTION #{quote_ident(trigger_name)}()
76
- RETURNS trigger AS $$
77
- BEGIN
78
- RAISE EXCEPTION 'Create partitions first.';
79
- END;
80
- $$ LANGUAGE plpgsql;
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 << <<-SQL
84
- CREATE TRIGGER #{quote_ident(trigger_name)}
85
- BEFORE INSERT ON #{quote_table(intermediate_table)}
86
- FOR EACH ROW EXECUTE PROCEDURE #{quote_ident(trigger_name)}();
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 << <<-SQL
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
 
@@ -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
- execute("SELECT COUNT(*) FROM pg_catalog.pg_tables WHERE schemaname = $1 AND tablename = $2", [schema, name]).first["count"].to_i > 0
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
- execute("SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND is_generated = 'NEVER'", [schema, name]).map { |r| r["column_name"] }
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 = <<-SQL
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
- execute("SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = #{regclass} AND contype ='f'").map { |r| r["pg_get_constraintdef"] }
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 = <<-SQL
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
- execute("SELECT pg_get_indexdef(indexrelid) FROM pg_index WHERE indrelid = #{regclass} AND indisprimary = 'f'").map { |r| r["pg_get_indexdef"] }
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
- 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"]
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
- conditions << "#{quote_ident(primary_key)} <= #{below}" if below
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 = <<-SQL
140
+ query = <<~SQL
117
141
  SELECT
118
- nmsp_child.nspname AS schema,
119
- child.relname AS name
142
+ nmsp_child.nspname AS schema,
143
+ child.relname AS name
120
144
  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
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(#{regclass}) AS comment")[0]
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
- execute("SELECT obj_description(oid, 'pg_trigger') AS comment FROM pg_trigger WHERE tgname = $1 AND tgrelid = #{regclass}", [trigger_name])[0]
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"
@@ -1,3 +1,3 @@
1
1
  module PgSlice
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.1"
3
3
  end
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.0
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.7
80
+ rubygems_version: 3.6.9
81
81
  specification_version: 4
82
82
  summary: Postgres partitioning as easy as pie
83
83
  test_files: []