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 +4 -4
- data/Gemfile +4 -8
- data/lib/tablature/adapters/postgres.rb +12 -0
- data/lib/tablature/adapters/postgres/connection.rb +7 -0
- data/lib/tablature/adapters/postgres/indexes.rb +96 -0
- data/lib/tablature/adapters/postgres/partitioned_tables.rb +5 -1
- data/lib/tablature/adapters/postgres/quoting.rb +6 -5
- data/lib/tablature/model.rb +2 -0
- data/lib/tablature/partitioned_table.rb +19 -0
- data/lib/tablature/schema_dumper.rb +65 -2
- data/lib/tablature/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a72b6d120e1af25bf4b044347d02c552898ede4de5082a01f8b4ec3e24f4f6b0
|
4
|
+
data.tar.gz: e011684eadd4c9f39b83db417c6ef282f5291ed947c23651e8e5c70fcb76aa53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b49e89742a179e6f50ccf28789bdb66999f5edb56a4cdcf57c273abc055b5bdc9bb6cbbb754639831613d0b90c034d9ee16c07ea6f56e14ca276a819081296f
|
7
|
+
data.tar.gz: 540d21c49ac1f7e74da0abb9c697cfbc94c72452408acfb891edaf377a5b199bda57ae670dfd051cf266e8c4da86ea51e699a5e4bc25b6e3f0cfc9ade27f378c
|
data/Gemfile
CHANGED
@@ -1,15 +1,11 @@
|
|
1
|
-
source
|
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(
|
7
|
+
rails_version = ENV.fetch('RAILS_VERSION', '6.0')
|
8
8
|
|
9
|
-
|
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
|
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.
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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)
|
data/lib/tablature/model.rb
CHANGED
@@ -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 || []) +
|
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
|
data/lib/tablature/version.rb
CHANGED
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.
|
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-
|
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
|