tablature 1.0.0.pre → 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: 8db9f3d3c82e1667d5931739f53bdc0c218f01281c56e3e1cd2541b51264ae4f
4
- data.tar.gz: 45fa5efd4a15199e22d5d15b8d0f84114de3ed634121171bb561ff206c5bcc28
3
+ metadata.gz: a72b6d120e1af25bf4b044347d02c552898ede4de5082a01f8b4ec3e24f4f6b0
4
+ data.tar.gz: e011684eadd4c9f39b83db417c6ef282f5291ed947c23651e8e5c70fcb76aa53
5
5
  SHA512:
6
- metadata.gz: d30f66c7398da2249edc10d3b4a5d2239aa017647f78c266fb07708abc943ee9d79f950d57e93a508c1d4211a408de17d7f97c96b092e9237a8962abdcfba58d
7
- data.tar.gz: db95e1b650a5337be05a61986ae99ed4adb829d9dd2d98642b34907bdd71416da8dbfe1f8ec66bdc9caa2ec0e0b178b433ec39afa625928afe6d1708c4912558
6
+ metadata.gz: 9b49e89742a179e6f50ccf28789bdb66999f5edb56a4cdcf57c273abc055b5bdc9bb6cbbb754639831613d0b90c034d9ee16c07ea6f56e14ca276a819081296f
7
+ data.tar.gz: 540d21c49ac1f7e74da0abb9c697cfbc94c72452408acfb891edaf377a5b199bda57ae670dfd051cf266e8c4da86ea51e699a5e4bc25b6e3f0cfc9ade27f378c
data/Gemfile CHANGED
@@ -1,15 +1,11 @@
1
- source "https://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
4
  gem 'pry'
5
5
  gem 'rubocop'
6
6
 
7
- rails_version = ENV.fetch("RAILS_VERSION", "6.0")
7
+ rails_version = ENV.fetch('RAILS_VERSION', '6.0')
8
8
 
9
- if rails_version == "master"
10
- rails_constraint = { github: "rails/rails" }
11
- else
12
- rails_constraint = "~> #{rails_version}.0"
13
- end
9
+ rails_constraint = rails_version == 'master' ? { github: 'rails/rails' } : "~> #{rails_version}.0"
14
10
 
15
- gem "rails", rails_constraint
11
+ gem 'rails', rails_constraint
@@ -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
@@ -185,6 +186,17 @@ module Tablature
185
186
  PartitionedTables.new(connection).all
186
187
  end
187
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
+
188
200
  private
189
201
 
190
202
  attr_reader :connectable
@@ -38,6 +38,13 @@ module Tablature
38
38
  postgresql_version >= 110_000
39
39
  end
40
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
+
41
48
  # An integer representing the version of Postgres we're connected to.
42
49
  #
43
50
  # +postgresql_version+ is public in Rails 5, but protected in earlier
@@ -0,0 +1,96 @@
1
+ module Tablature
2
+ module Adapters
3
+ class Postgres
4
+ # Fetches indexes on objects from the Postgres connection.
5
+ #
6
+ # @api private
7
+ class Indexes
8
+ def initialize(connection)
9
+ @connection = connection
10
+ end
11
+
12
+ def on(name)
13
+ indexes_on(name).map(&method(:index_from_database))
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :connection
19
+
20
+ def indexes_on(name)
21
+ connection.exec_query(<<-SQL, 'SCHEMA').to_a
22
+ SELECT DISTINCT
23
+ i.relname AS index_name,
24
+ d.indisunique AS is_unique,
25
+ d.indkey AS index_keys,
26
+ pg_get_indexdef(d.indexrelid) AS definition,
27
+ t.oid AS oid,
28
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment,
29
+ t.relname AS table_name,
30
+ string_agg(a.attname, ',') OVER (PARTITION BY i.relname) AS column_names
31
+ FROM pg_class t
32
+ INNER JOIN pg_index d ON t.oid = d.indrelid
33
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
34
+ LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
35
+ LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY (d.indkey)
36
+ WHERE i.relkind = 'I'
37
+ AND d.indisprimary = 'f'
38
+ AND t.relname = '#{name}'
39
+ AND n.nspname = ANY (current_schemas(false))
40
+ ORDER BY i.relname
41
+ SQL
42
+ end
43
+
44
+ def index_from_database(result)
45
+ result = format_result(result)
46
+
47
+ if rails_version >= Gem::Version.new('5.2')
48
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
49
+ result['table_name'], result['index_name'], result['is_unique'], result['columns'],
50
+ lengths: {}, orders: result['orders'], opclasses: result['opclasses'],
51
+ where: result['where'], using: result['using'].to_sym,
52
+ comment: result['comment'].presence
53
+ )
54
+ elsif rails_version >= Gem::Version.new('5.0')
55
+ ActiveRecord::ConnectionAdapters::IndexDefinition.new(
56
+ result['table_name'], result['index_name'], result['is_unique'], result['columns'],
57
+ {}, result['orders'], result['where'], nil, result['using'].to_sym,
58
+ result['comment'].presence
59
+ )
60
+ end
61
+ end
62
+
63
+ INDEX_PATTERN = /(?<column>\w+)"?\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/.freeze
64
+ private_constant :INDEX_PATTERN
65
+
66
+ USING_PATTERN = / USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m.freeze
67
+ private_constant :USING_PATTERN
68
+
69
+ def format_result(result)
70
+ result['index_keys'] = result['index_keys'].split.map(&:to_i)
71
+ result['column_names'] = result['column_names'].split(',')
72
+ result['using'], expressions, result['where'] = result['definition'].scan(USING_PATTERN).flatten
73
+ result['columns'] = result['index_keys'].include?(0) ? expressions : result['column_names']
74
+
75
+ result['orders'] = {}
76
+ result['opclasses'] = {}
77
+
78
+ expressions.scan(INDEX_PATTERN).each do |column, opclass, desc, nulls|
79
+ result['opclasses'][column] = opclass.to_sym if opclass
80
+ if nulls
81
+ result['orders'][column] = [desc, nulls].compact.join(' ')
82
+ elsif desc
83
+ result['orders'][column] = :desc
84
+ end
85
+ end
86
+
87
+ result
88
+ end
89
+
90
+ def rails_version
91
+ @rails_version ||= Gem::Version.new(Rails.version)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -19,8 +19,9 @@ module Tablature
19
19
 
20
20
  attr_reader :connection
21
21
 
22
+ # rubocop:disable Metrics/MethodLength
22
23
  def partitions
23
- connection.execute(<<-SQL)
24
+ result = connection.exec_query(<<-SQL, 'SCHEMA')
24
25
  SELECT
25
26
  c.oid,
26
27
  i.inhrelid,
@@ -39,7 +40,10 @@ module Tablature
39
40
  AND n.nspname = ANY (current_schemas(false))
40
41
  ORDER BY c.oid
41
42
  SQL
43
+
44
+ result.to_a
42
45
  end
46
+ # rubocop:enable Metrics/MethodLength
43
47
 
44
48
  STRATEGY_MAP = {
45
49
  'l' => :list,
@@ -4,11 +4,12 @@ module Tablature
4
4
  # @api private
5
5
  module Quoting
6
6
  def quote_partition_key(key)
7
- if key.respond_to?(:call)
8
- key.call.to_s
9
- else
10
- key.to_s.split('::').map(&method(:quote_column_name)).join('::')
11
- end
7
+ return key.call.to_s if key.respond_to?(:call)
8
+ # Don't bother quoting the key if it is already quoted (when loading the schema for
9
+ # example).
10
+ return key if key.to_s.include?("'") || key.to_s.include?('"')
11
+
12
+ key.to_s.split('::').map(&method(:quote_column_name)).join('::')
12
13
  end
13
14
 
14
15
  def quote_collection(values)
@@ -1,3 +1,5 @@
1
+ require 'forwardable'
2
+
1
3
  module Tablature
2
4
  module Model
3
5
  module ListPartitionMethods
@@ -54,5 +54,24 @@ module Tablature
54
54
  def default_partition
55
55
  partitions.find(&:default_partition?)
56
56
  end
57
+
58
+ PARTITION_METHOD_MAP = {
59
+ list: 'create_list_partition',
60
+ range: 'create_range_partition'
61
+ }.freeze
62
+ private_constant :PARTITION_METHOD_MAP
63
+
64
+ def to_schema
65
+ return nil unless PARTITION_METHOD_MAP.key?(partitioning_strategy)
66
+
67
+ creation_method = PARTITION_METHOD_MAP[partitioning_strategy]
68
+ <<-CONTENT
69
+ #{creation_method} #{name.inspect}, partition_key: #{partition_key.inspect} do |t|
70
+ CONTENT
71
+ end
72
+
73
+ def <=>(other)
74
+ name <=> other.name
75
+ end
57
76
  end
58
77
  end
@@ -1,18 +1,81 @@
1
+ # TODO: Try to replace the creation methods in the main stream instead of dumping the partitioned
2
+ # tables at the end of the schema.
1
3
  module Tablature
2
4
  # @api private
3
5
  module SchemaDumper
4
6
  def tables(stream)
5
7
  # Add partitions to the list of ignored tables.
6
8
  ActiveRecord::SchemaDumper.ignore_tables =
7
- (ActiveRecord::SchemaDumper.ignore_tables || []) + partitions
9
+ (ActiveRecord::SchemaDumper.ignore_tables || []) +
10
+ dumpable_partitioned_tables.map(&:name) +
11
+ partitions
8
12
 
9
13
  super
14
+
15
+ partitioned_tables(stream)
16
+ end
17
+
18
+ def partitioned_tables(stream)
19
+ stream.puts if dumpable_partitioned_tables.any?
20
+
21
+ dumpable_partitioned_tables.each do |partitioned_table|
22
+ dump_partitioned_table(partitioned_table, stream)
23
+ dump_partition_indexes(partitioned_table, stream)
24
+ dump_foreign_keys(partitioned_table, stream)
25
+ end
10
26
  end
11
27
 
12
28
  private
13
29
 
30
+ attr_reader :connection
31
+ delegate :quote_table_name, :quote, to: :connection
32
+
33
+ PARTITION_METHOD_MAP = {
34
+ list: 'create_list_partition',
35
+ range: 'create_range_partition'
36
+ }.freeze
37
+ private_constant :PARTITION_METHOD_MAP
38
+
39
+ def dump_partitioned_table(partitioned_table, main_stream)
40
+ # Pretend the partitioned table is a regular table and dump it in an alternate stream.
41
+ stream = StringIO.new
42
+ table(partitioned_table.name, stream)
43
+
44
+ header = partitioned_table.to_schema
45
+ if header.nil?
46
+ main_stream.puts <<~MESSAGE
47
+ # Unknown partitioning strategy "#{partitioned_table.partitioning_strategy}" for partitioned table "#{partitioned_table.name}".
48
+ # Dumping table as a regular table.
49
+ MESSAGE
50
+ main_stream.puts(stream.tap(&:rewind).read)
51
+ else
52
+ content = stream.tap(&:rewind).read.gsub(/create_table.*/, header)
53
+ main_stream.puts(content)
54
+ end
55
+ end
56
+
57
+ # Delegate to the adapter the dumping of indexes.
58
+ def dump_partition_indexes(partitioned_table, stream)
59
+ return unless Tablature.database.respond_to?(:indexes_on)
60
+
61
+ indexes = Tablature.database.indexes_on(partitioned_table.name)
62
+ return if indexes.empty?
63
+
64
+ add_index_statements = indexes.map do |index|
65
+ table_name = remove_prefix_and_suffix(index.table).inspect
66
+ " add_index #{([table_name] + index_parts(index)).join(', ')}"
67
+ end
68
+
69
+ stream.puts add_index_statements.sort.join("\n")
70
+ stream.puts
71
+ end
72
+
73
+ def dump_foreign_keys(partitioned_table, stream)
74
+ foreign_keys(partitioned_table.name, stream)
75
+ end
76
+
14
77
  def dumpable_partitioned_tables
15
- Tablature.database.partitioned_tables
78
+ Tablature.database.partitioned_tables.sort
16
79
  end
17
80
 
18
81
  def partitions
@@ -1,3 +1,3 @@
1
1
  module Tablature
2
- VERSION = '1.0.0.pre'.freeze
2
+ VERSION = '1.0.0.pre2'.freeze
3
3
  end
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: 1.0.0.pre
4
+ version: 1.0.0.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aliou Diallo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-10 00:00:00.000000000 Z
11
+ date: 2020-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -146,6 +146,7 @@ files:
146
146
  - lib/tablature/adapters/postgres/handlers/base.rb
147
147
  - lib/tablature/adapters/postgres/handlers/list.rb
148
148
  - lib/tablature/adapters/postgres/handlers/range.rb
149
+ - lib/tablature/adapters/postgres/indexes.rb
149
150
  - lib/tablature/adapters/postgres/partitioned_tables.rb
150
151
  - lib/tablature/adapters/postgres/quoting.rb
151
152
  - lib/tablature/adapters/postgres/uuid.rb