pgslice 0.4.2 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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