tablature 1.0.0.pre → 1.0.0.pre2

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