pg_partitioning 0.0.1

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