tablature 0.0.1 → 0.1.0

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: 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