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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module PgSlice
2
- VERSION = "0.4.2"
2
+ VERSION = "0.4.7"
3
3
  end
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.2
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: 2018-07-24 00:00:00.000000000 Z
11
+ date: 2020-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: slop
14
+ name: thor
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.2.0
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: 4.2.0
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: '0'
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: '0'
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
- rubyforge_project:
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