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