tablature 0.1.1 → 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/.github/workflows/ci.yml +68 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -1
- data/.yardopts +1 -0
- data/Gemfile +8 -0
- data/README.md +99 -2
- data/lib/tablature.rb +8 -1
- data/lib/tablature/adapters/postgres.rb +76 -5
- data/lib/tablature/adapters/postgres/connection.rb +14 -0
- data/lib/tablature/adapters/postgres/errors.rb +19 -2
- data/lib/tablature/adapters/postgres/handlers/base.rb +4 -0
- data/lib/tablature/adapters/postgres/handlers/list.rb +57 -3
- data/lib/tablature/adapters/postgres/handlers/range.rb +62 -3
- data/lib/tablature/adapters/postgres/indexes.rb +96 -0
- data/lib/tablature/adapters/postgres/partitioned_tables.rb +20 -8
- data/lib/tablature/adapters/postgres/quoting.rb +5 -0
- data/lib/tablature/command_recorder.rb +93 -0
- data/lib/tablature/model.rb +70 -0
- data/lib/tablature/partition.rb +23 -0
- data/lib/tablature/partitioned_table.rb +51 -7
- data/lib/tablature/schema_dumper.rb +66 -3
- data/lib/tablature/statements.rb +67 -11
- data/lib/tablature/version.rb +1 -1
- data/tablature.gemspec +6 -4
- metadata +18 -19
- data/.circleci/config.yml +0 -81
- data/Gemfile.lock +0 -106
- data/bin/console +0 -10
- data/bin/setup +0 -8
@@ -13,6 +13,10 @@ module Tablature
|
|
13
13
|
|
14
14
|
protected
|
15
15
|
|
16
|
+
def raise_unless_default_partition_supported
|
17
|
+
raise DefaultPartitionNotSupportedError unless connection.supports_default_partitions?
|
18
|
+
end
|
19
|
+
|
16
20
|
def create_partition(table_name, id_options, table_options, &block)
|
17
21
|
create_table(table_name, table_options) do |td|
|
18
22
|
# TODO: Handle the id things here (depending on the postgres version)
|
@@ -29,15 +29,50 @@ module Tablature
|
|
29
29
|
|
30
30
|
def create_list_partition_of(parent_table, options)
|
31
31
|
values = options.fetch(:values, [])
|
32
|
-
|
32
|
+
as_default = options.fetch(:default, false)
|
33
|
+
|
34
|
+
raise_unless_default_partition_supported if as_default
|
35
|
+
raise MissingListPartitionValuesError if values.blank? && !as_default
|
33
36
|
|
34
37
|
name = options.fetch(:name, partition_name(parent_table, values))
|
35
38
|
# TODO: Call `create_table` here instead of running the query.
|
36
39
|
# TODO: Pass the options to `create_table` to allow further configuration of the table,
|
37
40
|
# e.g. sub-partitioning the table.
|
38
|
-
|
41
|
+
|
42
|
+
query = <<~SQL
|
39
43
|
CREATE TABLE #{quote_table_name(name)} PARTITION OF #{quote_table_name(parent_table)}
|
40
|
-
|
44
|
+
SQL
|
45
|
+
|
46
|
+
query += if as_default
|
47
|
+
'DEFAULT'
|
48
|
+
else
|
49
|
+
"FOR VALUES IN (#{quote_collection(values)})"
|
50
|
+
end
|
51
|
+
|
52
|
+
execute(query)
|
53
|
+
end
|
54
|
+
|
55
|
+
def attach_to_list_partition(parent_table, options)
|
56
|
+
values = options.fetch(:values, [])
|
57
|
+
as_default = options.fetch(:default, false)
|
58
|
+
|
59
|
+
raise_unless_default_partition_supported if as_default
|
60
|
+
raise MissingListPartitionValuesError if values.blank? && !as_default
|
61
|
+
|
62
|
+
name = options.fetch(:name) { raise MissingPartitionName }
|
63
|
+
|
64
|
+
if as_default
|
65
|
+
attach_default_partition(parent_table, name)
|
66
|
+
else
|
67
|
+
attach_partition(parent_table, name, values)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def detach_from_list_partition(parent_table, options)
|
72
|
+
name = options.fetch(:name) { raise MissingPartitionName }
|
73
|
+
query = <<~SQL.strip
|
74
|
+
ALTER TABLE #{quote_table_name(parent_table)}
|
75
|
+
DETACH PARTITION #{quote_table_name(name)}
|
41
76
|
SQL
|
42
77
|
|
43
78
|
execute(query)
|
@@ -59,6 +94,25 @@ module Tablature
|
|
59
94
|
key = values.inspect
|
60
95
|
"#{parent_table}_#{Digest::MD5.hexdigest(key)[0..6]}"
|
61
96
|
end
|
97
|
+
|
98
|
+
def attach_default_partition(parent_table, partition_name)
|
99
|
+
query = <<~SQL.strip
|
100
|
+
ALTER TABLE #{quote_table_name(parent_table)}
|
101
|
+
ATTACH PARTITION #{quote_table_name(partition_name)} DEFAULT
|
102
|
+
SQL
|
103
|
+
|
104
|
+
execute(query)
|
105
|
+
end
|
106
|
+
|
107
|
+
def attach_partition(parent_table, partition_name, values)
|
108
|
+
query = <<~SQL.strip
|
109
|
+
ALTER TABLE #{quote_table_name(parent_table)}
|
110
|
+
ATTACH PARTITION #{quote_table_name(partition_name)}
|
111
|
+
FOR VALUES IN (#{quote_collection(values)})
|
112
|
+
SQL
|
113
|
+
|
114
|
+
execute(query)
|
115
|
+
end
|
62
116
|
end
|
63
117
|
end
|
64
118
|
end
|
@@ -31,16 +31,56 @@ module Tablature
|
|
31
31
|
def create_range_partition_of(parent_table, options)
|
32
32
|
range_start = options.fetch(:range_start, nil)
|
33
33
|
range_end = options.fetch(:range_end, nil)
|
34
|
+
as_default = options.fetch(:default, false)
|
34
35
|
|
35
|
-
|
36
|
+
raise_unless_default_partition_supported if as_default
|
37
|
+
if (range_start.nil? || range_end.nil?) && !as_default
|
38
|
+
raise MissingRangePartitionBoundsError
|
39
|
+
end
|
36
40
|
|
37
41
|
name = options.fetch(:name, partition_name(parent_table, range_start, range_end))
|
38
42
|
# TODO: Call `create_table` here instead of running the query.
|
39
43
|
# TODO: Pass the options to `create_table` to allow further configuration of the table,
|
40
44
|
# e.g. sub-partitioning the table.
|
41
|
-
|
45
|
+
|
46
|
+
query = <<~SQL
|
42
47
|
CREATE TABLE #{quote_table_name(name)} PARTITION OF #{quote_table_name(parent_table)}
|
43
|
-
|
48
|
+
SQL
|
49
|
+
|
50
|
+
query += if as_default
|
51
|
+
'DEFAULT'
|
52
|
+
else
|
53
|
+
"FOR VALUES FROM (#{quote(range_start)}) TO (#{quote(range_end)})"
|
54
|
+
end
|
55
|
+
|
56
|
+
execute(query)
|
57
|
+
end
|
58
|
+
|
59
|
+
def attach_to_range_partition(parent_table, options)
|
60
|
+
range_start = options.fetch(:range_start, nil)
|
61
|
+
range_end = options.fetch(:range_end, nil)
|
62
|
+
as_default = options.fetch(:default, false)
|
63
|
+
|
64
|
+
raise_unless_default_partition_supported if as_default
|
65
|
+
if (range_start.nil? || range_end.nil?) && !as_default
|
66
|
+
raise MissingRangePartitionBoundsError
|
67
|
+
end
|
68
|
+
|
69
|
+
name = options.fetch(:name) { raise MissingPartitionName }
|
70
|
+
|
71
|
+
if as_default
|
72
|
+
attach_default_partition(parent_table, name)
|
73
|
+
else
|
74
|
+
attach_partition(parent_table, name, range_start, range_end)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def detach_from_range_partition(parent_table, options)
|
79
|
+
name = options.fetch(:name) { raise MissingPartitionName }
|
80
|
+
|
81
|
+
query = <<~SQL.strip
|
82
|
+
ALTER TABLE #{quote_table_name(parent_table)}
|
83
|
+
DETACH PARTITION #{quote_table_name(name)}
|
44
84
|
SQL
|
45
85
|
|
46
86
|
execute(query)
|
@@ -61,6 +101,25 @@ module Tablature
|
|
61
101
|
key = [range_start, range_end].join(', ')
|
62
102
|
"#{parent_table}_#{Digest::MD5.hexdigest(key)[0..6]}"
|
63
103
|
end
|
104
|
+
|
105
|
+
def attach_default_partition(parent_table, partition_name)
|
106
|
+
query = <<~SQL.strip
|
107
|
+
ALTER TABLE #{quote_table_name(parent_table)}
|
108
|
+
ATTACH PARTITION #{quote_table_name(partition_name)} DEFAULT
|
109
|
+
SQL
|
110
|
+
|
111
|
+
execute(query)
|
112
|
+
end
|
113
|
+
|
114
|
+
def attach_partition(parent_table, partition_name, range_start, range_end)
|
115
|
+
query = <<~SQL.strip
|
116
|
+
ALTER TABLE #{quote_table_name(parent_table)}
|
117
|
+
ATTACH PARTITION #{quote_table_name(partition_name)}
|
118
|
+
FOR VALUES FROM (#{quote(range_start)}) TO (#{quote(range_end)})
|
119
|
+
SQL
|
120
|
+
|
121
|
+
execute(query)
|
122
|
+
end
|
64
123
|
end
|
65
124
|
end
|
66
125
|
end
|
@@ -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,13 +19,17 @@ 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,
|
27
|
+
i.inhrelid,
|
26
28
|
c.relname AS table_name,
|
27
|
-
p.partstrat AS
|
28
|
-
(i.inhrelid::REGCLASS)::TEXT AS partition_name
|
29
|
+
p.partstrat AS strategy,
|
30
|
+
(i.inhrelid::REGCLASS)::TEXT AS partition_name,
|
31
|
+
#{connection.supports_default_partitions? ? 'i.inhrelid = p.partdefid AS is_default_partition,' : ''}
|
32
|
+
pg_get_partkeydef(c.oid) AS partition_key_definition
|
29
33
|
FROM pg_class c
|
30
34
|
INNER JOIN pg_partitioned_table p ON c.oid = p.partrelid
|
31
35
|
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
@@ -36,22 +40,30 @@ module Tablature
|
|
36
40
|
AND n.nspname = ANY (current_schemas(false))
|
37
41
|
ORDER BY c.oid
|
38
42
|
SQL
|
43
|
+
|
44
|
+
result.to_a
|
39
45
|
end
|
46
|
+
# rubocop:enable Metrics/MethodLength
|
40
47
|
|
41
|
-
|
48
|
+
STRATEGY_MAP = {
|
42
49
|
'l' => :list,
|
43
50
|
'r' => :range,
|
44
51
|
'h' => :hash
|
45
52
|
}.freeze
|
46
|
-
private_constant :
|
53
|
+
private_constant :STRATEGY_MAP
|
47
54
|
|
48
55
|
def to_tablature_table(table_name, rows)
|
49
56
|
result = rows.first
|
50
|
-
|
51
|
-
|
57
|
+
partitioning_strategy = STRATEGY_MAP.fetch(result['strategy'])
|
58
|
+
# This is very fragile code. This makes the assumption that:
|
59
|
+
# - Postgres will always have a function `pg_get_partkeydef` that returns the partition
|
60
|
+
# strategy with the partition key
|
61
|
+
# - Postgres will never have a partition strategy with two words in its name.
|
62
|
+
_, partition_key = result['partition_key_definition'].split(' ', 2)
|
52
63
|
|
53
64
|
Tablature::PartitionedTable.new(
|
54
|
-
name: table_name,
|
65
|
+
name: table_name, partitioning_strategy: partitioning_strategy,
|
66
|
+
partitions: rows, partition_key: partition_key
|
55
67
|
)
|
56
68
|
end
|
57
69
|
|
@@ -4,6 +4,11 @@ module Tablature
|
|
4
4
|
# @api private
|
5
5
|
module Quoting
|
6
6
|
def quote_partition_key(key)
|
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
|
+
|
7
12
|
key.to_s.split('::').map(&method(:quote_column_name)).join('::')
|
8
13
|
end
|
9
14
|
|
@@ -1,4 +1,97 @@
|
|
1
1
|
module Tablature
|
2
|
+
# @api private
|
2
3
|
module CommandRecorder
|
4
|
+
def create_list_partition(*args)
|
5
|
+
record(:create_list_partition, args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_list_partition_of(*args)
|
9
|
+
record(:create_list_partition_of, args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def attach_to_list_partition(*args)
|
13
|
+
record(:attach_to_list_partition, args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def detach_from_list_partition(*args)
|
17
|
+
record(:detach_from_list_partition, args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_range_partition(*args)
|
21
|
+
record(:create_range_partition, args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_range_partition_of(*args)
|
25
|
+
record(:create_range_partition_of, args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def attach_to_range_partition(*args)
|
29
|
+
record(:attach_to_range_partition, args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def detach_from_range_partition(*args)
|
33
|
+
record(:detach_from_range_partition, args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def invert_create_partition(args)
|
37
|
+
[:drop_table, [args.first]]
|
38
|
+
end
|
39
|
+
|
40
|
+
alias :invert_create_list_partition :invert_create_partition
|
41
|
+
alias :invert_create_range_partition :invert_create_partition
|
42
|
+
|
43
|
+
def invert_create_partition_of(args)
|
44
|
+
_parent_table_name, options = args
|
45
|
+
partition_name = options[:name]
|
46
|
+
|
47
|
+
[:drop_table, [partition_name]]
|
48
|
+
end
|
49
|
+
|
50
|
+
alias :invert_create_list_partition_of :invert_create_partition_of
|
51
|
+
alias :invert_create_range_partition_of :invert_create_partition_of
|
52
|
+
|
53
|
+
def invert_attach_to_range_partition(args)
|
54
|
+
[:detach_from_range_partition, args]
|
55
|
+
end
|
56
|
+
|
57
|
+
def invert_detach_from_range_partition(args)
|
58
|
+
parent_table_name, options = args
|
59
|
+
options ||= {}
|
60
|
+
_partition_name = options[:name]
|
61
|
+
|
62
|
+
range_start = options[:range_start]
|
63
|
+
range_end = options[:range_end]
|
64
|
+
default = options[:default]
|
65
|
+
|
66
|
+
if (range_start.nil? || range_end.nil?) && default.blank?
|
67
|
+
message = <<-MESSAGE
|
68
|
+
invert_detach_from_range_partition is reversible only if given bounds or the default option
|
69
|
+
MESSAGE
|
70
|
+
raise ActiveRecord::IrreversibleMigration, message
|
71
|
+
end
|
72
|
+
|
73
|
+
[:attach_to_range_partition, [parent_table_name, options]]
|
74
|
+
end
|
75
|
+
|
76
|
+
def invert_attach_to_list_partition(args)
|
77
|
+
[:detach_from_list_partition, args]
|
78
|
+
end
|
79
|
+
|
80
|
+
def invert_detach_from_list_partition(args)
|
81
|
+
partitioned_table, options = args
|
82
|
+
options ||= {}
|
83
|
+
|
84
|
+
default = options[:default]
|
85
|
+
values = options[:values] || []
|
86
|
+
|
87
|
+
if values.blank? && default.blank?
|
88
|
+
message = <<-MESSAGE
|
89
|
+
invert_detach_from_list_partition is reversible only if given the value list or the default option
|
90
|
+
MESSAGE
|
91
|
+
raise ActiveRecord::IrreversibleMigration, message
|
92
|
+
end
|
93
|
+
|
94
|
+
[:attach_to_list_partition, [partitioned_table, options]]
|
95
|
+
end
|
3
96
|
end
|
4
97
|
end
|
data/lib/tablature/model.rb
CHANGED
@@ -1,4 +1,74 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
module Tablature
|
2
4
|
module Model
|
5
|
+
module ListPartitionMethods
|
6
|
+
def create_list_partition(options)
|
7
|
+
Tablature.database.create_list_partition_of(tablature_partition.name, options)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module RangePartitionMethods
|
12
|
+
def create_range_partition(options)
|
13
|
+
Tablature.database.create_range_partition_of(tablature_partition.name, options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
def_delegators :tablature_partition, :partitions, :partition_key, :partitioning_strategy
|
21
|
+
|
22
|
+
def partitioned?
|
23
|
+
begin
|
24
|
+
tablature_partition
|
25
|
+
rescue Tablature::MissingPartition
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def tablature_partition
|
33
|
+
return @tablature_partition if defined?(@tablature_partition)
|
34
|
+
|
35
|
+
@tablature_partition = Tablature.database.partitioned_tables.find do |pt|
|
36
|
+
pt.name == partition_name.to_s
|
37
|
+
end
|
38
|
+
raise Tablature::MissingPartition if @tablature_partition.nil?
|
39
|
+
|
40
|
+
@tablature_partition
|
41
|
+
end
|
42
|
+
|
43
|
+
def list_partition(partition_name = table_name)
|
44
|
+
setup_partition(partition_name)
|
45
|
+
extend(ListPartitionMethods)
|
46
|
+
end
|
47
|
+
|
48
|
+
def range_partition(partition_name = table_name)
|
49
|
+
setup_partition(partition_name)
|
50
|
+
extend(RangePartitionMethods)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def inspect
|
55
|
+
return super unless partitioned?
|
56
|
+
|
57
|
+
# Copied from the Rails source.
|
58
|
+
attr_list = attribute_types.map { |name, type| "#{name}: #{type.type}" } * ', '
|
59
|
+
"#{self}(#{attr_list})"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def setup_partition(partition_name)
|
65
|
+
self.partition_name = partition_name
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.included(klass)
|
70
|
+
klass.extend ClassMethods
|
71
|
+
klass.class_attribute(:partition_name)
|
72
|
+
end
|
3
73
|
end
|
4
74
|
end
|