ridgepole 1.0.7 → 1.2.0
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/.rubocop.yml +0 -6
- data/Appraisals +3 -12
- data/CHANGELOG.md +14 -0
- data/README.md +5 -28
- data/docker-compose.yml +3 -3
- data/gemfiles/activerecord_6.0.gemfile +1 -1
- data/gemfiles/activerecord_6.1.gemfile +1 -1
- data/gemfiles/activerecord_7.0.gemfile +1 -1
- data/lib/ridgepole/client.rb +0 -5
- data/lib/ridgepole/delta.rb +42 -22
- data/lib/ridgepole/diff.rb +33 -53
- data/lib/ridgepole/dsl_parser/context.rb +12 -16
- data/lib/ridgepole/dsl_parser/table_definition.rb +38 -20
- data/lib/ridgepole/ext/schema_dumper.rb +0 -24
- data/lib/ridgepole/version.rb +1 -1
- data/lib/ridgepole.rb +0 -3
- metadata +2 -10
- data/gemfiles/activerecord_5.1.gemfile +0 -7
- data/gemfiles/activerecord_5.2.gemfile +0 -8
- data/lib/ridgepole/ext/abstract_adapter/partition_definition.rb +0 -19
- data/lib/ridgepole/ext/abstract_adapter/partition_options.rb +0 -34
- data/lib/ridgepole/ext/abstract_adapter/partitioning.rb +0 -40
- data/lib/ridgepole/ext/abstract_mysql_adapter/partitioning.rb +0 -72
- data/lib/ridgepole/ext/abstract_mysql_adapter/schema_creation.rb +0 -46
- data/lib/ridgepole/ext/postgresql_adapter/partitioning.rb +0 -138
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '068fca35b633df2e4bc3b2d8ad5d2553496396a78c16ca371f18efc4dbad6adf'
|
4
|
+
data.tar.gz: c908229a06ddcbbf28e14b28af7f52f63c933d271db2e24231f6ed588fe5a1a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6456e47a675dbdeb21ce56c9ed6091e823031ca082b191b7fb0438880d54ee6aea51988d4d9c5e07ded0e684a683d22f0d613cd3697c7c6d12ef6948b41e972e
|
7
|
+
data.tar.gz: 4b9f3b573975e0be50dec442e7db23688374754306082275bcc711e20e5813f2c4ca1975993797e02fd5b963cf396b7f5bbd2cd869f02aee468c8d07ce98e3c1
|
data/.rubocop.yml
CHANGED
@@ -30,12 +30,6 @@ Metrics/ParameterLists:
|
|
30
30
|
Enabled: false
|
31
31
|
Metrics/PerceivedComplexity:
|
32
32
|
Enabled: false
|
33
|
-
Naming/MethodName:
|
34
|
-
Exclude:
|
35
|
-
- "lib/ridgepole/ext/abstract_mysql_adapter/schema_creation.rb"
|
36
|
-
Naming/MethodParameterName:
|
37
|
-
Exclude:
|
38
|
-
- "lib/ridgepole/ext/abstract_mysql_adapter/schema_creation.rb"
|
39
33
|
Style/Documentation:
|
40
34
|
Enabled: false
|
41
35
|
Style/GuardClause:
|
data/Appraisals
CHANGED
@@ -1,22 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
appraise 'activerecord-5.1' do
|
4
|
-
gem 'activerecord', '~> 5.1.0'
|
5
|
-
end
|
6
|
-
|
7
|
-
appraise 'activerecord-5.2' do
|
8
|
-
gem 'activerecord', '~> 5.2.0'
|
9
|
-
gem 'mysql2', '~> 0.4.4'
|
10
|
-
end
|
11
|
-
|
12
3
|
appraise 'activerecord-6.0' do
|
13
|
-
gem 'activerecord', '~> 6.0.
|
4
|
+
gem 'activerecord', '~> 6.0.6'
|
14
5
|
end
|
15
6
|
|
16
7
|
appraise 'activerecord-6.1' do
|
17
|
-
gem 'activerecord', '~> 6.1.
|
8
|
+
gem 'activerecord', '~> 6.1.7'
|
18
9
|
end
|
19
10
|
|
20
11
|
appraise 'activerecord-7.0' do
|
21
|
-
gem 'activerecord', '~> 7.0.
|
12
|
+
gem 'activerecord', '~> 7.0.4'
|
22
13
|
end
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 1.2
|
4
|
+
|
5
|
+
### 1.2.0 (2022/09/24)
|
6
|
+
|
7
|
+
* Updated supported column types [pull#399](https://github.com/ridgepole/ridgepole/pull/399) [pull#400](https://github.com/ridgepole/ridgepole/pull/400)
|
8
|
+
* Support check constraint [pull#393](https://github.com/ridgepole/ridgepole/pull/393) [pull#397](https://github.com/ridgepole/ridgepole/pull/397)
|
9
|
+
* Drop suport Rails 5.x [pull#395](https://github.com/ridgepole/ridgepole/pull/395 )
|
10
|
+
|
11
|
+
## 1.1
|
12
|
+
|
13
|
+
### 1.1.0 (2022/06/18)
|
14
|
+
|
15
|
+
* Revert partitioning support [pull#392](https://github.com/ridgepole/ridgepole/pull/392)
|
16
|
+
|
3
17
|
## 1.0
|
4
18
|
|
5
19
|
### 1.0.7 (2022/06/09)
|
data/README.md
CHANGED
@@ -6,11 +6,13 @@ It defines DB schema using [Rails DSL](http://guides.rubyonrails.org/migrations.
|
|
6
6
|
(like Chef/Puppet)
|
7
7
|
|
8
8
|
[](http://badge.fury.io/rb/ridgepole)
|
9
|
-
[](https://github.com/ridgepole/ridgepole/actions)
|
10
|
+
[](https://coveralls.io/github/ridgepole/ridgepole?branch=1.2)
|
11
11
|
|
12
12
|
**Notice**
|
13
13
|
|
14
|
+
* Drop support ActiveRecord 5.x in ridgepole v1.2.0.
|
15
|
+
* Partitioning is no longer supported in ridgepole v1.1.0.
|
14
16
|
* ActiveRecord 7.x has some incompatible changes. If you get unintended differences in `datetime`, add `precision`.
|
15
17
|
* cf. https://github.com/ridgepole/ridgepole/issues/381
|
16
18
|
* For ActiveRecord 7.x series, please use AcriveRecord 7.0.2 or higher / Ridgepole 1.0.3 or higher.
|
@@ -216,7 +218,7 @@ See `mysql> show character set;` to find charset / collation pair for your syste
|
|
216
218
|
|
217
219
|
## Generated Column (MySQL)
|
218
220
|
|
219
|
-
There should be NO extra white spaces in the expression (such as after comma).
|
221
|
+
There should be NO extra white spaces in the expression (such as after comma).
|
220
222
|
Quotes in expression may cause the operations failure with MySQL 8.0.
|
221
223
|
|
222
224
|
```ruby
|
@@ -318,31 +320,6 @@ Apply `Schemafile`
|
|
318
320
|
...
|
319
321
|
```
|
320
322
|
|
321
|
-
## Partitioning
|
322
|
-
|
323
|
-
**Notice:** PostgreSQL `PARTITION BY` must be specified with the create_table option.
|
324
|
-
|
325
|
-
### List Partitioning
|
326
|
-
|
327
|
-
```ruby
|
328
|
-
create_table "articles", force: :cascade, options: "PARTITION BY LIST(id)" do |t|
|
329
|
-
end
|
330
|
-
|
331
|
-
add_partition("articles", :list, :id, partition_definitions: [{ name: 'p0', values: { in: [0,1,2] } }, { name: 'p1', values: { in: [3,4,5] } }])
|
332
|
-
```
|
333
|
-
|
334
|
-
### Range Partitioning
|
335
|
-
|
336
|
-
```ruby
|
337
|
-
create_table "articles", force: :cascade, options: "PARTITION BY RANGE(id)" do |t|
|
338
|
-
end
|
339
|
-
|
340
|
-
# postgresql
|
341
|
-
add_partition("articles", :range, :id, partition_definitions: [{ name: 'p0', values: { from: 'MINVALUE', to: 5 }}, { name: 'p1', values: { from: 5, to: 10 } }])
|
342
|
-
# mysql
|
343
|
-
add_partition("articles", :range, :id, partition_definitions: [{ name: 'p0', values: { to: 5 }}, { name: 'p1', values: { to: 10 } }])
|
344
|
-
```
|
345
|
-
|
346
323
|
## Run tests
|
347
324
|
|
348
325
|
|
data/docker-compose.yml
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
version: "3.8"
|
2
2
|
services:
|
3
3
|
mysql:
|
4
|
-
image: "mysql:5.6"
|
4
|
+
image: "mysql:5.6.51"
|
5
5
|
ports:
|
6
6
|
- "13316:3306"
|
7
7
|
environment:
|
8
8
|
MYSQL_ROOT_PASSWORD: password
|
9
9
|
mysql57:
|
10
|
-
image: "mysql:5.7"
|
10
|
+
image: "mysql:5.7.39"
|
11
11
|
ports:
|
12
12
|
- "13317:3306"
|
13
13
|
environment:
|
14
14
|
MYSQL_ROOT_PASSWORD: password
|
15
15
|
mysql80:
|
16
|
-
image: "mysql:8.0"
|
16
|
+
image: "mysql:8.0.30"
|
17
17
|
ports:
|
18
18
|
- "13318:3306"
|
19
19
|
environment:
|
data/lib/ridgepole/client.rb
CHANGED
@@ -15,12 +15,7 @@ module Ridgepole
|
|
15
15
|
@parser = Ridgepole::DSLParser.new(@options)
|
16
16
|
@diff = Ridgepole::Diff.new(@options)
|
17
17
|
|
18
|
-
if Ridgepole::ConnectionAdapters.mysql?
|
19
|
-
require 'ridgepole/ext/abstract_mysql_adapter/partitioning'
|
20
|
-
require 'ridgepole/ext/abstract_mysql_adapter/schema_creation'
|
21
|
-
end
|
22
18
|
require 'ridgepole/ext/abstract_mysql_adapter/dump_auto_increment' if @options[:mysql_dump_auto_increment]
|
23
|
-
require 'ridgepole/ext/postgresql_adapter/partitioning' if Ridgepole::ConnectionAdapters.postgresql?
|
24
19
|
end
|
25
20
|
|
26
21
|
def dump(&block)
|
data/lib/ridgepole/delta.rb
CHANGED
@@ -245,6 +245,12 @@ create_table(#{table_name.inspect}, #{inspect_options_include_default_proc(optio
|
|
245
245
|
end
|
246
246
|
end
|
247
247
|
|
248
|
+
unless (check_constraints = attrs[:check_constraints] || {}).empty?
|
249
|
+
check_constraints.each do |_, check_constraint_attrs|
|
250
|
+
append_add_check_constraint(table_name, check_constraint_attrs, buf, true)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
248
254
|
buf.puts(<<-RUBY)
|
249
255
|
end
|
250
256
|
RUBY
|
@@ -310,29 +316,12 @@ execute "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name
|
|
310
316
|
append_change_table_options(table_name, comment_literal, buf)
|
311
317
|
end
|
312
318
|
|
313
|
-
def append_change_partition(table_name, delta, buf)
|
314
|
-
(delta[:add] || {}).each do |_, attrs|
|
315
|
-
buf.puts "create_partition #{table_name.inspect}, **#{attrs.inspect}"
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
def append_change_partition_definitions(table_name, partition_definitions, buf, _post_buf_for_fk)
|
320
|
-
(partition_definitions[:add] || []).each do |partition_name, attrs|
|
321
|
-
buf.puts "add_partition #{table_name.inspect}, name: #{partition_name.inspect}, values: #{attrs[:values].inspect}"
|
322
|
-
end
|
323
|
-
|
324
|
-
(partition_definitions[:delete] || []).each do |partition_name, _attrs|
|
325
|
-
buf.puts "remove_partition #{table_name.inspect}, name: #{partition_name.inspect}"
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
319
|
def append_change(table_name, attrs, buf, pre_buf_for_fk, post_buf_for_fk)
|
330
320
|
definition = attrs[:definition] || {}
|
331
321
|
primary_key_definition = attrs[:primary_key_definition] || {}
|
332
322
|
indices = attrs[:indices] || {}
|
333
323
|
foreign_keys = attrs[:foreign_keys] || {}
|
334
|
-
|
335
|
-
partition_definitions = attrs[:partition_definitions] || {}
|
324
|
+
check_constraints = attrs[:check_constraints] || {}
|
336
325
|
table_options = attrs[:table_options]
|
337
326
|
table_charset = attrs[:table_charset]
|
338
327
|
table_collation = attrs[:table_collation]
|
@@ -348,6 +337,7 @@ execute "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name
|
|
348
337
|
end
|
349
338
|
|
350
339
|
append_change_foreign_keys(table_name, foreign_keys, pre_buf_for_fk, post_buf_for_fk, @options) unless foreign_keys.empty?
|
340
|
+
append_change_check_constraints(table_name, check_constraints, buf) unless check_constraints.empty?
|
351
341
|
|
352
342
|
if table_options || table_charset || table_collation
|
353
343
|
append_change_table_raw_options(table_name, table_options, table_charset, table_collation,
|
@@ -356,10 +346,6 @@ execute "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name
|
|
356
346
|
|
357
347
|
append_change_table_comment(table_name, table_comment, buf) if table_comment
|
358
348
|
|
359
|
-
append_change_partition(table_name, partition, buf) unless partition.empty?
|
360
|
-
|
361
|
-
append_change_partition_definitions(table_name, partition_definitions, buf, post_buf_for_fk) unless partition_definitions.empty?
|
362
|
-
|
363
349
|
buf.puts
|
364
350
|
pre_buf_for_fk.puts
|
365
351
|
post_buf_for_fk.puts
|
@@ -533,6 +519,40 @@ remove_foreign_key(#{table_name.inspect}, #{target})
|
|
533
519
|
RUBY
|
534
520
|
end
|
535
521
|
|
522
|
+
def append_change_check_constraints(table_name, delta, buf)
|
523
|
+
(delta[:delete] || {}).each do |_, attrs|
|
524
|
+
append_remove_check_constraint(table_name, attrs, buf)
|
525
|
+
end
|
526
|
+
|
527
|
+
(delta[:add] || {}).each do |_, attrs|
|
528
|
+
append_add_check_constraint(table_name, attrs, buf)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def append_add_check_constraint(table_name, attrs, buf, force_bulk_change = false)
|
533
|
+
expression = attrs.fetch(:expression)
|
534
|
+
attrs_options = attrs[:options] || {}
|
535
|
+
|
536
|
+
if force_bulk_change
|
537
|
+
buf.puts(<<-RUBY)
|
538
|
+
t.check_constraint(#{expression.inspect}, **#{attrs_options.inspect})
|
539
|
+
RUBY
|
540
|
+
else
|
541
|
+
buf.puts(<<-RUBY)
|
542
|
+
add_check_constraint(#{table_name.inspect}, #{expression.inspect}, **#{attrs_options.inspect})
|
543
|
+
RUBY
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
def append_remove_check_constraint(table_name, attrs, buf)
|
548
|
+
expression = attrs.fetch(:expression)
|
549
|
+
attrs_options = attrs[:options] || {}
|
550
|
+
|
551
|
+
buf.puts(<<-RUBY)
|
552
|
+
remove_check_constraint(#{table_name.inspect}, #{expression.inspect}, **#{attrs_options.inspect})
|
553
|
+
RUBY
|
554
|
+
end
|
555
|
+
|
536
556
|
def delta_execute
|
537
557
|
@delta[:execute] || []
|
538
558
|
end
|
data/lib/ridgepole/diff.rb
CHANGED
@@ -37,9 +37,6 @@ module Ridgepole
|
|
37
37
|
delta[:add] ||= {}
|
38
38
|
delta[:add][table_name] = to_attrs
|
39
39
|
end
|
40
|
-
delta[:change] ||= {}
|
41
|
-
delta[:change][table_name] ||= {}
|
42
|
-
scan_partition_change(table_name, from_attrs&.fetch(:partition, nil), to_attrs&.fetch(:partition, nil), delta[:change][table_name])
|
43
40
|
end
|
44
41
|
|
45
42
|
scan_relation_info(relation_info)
|
@@ -104,6 +101,7 @@ module Ridgepole
|
|
104
101
|
scan_definition_change(from[:definition], to[:definition], from[:indices], table_name, from[:options], table_delta)
|
105
102
|
scan_indices_change(from[:indices], to[:indices], to[:definition], table_delta, from[:options], to[:options])
|
106
103
|
scan_foreign_keys_change(from[:foreign_keys], to[:foreign_keys], table_delta, @options)
|
104
|
+
scan_check_constraints_change(from[:check_constraints], to[:check_constraints], table_delta)
|
107
105
|
|
108
106
|
unless table_delta.empty?
|
109
107
|
delta[:change] ||= {}
|
@@ -475,6 +473,38 @@ module Ridgepole
|
|
475
473
|
table_delta[:foreign_keys] = foreign_keys_delta unless foreign_keys_delta.empty?
|
476
474
|
end
|
477
475
|
|
476
|
+
def scan_check_constraints_change(from, to, table_delta)
|
477
|
+
from = (from || {}).dup
|
478
|
+
to = (to || {}).dup
|
479
|
+
check_constraints_delta = {}
|
480
|
+
|
481
|
+
to.each do |name, to_attrs|
|
482
|
+
from_attrs = from.delete(name)
|
483
|
+
|
484
|
+
if from_attrs
|
485
|
+
if from_attrs != to_attrs
|
486
|
+
check_constraints_delta[:add] ||= {}
|
487
|
+
check_constraints_delta[:add][name] = to_attrs
|
488
|
+
|
489
|
+
check_constraints_delta[:delete] ||= {}
|
490
|
+
check_constraints_delta[:delete][name] = from_attrs
|
491
|
+
end
|
492
|
+
else
|
493
|
+
check_constraints_delta[:add] ||= {}
|
494
|
+
check_constraints_delta[:add][name] = to_attrs
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
unless @options[:merge]
|
499
|
+
from.each do |name, from_attrs|
|
500
|
+
check_constraints_delta[:delete] ||= {}
|
501
|
+
check_constraints_delta[:delete][name] = from_attrs
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
table_delta[:check_constraints] = check_constraints_delta unless check_constraints_delta.empty?
|
506
|
+
end
|
507
|
+
|
478
508
|
# XXX: MySQL only?
|
479
509
|
# https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L760
|
480
510
|
# https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb#L102
|
@@ -620,56 +650,6 @@ module Ridgepole
|
|
620
650
|
end
|
621
651
|
end
|
622
652
|
|
623
|
-
def scan_partition_change(table_name, from, to, table_delta)
|
624
|
-
from = (from || {}).dup
|
625
|
-
to = (to || {}).dup
|
626
|
-
partition_delta = {}
|
627
|
-
|
628
|
-
return if to.empty? && from.empty?
|
629
|
-
|
630
|
-
if from.empty? && Ridgepole::ConnectionAdapters.mysql?
|
631
|
-
partition_delta[:add] ||= {}
|
632
|
-
partition_delta[:add][table_name] = to
|
633
|
-
else
|
634
|
-
if from.present? && (to[:type] != from[:type] || to[:columns] != from[:columns])
|
635
|
-
@logger.warn(<<-MSG)
|
636
|
-
"[WARNING] '#{table_name}' partition is skipped because of the different partition type.
|
637
|
-
to: #{to[:type]} #{to[:columns]}
|
638
|
-
from: #{from[:type]}" #{from[:columns]}
|
639
|
-
MSG
|
640
|
-
return
|
641
|
-
end
|
642
|
-
|
643
|
-
if from[:partition_definitions].present? && (to[:partition_definitions] & from[:partition_definitions]).empty?
|
644
|
-
raise "All partition is different. please check partition settings.to: #{to}, from: #{from}"
|
645
|
-
end
|
646
|
-
|
647
|
-
scan_partition_definition_chanage(from, to, table_delta)
|
648
|
-
end
|
649
|
-
|
650
|
-
table_delta[:partition] = partition_delta unless partition_delta.empty?
|
651
|
-
end
|
652
|
-
|
653
|
-
def scan_partition_definition_chanage(from, to, table_delta)
|
654
|
-
partition_definitions_delta = {}
|
655
|
-
attrs = { type: from[:type] || to[:type] }
|
656
|
-
|
657
|
-
from_partitions = (from[:partition_definitions] || []).index_by { |partition| partition[:name] }
|
658
|
-
to_partitions = (to[:partition_definitions] || []).index_by { |partition| partition[:name] }
|
659
|
-
|
660
|
-
(from_partitions.keys - to_partitions.keys).each do |name|
|
661
|
-
partition_definitions_delta[:delete] ||= {}
|
662
|
-
partition_definitions_delta[:delete][name] = attrs.merge(values: from_partitions[name][:values])
|
663
|
-
end
|
664
|
-
|
665
|
-
(to_partitions.keys - from_partitions.keys).each do |name|
|
666
|
-
partition_definitions_delta[:add] ||= {}
|
667
|
-
partition_definitions_delta[:add][name] = attrs.merge(values: to_partitions[name][:values])
|
668
|
-
end
|
669
|
-
|
670
|
-
table_delta[:partition_definitions] = partition_definitions_delta unless partition_definitions_delta.empty?
|
671
|
-
end
|
672
|
-
|
673
653
|
def check_table_existence(definition)
|
674
654
|
return unless @options[:tables]
|
675
655
|
|
@@ -91,22 +91,18 @@ module Ridgepole
|
|
91
91
|
}
|
92
92
|
end
|
93
93
|
|
94
|
-
def
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
@__definition[table_name][:partition] = {
|
107
|
-
type: type,
|
108
|
-
columns: Array.wrap(columns),
|
109
|
-
partition_definitions: partition_definitions,
|
94
|
+
def add_check_constraint(table_name, expression, options = {})
|
95
|
+
table_name = table_name.to_s
|
96
|
+
expression = expression.to_s
|
97
|
+
options[:name] = options[:name].to_s if options[:name]
|
98
|
+
|
99
|
+
idx = options[:name] || expression
|
100
|
+
|
101
|
+
@__definition[table_name] ||= {}
|
102
|
+
@__definition[table_name][:check_constraints] ||= {}
|
103
|
+
@__definition[table_name][:check_constraints][idx] = {
|
104
|
+
expression: expression,
|
105
|
+
options: options,
|
110
106
|
}
|
111
107
|
end
|
112
108
|
|
@@ -26,32 +26,41 @@ module Ridgepole
|
|
26
26
|
DEFAULT_PRIMARY_KEY_TYPE = :bigint
|
27
27
|
|
28
28
|
TYPES = {
|
29
|
-
# https://github.com/rails/rails/blob/
|
30
|
-
string: {},
|
31
|
-
text: {},
|
32
|
-
integer: {},
|
29
|
+
# https://github.com/rails/rails/blob/main/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L300-L301
|
33
30
|
bigint: {},
|
34
|
-
float: {},
|
35
|
-
decimal: {},
|
36
|
-
datetime: {},
|
37
|
-
timestamp: {},
|
38
|
-
time: {},
|
39
|
-
date: {},
|
40
31
|
binary: {},
|
41
32
|
boolean: {},
|
33
|
+
date: {},
|
34
|
+
datetime: {},
|
35
|
+
decimal: {},
|
36
|
+
float: {},
|
37
|
+
integer: {},
|
38
|
+
json: {},
|
39
|
+
string: {},
|
40
|
+
text: {},
|
41
|
+
time: {},
|
42
|
+
timestamp: {},
|
43
|
+
virtual: {},
|
42
44
|
|
43
|
-
# https://github.com/rails/rails/blob/
|
45
|
+
# https://github.com/rails/rails/blob/v6.0.6/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L101
|
44
46
|
serial: { null: false },
|
45
47
|
bigserial: { null: false },
|
48
|
+
# string: {},
|
49
|
+
# text: {},
|
50
|
+
# integer: {},
|
51
|
+
# float: {},
|
52
|
+
# decimal: {},
|
53
|
+
# datetime: {},
|
54
|
+
# time: {},
|
55
|
+
# date: {},
|
46
56
|
daterange: {},
|
47
57
|
numrange: {},
|
48
58
|
tsrange: {},
|
49
59
|
tstzrange: {},
|
50
60
|
int4range: {},
|
51
61
|
int8range: {},
|
52
|
-
# binary: {},
|
53
|
-
# boolean: {},
|
54
|
-
# bigint: {}, # dup key
|
62
|
+
# binary: {},
|
63
|
+
# boolean: {},
|
55
64
|
xml: {},
|
56
65
|
tsvector: {},
|
57
66
|
hstore: {},
|
@@ -59,20 +68,25 @@ module Ridgepole
|
|
59
68
|
cidr: {},
|
60
69
|
macaddr: {},
|
61
70
|
uuid: {},
|
62
|
-
json: {},
|
71
|
+
# json: {},
|
63
72
|
jsonb: {},
|
64
73
|
ltree: {},
|
65
74
|
citext: {},
|
66
75
|
point: {},
|
76
|
+
line: {},
|
77
|
+
lseg: {},
|
78
|
+
box: {},
|
79
|
+
path: {},
|
80
|
+
polygon: {},
|
81
|
+
circle: {},
|
67
82
|
bit: {},
|
68
83
|
bit_varying: {},
|
69
84
|
money: {},
|
85
|
+
interval: {},
|
86
|
+
oid: {},
|
70
87
|
|
71
|
-
# https://github.com/
|
72
|
-
|
73
|
-
|
74
|
-
# https://github.com/rails/rails/blob/v5.0.4/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L53
|
75
|
-
# json: {}, # dup key
|
88
|
+
# https://github.com/ridgepole/ridgepole/issues/394
|
89
|
+
timestamptz: {},
|
76
90
|
}.freeze
|
77
91
|
|
78
92
|
TYPES.each do |column_type, default_options|
|
@@ -159,6 +173,10 @@ module Ridgepole
|
|
159
173
|
end
|
160
174
|
end
|
161
175
|
alias belongs_to references
|
176
|
+
|
177
|
+
def check_constraint(expression, options = {})
|
178
|
+
@base.add_check_constraint(@table_name, expression, options)
|
179
|
+
end
|
162
180
|
end
|
163
181
|
end
|
164
182
|
end
|
@@ -45,30 +45,6 @@ module Ridgepole
|
|
45
45
|
stream.puts add_foreign_key_statements.sort.join("\n")
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
49
|
-
def tables(stream)
|
50
|
-
original = ignore_tables.dup
|
51
|
-
ignore_tables.concat(@connection.partition_tables)
|
52
|
-
super
|
53
|
-
ensure
|
54
|
-
self.ignore_tables = original
|
55
|
-
end
|
56
|
-
|
57
|
-
def table(table, stream)
|
58
|
-
super
|
59
|
-
partition(table, stream)
|
60
|
-
end
|
61
|
-
|
62
|
-
def partition(table, stream)
|
63
|
-
if (partition = @connection.partition(table))
|
64
|
-
partition_definitions = partition.partition_definitions.map do |partition_definition|
|
65
|
-
"{ name: #{partition_definition.name.inspect}, values: #{partition_definition.values} }"
|
66
|
-
end.join(' ,')
|
67
|
-
|
68
|
-
stream.puts " add_partition #{partition.table.inspect}, #{partition.type.inspect}, #{partition.columns.inspect}, partition_definitions: [#{partition_definitions}]"
|
69
|
-
stream.puts
|
70
|
-
end
|
71
|
-
end
|
72
48
|
end
|
73
49
|
end
|
74
50
|
end
|
data/lib/ridgepole/version.rb
CHANGED
data/lib/ridgepole.rb
CHANGED
@@ -16,9 +16,6 @@ require 'diffy'
|
|
16
16
|
module Ridgepole; end
|
17
17
|
|
18
18
|
require 'ridgepole/ext/abstract_adapter/disable_table_options'
|
19
|
-
require 'ridgepole/ext/abstract_adapter/partition_definition'
|
20
|
-
require 'ridgepole/ext/abstract_adapter/partition_options'
|
21
|
-
require 'ridgepole/ext/abstract_adapter/partitioning'
|
22
19
|
require 'ridgepole/ext/pp_sort_hash'
|
23
20
|
require 'ridgepole/ext/schema_dumper'
|
24
21
|
require 'ridgepole/client'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ridgepole
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Genki Sugawara
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-09-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -288,8 +288,6 @@ files:
|
|
288
288
|
- Rakefile
|
289
289
|
- bin/ridgepole
|
290
290
|
- docker-compose.yml
|
291
|
-
- gemfiles/activerecord_5.1.gemfile
|
292
|
-
- gemfiles/activerecord_5.2.gemfile
|
293
291
|
- gemfiles/activerecord_6.0.gemfile
|
294
292
|
- gemfiles/activerecord_6.1.gemfile
|
295
293
|
- gemfiles/activerecord_7.0.gemfile
|
@@ -306,13 +304,7 @@ files:
|
|
306
304
|
- lib/ridgepole/dumper.rb
|
307
305
|
- lib/ridgepole/execute_expander.rb
|
308
306
|
- lib/ridgepole/ext/abstract_adapter/disable_table_options.rb
|
309
|
-
- lib/ridgepole/ext/abstract_adapter/partition_definition.rb
|
310
|
-
- lib/ridgepole/ext/abstract_adapter/partition_options.rb
|
311
|
-
- lib/ridgepole/ext/abstract_adapter/partitioning.rb
|
312
307
|
- lib/ridgepole/ext/abstract_mysql_adapter/dump_auto_increment.rb
|
313
|
-
- lib/ridgepole/ext/abstract_mysql_adapter/partitioning.rb
|
314
|
-
- lib/ridgepole/ext/abstract_mysql_adapter/schema_creation.rb
|
315
|
-
- lib/ridgepole/ext/postgresql_adapter/partitioning.rb
|
316
308
|
- lib/ridgepole/ext/pp_sort_hash.rb
|
317
309
|
- lib/ridgepole/ext/schema_dumper.rb
|
318
310
|
- lib/ridgepole/external_sql_executer.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
-
|
5
|
-
module ActiveRecord
|
6
|
-
module ConnectionAdapters
|
7
|
-
class PartitionDefinition
|
8
|
-
attr_reader :name, :values
|
9
|
-
|
10
|
-
def initialize(
|
11
|
-
name,
|
12
|
-
values
|
13
|
-
)
|
14
|
-
@name = name
|
15
|
-
@values = values
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
-
|
5
|
-
module ActiveRecord
|
6
|
-
module ConnectionAdapters
|
7
|
-
class PartitionOptions
|
8
|
-
attr_reader :table, :type, :columns, :partition_definitions
|
9
|
-
|
10
|
-
TYPES = %i[range list].freeze
|
11
|
-
|
12
|
-
def initialize(
|
13
|
-
table, type,
|
14
|
-
columns,
|
15
|
-
partition_definitions: []
|
16
|
-
)
|
17
|
-
@table = table
|
18
|
-
@type = type
|
19
|
-
@columns = Array.wrap(columns)
|
20
|
-
@partition_definitions = build_definitions(partition_definitions)
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def build_definitions(definitions)
|
26
|
-
definitions.map do |definition|
|
27
|
-
next if definition.is_a?(PartitionDefinition)
|
28
|
-
|
29
|
-
PartitionDefinition.new(definition.fetch(:name), definition.fetch(:values))
|
30
|
-
end.compact
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record/connection_adapters/abstract_adapter'
|
4
|
-
|
5
|
-
module Ridgepole
|
6
|
-
module Ext
|
7
|
-
module AbstractAdapter
|
8
|
-
module Partitioning
|
9
|
-
def partition(*)
|
10
|
-
nil
|
11
|
-
end
|
12
|
-
|
13
|
-
def partition_tables
|
14
|
-
[]
|
15
|
-
end
|
16
|
-
|
17
|
-
# SchemaStatements
|
18
|
-
def create_partition(*)
|
19
|
-
raise NotImplementedError
|
20
|
-
end
|
21
|
-
|
22
|
-
def add_partition(*)
|
23
|
-
raise NotImplementedError
|
24
|
-
end
|
25
|
-
|
26
|
-
def remove_partition(*)
|
27
|
-
raise NotImplementedError
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
module ActiveRecord
|
35
|
-
module ConnectionAdapters
|
36
|
-
class AbstractAdapter
|
37
|
-
prepend Ridgepole::Ext::AbstractAdapter::Partitioning
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,72 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
4
|
-
|
5
|
-
module Ridgepole
|
6
|
-
module Ext
|
7
|
-
module AbstractMysqlAdapter
|
8
|
-
module Partitioning
|
9
|
-
def partition(table_name)
|
10
|
-
scope = quoted_scope(table_name)
|
11
|
-
|
12
|
-
partition_info = exec_query(<<~SQL, 'SCHEMA')
|
13
|
-
SELECT PARTITION_NAME, PARTITION_DESCRIPTION, PARTITION_METHOD, PARTITION_EXPRESSION
|
14
|
-
FROM information_schema.partitions
|
15
|
-
WHERE partition_name IS NOT NULL
|
16
|
-
AND table_schema = #{scope[:schema]}
|
17
|
-
AND table_name = #{scope[:name]}
|
18
|
-
SQL
|
19
|
-
return if partition_info.count == 0
|
20
|
-
|
21
|
-
type = case partition_info.first['PARTITION_METHOD']
|
22
|
-
when 'LIST COLUMNS'
|
23
|
-
:list
|
24
|
-
when 'RANGE COLUMNS'
|
25
|
-
:range
|
26
|
-
else
|
27
|
-
raise NotImplementedError, partition_info.first['PARTITION_METHOD'].to_s
|
28
|
-
end
|
29
|
-
columns = partition_info.first['PARTITION_EXPRESSION'].delete('`').split(',').map(&:to_sym)
|
30
|
-
|
31
|
-
partition_definitions = partition_info.map do |row|
|
32
|
-
values = case type
|
33
|
-
when :list
|
34
|
-
{ in: instance_eval("[#{row['PARTITION_DESCRIPTION'].gsub(/\(/, '[').gsub(/\)/, ']')}] # [1,2]", __FILE__, __LINE__) }
|
35
|
-
when :range
|
36
|
-
{ to: instance_eval("[#{row['PARTITION_DESCRIPTION']}] # [1,2]", __FILE__, __LINE__) }
|
37
|
-
else
|
38
|
-
raise NotImplementedError
|
39
|
-
end
|
40
|
-
|
41
|
-
{ name: row['PARTITION_NAME'], values: values }
|
42
|
-
end
|
43
|
-
|
44
|
-
ActiveRecord::ConnectionAdapters::PartitionOptions.new(table_name, type, columns, partition_definitions: partition_definitions)
|
45
|
-
end
|
46
|
-
|
47
|
-
# SchemaStatements
|
48
|
-
def create_partition(table_name, type:, columns:, partition_definitions:)
|
49
|
-
execute schema_creation.accept(ActiveRecord::ConnectionAdapters::PartitionOptions.new(table_name, type, columns,
|
50
|
-
partition_definitions: partition_definitions))
|
51
|
-
end
|
52
|
-
|
53
|
-
def add_partition(table_name, name:, values:)
|
54
|
-
pd = ActiveRecord::ConnectionAdapters::PartitionDefinition.new(name, values)
|
55
|
-
execute "ALTER TABLE #{quote_table_name(table_name)} ADD PARTITION (#{schema_creation.accept(pd)})"
|
56
|
-
end
|
57
|
-
|
58
|
-
def remove_partition(table_name, name:)
|
59
|
-
execute "ALTER TABLE #{quote_table_name(table_name)} DROP PARTITION #{name}"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
module ActiveRecord
|
67
|
-
module ConnectionAdapters
|
68
|
-
class AbstractMysqlAdapter < AbstractAdapter
|
69
|
-
prepend Ridgepole::Ext::AbstractMysqlAdapter::Partitioning
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
@@ -1,46 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record/connection_adapters/mysql/schema_creation'
|
4
|
-
|
5
|
-
module Ridgepole
|
6
|
-
module Ext
|
7
|
-
module AbstractMysqlAdapter
|
8
|
-
module SchemaCreation
|
9
|
-
def visit_PartitionOptions(o)
|
10
|
-
sqls = o.partition_definitions.map { |partition_definition| accept partition_definition }
|
11
|
-
function = case o.type
|
12
|
-
when :list
|
13
|
-
"LIST COLUMNS(#{o.columns.map { |column| quote_column_name(column) }.join(',')})"
|
14
|
-
when :range
|
15
|
-
"RANGE COLUMNS(#{o.columns.map { |column| quote_column_name(column) }.join(',')})"
|
16
|
-
else
|
17
|
-
raise NotImplementedError
|
18
|
-
end
|
19
|
-
"ALTER TABLE #{quote_table_name(o.table)} PARTITION BY #{function} (#{sqls.join(',')})"
|
20
|
-
end
|
21
|
-
|
22
|
-
def visit_PartitionDefinition(o)
|
23
|
-
if o.values.key?(:in)
|
24
|
-
"PARTITION #{o.name} VALUES IN (#{o.values[:in].map do |value|
|
25
|
-
value.is_a?(Array) ? "(#{value.map(&:inspect).join(',')})" : value.inspect
|
26
|
-
end.join(',')})"
|
27
|
-
elsif o.values.key?(:to)
|
28
|
-
"PARTITION #{o.name} VALUES LESS THAN (#{o.values[:to].map(&:inspect).join(',')})"
|
29
|
-
else
|
30
|
-
raise NotImplementedError
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
module ActiveRecord
|
39
|
-
module ConnectionAdapters
|
40
|
-
module MySQL
|
41
|
-
class SchemaCreation
|
42
|
-
prepend Ridgepole::Ext::AbstractMysqlAdapter::SchemaCreation
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
@@ -1,138 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'active_record/connection_adapters/postgresql_adapter'
|
4
|
-
|
5
|
-
module Ridgepole
|
6
|
-
module Ext
|
7
|
-
module PostgreSQLAdapter
|
8
|
-
module Partitioning
|
9
|
-
def supports_partitions?
|
10
|
-
ActiveRecord::VERSION::MAJOR >= 6 && postgresql_version >= 100_000 # >= 10.0
|
11
|
-
end
|
12
|
-
|
13
|
-
def table_options(table_name)
|
14
|
-
options = partition_options(table_name)
|
15
|
-
if options
|
16
|
-
(super || {}).merge(options: "PARTITION BY #{options[:type].to_s.upcase}(#{options[:columns].join(',')})")
|
17
|
-
else
|
18
|
-
super
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def partition_options(table_name)
|
23
|
-
return unless supports_partitions?
|
24
|
-
|
25
|
-
scope = quoted_scope(table_name)
|
26
|
-
result = query_value(<<~SQL, 'SCHEMA')
|
27
|
-
SELECT pg_get_partkeydef(t.oid)
|
28
|
-
FROM pg_class t
|
29
|
-
LEFT JOIN pg_namespace n ON n.oid = t.relnamespace
|
30
|
-
WHERE t.relname = #{scope[:name]}
|
31
|
-
AND n.nspname = #{scope[:schema]}
|
32
|
-
SQL
|
33
|
-
return unless result
|
34
|
-
|
35
|
-
type, *columns = result.scan(/\w+/).map { |value| value.downcase.to_sym }
|
36
|
-
{ type: type, columns: columns }
|
37
|
-
end
|
38
|
-
|
39
|
-
def partition(table_name)
|
40
|
-
options = partition_options(table_name)
|
41
|
-
return unless options
|
42
|
-
|
43
|
-
scope = quoted_scope(table_name)
|
44
|
-
partition_info = query(<<~SQL, 'SCHEMA')
|
45
|
-
SELECT p.relname, pg_get_expr(p.relpartbound, p.oid, true)
|
46
|
-
FROM pg_class t
|
47
|
-
JOIN pg_inherits i on i.inhparent = t.oid
|
48
|
-
JOIN pg_class p on p.oid = i.inhrelid
|
49
|
-
WHERE t.relname = #{scope[:name]}
|
50
|
-
AND p.relnamespace::regnamespace::text = #{scope[:schema]}
|
51
|
-
ORDER BY p.relname
|
52
|
-
SQL
|
53
|
-
|
54
|
-
partition_definitions = partition_info.map do |name, val_str|
|
55
|
-
values = if val_str == 'DEFAULT'
|
56
|
-
{ default: true }
|
57
|
-
else
|
58
|
-
case options[:type]
|
59
|
-
when :list
|
60
|
-
values = val_str.match(/FOR VALUES IN \((?<csv>.+)\)$/)[:csv].split(',').map(&:strip).map { |value| cast_value(value) }
|
61
|
-
{ in: Array.wrap(values) }
|
62
|
-
when :range
|
63
|
-
match = val_str.match(/FOR VALUES FROM \((?<from>.+)\) TO \((?<to>.+)\)/)
|
64
|
-
from = match[:from].split(',').map(&:strip).map { |value| cast_value(value) }
|
65
|
-
to = match[:to].split(',').map(&:strip).map { |value| cast_value(value) }
|
66
|
-
{ from: from, to: to }
|
67
|
-
when :hash
|
68
|
-
match = val_str.match(/FOR VALUES WITH \(modulus (?<modulus>\d+), remainder (?<remainder>\d+)\)/)
|
69
|
-
{ modulus: match[:modulus].to_i, remainder: match[:remainder].to_i }
|
70
|
-
else
|
71
|
-
raise NotImplementedError
|
72
|
-
end
|
73
|
-
end
|
74
|
-
{ name: name, values: values }
|
75
|
-
end
|
76
|
-
|
77
|
-
ActiveRecord::ConnectionAdapters::PartitionOptions.new(table_name, options[:type], options[:columns],
|
78
|
-
partition_definitions: partition_definitions)
|
79
|
-
end
|
80
|
-
|
81
|
-
def cast_value(value)
|
82
|
-
Integer(value)
|
83
|
-
rescue ArgumentError
|
84
|
-
value.delete(%q("')) # "
|
85
|
-
end
|
86
|
-
|
87
|
-
def quote_value(value)
|
88
|
-
if %w[MINVALUE MAXVALUE].include?(value)
|
89
|
-
value
|
90
|
-
else
|
91
|
-
quote(value)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
def partition_tables
|
96
|
-
partition_info = query(<<~SQL, 'SCHEMA')
|
97
|
-
SELECT p.relname
|
98
|
-
FROM pg_class t
|
99
|
-
JOIN pg_inherits i on i.inhparent = t.oid
|
100
|
-
JOIN pg_class p on p.oid = i.inhrelid
|
101
|
-
ORDER BY p.relname
|
102
|
-
SQL
|
103
|
-
partition_info.map { |row| row[0] }
|
104
|
-
end
|
105
|
-
|
106
|
-
# SchemaStatements
|
107
|
-
def add_partition(table_name, name:, values:)
|
108
|
-
condition = if values.key?(:default)
|
109
|
-
'DEFAULT'
|
110
|
-
elsif values.key?(:in)
|
111
|
-
"FOR VALUES IN (#{values[:in].map { |v| quote_value(v) }.join(',')})"
|
112
|
-
elsif values.key?(:to)
|
113
|
-
from = values[:from].map { |v| quote_value(v) }.join(',')
|
114
|
-
to = values[:to].map { |v| quote_value(v) }.join(',')
|
115
|
-
"FOR VALUES FROM (#{from}) TO (#{to})"
|
116
|
-
elsif values.key?(:modulus)
|
117
|
-
"FOR VALUES WITH (modulus #{values[:modulus]}, remainder #{values[:remainder]})"
|
118
|
-
else
|
119
|
-
raise NotImplementedError
|
120
|
-
end
|
121
|
-
create_table(name, id: false, options: "PARTITION OF #{table_name} #{condition}")
|
122
|
-
end
|
123
|
-
|
124
|
-
def remove_partition(_table_name, name:)
|
125
|
-
drop_table(name)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
module ActiveRecord
|
133
|
-
module ConnectionAdapters
|
134
|
-
class PostgreSQLAdapter
|
135
|
-
prepend Ridgepole::Ext::PostgreSQLAdapter::Partitioning
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|