tablature 0.1.1 → 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/.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
|