pg_partitioning 0.0.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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +97 -0
  4. data/Rakefile +23 -0
  5. data/config/locales/en.yml +28 -0
  6. data/lib/generators/partitioning_generator.rb +18 -0
  7. data/lib/pg_partitioning/input_master.rb +83 -0
  8. data/lib/pg_partitioning/partitioning_master.rb +78 -0
  9. data/lib/pg_partitioning/printer.rb +28 -0
  10. data/lib/pg_partitioning/strategies/base.rb +72 -0
  11. data/lib/pg_partitioning/strategies/date.rb +73 -0
  12. data/lib/pg_partitioning/strategies/equal.rb +43 -0
  13. data/lib/pg_partitioning/strategies/step.rb +57 -0
  14. data/lib/pg_partitioning/version.rb +3 -0
  15. data/lib/pg_partitioning.rb +4 -0
  16. data/lib/tasks/pg_partitioning_tasks.rake +4 -0
  17. data/spec/db_helpers.rb +36 -0
  18. data/spec/dummy/README.rdoc +28 -0
  19. data/spec/dummy/Rakefile +6 -0
  20. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  21. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  22. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  23. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  24. data/spec/dummy/app/models/bandit.rb +4 -0
  25. data/spec/dummy/app/models/crime.rb +3 -0
  26. data/spec/dummy/app/models/gang.rb +3 -0
  27. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/spec/dummy/bin/bundle +3 -0
  29. data/spec/dummy/bin/rails +4 -0
  30. data/spec/dummy/bin/rake +4 -0
  31. data/spec/dummy/bin/setup +29 -0
  32. data/spec/dummy/config/application.rb +32 -0
  33. data/spec/dummy/config/boot.rb +5 -0
  34. data/spec/dummy/config/database.yml +28 -0
  35. data/spec/dummy/config/environment.rb +5 -0
  36. data/spec/dummy/config/environments/development.rb +41 -0
  37. data/spec/dummy/config/environments/production.rb +79 -0
  38. data/spec/dummy/config/environments/test.rb +42 -0
  39. data/spec/dummy/config/initializers/assets.rb +11 -0
  40. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  42. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  43. data/spec/dummy/config/initializers/inflections.rb +16 -0
  44. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  45. data/spec/dummy/config/initializers/session_store.rb +3 -0
  46. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  47. data/spec/dummy/config/locales/en.yml +23 -0
  48. data/spec/dummy/config/routes.rb +56 -0
  49. data/spec/dummy/config/secrets.yml +22 -0
  50. data/spec/dummy/config.ru +4 -0
  51. data/spec/dummy/db/migrate/20160306173540_create_gangs.rb +9 -0
  52. data/spec/dummy/db/migrate/20160306174017_create_bandits.rb +12 -0
  53. data/spec/dummy/db/migrate/20160306174042_create_crimes.rb +10 -0
  54. data/spec/dummy/db/schema.rb +47 -0
  55. data/spec/dummy/log/development.log +3421 -0
  56. data/spec/dummy/log/production.log +0 -0
  57. data/spec/dummy/log/test.log +154834 -0
  58. data/spec/dummy/public/404.html +67 -0
  59. data/spec/dummy/public/422.html +67 -0
  60. data/spec/dummy/public/500.html +66 -0
  61. data/spec/dummy/public/favicon.ico +0 -0
  62. data/spec/factories/bandits.rb +23 -0
  63. data/spec/factories/crimes.rb +6 -0
  64. data/spec/factories/gangs.rb +5 -0
  65. data/spec/input_master_spec.rb +74 -0
  66. data/spec/models/bandit_spec.rb +13 -0
  67. data/spec/models/crime_spec.rb +10 -0
  68. data/spec/models/gang_spec.rb +10 -0
  69. data/spec/partitioning_master_spec.rb +156 -0
  70. data/spec/rails_helper.rb +67 -0
  71. data/spec/shared_examples.rb +28 -0
  72. data/spec/spec_helper.rb +92 -0
  73. metadata +213 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3218f90c14f4626030e7eee68580ba3e8ab6beec
4
+ data.tar.gz: 6f71ac05ee7bb5fdf82f65335aeba6ab7cc1073a
5
+ SHA512:
6
+ metadata.gz: 2051b32762a47edc84ce1cf597282dc4700280dd7bb32f747497b6d143a7eefe32f712cb44937dd968bddadfc32e2aec5999d14a05b55c3cf22d1177fa820231
7
+ data.tar.gz: 0b3f91c0c0f69906e733ac45ce794ddacb773bde5bd0e595f2da720d71168a360f9c8711ff96f28044b11d31e333c8595f0dcd4851052633bf97e2fe66748a73
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Victor M.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,97 @@
1
+ = PgPartitioning
2
+
3
+ {<img src="https://codeclimate.com/github/victor-magarlamov/pg_partitioning/badges/gpa.svg" />}[https://codeclimate.com/github/victor-magarlamov/pg_partitioning]
4
+ {<img src="https://codeclimate.com/github/victor-magarlamov/pg_partitioning/badges/coverage.svg" />}[https://codeclimate.com/github/victor-magarlamov/pg_partitioning/coverage]
5
+
6
+ This project rocks and uses MIT-LICENSE.
7
+
8
+ == Install
9
+
10
+ Put this line in your Gemfile:
11
+ gem 'pg_partitioning'
12
+
13
+ Then bundle:
14
+ % bundle install
15
+
16
+ == Usage
17
+
18
+ % RAILS_ENV=production rails g partitioning
19
+
20
+ 1) Select the partitioning mode:
21
+ * by single column +value+
22
+ * by dates +template+
23
+ * by +range+
24
+
25
+ 2) Enter +table+ name.
26
+
27
+ 3) Enter +column+ name.
28
+
29
+ 4) Enter condition depending on the selected mode - template +pattern+ or range +step+.
30
+
31
+ What will happen then...
32
+
33
+ 1) The generator will create two triggers: before insert and after insert. The before insert trigger executes the procedure that created nested table and inserts a new record into it. The after trigger clears the master table.
34
+
35
+ 2) Foreign keys which reference to the master table will delete.
36
+
37
+ 3) Old data will be migrated from master table to child tables.
38
+
39
+ For more details, see ...
40
+
41
+ == Examples
42
+
43
+ Given we have table 'bandits':
44
+
45
+ | id | name | specialization | born_at
46
+
47
+ === Example 1: Partitioning by single column value
48
+
49
+ Enter mode: 0
50
+ Enter table: bandits
51
+ Enter column: specialization
52
+
53
+ Create two bandits:
54
+
55
+ Bandit.create([{name: 'Al Capone', specialization: 'bootlegger'},
56
+ {name: 'Black Bart', specialization: 'robber'}])
57
+
58
+ Now we have three tables:
59
+
60
+ * bandits - master - not contains real data
61
+ * bandits_bootlegger - nested - contains only the bootleggers
62
+ * bandits_robber - nested - contains only the robbers
63
+
64
+ Check this...
65
+
66
+ SELECT COUNT(*) FROM bandits; (2)
67
+ SELECT COUNT(*) FROM ONLY bandits; (0)
68
+ SELECT COUNT(*) FROM ONLY bandits_bootlegger; (1)
69
+ SELECT COUNT(*) FROM ONLY bandits_robber; (1)
70
+
71
+ === Example 2: Partitioning by range
72
+
73
+ Enter mode: 1
74
+ Enter table: bandits
75
+ Enter column: id
76
+ Enter step: 10
77
+
78
+ All bandits with ID from 0 to 9 will be recorded to table 'bandits_0'.
79
+ All bandits with ID from 10 to 19 will be recorded to table 'bandits_1'.
80
+ All bandits with ID from 20 to 29 will be recorded to table 'bandits_2' etc.
81
+
82
+ === Example 3: Partitioning by date template
83
+
84
+ Enter mode: 2
85
+ Enter table: bandits
86
+ Enter column: born_at
87
+ Enter pattern: YYYYMM
88
+
89
+ Create two bandits:
90
+
91
+ Bandit.create([{name: 'Al Capone', born_at: '1899-01-17'},
92
+ {name: 'Charles Luciano', born_at: '1897-11-24'}])
93
+
94
+ And now we have two child tables:
95
+
96
+ * bandits_1899_01
97
+ * bandits_1897_11
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'PgPartitioning'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
@@ -0,0 +1,28 @@
1
+ en:
2
+ pg_partitioning:
3
+ enter: "Enter the %{quest}"
4
+ quests:
5
+ mode: "type of the partitioning - equal(0), step(1), date(2): "
6
+ table_name: "name of the table: "
7
+ column_name: "name of the column: "
8
+ cond_step: "step (e.g. 1000): "
9
+ cond_date: "pattern (e.g. YYYY_MM or MM): "
10
+ failure:
11
+ answer: "I cannot accept your answer!"
12
+ mode: "Invalid value."
13
+ step: "Invalid value. Should be eat then 0"
14
+ pattern: "Invalid value."
15
+ no_table: "Table with some column is not exists!"
16
+ column_type: "Invalid column type."
17
+ blank_cond: "Value is not set."
18
+ other: "An error has occurred."
19
+ progress:
20
+ insert_master: "Insert master func: %{state}"
21
+ before_insert_trigger: "Before Insert trigger: %{state}"
22
+ drop_master: "Drop master func: %{state}"
23
+ after_insert_trigger: "After Insert trigger: %{state}"
24
+ drop_fk: "Delete foreign keys: %{state}"
25
+ migration: "Migrate old data: %{state}"
26
+ finish: "Done!"
27
+ messages:
28
+ partition_mode: "Recommend to set constraint_exclusion in 'partition' state for best perfomance (your current state is %{current})."
@@ -0,0 +1,18 @@
1
+ require 'pg_partitioning/input_master'
2
+ require 'pg_partitioning/partitioning_master'
3
+
4
+ class PartitioningGenerator < Rails::Generators::Base
5
+
6
+ def partitioning
7
+ input_master = PgPartitioning::InputMaster.new
8
+ input_master.intro
9
+
10
+ mode = input_master.ask_mode.to_i
11
+ table_name = input_master.ask_table_name
12
+ column_name = input_master.ask_column_name
13
+ cond = input_master.ask_cond(PgPartitioning::PartitioningMaster::MODES[mode])
14
+
15
+ master = PgPartitioning::PartitioningMaster.new(table_name, column_name, mode, cond)
16
+ master.partitioning
17
+ end
18
+ end
@@ -0,0 +1,83 @@
1
+ require 'pg_partitioning/printer'
2
+
3
+ module PgPartitioning
4
+ class InputMaster
5
+ include Printer
6
+
7
+ %w(mode table_name column_name cond).each do |act|
8
+ define_method "ask_#{act}" do |argument = nil|
9
+ begin
10
+ quest_key = argument.blank? ? act : "#{act}_#{argument}"
11
+ quest_val = I18n.t "pg_partitioning.quests.#{quest_key}", raise: true
12
+ res = ask quest_val
13
+ if send "#{quest_key}_valid?", res
14
+ return res
15
+ else
16
+ alert @error_message
17
+ send "ask_#{act}", argument unless ENV['RAILS_ENV'] == 'test'
18
+ end
19
+ rescue I18n::MissingTranslationData
20
+ return nil
21
+ end
22
+ end
23
+ end
24
+
25
+ def method_missing(name, *args)
26
+ valid?(*args) if name.to_s.include?('_valid?')
27
+ end
28
+
29
+ def intro
30
+ info <<-INTRO
31
+ Hi! It's time to do partitioning)
32
+ I can do it in different ways:
33
+
34
+ by EQUALity
35
+ - partition will be created by value of column,
36
+ - in this case you should only set column name;
37
+
38
+ by STEP
39
+ - partition will be created by value of column divided by the step and rounded,
40
+ - in this case you should else set number of the step,
41
+ - a type of the column should be a numeric;
42
+
43
+ by DATE
44
+ - partition will be created by pattern from date,
45
+ - in this case you should else set pattern for parsing the date,
46
+ - acceptable (itself or in combination): Y, YY, YYY, YYYY, MM, D, DD, DDD, W, WW, HH24
47
+ - a type of the column should be a date/timestamp.
48
+ INTRO
49
+ end
50
+
51
+ private
52
+ def ask(text)
53
+ text_color WHITE
54
+ print I18n.t('pg_partitioning.enter', quest: text)
55
+ gets.chomp
56
+ end
57
+
58
+ def valid?(text)
59
+ res = !text.blank?
60
+ @error_message = I18n.t 'pg_partitioning.failure.answer' unless res
61
+ res
62
+ end
63
+
64
+ def mode_valid?(text)
65
+ res = %w(0 1 2).include? text
66
+ @error_message = I18n.t 'pg_partitioning.failure.mode' unless res
67
+ res
68
+ end
69
+
70
+ def cond_step_valid?(text)
71
+ res = text.to_i > 0
72
+ @error_message = I18n.t "pg_partitioning.failure.step" unless res
73
+ res
74
+ end
75
+
76
+ def cond_date_valid?(text)
77
+ res = /Y{1,4}|M{2}|D{1,3}|W{1,2}|HH24/ =~ text
78
+ @error_message = I18n.t "pg_partitioning.failure.pattern" unless res
79
+ res
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,78 @@
1
+ require 'pg_partitioning/printer'
2
+ require 'pg_partitioning/strategies/equal'
3
+ require 'pg_partitioning/strategies/date'
4
+ require 'pg_partitioning/strategies/step'
5
+
6
+ module PgPartitioning
7
+ class PartitioningMaster
8
+ include Printer
9
+
10
+ MODES = %w(equal step date).freeze
11
+
12
+ def initialize(table_name, column_name, mode, cond=nil)
13
+ @table_name = table_name
14
+ @column_name = column_name
15
+ @cond = cond
16
+ @sql = ActiveRecord::Base.connection()
17
+
18
+ klass = "PgPartitioning::Strategies::#{MODES[mode].classify}"
19
+ @strategy = klass.constantize.new(@table_name, @column_name, @cond, @sql)
20
+ end
21
+
22
+ def partitioning
23
+ @strategy.partitioning!
24
+
25
+ drop_foreign_keys
26
+ migration
27
+
28
+ mode = show_value_of('constraint_exclusion')
29
+ if mode != 'partition'
30
+ alert I18n.t('pg_partitioning.messages.partition_mode', current: mode)
31
+ end
32
+
33
+ info I18n.t 'pg_partitioning.progress.finish'
34
+ rescue => e
35
+ alert e.message || I18n.t('pg_partitioning.failure.other')
36
+ end
37
+
38
+ private
39
+ def drop_foreign_keys
40
+ fk_info = []
41
+ query = ActiveRecord::Base.send(
42
+ :sanitize_sql_array,
43
+ ["SELECT tc.constraint_name, tc.table_name, kcu.column_name,
44
+ ccu.table_name AS foreign_table_name,
45
+ ccu.column_name AS foreign_column_name
46
+ FROM information_schema.table_constraints AS tc
47
+ JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
48
+ JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
49
+ WHERE constraint_type = 'FOREIGN KEY' AND ccu.table_name=?;", @table_name])
50
+
51
+ @sql.execute(query).each do |r|
52
+ fk_info << r
53
+ end
54
+
55
+ fk_info.each do |r|
56
+ @sql.execute "ALTER TABLE #{r['table_name']} DROP CONSTRAINT #{r['constraint_name']};"
57
+ end
58
+
59
+ info I18n.t('pg_partitioning.progress.drop_fk', state: 'OK')
60
+ end
61
+
62
+ def migration
63
+ @sql.execute <<-SQL
64
+ CREATE MATERIALIZED VIEW temp_table AS SELECT * FROM #{@table_name};
65
+ DELETE FROM #{@table_name};
66
+ INSERT INTO #{@table_name} (SELECT * FROM temp_table);
67
+ DROP MATERIALIZED VIEW temp_table;
68
+ SQL
69
+ info I18n.t('pg_partitioning.progress.migration', state: 'OK')
70
+ end
71
+
72
+ def show_value_of(param)
73
+ res = ''
74
+ @sql.execute("SHOW #{param};").each{ |r| res = r[param] }
75
+ res
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,28 @@
1
+ module PgPartitioning
2
+ module Printer
3
+ COLORS = [GREEN="\e[32m", WHITE="\e[0m", RED="\e[31m"]
4
+
5
+ def text_color(color)
6
+ print color
7
+ end
8
+
9
+ def print_row(mes, color = WHITE)
10
+ text_color color
11
+ print mes + "\n"
12
+ text_color WHITE
13
+ end
14
+
15
+ def info(mes)
16
+ print_row mes, GREEN
17
+ end
18
+
19
+ def alert(mes)
20
+ print_row mes, RED
21
+ end
22
+
23
+ def message(mes)
24
+ print_row mes
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,72 @@
1
+ require 'pg_partitioning/printer'
2
+
3
+ module PgPartitioning
4
+ module Strategies
5
+ class Base
6
+ include Printer
7
+
8
+ def initialize(table, column, cond, sql_conn)
9
+ @table_name = table
10
+ @column_name = column
11
+ @cond = cond
12
+ @sql = sql_conn
13
+ end
14
+
15
+ def partitioning!
16
+ raise error_message unless valid?
17
+ create_insert_master_function
18
+ create_trigger('insert_master', 'before')
19
+ create_drop_function
20
+ create_trigger('delete_master', 'after')
21
+ end
22
+
23
+ protected
24
+
25
+ def valid?
26
+ @data_type = column_data_type
27
+ @error_message = I18n.t "pg_partitioning.failure.no_table" if @data_type.blank?
28
+ @error_message.blank?
29
+ end
30
+
31
+ def error_message
32
+ @error_message || I18n.t("pg_partitioning.failure.other")
33
+ end
34
+
35
+ def column_data_type
36
+ res = nil
37
+ query = ActiveRecord::Base.send(:sanitize_sql_array,
38
+ ["SELECT data_type FROM information_schema.columns
39
+ WHERE table_name = ? AND column_name = ?;",
40
+ @table_name, @column_name])
41
+ @sql.execute(query).each do |r|
42
+ res = r['data_type']
43
+ end
44
+ res
45
+ end
46
+
47
+ def create_trigger(master, mode)
48
+ drop_trigger(mode)
49
+ @sql.execute "CREATE TRIGGER #{@table_name}_#{mode}_insert_trigger
50
+ #{mode} INSERT ON #{@table_name}
51
+ FOR EACH ROW EXECUTE PROCEDURE #{@table_name}_#{master}();"
52
+ info I18n.t("pg_partitioning.progress.#{mode}_insert_trigger", state: "OK")
53
+ end
54
+
55
+ def drop_trigger(mode)
56
+ @sql.execute "DROP TRIGGER IF EXISTS #{@table_name}_#{mode}_insert_trigger ON #{@table_name};"
57
+ end
58
+
59
+ def create_drop_function
60
+ @sql.execute "CREATE OR REPLACE FUNCTION #{@table_name}_delete_master() RETURNS TRIGGER AS $$
61
+ DECLARE
62
+ row #{@table_name.to_sym}%rowtype;
63
+ BEGIN
64
+ DELETE FROM ONLY #{@table_name} WHERE id = NEW.id RETURNING * INTO row;
65
+ RETURN row;
66
+ END;
67
+ $$ LANGUAGE plpgsql;"
68
+ info I18n.t("pg_partitioning.progress.drop_master", state: "OK")
69
+ end
70
+ end
71
+ end
72
+ end