tablature 0.0.1 → 0.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a380696b917f7e478ea8823e9f9cd5fd0d1d77fbdfc540653d07fe7fc757922
4
- data.tar.gz: 7bb88dc5ecc00fcba4c2fcb59a081bd89d183512bb9813287357e8bb54b4bdae
3
+ metadata.gz: 78e1704fd9d861cc494afaea95d8b823cd9067ff7100215fee67a80fd3a6ed8c
4
+ data.tar.gz: d4cbb87e57da6eca06be82239cd9f2ed57e0ddfedc64313f7d065b72b63d12e4
5
5
  SHA512:
6
- metadata.gz: c18d7668835780d2df9b692b53ae81f2fb81eb16d26c2f2a4bf45cfb77f519e9b5c7f9cbd591c6a246f29d321f3c77187fa52a471af796b50a3b1142cd8fb5bc
7
- data.tar.gz: 3f4aa6a40d39775bf14a714bc025f4d0d50d5395ba8e4c3e10c6616401b00ab160f1d1cf29108fec628eb6ffff4f9a5ea91082e6282c95eba4d8e9a7679f9b77
6
+ metadata.gz: ccaf49c24f56fd9dedc98c852fb7c0e79b90b80cb56822257c8ae3068d23afc5b0e116b3f3f09491c2f0f0a17d2d474ace5038e3907747b3a8fce193eb7c36b7
7
+ data.tar.gz: af0ac14e53c058c9c69700e9f8ce8e8195935ec4fa2719212243fec0ed6a93827f10807dfccc65c83dff0145241b389dc3db3a5b28a5938242bf2bd377e58345
@@ -0,0 +1,81 @@
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/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
1
4
  Metrics/BlockLength:
2
5
  Exclude:
3
6
  - 'spec/**/*'
data/Gemfile CHANGED
@@ -1 +1,3 @@
1
1
  gemspec
2
+
3
+ gem 'pry'
data/Gemfile.lock CHANGED
@@ -1,46 +1,48 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tablature (0.0.1)
4
+ tablature (0.1.0)
5
5
  activerecord (>= 5.0.0)
6
6
  railties (>= 5.0.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- actionpack (5.2.1)
12
- actionview (= 5.2.1)
13
- activesupport (= 5.2.1)
11
+ actionpack (5.2.2)
12
+ actionview (= 5.2.2)
13
+ activesupport (= 5.2.2)
14
14
  rack (~> 2.0)
15
15
  rack-test (>= 0.6.3)
16
16
  rails-dom-testing (~> 2.0)
17
17
  rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
- actionview (5.2.1)
19
- activesupport (= 5.2.1)
18
+ actionview (5.2.2)
19
+ activesupport (= 5.2.2)
20
20
  builder (~> 3.1)
21
21
  erubi (~> 1.4)
22
22
  rails-dom-testing (~> 2.0)
23
23
  rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
- activemodel (5.2.1)
25
- activesupport (= 5.2.1)
26
- activerecord (5.2.1)
27
- activemodel (= 5.2.1)
28
- activesupport (= 5.2.1)
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
29
  arel (>= 9.0)
30
- activesupport (5.2.1)
30
+ activesupport (5.2.2)
31
31
  concurrent-ruby (~> 1.0, >= 1.0.2)
32
32
  i18n (>= 0.7, < 2)
33
33
  minitest (~> 5.1)
34
34
  tzinfo (~> 1.1)
35
35
  arel (9.0.0)
36
36
  builder (3.2.3)
37
+ coderay (1.1.2)
37
38
  concurrent-ruby (1.1.3)
38
39
  crass (1.0.4)
40
+ database_cleaner (1.7.0)
39
41
  diff-lcs (1.3)
40
42
  erubi (1.7.1)
41
43
  i18n (1.1.1)
42
44
  concurrent-ruby (~> 1.0)
43
- loofah (2.2.2)
45
+ loofah (2.2.3)
44
46
  crass (~> 1.0.2)
45
47
  nokogiri (>= 1.5.9)
46
48
  method_source (0.9.0)
@@ -49,6 +51,9 @@ GEM
49
51
  nokogiri (1.8.5)
50
52
  mini_portile2 (~> 2.3.0)
51
53
  pg (0.21.0)
54
+ pry (0.12.2)
55
+ coderay (~> 1.1.0)
56
+ method_source (~> 0.9.0)
52
57
  rack (2.0.6)
53
58
  rack-test (1.1.0)
54
59
  rack (>= 1.0, < 3)
@@ -57,9 +62,9 @@ GEM
57
62
  nokogiri (>= 1.6)
58
63
  rails-html-sanitizer (1.0.4)
59
64
  loofah (~> 2.2, >= 2.2.2)
60
- railties (5.2.1)
61
- actionpack (= 5.2.1)
62
- activesupport (= 5.2.1)
65
+ railties (5.2.2)
66
+ actionpack (= 5.2.2)
67
+ activesupport (= 5.2.2)
63
68
  method_source
64
69
  rake (>= 0.8.7)
65
70
  thor (>= 0.19.0, < 2.0)
@@ -79,7 +84,7 @@ GEM
79
84
  diff-lcs (>= 1.2.0, < 2.0)
80
85
  rspec-support (~> 3.7.0)
81
86
  rspec-support (3.7.1)
82
- thor (0.20.0)
87
+ thor (0.20.3)
83
88
  thread_safe (0.3.6)
84
89
  tzinfo (1.2.5)
85
90
  thread_safe (~> 0.1)
@@ -89,7 +94,9 @@ PLATFORMS
89
94
 
90
95
  DEPENDENCIES
91
96
  bundler (~> 1.16)
97
+ database_cleaner
92
98
  pg (~> 0.19)
99
+ pry
93
100
  rake (~> 10.0)
94
101
  rspec (~> 3.0)
95
102
  rspec-instafail
data/README.md CHANGED
@@ -1,11 +1,20 @@
1
1
  # Tablature
2
2
 
3
+ Tablature is a library built on top of ActiveRecord to simplify management of partitioned tables in Rails applications.
4
+ It ships with Postgres support and can easily supports other databases through adapters.
5
+
3
6
  ## Installation
4
7
 
8
+ ##### Requirements
9
+
10
+ Tablature requires Rails 5+ and Postgres 10+.
11
+
12
+ ##### Installation
13
+
5
14
  Add this line to your application's Gemfile:
6
15
 
7
16
  ```ruby
8
- gem 'tablature', github: 'aliou/tablature'
17
+ gem 'tablature'
9
18
  ```
10
19
 
11
20
  And then execute:
@@ -18,15 +27,56 @@ Or install it yourself as:
18
27
 
19
28
  ## Usage
20
29
 
30
+ ### Partitioning a table
31
+
32
+ ```ruby
33
+ class CreateEvents < ActiveRecord::Migration[5.0]
34
+ def up
35
+ # Create the events table as a partitioned table using range as partitioning method
36
+ # and `event_date` as partition key.
37
+ create_range_partition :events_by_range, partition_key: 'event_date' do |t|
38
+ t.string :event_type, null: false
39
+ t.integer :value, null: false
40
+ t.date :event_date, null: false
41
+ end
42
+
43
+ # Create partitions with the bounds of the partition.
44
+ create_range_partition_of :events_by_range,
45
+ name: 'events_range_y2018m12', range_start: '2018-12-01', range_end: '2019-01-01'
46
+
47
+ # Create the events table as a partitioned table using list as partitioning method
48
+ # and `event_date` as partition key.
49
+ create_list_partition :events_by_list, partition_key: 'event_date' do |t|
50
+ t.string :event_type, null: false
51
+ t.integer :value, null: false
52
+ t.date :event_date, null: false
53
+ end
54
+
55
+ # Create partitions with the bounds of the partition.
56
+ create_list_partition_of :events_by_list,
57
+ name: 'events_list_y2018m12', values: (Date.parse('2018-12-01')..Date.parse('2018-12-31')).to_a
58
+ end
59
+
60
+ def down
61
+ drop_table :events
62
+ end
63
+ end
64
+ ```
65
+
21
66
  ## Development
22
67
 
23
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
68
+ After checking out the repo, run `bin/setup` to install dependencies.
69
+ Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
24
70
 
25
71
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
26
72
 
73
+ ## Acknowledgements
74
+ Tablature's structure is heavily inspired by [Scenic](https://github.com/scenic-views/scenic) and [F(x)](http://github.com/teoljungberg/fx).
75
+ Tablature's features are heavily inspired by [PgParty](https://github.com/rkrage/pg_party).
76
+
27
77
  ## Contributing
28
78
 
29
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tablature.
79
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aliou/tablature.
30
80
 
31
81
  ## License
32
82
 
data/Rakefile CHANGED
@@ -3,4 +3,9 @@ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
+ namespace :dummy do
7
+ require_relative 'spec/dummy/config/application'
8
+ Dummy::Application.load_tasks
9
+ end
10
+
6
11
  task default: :spec
data/bin/console CHANGED
@@ -3,12 +3,8 @@
3
3
  require 'bundler/setup'
4
4
  require 'tablature'
5
5
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
6
+ # Require the dummy test app in the console.
7
+ require File.expand_path('spec/dummy/config/environment')
8
8
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require 'irb'
14
- IRB.start(__FILE__)
9
+ require 'pry'
10
+ Pry.start
@@ -0,0 +1,56 @@
1
+ module Tablature
2
+ module Adapters
3
+ class Postgres
4
+ # Decorates an ActiveRecord connection with methods that help determine
5
+ # the connections capabilities.
6
+ #
7
+ # Every attempt is made to use the versions of these methods defined by
8
+ # Rails where they are available and public before falling back to our own
9
+ # implementations for older Rails versions.
10
+ #
11
+ # @api private
12
+ class Connection < SimpleDelegator
13
+ # True if the connection supports range partitions.
14
+ #
15
+ # @return [Boolean]
16
+ def supports_range_partitions?
17
+ postgresql_version >= 100_000
18
+ end
19
+
20
+ # True if the connection supports list partitions.
21
+ #
22
+ # @return [Boolean]
23
+ def supports_list_partitions?
24
+ postgresql_version >= 100_000
25
+ end
26
+
27
+ # True if the connection supports hash partitions.
28
+ #
29
+ # @return [Boolean]
30
+ def supports_hash_partitions?
31
+ postgresql_version >= 110_000
32
+ end
33
+
34
+ # An integer representing the version of Postgres we're connected to.
35
+ #
36
+ # +postgresql_version+ is public in Rails 5, but protected in earlier
37
+ # versions.
38
+ #
39
+ # @return [Integer]
40
+ def postgresql_version
41
+ if undecorated_connection.respond_to?(:postgresql_version)
42
+ super
43
+ else
44
+ undecorated_connection.send(:postgresql_version)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def undecorated_connection
51
+ __getobj__
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,41 @@
1
+ module Tablature
2
+ module Adapters
3
+ class Postgres
4
+ # Raised when a list partition operation is attempted on a database
5
+ # version that does not support list partitions.
6
+ #
7
+ # List partitions are supported on Postgres 10 or newer.
8
+ class ListPartitionsNotSupportedError < StandardError
9
+ def initialize
10
+ super('List partitions require Postgres 10 or newer')
11
+ end
12
+ end
13
+
14
+ # Raised when trying to create a list partition without specifying the values of the partition
15
+ # key.
16
+ class MissingListPartitionValuesError < StandardError
17
+ def initialize
18
+ super('Missing values for of list partition')
19
+ end
20
+ end
21
+
22
+ # Raised when a range partition operation is attempted on a database
23
+ # version that does not support range partitions.
24
+ #
25
+ # Range partitions are supported on Postgres 10 or newer.
26
+ class RangePartitionsNotSupportedError < StandardError
27
+ def initialize
28
+ super('Range partitions require Postgres 10 or newer')
29
+ end
30
+ end
31
+
32
+ # Raised when trying to create a range partition without specifying the range of the partition
33
+ # key.
34
+ class MissingRangePartitionBoundsError < StandardError
35
+ def initialize
36
+ super('Missing bounds for of range partition')
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ require 'tablature/adapters/postgres/quoting'
2
+ require 'tablature/adapters/postgres/uuid'
3
+
4
+ module Tablature
5
+ module Adapters
6
+ class Postgres
7
+ # @api private
8
+ module Handlers
9
+ # @api private
10
+ class Base
11
+ include Postgres::Quoting
12
+ include Postgres::UUID
13
+
14
+ protected
15
+
16
+ def create_partition(table_name, id_options, table_options, &block)
17
+ create_table(table_name, table_options) do |td|
18
+ # TODO: Handle the id things here (depending on the postgres version)
19
+ if id_options[:type] == :uuid
20
+ td.column(
21
+ id_options[:column_name], id_options[:type], null: false, default: uuid_function
22
+ )
23
+ elsif id_options[:type]
24
+ td.column(id_options[:column_name], id_options[:type], null: false)
25
+ end
26
+
27
+ yield(td) if block.present?
28
+ end
29
+ end
30
+
31
+ def extract_primary_key!(options)
32
+ type = options.fetch(:id, :bigserial)
33
+ column_name = options.fetch(:primary_key, :id)
34
+
35
+ raise ArgumentError, 'composite primary key not supported' if column_name.is_a?(Array)
36
+
37
+ { type: type, column_name: column_name }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ require_relative 'base'
2
+
3
+ module Tablature
4
+ module Adapters
5
+ class Postgres
6
+ module Handlers
7
+ # @api private
8
+ class List < Base
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ def create_list_partition(table_name, options, &block)
14
+ raise_unless_list_partition_supported
15
+
16
+ # Postgres 10 does not handle indexes and therefore primary keys.
17
+ # Therefore we manually create an `id` column.
18
+ # TODO: Either make the library Postgres 11 only, or two code paths between Postgres 10
19
+ # and Postgres 11.
20
+ modified_options = options.except(:id, :primary_key, :partition_key)
21
+ id_options = extract_primary_key!(options.slice(:id, :primary_key))
22
+ partition_key = options.fetch(:partition_key)
23
+
24
+ modified_options[:id] = false
25
+ modified_options[:options] = "PARTITION BY LIST (#{quote_partition_key(partition_key)})"
26
+
27
+ create_partition(table_name, id_options, modified_options, &block)
28
+ end
29
+
30
+ def create_list_partition_of(parent_table, options)
31
+ values = options.fetch(:values, [])
32
+ raise MissingListPartitionValuesError if values.blank?
33
+
34
+ name = options.fetch(:name, partition_name(parent_table, values))
35
+ # TODO: Call `create_table` here instead of running the query.
36
+ # TODO: Pass the options to `create_table` to allow further configuration of the table,
37
+ # e.g. sub-partitioning the table.
38
+ query = <<~SQL.strip
39
+ CREATE TABLE #{quote_table_name(name)} PARTITION OF #{quote_table_name(parent_table)}
40
+ FOR VALUES IN (#{quote_collection(values)})
41
+ SQL
42
+
43
+ execute(query)
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :connection
49
+
50
+ delegate :execute, :quote, :quote_column_name, :quote_table_name, :create_table,
51
+ to: :connection
52
+
53
+ def raise_unless_list_partition_supported
54
+ raise ListPartitionsNotSupportedError unless connection.supports_list_partitions?
55
+ end
56
+
57
+ # TODO: Better ?
58
+ def partition_name(parent_table, values)
59
+ key = values.inspect
60
+ "#{parent_table}_#{Digest::MD5.hexdigest(key)[0..6]}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'base'
2
+
3
+ module Tablature
4
+ module Adapters
5
+ class Postgres
6
+ module Handlers
7
+ # @api private
8
+ class Range < Base
9
+ def initialize(connection)
10
+ @connection = connection
11
+ end
12
+
13
+ def create_range_partition(table_name, options, &block)
14
+ raise_unless_range_partition_supported
15
+
16
+ # Postgres 10 does not handle indexes and therefore primary keys.
17
+ # Therefore we manually create an `id` column.
18
+ # TODO: Either make the library Postgres 11 only, or two code paths between Postgres 10
19
+ # and Postgres 11.
20
+ modified_options = options.except(:id, :primary_key, :partition_key)
21
+ id_options = extract_primary_key!(options.slice(:id, :primary_key))
22
+ partition_key = options.fetch(:partition_key)
23
+
24
+ modified_options[:id] = false
25
+ modified_options[:options] =
26
+ "PARTITION BY RANGE (#{quote_partition_key(partition_key)})"
27
+
28
+ create_partition(table_name, id_options, modified_options, &block)
29
+ end
30
+
31
+ def create_range_partition_of(parent_table, options)
32
+ range_start = options.fetch(:range_start, nil)
33
+ range_end = options.fetch(:range_end, nil)
34
+
35
+ raise MissingRangePartitionBoundsError if range_start.nil? || range_end.nil?
36
+
37
+ name = options.fetch(:name, partition_name(parent_table, range_start, range_end))
38
+ # TODO: Call `create_table` here instead of running the query.
39
+ # TODO: Pass the options to `create_table` to allow further configuration of the table,
40
+ # e.g. sub-partitioning the table.
41
+ query = <<~SQL.strip
42
+ CREATE TABLE #{quote_table_name(name)} PARTITION OF #{quote_table_name(parent_table)}
43
+ FOR VALUES FROM (#{quote(range_start)}) TO (#{quote(range_end)});
44
+ SQL
45
+
46
+ execute(query)
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :connection
52
+
53
+ delegate :execute, :quote, :quote_column_name, :quote_table_name, :create_table,
54
+ to: :connection
55
+
56
+ def raise_unless_range_partition_supported
57
+ raise RangePartitionsNotSupportedError unless connection.supports_range_partitions?
58
+ end
59
+
60
+ def partition_name(parent_table, range_start, range_end)
61
+ key = [range_start, range_end].join(', ')
62
+ "#{parent_table}_#{Digest::MD5.hexdigest(key)[0..6]}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,60 @@
1
+ module Tablature
2
+ module Adapters
3
+ class Postgres
4
+ # Fetches the defined partitioned tables from the postgres connection.
5
+ # @api private
6
+ class PartitionedTables
7
+ def initialize(connection)
8
+ @connection = connection
9
+ end
10
+
11
+ # All of the partitioned table that this connection has defined.
12
+ #
13
+ # @return [Array<Tablature::PartitionedTable>]
14
+ def all
15
+ partitions.group_by { |row| row['table_name'] }.map(&method(:to_tablature_table))
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :connection
21
+
22
+ def partitions
23
+ connection.execute(<<-SQL)
24
+ SELECT
25
+ c.oid,
26
+ c.relname AS table_name,
27
+ p.partstrat AS type,
28
+ (i.inhrelid::REGCLASS)::TEXT AS partition_name
29
+ FROM pg_class c
30
+ INNER JOIN pg_partitioned_table p ON c.oid = p.partrelid
31
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
32
+ FULL OUTER JOIN pg_catalog.pg_inherits i ON c.oid = i.inhparent
33
+ WHERE
34
+ p.partstrat IN ('l', 'r', 'h')
35
+ AND c.relname NOT IN (SELECT extname FROM pg_extension)
36
+ AND n.nspname = ANY (current_schemas(false))
37
+ ORDER BY c.oid
38
+ SQL
39
+ end
40
+
41
+ METHOD_MAP = {
42
+ 'l' => :list,
43
+ 'r' => :range,
44
+ 'h' => :hash
45
+ }.freeze
46
+ private_constant :METHOD_MAP
47
+
48
+ def to_tablature_table(table_name, rows)
49
+ result = rows.first
50
+ partioning_method = METHOD_MAP.fetch(result['type'])
51
+ partitions = rows.map { |row| row['partition_name'] }.compact
52
+
53
+ Tablature::PartitionedTable.new(
54
+ name: table_name, partioning_method: partioning_method, partitions: partitions
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ module Tablature
2
+ module Adapters
3
+ class Postgres
4
+ # @api private
5
+ module Quoting
6
+ def quote_partition_key(key)
7
+ key.to_s.split('::').map(&method(:quote_column_name)).join('::')
8
+ end
9
+
10
+ def quote_collection(values)
11
+ Array.wrap(values).map(&method(:quote)).join(',')
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Tablature
2
+ module Adapters
3
+ class Postgres
4
+ # @api private
5
+ module UUID
6
+ def uuid_function
7
+ try(:supports_pgcrypto_uuid?) ? 'gen_random_uuid()' : 'uuid_generate_v4()'
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,11 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ require_relative 'postgres/connection'
4
+ require_relative 'postgres/errors'
5
+ require_relative 'postgres/handlers/list'
6
+ require_relative 'postgres/handlers/range'
7
+ require_relative 'postgres/partitioned_tables'
8
+
1
9
  module Tablature
2
10
  # Tablature database adapters.
3
11
  #
@@ -7,7 +15,7 @@ module Tablature
7
15
  module Adapters
8
16
  # An adapter for managing Postgres views.
9
17
  #
10
- # These methods are used interally by Tablature and are not intended for direct
18
+ # These methods are used internally by Tablature and are not intended for direct
11
19
  # use. Methods that alter database schema are intended to be called via
12
20
  # {Statements}.
13
21
  #
@@ -21,7 +29,7 @@ module Tablature
21
29
  # would explicitly set it.
22
30
  #
23
31
  # @param [#connection] connectable An object that returns the connection
24
- # for Tablature to use. Defaults to `ActiveRecord::Base`.
32
+ # for Tablature to use. Defaults to +ActiveRecord::Base+.
25
33
  #
26
34
  # @example
27
35
  # Tablature.configure do |config|
@@ -30,6 +38,109 @@ module Tablature
30
38
  def initialize(connectable = ActiveRecord::Base)
31
39
  @connectable = connectable
32
40
  end
41
+
42
+ # @!method create_list_partition(table_name, options, &block)
43
+ # Creates a partitioned table using the list partition method.
44
+ #
45
+ # This is called in a migration via {Statements#create_list_partition}.
46
+ #
47
+ # @param [String, Symbol] table_name The name of the table to partition.
48
+ # @param [Hash] options The options to create the partition. Keys besides +:partition_key+
49
+ # will be passed to +create_table+.
50
+ # @option options [String, Symbol] :partition_key The partition key.
51
+ # @yield [td] A TableDefinition object. This allows creating the table columns the same way
52
+ # as Rails's +create_table+ does.
53
+ #
54
+ # @example
55
+ # create_list_partition :events, partition_key: :id
56
+ #
57
+ # @example
58
+ # create_list_partition :events, partition_key: :date do |t|
59
+ # t.date :date, null: false
60
+ # end
61
+ delegate :create_list_partition, to: :list_handler
62
+
63
+ # @!method create_list_partition_of(parent_table_name, options)
64
+ # Creates a partition of a parent by specifying the key values appearing in the partition.
65
+ #
66
+ # @param parent_table_name [String, Symbol] The name of the parent table.
67
+ # @param [Hash] options The options to create the partition.
68
+ # @option options [String, Symbol] :values The values appearing in the partition.
69
+ # @option options [String, Symbol] :name The name of the partition. If it is not given, this
70
+ # will be randomly generated.
71
+ #
72
+ # @example
73
+ # # With a table :events partitioned using the list method on the partition key `date`:
74
+ # 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
+ # ]
77
+ delegate :create_list_partition_of, to: :list_handler
78
+
79
+ # @!method create_range_partition(table_name, options, &block)
80
+ # Creates a partitioned table using the range partition method.
81
+ #
82
+ # This is called in a migration via {Statements#create_range_partition}.
83
+ #
84
+ # @param [String, Symbol] table_name The name of the table to partition.
85
+ # @param [Hash] options The options to create the partition. Keys besides +:partition_key+
86
+ # will be passed to +create_table+.
87
+ # @option options [String, Symbol] :partition_key The partition key.
88
+ # @yield [td] A TableDefinition object. This allows creating the table columns the same way
89
+ # as Rails's +create_table+ does.
90
+ #
91
+ # @example
92
+ # create_range_partition :events, partition_key: :id
93
+ #
94
+ # @example
95
+ # create_range_partition :events, partition_key: :date do |t|
96
+ # t.date :date, null: false
97
+ # end
98
+ delegate :create_range_partition, to: :range_handler
99
+
100
+ # @!method create_range_partition_of(parent_table_name, options)
101
+ # Creates a partition of a parent by specifying the bound of the values appearing in the
102
+ # partition.
103
+ #
104
+ # @param parent_table_name [String, Symbol] The name of the parent table.
105
+ # @param [Hash] options The options to create the partition.
106
+ # @option options [String, Symbol] :range_start The start of the range of values appearing in
107
+ # the partition.
108
+ # @option options [String, Symbol] :range_end The end of the range of values appearing in
109
+ # the partition.
110
+ # @option options [String, Symbol] :name The name of the partition. If it is not given, this
111
+ # will be randomly generated.
112
+ #
113
+ # @example
114
+ # # With a table :events partitioned using the range method on the partition key `date`:
115
+ # create_range_partition_of :events, name: "events_2018-W49", range_start: '2018-12-03',
116
+ # range_end: '2018-12-10'
117
+ delegate :create_range_partition_of, to: :range_handler
118
+
119
+ # Returns an array of partitioned tables in the database.
120
+ #
121
+ # This collection of tables is used by the [Tablature::SchemaDumper] to populate the schema.rb
122
+ # file.
123
+ #
124
+ # @return [Array<Tablature::PartitionedTable]
125
+ def partitioned_tables
126
+ PartitionedTables.new(connection).all
127
+ end
128
+
129
+ private
130
+
131
+ attr_reader :connectable
132
+
133
+ def connection
134
+ Connection.new(connectable.connection)
135
+ end
136
+
137
+ def list_handler
138
+ @list_handler ||= Handlers::List.new(connection)
139
+ end
140
+
141
+ def range_handler
142
+ @range_handler ||= Handlers::Range.new(connection)
143
+ end
33
144
  end
34
145
  end
35
146
  end
@@ -0,0 +1,4 @@
1
+ module Tablature
2
+ module CommandRecorder
3
+ end
4
+ end
@@ -26,11 +26,10 @@ module Tablature
26
26
  # Modify Tablature's current configuration
27
27
  #
28
28
  # @yieldparam [Tablature::Configuration] config current Tablature config
29
- # ```
30
- # Tablature.configure do |config|
31
- # config.database = Tablature::Adapters::Postgres.new
32
- # end
33
- # ```
29
+ # @example
30
+ # Tablature.configure do |config|
31
+ # config.database = Tablature::Adapters::Postgres.new
32
+ # end
34
33
  def self.configure
35
34
  yield configuration
36
35
  end
@@ -0,0 +1,4 @@
1
+ module Tablature
2
+ module Model
3
+ end
4
+ end
@@ -0,0 +1,33 @@
1
+ module Tablature
2
+ # The in-memory representation of a partitioned table definition.
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 extension
9
+ class PartitionedTable
10
+ # The name of the partitioned table
11
+ # @return [String]
12
+ attr_reader :name
13
+
14
+ # The partitioning method of the table
15
+ # @return [Symbol]
16
+ attr_reader :partioning_method
17
+
18
+ # The partitions of the table.
19
+ # @return [Array]
20
+ attr_reader :partitions
21
+
22
+ # Returns a new instance of PartitionTable.
23
+ #
24
+ # @param name [String] The name of the view.
25
+ # @param partioning_method [:symbol] One of :range, :list or :hash
26
+ # @param partitions [Array] The partitions of the table.
27
+ def initialize(name:, partioning_method:, partitions: [])
28
+ @name = name
29
+ @partioning_method = partioning_method
30
+ @partitions = partitions
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,22 @@
1
+ module Tablature
2
+ # @api private
3
+ module SchemaDumper
4
+ def tables(stream)
5
+ # Add partitions to the list of ignored tables.
6
+ ActiveRecord::SchemaDumper.ignore_tables =
7
+ (ActiveRecord::SchemaDumper.ignore_tables || []) + partitions
8
+
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ def dumpable_partitioned_tables
15
+ Tablature.database.partitioned_tables
16
+ end
17
+
18
+ def partitions
19
+ dumpable_partitioned_tables.flat_map(&:partitions)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ module Tablature
2
+ # Methods that are made available in migrations.
3
+ module Statements
4
+ # Creates a partitioned table using the list partition method.
5
+ #
6
+ # @param name [String, Symbol] The name of the partition.
7
+ # @param options [Hash] The options to create the partition.
8
+ # @yield [td] A TableDefinition object. This allows creating the table columns the same way
9
+ # as Rails's +create_table+ does.
10
+ # @see Tablature::Adapters::Postgres#create_list_partition
11
+ def create_list_partition(name, options, &block)
12
+ raise ArgumentError, 'partition_key must be defined' if options[:partition_key].nil?
13
+
14
+ Tablature.database.create_list_partition(name, options, &block)
15
+ end
16
+
17
+ # Creates a partition of a parent by specifying the key values appearing in the partition.
18
+ #
19
+ # @param parent_table_name [String, Symbol] The name of the parent table.
20
+ # @param [Hash] options The options to create the partition.
21
+ #
22
+ # @see Tablature::Adapters::Postgres#create_list_partition_of
23
+ def create_list_partition_of(parent_table_name, options)
24
+ raise ArgumentError, 'values must be defined' if options[:values].nil?
25
+
26
+ Tablature.database.create_list_partition_of(parent_table_name, options)
27
+ end
28
+
29
+ # Creates a partitioned table using the range partition method.
30
+ #
31
+ # @param name [String, Symbol] The name of the partition.
32
+ # @param options [Hash] The options to create the partition.
33
+ # @yield [td] A TableDefinition object. This allows creating the table columns the same way
34
+ # as Rails's +create_table+ does.
35
+ #
36
+ # @see Tablature::Adapters::Postgres#create_range_partition
37
+ def create_range_partition(name, options, &block)
38
+ raise ArgumentError, 'partition_key must be defined' if options[:partition_key].nil?
39
+
40
+ Tablature.database.create_range_partition(name, options, &block)
41
+ end
42
+
43
+ # Creates a partition of a parent by specifying the key values appearing in the partition.
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#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'
52
+ end
53
+
54
+ Tablature.database.create_range_partition_of(parent_table, options)
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,3 @@
1
1
  module Tablature
2
- VERSION = '0.0.1'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
data/lib/tablature.rb CHANGED
@@ -1,16 +1,31 @@
1
1
  require 'tablature/adapters/postgres'
2
+ require 'tablature/command_recorder'
2
3
  require 'tablature/configuration'
4
+ require 'tablature/model'
5
+ require 'tablature/partitioned_table'
3
6
  require 'tablature/railtie'
7
+ require 'tablature/schema_dumper'
8
+ require 'tablature/statements'
4
9
  require 'tablature/version'
5
10
 
6
11
  require 'active_record'
7
12
 
8
- # Tablature adds methods to `ActiveRecord::Migration` to create and manage database
9
- # views in Rails applications.
13
+ # Tablature adds methods to `ActiveRecord::Migration` to create and manage partitioned
14
+ # tables in Rails applications.
10
15
  module Tablature
16
+ # Hooks Tablature into Rails.
17
+ #
18
+ # Enables tablature migration methods.
11
19
  def self.load
20
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include Tablature::Statements
21
+ ActiveRecord::Migration::CommandRecorder.include Tablature::CommandRecorder
22
+ ActiveRecord::SchemaDumper.prepend Tablature::SchemaDumper
23
+ ActiveRecord::Base.prepend Tablature::Model
12
24
  end
13
25
 
26
+ # The current database adapter used by Tablature.
27
+ #
28
+ # This defaults to {Adapters::Postgres} by can be overriden via {Configuration}.
14
29
  def self.database
15
30
  configuration.database
16
31
  end
data/tablature.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = 'Rails + Postgres Partitions'
12
12
  spec.description = 'Rails + Postgres Partitions'
13
- spec.homepage = "https://aliou.me"
13
+ spec.homepage = 'https://aliou.me'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
@@ -21,10 +21,11 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ['lib']
22
22
 
23
23
  spec.add_development_dependency 'bundler', '~> 1.16'
24
+ spec.add_development_dependency 'database_cleaner'
25
+ spec.add_development_dependency 'pg', '~> 0.19'
24
26
  spec.add_development_dependency 'rake', '~> 10.0'
25
27
  spec.add_development_dependency 'rspec', '~> 3.0'
26
28
  spec.add_development_dependency 'rspec-instafail'
27
- spec.add_development_dependency 'pg', '~> 0.19'
28
29
 
29
30
  spec.add_dependency 'activerecord', '>= 5.0.0'
30
31
  spec.add_dependency 'railties', '>= 5.0.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tablature
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aliou Diallo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-12-01 00:00:00.000000000 Z
11
+ date: 2018-12-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: database_cleaner
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pg
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.19'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.19'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rake
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -66,20 +94,6 @@ dependencies:
66
94
  - - ">="
67
95
  - !ruby/object:Gem::Version
68
96
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: pg
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '0.19'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '0.19'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: activerecord
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +129,7 @@ executables: []
115
129
  extensions: []
116
130
  extra_rdoc_files: []
117
131
  files:
132
+ - ".circleci/config.yml"
118
133
  - ".gitignore"
119
134
  - ".rspec"
120
135
  - ".rubocop.yml"
@@ -128,8 +143,21 @@ files:
128
143
  - bin/setup
129
144
  - lib/tablature.rb
130
145
  - lib/tablature/adapters/postgres.rb
146
+ - lib/tablature/adapters/postgres/connection.rb
147
+ - lib/tablature/adapters/postgres/errors.rb
148
+ - lib/tablature/adapters/postgres/handlers/base.rb
149
+ - lib/tablature/adapters/postgres/handlers/list.rb
150
+ - lib/tablature/adapters/postgres/handlers/range.rb
151
+ - lib/tablature/adapters/postgres/partitioned_tables.rb
152
+ - lib/tablature/adapters/postgres/quoting.rb
153
+ - lib/tablature/adapters/postgres/uuid.rb
154
+ - lib/tablature/command_recorder.rb
131
155
  - lib/tablature/configuration.rb
156
+ - lib/tablature/model.rb
157
+ - lib/tablature/partitioned_table.rb
132
158
  - lib/tablature/railtie.rb
159
+ - lib/tablature/schema_dumper.rb
160
+ - lib/tablature/statements.rb
133
161
  - lib/tablature/version.rb
134
162
  - tablature.gemspec
135
163
  homepage: https://aliou.me