tablature 0.3.1 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d2ff14d9d94a6ab38483d133cfc3679a1647888683ca767e8201f5ca1db5357
4
- data.tar.gz: dd65acaba0dae0cc4d659e3d9505c02483dac1bc5ae04d701741db3fad74f2ad
3
+ metadata.gz: 8db9f3d3c82e1667d5931739f53bdc0c218f01281c56e3e1cd2541b51264ae4f
4
+ data.tar.gz: 45fa5efd4a15199e22d5d15b8d0f84114de3ed634121171bb561ff206c5bcc28
5
5
  SHA512:
6
- metadata.gz: 25d6f5d66c1b72e7887eeae052476a2bdd161c59023c882edbc2cfe52673d279448102092810119990f9057ff6844c8f887921e7d5093903c5ebb2cabe333447
7
- data.tar.gz: 65aa6da3dbcd404efb155665f4ce88481b3dbe6858170b6259292d09563880718656ea22eff17a885c7d74120337a8d05d38927b1c68f36d09f00d7d0389af8a
6
+ metadata.gz: d30f66c7398da2249edc10d3b4a5d2239aa017647f78c266fb07708abc943ee9d79f950d57e93a508c1d4211a408de17d7f97c96b092e9237a8962abdcfba58d
7
+ data.tar.gz: db95e1b650a5337be05a61986ae99ed4adb829d9dd2d98642b34907bdd71416da8dbfe1f8ec66bdc9caa2ec0e0b178b433ec39afa625928afe6d1708c4912558
@@ -0,0 +1,68 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: master
6
+ pull_request:
7
+ branches: "*"
8
+
9
+ jobs:
10
+ build:
11
+ name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}, Postgres ${{ matrix.postgres }}
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ["2.5", "2.7"]
17
+ rails: ["5.2", "6.0", "master"]
18
+ postgres: ["10.12", "11.7", "12.2"]
19
+ include:
20
+ - postgres: "10.12"
21
+ rspec_tag: --tag ~postgres_11
22
+ - rails: "master"
23
+ continue-on-error: true
24
+
25
+ runs-on: ubuntu-latest
26
+
27
+ services:
28
+ postgres:
29
+ image: postgres:${{ matrix.postgres }}-alpine
30
+ env:
31
+ POSTGRES_PASSWORD: postgres
32
+ ports:
33
+ - 5432:5432
34
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
35
+
36
+ env:
37
+ RAILS_VERSION: ${{ matrix.rails }}
38
+ POSTGRES_USER: "postgres"
39
+ POSTGRES_PASSWORD: "postgres"
40
+ CI: "true"
41
+
42
+ steps:
43
+ - name: Checkout
44
+ uses: actions/checkout@v2
45
+
46
+ - name: Install dependent libraries
47
+ run: sudo apt-get install libpq-dev
48
+
49
+ - name: Install Ruby ${{ matrix.ruby }}
50
+ uses: ruby/setup-ruby@v1.31.0
51
+ with:
52
+ ruby-version: ${{ matrix.ruby }}
53
+
54
+ - name: Generate lockfile
55
+ run: bundle lock
56
+
57
+ - name: Cache dependencies
58
+ uses: actions/cache@v1
59
+ with:
60
+ path: vendor/bundle
61
+ key: bundle-${{ hashFiles('Gemfile.lock') }}
62
+
63
+ - name: Set up Tablature
64
+ run: bin/setup
65
+
66
+ - name: Run tests
67
+ run: bundle exec rspec ${{ matrix.rspec_tag }}
68
+ continue-on-error: ${{ matrix.continue-on-error }}
data/.gitignore CHANGED
@@ -9,3 +9,5 @@
9
9
 
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
+
13
+ Gemfile.lock
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.3
2
+ TargetRubyVersion: 2.5
3
3
 
4
4
  Metrics/BlockLength:
5
5
  Exclude:
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown
data/Gemfile CHANGED
@@ -1,3 +1,15 @@
1
+ source "https://rubygems.org"
1
2
  gemspec
2
3
 
3
4
  gem 'pry'
5
+ gem 'rubocop'
6
+
7
+ rails_version = ENV.fetch("RAILS_VERSION", "6.0")
8
+
9
+ if rails_version == "master"
10
+ rails_constraint = { github: "rails/rails" }
11
+ else
12
+ rails_constraint = "~> #{rails_version}.0"
13
+ end
14
+
15
+ gem "rails", rails_constraint
data/README.md CHANGED
@@ -7,7 +7,7 @@ It ships with Postgres support and can easily supports other databases through a
7
7
 
8
8
  ##### Requirements
9
9
 
10
- Tablature requires Rails 5+ and Postgres 10+.
10
+ Tablature requires Ruby 2.5+, Rails 5+ and Postgres 10+.
11
11
 
12
12
  ##### Installation
13
13
 
@@ -67,7 +67,6 @@ end
67
67
 
68
68
  ### Having a partition back a model
69
69
 
70
-
71
70
  In your migration:
72
71
  ```ruby
73
72
  # db/migrate/create_events.rb
@@ -93,7 +92,7 @@ end
93
92
 
94
93
  In your model, calling one of `range_partition` or `list_partition` to inject
95
94
  methods:
96
- ```
95
+ ```ruby
97
96
  # app/models/event.rb
98
97
  class Event < ApplicationRecord
99
98
  range_partition
@@ -118,6 +117,49 @@ You can also create new partitions directly from the model :
118
117
  # => ["events_y2018m12", "events_y2019m01", "events_y2019m02"]
119
118
  ```
120
119
 
120
+ ### Partitioning an existing table
121
+ Start by renaming your table and create the partition table:
122
+ ```ruby
123
+ class PartitionEvents < ActiveRecord::Migration
124
+ def change
125
+ # Get the bounds of the events.
126
+ min_month = Event.minimum(:timestamp).beginning_of_month.to_date
127
+ max_month = Event.maximum(:timestamp).beginning_of_month.to_date
128
+
129
+ # Create the partition bounds based on the existing data. In this example,
130
+ # we generate an array with the ranges.
131
+ months = min_month.upto(max_month).uniq(&:beginning_of_month)
132
+
133
+ # Rename the existing table.
134
+ rename_table :events, :old_events
135
+
136
+ # Create the partitioned table.
137
+ create_range_partition :events, partition_key: -> { '(timestamp::DATE)' } do |t|
138
+ t.string :event_type, null: false
139
+ t.integer :value, null: false
140
+ t.datetime :timestamp, null: false
141
+ t.timestamps
142
+ end
143
+
144
+ # Create the partitions based on the bounds generated before:
145
+ months.each do |month|
146
+ # Creates a name like "events_y2018m12"
147
+ partition_name = "events_y#{month.year}m#{month.month}"
148
+
149
+ create_range_partition_of :events,
150
+ name: partition_name, range_start: month, range_end: month.next_month
151
+ end
152
+
153
+ # Finally, add the rows from the old table to the new partitioned table.
154
+ # This might take some time depending on the size of your old table.
155
+ execute(<<~SQL)
156
+ INSERT INTO events
157
+ SELECT * FROM old_events
158
+ SQL
159
+ end
160
+ end
161
+ ```
162
+
121
163
  ## Development
122
164
 
123
165
  After checking out the repo, run `bin/setup` to install dependencies.
@@ -31,6 +31,13 @@ module Tablature
31
31
  postgresql_version >= 110_000
32
32
  end
33
33
 
34
+ # True if the connection supports default partitions.
35
+ #
36
+ # @return [Boolean]
37
+ def supports_default_partitions?
38
+ postgresql_version >= 110_000
39
+ end
40
+
34
41
  # An integer representing the version of Postgres we're connected to.
35
42
  #
36
43
  # +postgresql_version+ is public in Rails 5, but protected in earlier
@@ -1,6 +1,23 @@
1
1
  module Tablature
2
2
  module Adapters
3
3
  class Postgres
4
+ # Raised when a setting a partition as default on a database
5
+ # version that does not support default partitions.
6
+ #
7
+ # Default partitions are supported on Postgres 11 or newer.
8
+ class DefaultPartitionNotSupportedError < StandardError
9
+ def initialize
10
+ super('Default partitions require Postgres 11 or newer')
11
+ end
12
+ end
13
+
14
+ # Raised when trying to attach or detach a partition without supplying a name.
15
+ class MissingPartitionName < StandardError
16
+ def initialize
17
+ super('Missing partition name')
18
+ end
19
+ end
20
+
4
21
  # Raised when a list partition operation is attempted on a database
5
22
  # version that does not support list partitions.
6
23
  #
@@ -15,7 +32,7 @@ module Tablature
15
32
  # key.
16
33
  class MissingListPartitionValuesError < StandardError
17
34
  def initialize
18
- super('Missing values for of list partition')
35
+ super('Missing values for list partition')
19
36
  end
20
37
  end
21
38
 
@@ -33,7 +50,7 @@ module Tablature
33
50
  # key.
34
51
  class MissingRangePartitionBoundsError < StandardError
35
52
  def initialize
36
- super('Missing bounds for of range partition')
53
+ super('Missing bounds for range partition')
37
54
  end
38
55
  end
39
56
  end
@@ -13,6 +13,10 @@ module Tablature
13
13
 
14
14
  protected
15
15
 
16
+ def raise_unless_default_partition_supported
17
+ raise DefaultPartitionNotSupportedError unless connection.supports_default_partitions?
18
+ end
19
+
16
20
  def create_partition(table_name, id_options, table_options, &block)
17
21
  create_table(table_name, table_options) do |td|
18
22
  # TODO: Handle the id things here (depending on the postgres version)
@@ -29,15 +29,50 @@ module Tablature
29
29
 
30
30
  def create_list_partition_of(parent_table, options)
31
31
  values = options.fetch(:values, [])
32
- raise MissingListPartitionValuesError if values.blank?
32
+ as_default = options.fetch(:default, false)
33
+
34
+ raise_unless_default_partition_supported if as_default
35
+ raise MissingListPartitionValuesError if values.blank? && !as_default
33
36
 
34
37
  name = options.fetch(:name, partition_name(parent_table, values))
35
38
  # TODO: Call `create_table` here instead of running the query.
36
39
  # TODO: Pass the options to `create_table` to allow further configuration of the table,
37
40
  # e.g. sub-partitioning the table.
38
- query = <<~SQL.strip
41
+
42
+ query = <<~SQL
39
43
  CREATE TABLE #{quote_table_name(name)} PARTITION OF #{quote_table_name(parent_table)}
40
- FOR VALUES IN (#{quote_collection(values)})
44
+ SQL
45
+
46
+ query += if as_default
47
+ 'DEFAULT'
48
+ else
49
+ "FOR VALUES IN (#{quote_collection(values)})"
50
+ end
51
+
52
+ execute(query)
53
+ end
54
+
55
+ def attach_to_list_partition(parent_table, options)
56
+ values = options.fetch(:values, [])
57
+ as_default = options.fetch(:default, false)
58
+
59
+ raise_unless_default_partition_supported if as_default
60
+ raise MissingListPartitionValuesError if values.blank? && !as_default
61
+
62
+ name = options.fetch(:name) { raise MissingPartitionName }
63
+
64
+ if as_default
65
+ attach_default_partition(parent_table, name)
66
+ else
67
+ attach_partition(parent_table, name, values)
68
+ end
69
+ end
70
+
71
+ def detach_from_list_partition(parent_table, options)
72
+ name = options.fetch(:name) { raise MissingPartitionName }
73
+ query = <<~SQL.strip
74
+ ALTER TABLE #{quote_table_name(parent_table)}
75
+ DETACH PARTITION #{quote_table_name(name)}
41
76
  SQL
42
77
 
43
78
  execute(query)
@@ -59,6 +94,25 @@ module Tablature
59
94
  key = values.inspect
60
95
  "#{parent_table}_#{Digest::MD5.hexdigest(key)[0..6]}"
61
96
  end
97
+
98
+ def attach_default_partition(parent_table, partition_name)
99
+ query = <<~SQL.strip
100
+ ALTER TABLE #{quote_table_name(parent_table)}
101
+ ATTACH PARTITION #{quote_table_name(partition_name)} DEFAULT
102
+ SQL
103
+
104
+ execute(query)
105
+ end
106
+
107
+ def attach_partition(parent_table, partition_name, values)
108
+ query = <<~SQL.strip
109
+ ALTER TABLE #{quote_table_name(parent_table)}
110
+ ATTACH PARTITION #{quote_table_name(partition_name)}
111
+ FOR VALUES IN (#{quote_collection(values)})
112
+ SQL
113
+
114
+ execute(query)
115
+ end
62
116
  end
63
117
  end
64
118
  end
@@ -31,16 +31,56 @@ module Tablature
31
31
  def create_range_partition_of(parent_table, options)
32
32
  range_start = options.fetch(:range_start, nil)
33
33
  range_end = options.fetch(:range_end, nil)
34
+ as_default = options.fetch(:default, false)
34
35
 
35
- raise MissingRangePartitionBoundsError if range_start.nil? || range_end.nil?
36
+ raise_unless_default_partition_supported if as_default
37
+ if (range_start.nil? || range_end.nil?) && !as_default
38
+ raise MissingRangePartitionBoundsError
39
+ end
36
40
 
37
41
  name = options.fetch(:name, partition_name(parent_table, range_start, range_end))
38
42
  # TODO: Call `create_table` here instead of running the query.
39
43
  # TODO: Pass the options to `create_table` to allow further configuration of the table,
40
44
  # e.g. sub-partitioning the table.
41
- query = <<~SQL.strip
45
+
46
+ query = <<~SQL
42
47
  CREATE TABLE #{quote_table_name(name)} PARTITION OF #{quote_table_name(parent_table)}
43
- FOR VALUES FROM (#{quote(range_start)}) TO (#{quote(range_end)});
48
+ SQL
49
+
50
+ query += if as_default
51
+ 'DEFAULT'
52
+ else
53
+ "FOR VALUES FROM (#{quote(range_start)}) TO (#{quote(range_end)})"
54
+ end
55
+
56
+ execute(query)
57
+ end
58
+
59
+ def attach_to_range_partition(parent_table, options)
60
+ range_start = options.fetch(:range_start, nil)
61
+ range_end = options.fetch(:range_end, nil)
62
+ as_default = options.fetch(:default, false)
63
+
64
+ raise_unless_default_partition_supported if as_default
65
+ if (range_start.nil? || range_end.nil?) && !as_default
66
+ raise MissingRangePartitionBoundsError
67
+ end
68
+
69
+ name = options.fetch(:name) { raise MissingPartitionName }
70
+
71
+ if as_default
72
+ attach_default_partition(parent_table, name)
73
+ else
74
+ attach_partition(parent_table, name, range_start, range_end)
75
+ end
76
+ end
77
+
78
+ def detach_from_range_partition(parent_table, options)
79
+ name = options.fetch(:name) { raise MissingPartitionName }
80
+
81
+ query = <<~SQL.strip
82
+ ALTER TABLE #{quote_table_name(parent_table)}
83
+ DETACH PARTITION #{quote_table_name(name)}
44
84
  SQL
45
85
 
46
86
  execute(query)
@@ -61,6 +101,25 @@ module Tablature
61
101
  key = [range_start, range_end].join(', ')
62
102
  "#{parent_table}_#{Digest::MD5.hexdigest(key)[0..6]}"
63
103
  end
104
+
105
+ def attach_default_partition(parent_table, partition_name)
106
+ query = <<~SQL.strip
107
+ ALTER TABLE #{quote_table_name(parent_table)}
108
+ ATTACH PARTITION #{quote_table_name(partition_name)} DEFAULT
109
+ SQL
110
+
111
+ execute(query)
112
+ end
113
+
114
+ def attach_partition(parent_table, partition_name, range_start, range_end)
115
+ query = <<~SQL.strip
116
+ ALTER TABLE #{quote_table_name(parent_table)}
117
+ ATTACH PARTITION #{quote_table_name(partition_name)}
118
+ FOR VALUES FROM (#{quote(range_start)}) TO (#{quote(range_end)})
119
+ SQL
120
+
121
+ execute(query)
122
+ end
64
123
  end
65
124
  end
66
125
  end
@@ -23,9 +23,11 @@ module Tablature
23
23
  connection.execute(<<-SQL)
24
24
  SELECT
25
25
  c.oid,
26
+ i.inhrelid,
26
27
  c.relname AS table_name,
27
- p.partstrat AS type,
28
+ p.partstrat AS strategy,
28
29
  (i.inhrelid::REGCLASS)::TEXT AS partition_name,
30
+ #{connection.supports_default_partitions? ? 'i.inhrelid = p.partdefid AS is_default_partition,' : ''}
29
31
  pg_get_partkeydef(c.oid) AS partition_key_definition
30
32
  FROM pg_class c
31
33
  INNER JOIN pg_partitioned_table p ON c.oid = p.partrelid
@@ -39,26 +41,25 @@ module Tablature
39
41
  SQL
40
42
  end
41
43
 
42
- METHOD_MAP = {
44
+ STRATEGY_MAP = {
43
45
  'l' => :list,
44
46
  'r' => :range,
45
47
  'h' => :hash
46
48
  }.freeze
47
- private_constant :METHOD_MAP
49
+ private_constant :STRATEGY_MAP
48
50
 
49
51
  def to_tablature_table(table_name, rows)
50
52
  result = rows.first
51
- partitioning_method = METHOD_MAP.fetch(result['type'])
52
- partitions = rows.map { |row| row['partition_name'] }.compact.map(&method(:unquote))
53
+ partitioning_strategy = STRATEGY_MAP.fetch(result['strategy'])
53
54
  # This is very fragile code. This makes the assumption that:
54
55
  # - Postgres will always have a function `pg_get_partkeydef` that returns the partition
55
- # method with the partition key
56
- # - Postgres will never have a partition method with two words in its name.
56
+ # strategy with the partition key
57
+ # - Postgres will never have a partition strategy with two words in its name.
57
58
  _, partition_key = result['partition_key_definition'].split(' ', 2)
58
59
 
59
60
  Tablature::PartitionedTable.new(
60
- name: table_name, partitioning_method: partitioning_method,
61
- partitions: partitions, partition_key: partition_key
61
+ name: table_name, partitioning_strategy: partitioning_strategy,
62
+ partitions: rows, partition_key: partition_key
62
63
  )
63
64
  end
64
65
 
@@ -68,14 +68,44 @@ module Tablature
68
68
  # @option options [String, Symbol] :values The values appearing in the partition.
69
69
  # @option options [String, Symbol] :name The name of the partition. If it is not given, this
70
70
  # will be randomly generated.
71
+ # @option options [Boolean] :default Whether the partition is the default partition or not.
71
72
  #
72
73
  # @example
73
74
  # # With a table :events partitioned using the list method on the partition key `date`:
74
75
  # create_list_partition_of :events, name: "events_2018-W49", values: [
75
- # "2018-12-03", "2018-12-04", "2018-12-05", "2018-12-06", "2018-12-07", "2018-12-08", "2018-12-09"
76
+ # "2018-12-03", "2018-12-04", "2018-12-05", "2018-12-06", "2018-12-07", "2018-12-08",
77
+ # "2018-12-09"
76
78
  # ]
77
79
  delegate :create_list_partition_of, to: :list_handler
78
80
 
81
+ # @!method attach_to_list_partition(parent_table_name, options)
82
+ # Attaches a partition to a parent by specifying the key values appearing in the partition.
83
+ #
84
+ # @param parent_table_name [String, Symbol] The name of the parent table.
85
+ # @param [Hash] options The options to attach the partition.
86
+ # @option options [String, Symbol] :name The name of the partition.
87
+ # @option options [String, Symbol] :values The values appearing in the partition.
88
+ #
89
+ # @example
90
+ # # With a table :events partitioned using the list method on the partition key `date`:
91
+ # attach_to_list_partition :events, name: "events_2018-W49", values: [
92
+ # "2018-12-03", "2018-12-04", "2018-12-05", "2018-12-06", "2018-12-07", "2018-12-08",
93
+ # "2018-12-09"
94
+ # ]
95
+ delegate :attach_to_list_partition, to: :list_handler
96
+
97
+ # @!method detach_from_list_partition(parent_table_name, options)
98
+ # Detaches a partition from a parent.
99
+ #
100
+ # @param parent_table_name [String, Symbol] The name of the parent table.
101
+ # @param [Hash] options The options to create the partition.
102
+ # @option options [String, Symbol] :name The name of the partition.
103
+ #
104
+ # @example
105
+ # # With a table :events partitioned using the list method on the partition key `date`:
106
+ # detach_from_list_partition :events, name: "events_2018-W49"
107
+ delegate :detach_from_list_partition, to: :list_handler
108
+
79
109
  # @!method create_range_partition(table_name, options, &block)
80
110
  # Creates a partitioned table using the range partition method.
81
111
  #
@@ -102,12 +132,13 @@ module Tablature
102
132
  #
103
133
  # @param parent_table_name [String, Symbol] The name of the parent table.
104
134
  # @param [Hash] options The options to create the partition.
135
+ # @option options [String, Symbol] :name The name of the partition. If it is not given, this
136
+ # will be randomly generated.
105
137
  # @option options [String, Symbol] :range_start The start of the range of values appearing in
106
138
  # the partition.
107
139
  # @option options [String, Symbol] :range_end The end of the range of values appearing in
108
140
  # the partition.
109
- # @option options [String, Symbol] :name The name of the partition. If it is not given, this
110
- # will be randomly generated.
141
+ # @option options [Boolean] :default Whether the partition is the default partition or not.
111
142
  #
112
143
  # @example
113
144
  # # With a table :events partitioned using the range method on the partition key `date`:
@@ -115,6 +146,35 @@ module Tablature
115
146
  # range_end: '2018-12-10'
116
147
  delegate :create_range_partition_of, to: :range_handler
117
148
 
149
+ # @!method attach_to_range_partition(parent_table_name, options)
150
+ # Attaches a partition to a parent by specifying the key values appearing in the partition.
151
+ #
152
+ # @param parent_table_name [String, Symbol] The name of the parent table.
153
+ # @param [Hash] options The options to create the partition.
154
+ # @option options [String, Symbol] :name The name of the partition.
155
+ # @option options [String, Symbol] :range_start The start of the range of values appearing in
156
+ # the partition.
157
+ # @option options [String, Symbol] :range_end The end of the range of values appearing in
158
+ # the partition.
159
+ # @option options [Boolean] :default Whether the partition is the default partition or not.
160
+ #
161
+ # @example
162
+ # # With a table :events partitioned using the range method on the partition key `date`:
163
+ # attach_to_range_partition :events, name: "events_2018-W49", range_start: '2018-12-03',
164
+ # range_end: '2018-12-10'
165
+ delegate :attach_to_range_partition, to: :range_handler
166
+
167
+ # @!method detach_from_range_partition(parent_table_name, options)
168
+ # Detaches a partition from a parent.
169
+ #
170
+ # @param parent_table_name [String, Symbol] The name of the parent table.
171
+ # @param [Hash] options The options to detach the partition.
172
+ # @option options [String, Symbol] :name The name of the partition.
173
+ #
174
+ # @example
175
+ # detach_from_range_partition :events, name: "events_2018-W49"
176
+ delegate :detach_from_range_partition, to: :range_handler
177
+
118
178
  # Returns an array of partitioned tables in the database.
119
179
  #
120
180
  # This collection of tables is used by the [Tablature::SchemaDumper] to populate the schema.rb
@@ -1,4 +1,97 @@
1
1
  module Tablature
2
+ # @api private
2
3
  module CommandRecorder
4
+ def create_list_partition(*args)
5
+ record(:create_list_partition, args)
6
+ end
7
+
8
+ def create_list_partition_of(*args)
9
+ record(:create_list_partition_of, args)
10
+ end
11
+
12
+ def attach_to_list_partition(*args)
13
+ record(:attach_to_list_partition, args)
14
+ end
15
+
16
+ def detach_from_list_partition(*args)
17
+ record(:detach_from_list_partition, args)
18
+ end
19
+
20
+ def create_range_partition(*args)
21
+ record(:create_range_partition, args)
22
+ end
23
+
24
+ def create_range_partition_of(*args)
25
+ record(:create_range_partition_of, args)
26
+ end
27
+
28
+ def attach_to_range_partition(*args)
29
+ record(:attach_to_range_partition, args)
30
+ end
31
+
32
+ def detach_from_range_partition(*args)
33
+ record(:detach_from_range_partition, args)
34
+ end
35
+
36
+ def invert_create_partition(args)
37
+ [:drop_table, [args.first]]
38
+ end
39
+
40
+ alias :invert_create_list_partition :invert_create_partition
41
+ alias :invert_create_range_partition :invert_create_partition
42
+
43
+ def invert_create_partition_of(args)
44
+ _parent_table_name, options = args
45
+ partition_name = options[:name]
46
+
47
+ [:drop_table, [partition_name]]
48
+ end
49
+
50
+ alias :invert_create_list_partition_of :invert_create_partition_of
51
+ alias :invert_create_range_partition_of :invert_create_partition_of
52
+
53
+ def invert_attach_to_range_partition(args)
54
+ [:detach_from_range_partition, args]
55
+ end
56
+
57
+ def invert_detach_from_range_partition(args)
58
+ parent_table_name, options = args
59
+ options ||= {}
60
+ _partition_name = options[:name]
61
+
62
+ range_start = options[:range_start]
63
+ range_end = options[:range_end]
64
+ default = options[:default]
65
+
66
+ if (range_start.nil? || range_end.nil?) && default.blank?
67
+ message = <<-MESSAGE
68
+ invert_detach_from_range_partition is reversible only if given bounds or the default option
69
+ MESSAGE
70
+ raise ActiveRecord::IrreversibleMigration, message
71
+ end
72
+
73
+ [:attach_to_range_partition, [parent_table_name, options]]
74
+ end
75
+
76
+ def invert_attach_to_list_partition(args)
77
+ [:detach_from_list_partition, args]
78
+ end
79
+
80
+ def invert_detach_from_list_partition(args)
81
+ partitioned_table, options = args
82
+ options ||= {}
83
+
84
+ default = options[:default]
85
+ values = options[:values] || []
86
+
87
+ if values.blank? && default.blank?
88
+ message = <<-MESSAGE
89
+ invert_detach_from_list_partition is reversible only if given the value list or the default option
90
+ MESSAGE
91
+ raise ActiveRecord::IrreversibleMigration, message
92
+ end
93
+
94
+ [:attach_to_list_partition, [partitioned_table, options]]
95
+ end
3
96
  end
4
97
  end
@@ -15,7 +15,7 @@ module Tablature
15
15
  module ClassMethods
16
16
  extend Forwardable
17
17
 
18
- def_delegators :tablature_partition, :partitions, :partition_key, :partitioning_method
18
+ def_delegators :tablature_partition, :partitions, :partition_key, :partitioning_strategy
19
19
 
20
20
  def partitioned?
21
21
  begin
@@ -28,12 +28,14 @@ module Tablature
28
28
  end
29
29
 
30
30
  def tablature_partition
31
- partition = Tablature.database.partitioned_tables.find do |pt|
31
+ return @tablature_partition if defined?(@tablature_partition)
32
+
33
+ @tablature_partition = Tablature.database.partitioned_tables.find do |pt|
32
34
  pt.name == partition_name.to_s
33
35
  end
34
- raise Tablature::MissingPartition if partition.nil?
36
+ raise Tablature::MissingPartition if @tablature_partition.nil?
35
37
 
36
- partition
38
+ @tablature_partition
37
39
  end
38
40
 
39
41
  def list_partition(partition_name = table_name)
@@ -0,0 +1,23 @@
1
+ module Tablature
2
+ # The in-memory representation of a partition.
3
+ #
4
+ # **This object is used internally by adapters and the schema dumper and is
5
+ # not intended to be used by application code. It is documented here for
6
+ # use by adapter gems.**
7
+ #
8
+ # @api private
9
+ class Partition
10
+ attr_reader :name
11
+ attr_reader :parent_table_name
12
+
13
+ def initialize(name:, parent_table_name:, default_partition: false)
14
+ @name = name
15
+ @parent_table_name = parent_table_name
16
+ @default_partition = default_partition
17
+ end
18
+
19
+ def default_partition?
20
+ @default_partition
21
+ end
22
+ end
23
+ end
@@ -5,35 +5,54 @@ module Tablature
5
5
  # not intended to be used by application code. It is documented here for
6
6
  # use by adapter gems.**
7
7
  #
8
- # @api extension
8
+ # @api private
9
9
  class PartitionedTable
10
10
  # The name of the partitioned table
11
11
  # @return [String]
12
+ # @api private
12
13
  attr_reader :name
13
14
 
14
- # The partitioning method of the table
15
+ # The partitioning strategy of the table
15
16
  # @return [Symbol]
16
- attr_reader :partitioning_method
17
+ # @api private
18
+ attr_reader :partitioning_strategy
17
19
 
18
20
  # The partitions of the table.
19
21
  # @return [Array]
22
+ # @api private
20
23
  attr_reader :partitions
21
24
 
22
25
  # The partition key expression.
23
26
  # @return [String]
27
+ # @api private
24
28
  attr_reader :partition_key
25
29
 
26
30
  # Returns a new instance of PartitionTable.
27
31
  #
28
32
  # @param name [String] The name of the view.
29
- # @param partitioning_method [:symbol] One of :range, :list or :hash
33
+ # @param partitioning_strategy [:symbol] One of :range, :list or :hash
30
34
  # @param partitions [Array] The partitions of the table.
31
35
  # @param partition_key [String] The partition key expression.
32
- def initialize(name:, partitioning_method:, partitions: [], partition_key:)
36
+ # @api private
37
+ def initialize(name:, partitioning_strategy:, partitions: [], partition_key:)
33
38
  @name = name
34
- @partitioning_method = partitioning_method
35
- @partitions = partitions
39
+ @partitioning_strategy = partitioning_strategy
40
+ @partitions = partitions.map do |row|
41
+ Tablature::Partition.new(
42
+ name: row['partition_name'],
43
+ parent_table_name: row['table_name'],
44
+ default_partition: row['is_default_partition']
45
+ )
46
+ end
36
47
  @partition_key = partition_key
37
48
  end
49
+
50
+ # Returns the representation of the default partition if present.
51
+ #
52
+ # @return [Tablature::Partition]
53
+ # @api private
54
+ def default_partition
55
+ partitions.find(&:default_partition?)
56
+ end
38
57
  end
39
58
  end
@@ -16,7 +16,7 @@ module Tablature
16
16
  end
17
17
 
18
18
  def partitions
19
- dumpable_partitioned_tables.flat_map(&:partitions)
19
+ dumpable_partitioned_tables.flat_map { |t| t.partitions.map(&:name) }
20
20
  end
21
21
  end
22
22
  end
@@ -3,15 +3,15 @@ module Tablature
3
3
  module Statements
4
4
  # Creates a partitioned table using the list partition method.
5
5
  #
6
- # @param name [String, Symbol] The name of the partition.
6
+ # @param table_name [String, Symbol] The name of the partition.
7
7
  # @param options [Hash] The options to create the partition.
8
8
  # @yield [td] A TableDefinition object. This allows creating the table columns the same way
9
9
  # as Rails's +create_table+ does.
10
10
  # @see Tablature::Adapters::Postgres#create_list_partition
11
- def create_list_partition(name, options, &block)
11
+ def create_list_partition(table_name, options, &block)
12
12
  raise ArgumentError, 'partition_key must be defined' if options[:partition_key].nil?
13
13
 
14
- Tablature.database.create_list_partition(name, options, &block)
14
+ Tablature.database.create_list_partition(table_name, options, &block)
15
15
  end
16
16
 
17
17
  # Creates a partition of a parent by specifying the key values appearing in the partition.
@@ -21,23 +21,52 @@ module Tablature
21
21
  #
22
22
  # @see Tablature::Adapters::Postgres#create_list_partition_of
23
23
  def create_list_partition_of(parent_table_name, options)
24
- raise ArgumentError, 'values must be defined' if options[:values].nil?
24
+ if options[:values].blank? && options[:default].blank?
25
+ raise ArgumentError, 'values or default must be defined'
26
+ end
25
27
 
26
28
  Tablature.database.create_list_partition_of(parent_table_name, options)
27
29
  end
28
30
 
31
+ # Attaches a partition to a parent by specifying the key values appearing in the partition.
32
+ #
33
+ # @param parent_table_name [String, Symbol] The name of the parent table.
34
+ # @param [Hash] options The options to attach the partition.
35
+ #
36
+ # @see Tablature::Adapters::Postgres#attach_to_list_partition
37
+ def attach_to_list_partition(parent_table_name, options)
38
+ raise ArgumentError, 'name must be defined' if options[:name].blank?
39
+
40
+ Tablature.database.attach_to_list_partition(parent_table_name, options)
41
+ end
42
+
43
+ # Detaches a partition from a parent.
44
+ #
45
+ # @param parent_table_name [String, Symbol] The name of the parent table.
46
+ # @param [Hash] options The options to create the partition.
47
+ #
48
+ # @see Tablature::Adapters::Postgres#detach_from_list_partition
49
+ def detach_from_list_partition(parent_table_name, options)
50
+ raise ArgumentError, 'name must be defined' if options[:name].blank?
51
+ if options[:values].blank? && options[:default].blank?
52
+ raise ArgumentError, 'values or default must be defined'
53
+ end
54
+
55
+ Tablature.database.attach_to_list_partition(parent_table_name, options)
56
+ end
57
+
29
58
  # Creates a partitioned table using the range partition method.
30
59
  #
31
- # @param name [String, Symbol] The name of the partition.
60
+ # @param table_name [String, Symbol] The name of the partition.
32
61
  # @param options [Hash] The options to create the partition.
33
62
  # @yield [td] A TableDefinition object. This allows creating the table columns the same way
34
63
  # as Rails's +create_table+ does.
35
64
  #
36
65
  # @see Tablature::Adapters::Postgres#create_range_partition
37
- def create_range_partition(name, options, &block)
66
+ def create_range_partition(table_name, options, &block)
38
67
  raise ArgumentError, 'partition_key must be defined' if options[:partition_key].nil?
39
68
 
40
- Tablature.database.create_range_partition(name, options, &block)
69
+ Tablature.database.create_range_partition(table_name, options, &block)
41
70
  end
42
71
 
43
72
  # Creates a partition of a parent by specifying the key values appearing in the partition.
@@ -46,12 +75,39 @@ module Tablature
46
75
  # @param [Hash] options The options to create the partition.
47
76
  #
48
77
  # @see Tablature::Adapters::Postgres#create_range_partition_of
49
- def create_range_partition_of(parent_table, options)
50
- if options[:range_start].nil? || options[:range_end].nil?
51
- raise ArgumentError, 'range_start and range_end must be defined'
78
+ def create_range_partition_of(parent_table_name, options)
79
+ if (options[:range_start].nil? || options[:range_end].nil?) && options[:default].blank?
80
+ raise ArgumentError, 'range_start and range_end or default must be defined'
52
81
  end
53
82
 
54
- Tablature.database.create_range_partition_of(parent_table, options)
83
+ Tablature.database.create_range_partition_of(parent_table_name, options)
84
+ end
85
+
86
+ # Attaches a partition to a parent by specifying the key values appearing in the partition.
87
+ #
88
+ # @param parent_table_name [String, Symbol] The name of the parent table.
89
+ # @param [Hash] options The options to create the partition.
90
+ #
91
+ # @see Tablature::Adapters::Postgres#attach_to_range_partition
92
+ def attach_to_range_partition(parent_table_name, options)
93
+ raise ArgumentError, 'name must be defined' if options[:name].blank?
94
+ if (options[:range_start].nil? || options[:range_end].nil?) && options[:default].blank?
95
+ raise ArgumentError, 'range_start and range_end or default must be defined'
96
+ end
97
+
98
+ Tablature.database.attach_to_range_partition(parent_table_name, options)
99
+ end
100
+
101
+ # Detaches a partition from a parent.
102
+ #
103
+ # @param parent_table_name [String, Symbol] The name of the parent table.
104
+ # @param [Hash] options The options to detach the partition.
105
+ #
106
+ # @see Tablature::Adapters::Postgres#detach_from_range_partition
107
+ def detach_from_range_partition(parent_table_name, options)
108
+ raise ArgumentError, 'name must be defined' if options[:name].blank?
109
+
110
+ Tablature.database.detach_from_range_partition(parent_table_name, options)
55
111
  end
56
112
  end
57
113
  end
@@ -1,3 +1,3 @@
1
1
  module Tablature
2
- VERSION = '0.3.1'.freeze
2
+ VERSION = '1.0.0.pre'.freeze
3
3
  end
data/lib/tablature.rb CHANGED
@@ -2,6 +2,7 @@ require 'tablature/adapters/postgres'
2
2
  require 'tablature/command_recorder'
3
3
  require 'tablature/configuration'
4
4
  require 'tablature/model'
5
+ require 'tablature/partition'
5
6
  require 'tablature/partitioned_table'
6
7
  require 'tablature/railtie'
7
8
  require 'tablature/schema_dumper'
data/tablature.gemspec CHANGED
@@ -20,10 +20,12 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.add_development_dependency 'bundler', '~> 1.16'
23
+ spec.required_ruby_version = '>= 2.5.0'
24
+
25
+ spec.add_development_dependency 'bundler'
24
26
  spec.add_development_dependency 'database_cleaner'
25
- spec.add_development_dependency 'pg', '~> 0.19'
26
- spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'pg', '~> 1.1'
28
+ spec.add_development_dependency 'rake', '~> 13.0'
27
29
  spec.add_development_dependency 'rspec', '~> 3.0'
28
30
  spec.add_development_dependency 'rspec-instafail'
29
31
 
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tablature
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.0.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aliou Diallo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-18 00:00:00.000000000 Z
11
+ date: 2020-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.16'
19
+ version: '0'
20
20
  type: :development
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: '1.16'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: database_cleaner
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.19'
47
+ version: '1.1'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.19'
54
+ version: '1.1'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '13.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '13.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -129,13 +129,13 @@ executables: []
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
- - ".circleci/config.yml"
132
+ - ".github/workflows/ci.yml"
133
133
  - ".gitignore"
134
134
  - ".rspec"
135
135
  - ".rubocop.yml"
136
136
  - ".travis.yml"
137
+ - ".yardopts"
137
138
  - Gemfile
138
- - Gemfile.lock
139
139
  - LICENSE.txt
140
140
  - README.md
141
141
  - Rakefile
@@ -152,6 +152,7 @@ files:
152
152
  - lib/tablature/command_recorder.rb
153
153
  - lib/tablature/configuration.rb
154
154
  - lib/tablature/model.rb
155
+ - lib/tablature/partition.rb
155
156
  - lib/tablature/partitioned_table.rb
156
157
  - lib/tablature/railtie.rb
157
158
  - lib/tablature/schema_dumper.rb
@@ -170,15 +171,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
170
171
  requirements:
171
172
  - - ">="
172
173
  - !ruby/object:Gem::Version
173
- version: '0'
174
+ version: 2.5.0
174
175
  required_rubygems_version: !ruby/object:Gem::Requirement
175
176
  requirements:
176
- - - ">="
177
+ - - ">"
177
178
  - !ruby/object:Gem::Version
178
- version: '0'
179
+ version: 1.3.1
179
180
  requirements: []
180
- rubyforge_project:
181
- rubygems_version: 2.7.6
181
+ rubygems_version: 3.0.3
182
182
  signing_key:
183
183
  specification_version: 4
184
184
  summary: Rails + Postgres Partitions
data/.circleci/config.yml DELETED
@@ -1,81 +0,0 @@
1
- # TODO: Simplify this
2
- version: 2
3
- jobs:
4
- postgres_10:
5
- working_directory: ~/tablature-rb
6
- docker:
7
- - image: circleci/ruby:2.4
8
- environment:
9
- DATABASE_URL: "postgres://postgres@localhost:5432/dummy_test"
10
- - image: circleci/postgres:10-alpine
11
- environment:
12
- POSTGRES_USER: postgres
13
- POSTGRES_DB: dummy_test
14
- POSTGRES_PASSWORD: ""
15
- steps:
16
- - checkout
17
-
18
- - type: cache-restore
19
- name: Restore bundle cache
20
- key: tablature-{{ checksum "tablature.gemspec" }}-{{ checksum "Gemfile" }}
21
-
22
- - run:
23
- name: Install dependencies
24
- command: bundle install --path vendor/bundle
25
-
26
- - type: cache-save
27
- name: Store bundle cache
28
- key: tablature-{{ checksum "tablature.gemspec" }}-{{ checksum "Gemfile" }}
29
- paths:
30
- - vendor/bundle
31
-
32
- - run:
33
- name: Wait for Postgres
34
- command: dockerize -wait tcp://localhost:5432 -timeout 1m
35
-
36
- - run:
37
- name: Run tests
38
- command: bundle exec rspec --tag ~postgres_11
39
-
40
- postgres_11:
41
- working_directory: ~/tablature-rb
42
- docker:
43
- - image: circleci/ruby:2.4
44
- environment:
45
- DATABASE_URL: "postgres://postgres@localhost:5432/dummy_test"
46
- - image: circleci/postgres:11-alpine
47
- environment:
48
- POSTGRES_USER: postgres
49
- POSTGRES_DB: dummy_test
50
- POSTGRES_PASSWORD: ""
51
- steps:
52
- - checkout
53
-
54
- - type: cache-restore
55
- name: Restore bundle cache
56
- key: tablature-{{ checksum "tablature.gemspec" }}-{{ checksum "Gemfile" }}
57
-
58
- - run:
59
- name: Install dependencies
60
- command: bundle install --path vendor/bundle
61
-
62
- - type: cache-save
63
- name: Store bundle cache
64
- key: tablature-{{ checksum "tablature.gemspec" }}-{{ checksum "Gemfile" }}
65
- paths:
66
- - vendor/bundle
67
-
68
- - run:
69
- name: Wait for Postgres
70
- command: dockerize -wait tcp://localhost:5432 -timeout 1m
71
-
72
- - run:
73
- name: Run tests
74
- command: bundle exec rspec
75
-
76
- workflows:
77
- version: 2
78
- tablature:
79
- jobs:
80
- - postgres_10
81
- - postgres_11
data/Gemfile.lock DELETED
@@ -1,106 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- tablature (0.3.1)
5
- activerecord (>= 5.0.0)
6
- railties (>= 5.0.0)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- actionpack (5.2.2)
12
- actionview (= 5.2.2)
13
- activesupport (= 5.2.2)
14
- rack (~> 2.0)
15
- rack-test (>= 0.6.3)
16
- rails-dom-testing (~> 2.0)
17
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
- actionview (5.2.2)
19
- activesupport (= 5.2.2)
20
- builder (~> 3.1)
21
- erubi (~> 1.4)
22
- rails-dom-testing (~> 2.0)
23
- rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
- activemodel (5.2.2)
25
- activesupport (= 5.2.2)
26
- activerecord (5.2.2)
27
- activemodel (= 5.2.2)
28
- activesupport (= 5.2.2)
29
- arel (>= 9.0)
30
- activesupport (5.2.2)
31
- concurrent-ruby (~> 1.0, >= 1.0.2)
32
- i18n (>= 0.7, < 2)
33
- minitest (~> 5.1)
34
- tzinfo (~> 1.1)
35
- arel (9.0.0)
36
- builder (3.2.3)
37
- coderay (1.1.2)
38
- concurrent-ruby (1.1.4)
39
- crass (1.0.4)
40
- database_cleaner (1.7.0)
41
- diff-lcs (1.3)
42
- erubi (1.8.0)
43
- i18n (1.5.1)
44
- concurrent-ruby (~> 1.0)
45
- loofah (2.2.3)
46
- crass (~> 1.0.2)
47
- nokogiri (>= 1.5.9)
48
- method_source (0.9.0)
49
- mini_portile2 (2.4.0)
50
- minitest (5.11.3)
51
- nokogiri (1.10.1)
52
- mini_portile2 (~> 2.4.0)
53
- pg (0.21.0)
54
- pry (0.12.2)
55
- coderay (~> 1.1.0)
56
- method_source (~> 0.9.0)
57
- rack (2.0.6)
58
- rack-test (1.1.0)
59
- rack (>= 1.0, < 3)
60
- rails-dom-testing (2.0.3)
61
- activesupport (>= 4.2.0)
62
- nokogiri (>= 1.6)
63
- rails-html-sanitizer (1.0.4)
64
- loofah (~> 2.2, >= 2.2.2)
65
- railties (5.2.2)
66
- actionpack (= 5.2.2)
67
- activesupport (= 5.2.2)
68
- method_source
69
- rake (>= 0.8.7)
70
- thor (>= 0.19.0, < 2.0)
71
- rake (10.5.0)
72
- rspec (3.7.0)
73
- rspec-core (~> 3.7.0)
74
- rspec-expectations (~> 3.7.0)
75
- rspec-mocks (~> 3.7.0)
76
- rspec-core (3.7.1)
77
- rspec-support (~> 3.7.0)
78
- rspec-expectations (3.7.0)
79
- diff-lcs (>= 1.2.0, < 2.0)
80
- rspec-support (~> 3.7.0)
81
- rspec-instafail (1.0.0)
82
- rspec
83
- rspec-mocks (3.7.0)
84
- diff-lcs (>= 1.2.0, < 2.0)
85
- rspec-support (~> 3.7.0)
86
- rspec-support (3.7.1)
87
- thor (0.20.3)
88
- thread_safe (0.3.6)
89
- tzinfo (1.2.5)
90
- thread_safe (~> 0.1)
91
-
92
- PLATFORMS
93
- ruby
94
-
95
- DEPENDENCIES
96
- bundler (~> 1.16)
97
- database_cleaner
98
- pg (~> 0.19)
99
- pry
100
- rake (~> 10.0)
101
- rspec (~> 3.0)
102
- rspec-instafail
103
- tablature!
104
-
105
- BUNDLED WITH
106
- 1.17.2