dynamic_migrations 3.8.6 → 3.8.8
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/CHANGELOG.md +27 -0
- data/lib/dynamic_migrations/active_record/migrators/validation.rb +2 -20
- data/lib/dynamic_migrations/postgres/generator/enum.rb +13 -9
- data/lib/dynamic_migrations/postgres/generator/fragment.rb +10 -3
- data/lib/dynamic_migrations/postgres/generator/function.rb +13 -13
- data/lib/dynamic_migrations/postgres/generator/migration.rb +45 -7
- data/lib/dynamic_migrations/postgres/generator/table_migration.rb +2 -0
- data/lib/dynamic_migrations/postgres/generator/validation.rb +1 -3
- data/lib/dynamic_migrations/postgres/generator/validation_template_base.rb +1 -7
- data/lib/dynamic_migrations/postgres/generator.rb +100 -46
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/extensions.rb +2 -2
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/enums.rb +10 -10
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/functions.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/columns.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/foreign_key_constraints.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/indexes.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/primary_key.rb +6 -6
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/triggers.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/unique_constraints.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables/validations.rb +11 -11
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas/tables.rb +8 -8
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations/schemas.rb +3 -3
- data/lib/dynamic_migrations/postgres/server/database/differences/to_migrations.rb +4 -4
- data/lib/dynamic_migrations/postgres/server/database/differences.rb +25 -20
- data/lib/dynamic_migrations/postgres/server/database/keys_and_unique_constraints_loader.rb +2 -2
- data/lib/dynamic_migrations/postgres/server/database/loaded_schemas_builder.rb +1 -1
- data/lib/dynamic_migrations/postgres/server/database/schema/enum.rb +9 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/function.rb +2 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/table/column.rb +6 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/table/columns.rb +0 -6
- data/lib/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rb +6 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/table/index.rb +7 -3
- data/lib/dynamic_migrations/postgres/server/database/schema/table/primary_key.rb +6 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/table/trigger.rb +10 -8
- data/lib/dynamic_migrations/postgres/server/database/schema/table/triggers.rb +2 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rb +6 -2
- data/lib/dynamic_migrations/postgres/server/database/schema/table/validation.rb +12 -19
- data/lib/dynamic_migrations/postgres/server/database/schema/table.rb +62 -2
- data/lib/dynamic_migrations/postgres/server/database/validations_loader.rb +1 -3
- data/lib/dynamic_migrations/version.rb +1 -1
- data/sig/dynamic_migrations/active_record/migrators/validation.rbs +1 -1
- data/sig/dynamic_migrations/postgres/generator/enum.rbs +2 -0
- data/sig/dynamic_migrations/postgres/generator/fragment.rbs +3 -0
- data/sig/dynamic_migrations/postgres/generator/function.rbs +1 -0
- data/sig/dynamic_migrations/postgres/generator/migration.rbs +1 -0
- data/sig/dynamic_migrations/postgres/generator/schema_migration.rbs +2 -0
- data/sig/dynamic_migrations/postgres/generator/table_migration.rbs +3 -0
- data/sig/dynamic_migrations/postgres/generator/validation_template_base.rbs +0 -1
- data/sig/dynamic_migrations/postgres/generator.rbs +3 -1
- data/sig/dynamic_migrations/postgres/server/database/schema/enum.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/column.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/foreign_key_constraint.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/index.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/primary_key.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/trigger.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/unique_constraint.rbs +3 -0
- data/sig/dynamic_migrations/postgres/server/database/schema/table/validation.rbs +4 -3
- data/sig/dynamic_migrations/postgres/server/database/schema/table.rbs +3 -0
- metadata +2 -3
- data/lib/dynamic_migrations/name_helper.rb +0 -13
@@ -23,6 +23,9 @@ module DynamicMigrations
|
|
23
23
|
class DuplicateColumnError < StandardError
|
24
24
|
end
|
25
25
|
|
26
|
+
class InvalidNameError < StandardError
|
27
|
+
end
|
28
|
+
|
26
29
|
attr_reader :table
|
27
30
|
attr_reader :name
|
28
31
|
attr_reader :deferrable
|
@@ -45,12 +48,13 @@ module DynamicMigrations
|
|
45
48
|
add_column column
|
46
49
|
end
|
47
50
|
|
48
|
-
raise
|
51
|
+
raise InvalidNameError, "Unexpected name `#{name}`. Name should be a Symbol" unless name.is_a? Symbol
|
52
|
+
raise InvalidNameError, "The name `#{name}` is too long. Names must be less than 64 characters" unless name.length < 64
|
49
53
|
@name = name
|
50
54
|
|
51
55
|
unless description.nil?
|
52
56
|
raise ExpectedStringError, description unless description.is_a? String
|
53
|
-
@description = description.strip
|
57
|
+
@description = description.strip.freeze
|
54
58
|
@description = nil if description == ""
|
55
59
|
end
|
56
60
|
|
@@ -26,25 +26,27 @@ module DynamicMigrations
|
|
26
26
|
class UnnormalizableCheckClauseError < StandardError
|
27
27
|
end
|
28
28
|
|
29
|
+
class InvalidNameError < StandardError
|
30
|
+
end
|
31
|
+
|
29
32
|
attr_reader :table
|
30
33
|
attr_reader :name
|
31
34
|
attr_reader :check_clause
|
32
|
-
attr_reader :deferrable
|
33
|
-
attr_reader :initially_deferred
|
34
35
|
attr_reader :description
|
35
36
|
attr_reader :template
|
36
37
|
|
37
38
|
# initialize a new object to represent a validation in a postgres table
|
38
|
-
def initialize source, table, columns, name, check_clause, description: nil,
|
39
|
+
def initialize source, table, columns, name, check_clause, description: nil, template: nil
|
39
40
|
super source
|
40
41
|
raise ExpectedTableError, table unless table.is_a? Table
|
41
42
|
@table = table
|
42
43
|
|
43
|
-
raise
|
44
|
+
raise InvalidNameError, "Unexpected name `#{name}`. Name should be a Symbol" unless name.is_a? Symbol
|
45
|
+
raise InvalidNameError, "The name `#{name}` is too long. Names must be less than 64 characters" unless name.length < 64
|
44
46
|
@name = name
|
45
47
|
|
46
48
|
raise ExpectedStringError, check_clause unless check_clause.is_a? String
|
47
|
-
@check_clause = check_clause.strip
|
49
|
+
@check_clause = check_clause.strip.freeze
|
48
50
|
|
49
51
|
# if this validation is created via configuration (apposed to being loaded) then they can be lazy loaded
|
50
52
|
unless from_configuration? && columns.nil?
|
@@ -61,16 +63,10 @@ module DynamicMigrations
|
|
61
63
|
|
62
64
|
unless description.nil?
|
63
65
|
raise ExpectedStringError, description unless description.is_a? String
|
64
|
-
@description = description.strip
|
66
|
+
@description = description.strip.freeze
|
65
67
|
@description = nil if description == ""
|
66
68
|
end
|
67
69
|
|
68
|
-
raise ExpectedBooleanError, deferrable unless [true, false].include?(deferrable)
|
69
|
-
@deferrable = deferrable
|
70
|
-
|
71
|
-
raise ExpectedBooleanError, initially_deferred unless [true, false].include?(initially_deferred)
|
72
|
-
@initially_deferred = initially_deferred
|
73
|
-
|
74
70
|
unless template.nil?
|
75
71
|
unless Generator::Validation.has_template? template
|
76
72
|
raise UnexpectedTemplateError, "Unrecognised template #{template}"
|
@@ -96,14 +92,12 @@ module DynamicMigrations
|
|
96
92
|
end
|
97
93
|
|
98
94
|
def column_names
|
99
|
-
columns.map(&:name)
|
95
|
+
columns.map(&:name).sort
|
100
96
|
end
|
101
97
|
|
102
98
|
def differences_descriptions other_validation
|
103
99
|
method_differences_descriptions other_validation, [
|
104
|
-
:normalized_check_clause
|
105
|
-
:deferrable,
|
106
|
-
:initially_deferred
|
100
|
+
:normalized_check_clause
|
107
101
|
]
|
108
102
|
end
|
109
103
|
|
@@ -134,7 +128,7 @@ module DynamicMigrations
|
|
134
128
|
|
135
129
|
temp_enums = table.create_temp_table(connection, "validation_normalized_check_clause_temp_table")
|
136
130
|
|
137
|
-
temp_check_clause = check_clause
|
131
|
+
temp_check_clause = check_clause.dup
|
138
132
|
# string replace any real enum names with their temp enum names
|
139
133
|
temp_enums.each do |temp_enum_name, enum|
|
140
134
|
temp_check_clause.gsub!("::#{enum.name}", "::#{temp_enum_name}")
|
@@ -182,8 +176,7 @@ module DynamicMigrations
|
|
182
176
|
|
183
177
|
# string replace any enum names with their real enum names
|
184
178
|
temp_enums.each do |temp_enum_name, enum|
|
185
|
-
|
186
|
-
check_clause_result.gsub!("::#{temp_enum_name}", "::#{real_enum_name}")
|
179
|
+
check_clause_result.gsub!("::#{temp_enum_name}", "::#{enum.full_name}")
|
187
180
|
end
|
188
181
|
|
189
182
|
column_names_result = column_names_string.gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym }
|
@@ -16,6 +16,9 @@ module DynamicMigrations
|
|
16
16
|
class PrimaryKeyAlreadyExistsError < StandardError
|
17
17
|
end
|
18
18
|
|
19
|
+
class MissingExtensionError < StandardError
|
20
|
+
end
|
21
|
+
|
19
22
|
include Columns
|
20
23
|
include Validations
|
21
24
|
include Indexes
|
@@ -40,7 +43,7 @@ module DynamicMigrations
|
|
40
43
|
|
41
44
|
unless description.nil?
|
42
45
|
raise ExpectedStringError, description unless description.is_a? String
|
43
|
-
@description = description.strip
|
46
|
+
@description = description.strip.freeze
|
44
47
|
@description = nil if description == ""
|
45
48
|
end
|
46
49
|
|
@@ -116,7 +119,64 @@ module DynamicMigrations
|
|
116
119
|
end
|
117
120
|
|
118
121
|
# in case any of the columnbs are citext columns
|
119
|
-
|
122
|
+
# in case any of the columns use the citext data type
|
123
|
+
required_extensions = []
|
124
|
+
if columns.any? { |column| column.data_type.start_with? "citext" }
|
125
|
+
required_extensions << "citext"
|
126
|
+
end
|
127
|
+
if columns.any? { |column| column.data_type.start_with? "postgis" }
|
128
|
+
required_extensions << "postgis"
|
129
|
+
end
|
130
|
+
|
131
|
+
required_extensions.each do |extension_name|
|
132
|
+
extension_result = connection.exec(<<~SQL)
|
133
|
+
SELECT
|
134
|
+
(
|
135
|
+
SELECT 1
|
136
|
+
FROM pg_available_extensions
|
137
|
+
WHERE name = '#{extension_name}'
|
138
|
+
) as is_available,
|
139
|
+
(
|
140
|
+
SELECT 1
|
141
|
+
FROM pg_extension
|
142
|
+
WHERE extname = '#{extension_name}'
|
143
|
+
) as is_installed
|
144
|
+
SQL
|
145
|
+
|
146
|
+
row = extension_result.first
|
147
|
+
raise MissingExtensionError, "unexpected error" if row.nil?
|
148
|
+
|
149
|
+
unless row["is_installed"]
|
150
|
+
detail = if row["is_available"]
|
151
|
+
<<~DETAIL
|
152
|
+
The `#{extension_name}` extension is available for installation,
|
153
|
+
but has not been installed for this database.
|
154
|
+
DETAIL
|
155
|
+
else
|
156
|
+
<<~DETAIL
|
157
|
+
The `#{extension_name}` extension is not installed, and does not
|
158
|
+
appear to be available for installation.
|
159
|
+
DETAIL
|
160
|
+
end
|
161
|
+
raise MissingExtensionError, <<~ERROR.tr!("\n", " ")
|
162
|
+
This table uses the `#{extension_name}` data type. #{detail}
|
163
|
+
Add the extension, then generate and run the migrations which will
|
164
|
+
enable the extension for your database before defining validations
|
165
|
+
or triggers which rely on it.
|
166
|
+
|
167
|
+
Note, the `#{extension_name}` extension is required even for defining
|
168
|
+
some validations and triggers. This library needs to connect to postgres
|
169
|
+
and gererate normalized versions of validation check clauses and trigger
|
170
|
+
action conditions before it can even compare them to validations or triggers
|
171
|
+
which may or may not already exist in the database.
|
172
|
+
ERROR
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# if any of the columns require postgis
|
177
|
+
if required_extensions.include? "postgis"
|
178
|
+
connection.exec("SET search_path TO public,postgis;")
|
179
|
+
end
|
120
180
|
|
121
181
|
# note, this is not actually a TEMP TABLE, it is created within a transaction
|
122
182
|
# and rolled back.
|
@@ -86,9 +86,7 @@ module DynamicMigrations
|
|
86
86
|
table[validation_name] = {
|
87
87
|
columns: row["columns"].gsub(/\A\{/, "").gsub(/\}\Z/, "").split(",").map { |column_name| column_name.to_sym },
|
88
88
|
check_clause: check_clause,
|
89
|
-
description: row["description"]
|
90
|
-
deferrable: row["deferrable"] == "TRUE",
|
91
|
-
initially_deferred: row["initially_deferred"] == "TRUE"
|
89
|
+
description: row["description"]
|
92
90
|
}
|
93
91
|
end
|
94
92
|
schemas
|
@@ -5,7 +5,7 @@ module DynamicMigrations
|
|
5
5
|
module ActiveRecord
|
6
6
|
module Migrators
|
7
7
|
module Validation
|
8
|
-
def add_validation: (Symbol table_name, name: Symbol, ?
|
8
|
+
def add_validation: (Symbol table_name, name: Symbol, ?comment: String?) -> void
|
9
9
|
def remove_validation: (Symbol table_name, Symbol name) -> void
|
10
10
|
def set_validation_comment: (Symbol table_name, Symbol validation_name, String comment) -> void
|
11
11
|
def remove_validation_comment: (Symbol table_name, Symbol validation_name) -> void
|
@@ -10,6 +10,8 @@ module DynamicMigrations
|
|
10
10
|
def drop_enum: (Server::Database::Schema::Enum enum, ?String? code_comment) -> Fragment
|
11
11
|
def set_enum_comment: (Server::Database::Schema::Enum enum, ?String? code_comment) -> Fragment
|
12
12
|
def remove_enum_comment: (Server::Database::Schema::Enum enum, ?String? code_comment) -> Fragment
|
13
|
+
def optional_enum_table: (Postgres::Server::Database::Schema::Enum enum) -> Server::Database::Schema::Table?
|
14
|
+
|
13
15
|
# these come from the generator object (which this module is included into)
|
14
16
|
def add_fragment: (migration_method: Symbol, object: untyped, migration: String, ?schema: Server::Database::Schema?, ?table: Server::Database::Schema::Table?, ?code_comment: String?, ?dependent_table: Server::Database::Schema::Table?, ?dependent_function: Server::Database::Schema::Function?, ?dependent_enum: Server::Database::Schema::Enum?) -> Fragment
|
15
17
|
def indent: (String migration, ?Integer levels) -> String
|
@@ -10,6 +10,7 @@ module DynamicMigrations
|
|
10
10
|
def drop_function: (Postgres::Server::Database::Schema::Function function, ?String? code_comment) -> Fragment
|
11
11
|
def set_function_comment: (Postgres::Server::Database::Schema::Function function, ?String? code_comment) -> Fragment
|
12
12
|
def remove_function_comment: (Postgres::Server::Database::Schema::Function function, ?String? code_comment) -> Fragment
|
13
|
+
def optional_function_table: (Postgres::Server::Database::Schema::Function function) -> Server::Database::Schema::Table?
|
13
14
|
|
14
15
|
# these come from the generator object (which this module is included into)
|
15
16
|
def add_fragment: (migration_method: Symbol, object: untyped, migration: String, ?schema: Server::Database::Schema?, ?table: Server::Database::Schema::Table?, ?code_comment: String?, ?dependent_table: Server::Database::Schema::Table?, ?dependent_function: Server::Database::Schema::Function?, ?dependent_enum: Server::Database::Schema::Enum?) -> Fragment
|
@@ -22,6 +22,7 @@ module DynamicMigrations
|
|
22
22
|
def enum_dependencies: -> Array[{schema_name: Symbol, enum_name: Symbol}]
|
23
23
|
def function_dependencies: -> Array[{schema_name: Symbol, function_name: Symbol}]
|
24
24
|
|
25
|
+
def fragments_with_table_dependency_count: (Symbol schema_name, Symbol table_name) -> Integer
|
25
26
|
def extract_fragments_with_table_dependency: (Symbol schema_name, Symbol table_name) -> Array[Fragment]
|
26
27
|
|
27
28
|
def content: -> String
|
@@ -15,7 +15,6 @@ module DynamicMigrations
|
|
15
15
|
def fragment_arguments: -> {schema: Postgres::Server::Database::Schema, table: Postgres::Server::Database::Schema::Table, migration_method: Symbol, object: untyped, code_comment: String?, migration: String, dependent_function: Postgres::Server::Database::Schema::Function?}
|
16
16
|
|
17
17
|
private
|
18
|
-
def assert_not_deferred!: -> void
|
19
18
|
def assert_column_count!: (?Integer count) -> void
|
20
19
|
def first_column: -> Postgres::Server::Database::Schema::Table::Column
|
21
20
|
def value_from_check_clause: (Regexp regex) -> untyped
|
@@ -7,6 +7,7 @@ module DynamicMigrations
|
|
7
7
|
include TSort
|
8
8
|
|
9
9
|
@fragments: Array[Fragment]
|
10
|
+
@logger: Logging::Logger
|
10
11
|
|
11
12
|
include Schema
|
12
13
|
include Table
|
@@ -27,13 +28,14 @@ module DynamicMigrations
|
|
27
28
|
}]
|
28
29
|
|
29
30
|
private
|
30
|
-
def
|
31
|
+
def resolve_circular_dependencies: (TableMigration table_migration, Array[TableMigration] all_table_migrations, Hash[Symbol, untyped] database_migrations, Array[TableMigration] completed_table_migrations, ?Array[String] stack) -> void
|
31
32
|
def supported_migration_method?: (Symbol migration_method) -> bool
|
32
33
|
def add_fragment: (migration_method: Symbol, object: untyped, migration: String, ?schema: Server::Database::Schema?, ?table: Server::Database::Schema::Table?, ?code_comment: String?, ?dependent_table: Server::Database::Schema::Table?, ?dependent_function: Server::Database::Schema::Function?, ?dependent_enum: Server::Database::Schema::Enum?) -> Fragment
|
33
34
|
def indent: (String migration, ?Integer levels) -> String
|
34
35
|
def tsort_each_node: -> Enumerator[untyped, untyped]
|
35
36
|
def tsort_each_child: (untyped node) -> untyped
|
36
37
|
def trim_lines: (String migration) -> String
|
38
|
+
def log: -> Logging::Logger
|
37
39
|
|
38
40
|
class ExpectedSymbolError < StandardError
|
39
41
|
end
|
@@ -11,12 +11,10 @@ module DynamicMigrations
|
|
11
11
|
attr_reader table: Table
|
12
12
|
attr_reader name: Symbol
|
13
13
|
attr_reader check_clause: String
|
14
|
-
attr_reader deferrable: bool
|
15
|
-
attr_reader initially_deferred: bool
|
16
14
|
attr_reader description: String?
|
17
15
|
attr_reader template: Symbol?
|
18
16
|
|
19
|
-
def initialize: (database_or_configuration source, Table table, Array[Column]? columns, Symbol name, String check_clause, ?
|
17
|
+
def initialize: (database_or_configuration source, Table table, Array[Column]? columns, Symbol name, String check_clause, ?description: String?, ?template: Symbol?) -> void
|
20
18
|
def columns: -> Array[Column]
|
21
19
|
def column_names: -> Array[Symbol]
|
22
20
|
def has_description?: -> bool
|
@@ -50,6 +48,9 @@ module DynamicMigrations
|
|
50
48
|
|
51
49
|
class UnnormalizableCheckClauseError < StandardError
|
52
50
|
end
|
51
|
+
|
52
|
+
class InvalidNameError < StandardError
|
53
|
+
end
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamic_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.8.
|
4
|
+
version: 3.8.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Craig Ulliott
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pg
|
@@ -96,7 +96,6 @@ files:
|
|
96
96
|
- lib/dynamic_migrations/expected_symbol_error.rb
|
97
97
|
- lib/dynamic_migrations/invalid_source_error.rb
|
98
98
|
- lib/dynamic_migrations/module_included_into_unexpected_target_error.rb
|
99
|
-
- lib/dynamic_migrations/name_helper.rb
|
100
99
|
- lib/dynamic_migrations/postgres.rb
|
101
100
|
- lib/dynamic_migrations/postgres/connections.rb
|
102
101
|
- lib/dynamic_migrations/postgres/generator.rb
|
@@ -1,13 +0,0 @@
|
|
1
|
-
module DynamicMigrations
|
2
|
-
module NameHelper
|
3
|
-
# shortens a table name like 'invoice_subscription_prepayments' to 'inv_sub_pre'
|
4
|
-
warn "no unit tests"
|
5
|
-
def abbreviate_table_name table_name
|
6
|
-
table_name_without_schema = table_name.to_s.split(".").last
|
7
|
-
if table_name_without_schema.nil?
|
8
|
-
raise "no table name provided"
|
9
|
-
end
|
10
|
-
table_name_without_schema.split("_").map { |v| v[0..2] }.join("_")
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|