tablature 0.1.1 → 1.0.0.pre2

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: eaec79244b29bb6514402673f8cb56f281c9cf25fb73c9a4dab413520e96b652
4
- data.tar.gz: 54cfe803f7387135d1334dba6b4b16a0c7d1946adb6c88e092889fe881203511
3
+ metadata.gz: a72b6d120e1af25bf4b044347d02c552898ede4de5082a01f8b4ec3e24f4f6b0
4
+ data.tar.gz: e011684eadd4c9f39b83db417c6ef282f5291ed947c23651e8e5c70fcb76aa53
5
5
  SHA512:
6
- metadata.gz: f30ffd3c9b8b14b08447b222a2bc5202042ef37f2c530beda33ed651c47ea7095f536bec494eef5767c44ba7a9b9751578bd9e3ff497e53203ac9bba477fc8a2
7
- data.tar.gz: 2553bb52f8fecfa642cf2b8c1f3908bcabf01c3a9c6e5902076e7d447e6cf64c8bd13069902d95cc7e4478daff7a08b1d899128b74722687ad5a0dcf6e0962a0
6
+ metadata.gz: 9b49e89742a179e6f50ccf28789bdb66999f5edb56a4cdcf57c273abc055b5bdc9bb6cbbb754639831613d0b90c034d9ee16c07ea6f56e14ca276a819081296f
7
+ data.tar.gz: 540d21c49ac1f7e74da0abb9c697cfbc94c72452408acfb891edaf377a5b199bda57ae670dfd051cf266e8c4da86ea51e699a5e4bc25b6e3f0cfc9ade27f378c
@@ -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
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.3
2
+ TargetRubyVersion: 2.5
3
3
 
4
4
  Metrics/BlockLength:
5
5
  Exclude:
@@ -0,0 +1 @@
1
+ -m markdown
data/Gemfile CHANGED
@@ -1,3 +1,11 @@
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
+ rails_constraint = rails_version == 'master' ? { github: 'rails/rails' } : "~> #{rails_version}.0"
10
+
11
+ 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
 
@@ -55,10 +55,107 @@ class CreateEvents < ActiveRecord::Migration[5.0]
55
55
  # Create partitions with the bounds of the partition.
56
56
  create_list_partition_of :events_by_list,
57
57
  name: 'events_list_y2018m12', values: (Date.parse('2018-12-01')..Date.parse('2018-12-31')).to_a
58
+
58
59
  end
59
60
 
60
61
  def down
61
- drop_table :events
62
+ drop_table :events_by_range
63
+ drop_table :events_by_list
64
+ end
65
+ end
66
+ ```
67
+
68
+ ### Having a partition back a model
69
+
70
+ In your migration:
71
+ ```ruby
72
+ # db/migrate/create_events.rb
73
+ class CreateEvents < ActiveRecord::Migration
74
+ def change
75
+ # You can use blocks when the partition key are SQL expression instead of
76
+ # being only a field.
77
+ create_range_partition :events, partition_key: -> { '(timestamp::DATE)' } do |t|
78
+ t.string :event_type, null: false
79
+ t.integer :value, null: false
80
+ t.datetime :timestamp, null: false
81
+ t.timestamps
82
+ end
83
+
84
+ create_range_partition_of :events,
85
+ name: 'events_y2018m12', range_start: '2018-12-01', range_end: '2019-01-01'
86
+
87
+ create_range_partition_of :events,
88
+ name: 'events_y2019m01', range_start: '2019-01-01', range_end: '2019-02-01'
89
+ end
90
+ end
91
+ ```
92
+
93
+ In your model, calling one of `range_partition` or `list_partition` to inject
94
+ methods:
95
+ ```ruby
96
+ # app/models/event.rb
97
+ class Event < ApplicationRecord
98
+ range_partition
99
+ end
100
+ ```
101
+
102
+ Finally, you can now list the partitions :
103
+ ```ruby
104
+ >> Event.partitions
105
+ # => ["events_y2018m12", "events_y2019m01"]
106
+ ```
107
+
108
+ You can also create new partitions directly from the model :
109
+ ```ruby
110
+ >> Event.create_range_partition(
111
+ name: 'events_y2019m02',
112
+ range_start: '2019-02-01'.to_date,
113
+ range_end: '2019-03-01'.to_date
114
+ )
115
+ # => ...
116
+ >> Event.partitions
117
+ # => ["events_y2018m12", "events_y2019m01", "events_y2019m02"]
118
+ ```
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
62
159
  end
63
160
  end
64
161
  ```
@@ -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'
@@ -20,7 +21,7 @@ module Tablature
20
21
  ActiveRecord::ConnectionAdapters::AbstractAdapter.include Tablature::Statements
21
22
  ActiveRecord::Migration::CommandRecorder.include Tablature::CommandRecorder
22
23
  ActiveRecord::SchemaDumper.prepend Tablature::SchemaDumper
23
- ActiveRecord::Base.prepend Tablature::Model
24
+ ActiveRecord::Base.include Tablature::Model
24
25
  end
25
26
 
26
27
  # The current database adapter used by Tablature.
@@ -29,4 +30,10 @@ module Tablature
29
30
  def self.database
30
31
  configuration.database
31
32
  end
33
+
34
+ class MissingPartition < StandardError
35
+ def initialize
36
+ super('Missing partition')
37
+ end
38
+ end
32
39
  end
@@ -4,6 +4,7 @@ require_relative 'postgres/connection'
4
4
  require_relative 'postgres/errors'
5
5
  require_relative 'postgres/handlers/list'
6
6
  require_relative 'postgres/handlers/range'
7
+ require_relative 'postgres/indexes'
7
8
  require_relative 'postgres/partitioned_tables'
8
9
 
9
10
  module Tablature
@@ -47,7 +48,7 @@ module Tablature
47
48
  # @param [String, Symbol] table_name The name of the table to partition.
48
49
  # @param [Hash] options The options to create the partition. Keys besides +:partition_key+
49
50
  # will be passed to +create_table+.
50
- # @option options [String, Symbol] :partition_key The partition key.
51
+ # @option options [String, Symbol, #call] :partition_key The partition key.
51
52
  # @yield [td] A TableDefinition object. This allows creating the table columns the same way
52
53
  # as Rails's +create_table+ does.
53
54
  #
@@ -68,14 +69,44 @@ module Tablature
68
69
  # @option options [String, Symbol] :values The values appearing in the partition.
69
70
  # @option options [String, Symbol] :name The name of the partition. If it is not given, this
70
71
  # will be randomly generated.
72
+ # @option options [Boolean] :default Whether the partition is the default partition or not.
71
73
  #
72
74
  # @example
73
75
  # # With a table :events partitioned using the list method on the partition key `date`:
74
76
  # 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"
77
+ # "2018-12-03", "2018-12-04", "2018-12-05", "2018-12-06", "2018-12-07", "2018-12-08",
78
+ # "2018-12-09"
76
79
  # ]
77
80
  delegate :create_list_partition_of, to: :list_handler
78
81
 
82
+ # @!method attach_to_list_partition(parent_table_name, options)
83
+ # Attaches a partition to a parent by specifying the key values appearing in the partition.
84
+ #
85
+ # @param parent_table_name [String, Symbol] The name of the parent table.
86
+ # @param [Hash] options The options to attach the partition.
87
+ # @option options [String, Symbol] :name The name of the partition.
88
+ # @option options [String, Symbol] :values The values appearing in the partition.
89
+ #
90
+ # @example
91
+ # # With a table :events partitioned using the list method on the partition key `date`:
92
+ # attach_to_list_partition :events, name: "events_2018-W49", values: [
93
+ # "2018-12-03", "2018-12-04", "2018-12-05", "2018-12-06", "2018-12-07", "2018-12-08",
94
+ # "2018-12-09"
95
+ # ]
96
+ delegate :attach_to_list_partition, to: :list_handler
97
+
98
+ # @!method detach_from_list_partition(parent_table_name, options)
99
+ # Detaches a partition from a parent.
100
+ #
101
+ # @param parent_table_name [String, Symbol] The name of the parent table.
102
+ # @param [Hash] options The options to create the partition.
103
+ # @option options [String, Symbol] :name The name of the partition.
104
+ #
105
+ # @example
106
+ # # With a table :events partitioned using the list method on the partition key `date`:
107
+ # detach_from_list_partition :events, name: "events_2018-W49"
108
+ delegate :detach_from_list_partition, to: :list_handler
109
+
79
110
  # @!method create_range_partition(table_name, options, &block)
80
111
  # Creates a partitioned table using the range partition method.
81
112
  #
@@ -84,7 +115,6 @@ module Tablature
84
115
  # @param [String, Symbol] table_name The name of the table to partition.
85
116
  # @param [Hash] options The options to create the partition. Keys besides +:partition_key+
86
117
  # will be passed to +create_table+.
87
- # @option options [String, Symbol] :partition_key The partition key.
88
118
  # @yield [td] A TableDefinition object. This allows creating the table columns the same way
89
119
  # as Rails's +create_table+ does.
90
120
  #
@@ -103,12 +133,13 @@ module Tablature
103
133
  #
104
134
  # @param parent_table_name [String, Symbol] The name of the parent table.
105
135
  # @param [Hash] options The options to create the partition.
136
+ # @option options [String, Symbol] :name The name of the partition. If it is not given, this
137
+ # will be randomly generated.
106
138
  # @option options [String, Symbol] :range_start The start of the range of values appearing in
107
139
  # the partition.
108
140
  # @option options [String, Symbol] :range_end The end of the range of values appearing in
109
141
  # the partition.
110
- # @option options [String, Symbol] :name The name of the partition. If it is not given, this
111
- # will be randomly generated.
142
+ # @option options [Boolean] :default Whether the partition is the default partition or not.
112
143
  #
113
144
  # @example
114
145
  # # With a table :events partitioned using the range method on the partition key `date`:
@@ -116,6 +147,35 @@ module Tablature
116
147
  # range_end: '2018-12-10'
117
148
  delegate :create_range_partition_of, to: :range_handler
118
149
 
150
+ # @!method attach_to_range_partition(parent_table_name, options)
151
+ # Attaches a partition to a parent by specifying the key values appearing in the partition.
152
+ #
153
+ # @param parent_table_name [String, Symbol] The name of the parent table.
154
+ # @param [Hash] options The options to create the partition.
155
+ # @option options [String, Symbol] :name The name of the partition.
156
+ # @option options [String, Symbol] :range_start The start of the range of values appearing in
157
+ # the partition.
158
+ # @option options [String, Symbol] :range_end The end of the range of values appearing in
159
+ # the partition.
160
+ # @option options [Boolean] :default Whether the partition is the default partition or not.
161
+ #
162
+ # @example
163
+ # # With a table :events partitioned using the range method on the partition key `date`:
164
+ # attach_to_range_partition :events, name: "events_2018-W49", range_start: '2018-12-03',
165
+ # range_end: '2018-12-10'
166
+ delegate :attach_to_range_partition, to: :range_handler
167
+
168
+ # @!method detach_from_range_partition(parent_table_name, options)
169
+ # Detaches a partition from a parent.
170
+ #
171
+ # @param parent_table_name [String, Symbol] The name of the parent table.
172
+ # @param [Hash] options The options to detach the partition.
173
+ # @option options [String, Symbol] :name The name of the partition.
174
+ #
175
+ # @example
176
+ # detach_from_range_partition :events, name: "events_2018-W49"
177
+ delegate :detach_from_range_partition, to: :range_handler
178
+
119
179
  # Returns an array of partitioned tables in the database.
120
180
  #
121
181
  # This collection of tables is used by the [Tablature::SchemaDumper] to populate the schema.rb
@@ -126,6 +186,17 @@ module Tablature
126
186
  PartitionedTables.new(connection).all
127
187
  end
128
188
 
189
+ # Indexes on the Partitioned Table.
190
+ #
191
+ # @param name [String] The name of the partitioned table we want indexes from.
192
+ # @return [Array<ActiveRecord::ConnectionAdapters::IndexDefinition>]
193
+ def indexes_on(partitioned_table)
194
+ return [] if Gem::Version.new(Rails.version) >= Gem::Version.new('6.0.3')
195
+ return [] unless connection.supports_indexes_on_partitioned_tables?
196
+
197
+ Indexes.new(connection).on(partitioned_table)
198
+ end
199
+
129
200
  private
130
201
 
131
202
  attr_reader :connectable
@@ -31,6 +31,20 @@ 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
+
41
+ # True if the connection supports indexes on partitioned tables.
42
+ #
43
+ # @return [Boolean]
44
+ def supports_indexes_on_partitioned_tables?
45
+ postgresql_version >= 110_000
46
+ end
47
+
34
48
  # An integer representing the version of Postgres we're connected to.
35
49
  #
36
50
  # +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