activerecord-sqlserver-adapter 6.0.1 → 6.1.1.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +26 -0
  3. data/CHANGELOG.md +29 -46
  4. data/README.md +32 -3
  5. data/RUNNING_UNIT_TESTS.md +1 -1
  6. data/VERSION +1 -1
  7. data/activerecord-sqlserver-adapter.gemspec +1 -1
  8. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +2 -0
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -10
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +9 -2
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +2 -0
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +2 -0
  13. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -4
  14. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +28 -16
  15. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +8 -7
  16. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +22 -1
  17. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +9 -3
  18. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +31 -9
  19. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +36 -7
  20. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +0 -1
  21. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +2 -2
  22. data/lib/active_record/connection_adapters/sqlserver/type.rb +1 -0
  23. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +2 -1
  24. data/lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb +22 -0
  25. data/lib/active_record/connection_adapters/sqlserver/utils.rb +1 -1
  26. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +100 -69
  27. data/lib/active_record/connection_adapters/sqlserver_column.rb +75 -19
  28. data/lib/active_record/sqlserver_base.rb +9 -15
  29. data/lib/active_record/tasks/sqlserver_database_tasks.rb +17 -14
  30. data/lib/arel/visitors/sqlserver.rb +125 -40
  31. data/test/cases/adapter_test_sqlserver.rb +50 -16
  32. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  33. data/test/cases/coerced_tests.rb +611 -78
  34. data/test/cases/column_test_sqlserver.rb +9 -2
  35. data/test/cases/disconnected_test_sqlserver.rb +39 -0
  36. data/test/cases/execute_procedure_test_sqlserver.rb +9 -0
  37. data/test/cases/fetch_test_sqlserver.rb +18 -0
  38. data/test/cases/in_clause_test_sqlserver.rb +27 -0
  39. data/test/cases/lateral_test_sqlserver.rb +35 -0
  40. data/test/cases/migration_test_sqlserver.rb +51 -0
  41. data/test/cases/optimizer_hints_test_sqlserver.rb +72 -0
  42. data/test/cases/order_test_sqlserver.rb +7 -0
  43. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  44. data/test/cases/rake_test_sqlserver.rb +38 -2
  45. data/test/cases/schema_dumper_test_sqlserver.rb +14 -3
  46. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  47. data/test/models/sqlserver/composite_pk.rb +9 -0
  48. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  49. data/test/schema/sqlserver_specific_schema.rb +25 -0
  50. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  51. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  52. data/test/support/sql_counter_sqlserver.rb +14 -12
  53. metadata +29 -9
  54. data/.travis.yml +0 -23
  55. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +0 -28
@@ -2,31 +2,87 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
- class SQLServerColumn < Column
6
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **sqlserver_options)
7
- @sqlserver_options = sqlserver_options
8
- super
9
- end
5
+ module SQLServer
6
+ class Column < ConnectionAdapters::Column
7
+ delegate :is_identity, :is_primary, :table_name, :ordinal_position, to: :sql_type_metadata
10
8
 
11
- def is_identity?
12
- @sqlserver_options[:is_identity]
13
- end
9
+ def initialize(*, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil, **)
10
+ super
11
+ @is_identity = is_identity
12
+ @is_primary = is_primary
13
+ @table_name = table_name
14
+ @ordinal_position = ordinal_position
15
+ end
14
16
 
15
- def is_primary?
16
- @sqlserver_options[:is_primary]
17
- end
17
+ def is_identity?
18
+ is_identity
19
+ end
18
20
 
19
- def table_name
20
- @sqlserver_options[:table_name]
21
- end
21
+ def is_primary?
22
+ is_primary
23
+ end
22
24
 
23
- def is_utf8?
24
- sql_type =~ /nvarchar|ntext|nchar/i
25
- end
25
+ def is_utf8?
26
+ sql_type =~ /nvarchar|ntext|nchar/i
27
+ end
28
+
29
+ def case_sensitive?
30
+ collation && collation.match(/_CS/)
31
+ end
26
32
 
27
- def case_sensitive?
28
- collation && collation.match(/_CS/)
33
+ def init_with(coder)
34
+ @is_identity = coder["is_identity"]
35
+ @is_primary = coder["is_primary"]
36
+ @table_name = coder["table_name"]
37
+ @ordinal_position = coder["ordinal_position"]
38
+ super
39
+ end
40
+
41
+ def encode_with(coder)
42
+ coder["is_identity"] = @is_identity
43
+ coder["is_primary"] = @is_primary
44
+ coder["table_name"] = @table_name
45
+ coder["ordinal_position"] = @ordinal_position
46
+ super
47
+ end
48
+
49
+ def ==(other)
50
+ other.is_a?(Column) &&
51
+ super &&
52
+ is_identity? == other.is_identity? &&
53
+ is_primary? == other.is_primary? &&
54
+ table_name == other.table_name &&
55
+ ordinal_position == other.ordinal_position
56
+ end
57
+ alias :eql? :==
58
+
59
+ def hash
60
+ Column.hash ^
61
+ super.hash ^
62
+ is_identity?.hash ^
63
+ is_primary?.hash ^
64
+ table_name.hash ^
65
+ ordinal_position.hash
66
+ end
67
+
68
+ private
69
+
70
+ # In the Rails version of this method there is an assumption that the `default` value will always be a
71
+ # `String` class, which must be true for the MySQL/PostgreSQL/SQLite adapters. However, in the SQL Server
72
+ # adapter the `default` value can also be Boolean/Date/Time/etc. Changed the implementation of this method
73
+ # to handle non-String `default` objects.
74
+ def deduplicated
75
+ @name = -name
76
+ @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
77
+ @default = (default.is_a?(String) ? -default : default.dup.freeze) if default
78
+ @default_function = -default_function if default_function
79
+ @collation = -collation if collation
80
+ @comment = -comment if comment
81
+ freeze
82
+ end
29
83
  end
84
+
85
+ SQLServerColumn = SQLServer::Column
30
86
  end
31
87
  end
32
88
  end
@@ -4,21 +4,15 @@ module ActiveRecord
4
4
  module ConnectionHandling
5
5
  def sqlserver_connection(config) #:nodoc:
6
6
  config = config.symbolize_keys
7
- config.reverse_merge! mode: :dblib
8
- mode = config[:mode].to_s.downcase.underscore.to_sym
9
- case mode
10
- when :dblib
11
- require "tiny_tds"
12
- else
13
- raise ArgumentError, "Unknown connection mode in #{config.inspect}."
14
- end
15
- ConnectionAdapters::SQLServerAdapter.new(nil, nil, config.merge(mode: mode))
16
- rescue TinyTds::Error => e
17
- if e.message.match(/database .* does not exist/i)
18
- raise ActiveRecord::NoDatabaseError
19
- else
20
- raise
21
- end
7
+ config.reverse_merge!(mode: :dblib)
8
+ config[:mode] = config[:mode].to_s.downcase.underscore.to_sym
9
+
10
+ ConnectionAdapters::SQLServerAdapter.new(
11
+ ConnectionAdapters::SQLServerAdapter.new_client(config),
12
+ logger,
13
+ nil,
14
+ config
15
+ )
22
16
  end
23
17
  end
24
18
  end
@@ -13,13 +13,18 @@ module ActiveRecord
13
13
  delegate :connection, :establish_connection, :clear_active_connections!,
14
14
  to: ActiveRecord::Base
15
15
 
16
+ def self.using_database_configurations?
17
+ true
18
+ end
19
+
16
20
  def initialize(configuration)
17
21
  @configuration = configuration
22
+ @configuration_hash = @configuration.configuration_hash
18
23
  end
19
24
 
20
25
  def create(master_established = false)
21
26
  establish_master_connection unless master_established
22
- connection.create_database configuration["database"], configuration.merge("collation" => default_collation)
27
+ connection.create_database configuration.database, configuration_hash.merge(collation: default_collation)
23
28
  establish_connection configuration
24
29
  rescue ActiveRecord::StatementInvalid => e
25
30
  if /database .* already exists/i === e.message
@@ -31,7 +36,7 @@ module ActiveRecord
31
36
 
32
37
  def drop
33
38
  establish_master_connection
34
- connection.drop_database configuration["database"]
39
+ connection.drop_database configuration.database
35
40
  end
36
41
 
37
42
  def charset
@@ -49,14 +54,14 @@ module ActiveRecord
49
54
  end
50
55
 
51
56
  def structure_dump(filename, extra_flags)
52
- server_arg = "-S #{Shellwords.escape(configuration['host'])}"
53
- server_arg += ":#{Shellwords.escape(configuration['port'])}" if configuration["port"]
57
+ server_arg = "-S #{Shellwords.escape(configuration_hash[:host])}"
58
+ server_arg += ":#{Shellwords.escape(configuration_hash[:port])}" if configuration_hash[:port]
54
59
  command = [
55
60
  "defncopy-ttds",
56
61
  server_arg,
57
- "-D #{Shellwords.escape(configuration['database'])}",
58
- "-U #{Shellwords.escape(configuration['username'])}",
59
- "-P #{Shellwords.escape(configuration['password'])}",
62
+ "-D #{Shellwords.escape(configuration_hash[:database])}",
63
+ "-U #{Shellwords.escape(configuration_hash[:username])}",
64
+ "-P #{Shellwords.escape(configuration_hash[:password])}",
60
65
  "-o #{Shellwords.escape(filename)}",
61
66
  ]
62
67
  table_args = connection.tables.map { |t| Shellwords.escape(t) }
@@ -80,16 +85,14 @@ module ActiveRecord
80
85
 
81
86
  private
82
87
 
83
- def configuration
84
- @configuration
85
- end
88
+ attr_reader :configuration, :configuration_hash
86
89
 
87
90
  def default_collation
88
- configuration["collation"] || DEFAULT_COLLATION
91
+ configuration_hash[:collation] || DEFAULT_COLLATION
89
92
  end
90
93
 
91
94
  def establish_master_connection
92
- establish_connection configuration.merge("database" => "master")
95
+ establish_connection configuration_hash.merge(database: "master")
93
96
  end
94
97
  end
95
98
 
@@ -110,9 +113,9 @@ module ActiveRecord
110
113
  end
111
114
 
112
115
  def configuration_host_ip(configuration)
113
- return nil unless configuration["host"]
116
+ return nil unless configuration.host
114
117
 
115
- Socket::getaddrinfo(configuration["host"], "echo", Socket::AF_INET)[0][3]
118
+ Socket::getaddrinfo(configuration.host, "echo", Socket::AF_INET)[0][3]
116
119
  end
117
120
 
118
121
  def local_ipaddr?(host_ip)
@@ -11,13 +11,14 @@ module Arel
11
11
 
12
12
  private
13
13
 
14
- # SQLServer ToSql/Visitor (Overides)
14
+ # SQLServer ToSql/Visitor (Overrides)
15
15
 
16
- def visit_Arel_Nodes_BindParam o, collector
17
- collector.add_bind(o.value) { |i| "@#{i - 1}" }
18
- end
16
+ BIND_BLOCK = proc { |i| "@#{i - 1}" }
17
+ private_constant :BIND_BLOCK
18
+
19
+ def bind_block; BIND_BLOCK; end
19
20
 
20
- def visit_Arel_Nodes_Bin o, collector
21
+ def visit_Arel_Nodes_Bin(o, collector)
21
22
  visit o.expr, collector
22
23
  collector << " #{ActiveRecord::ConnectionAdapters::SQLServerAdapter.cs_equality_operator} "
23
24
  end
@@ -28,26 +29,26 @@ module Arel
28
29
  visit o.right, collector
29
30
  end
30
31
 
31
- def visit_Arel_Nodes_UpdateStatement(o, a)
32
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
32
33
  if o.orders.any? && o.limit.nil?
33
34
  o.limit = Nodes::Limit.new(9_223_372_036_854_775_807)
34
35
  end
35
36
  super
36
37
  end
37
38
 
38
- def visit_Arel_Nodes_Lock o, collector
39
+ def visit_Arel_Nodes_Lock(o, collector)
39
40
  o.expr = Arel.sql("WITH(UPDLOCK)") if o.expr.to_s =~ /FOR UPDATE/
40
41
  collector << " "
41
42
  visit o.expr, collector
42
43
  end
43
44
 
44
- def visit_Arel_Nodes_Offset o, collector
45
+ def visit_Arel_Nodes_Offset(o, collector)
45
46
  collector << OFFSET
46
47
  visit o.expr, collector
47
48
  collector << ROWS
48
49
  end
49
50
 
50
- def visit_Arel_Nodes_Limit o, collector
51
+ def visit_Arel_Nodes_Limit(o, collector)
51
52
  if node_value(o) == 0
52
53
  collector << FETCH0
53
54
  collector << ROWS_ONLY
@@ -63,7 +64,38 @@ module Arel
63
64
  super
64
65
  end
65
66
 
66
- def visit_Arel_Nodes_SelectStatement o, collector
67
+ def visit_Arel_Nodes_HomogeneousIn(o, collector)
68
+ collector.preparable = false
69
+
70
+ collector << quote_table_name(o.table_name) << "." << quote_column_name(o.column_name)
71
+
72
+ if o.type == :in
73
+ collector << " IN ("
74
+ else
75
+ collector << " NOT IN ("
76
+ end
77
+
78
+ values = o.casted_values
79
+
80
+ if values.empty?
81
+ collector << @connection.quote(nil)
82
+ elsif @connection.prepared_statements
83
+ # Monkey-patch start. Add query attribute bindings rather than just values.
84
+ column_name = o.column_name
85
+ column_type = o.attribute.relation.type_for_attribute(o.column_name)
86
+ attrs = values.map { |value| ActiveRecord::Relation::QueryAttribute.new(column_name, value, column_type) }
87
+
88
+ collector.add_binds(attrs, &bind_block)
89
+ # Monkey-patch end.
90
+ else
91
+ collector.add_binds(values, &bind_block)
92
+ end
93
+
94
+ collector << ")"
95
+ collector
96
+ end
97
+
98
+ def visit_Arel_Nodes_SelectStatement(o, collector)
67
99
  @select_statement = o
68
100
  distinct_One_As_One_Is_So_Not_Fetch o
69
101
  if o.with
@@ -80,7 +112,17 @@ module Arel
80
112
  @select_statement = nil
81
113
  end
82
114
 
83
- def visit_Arel_Table o, collector
115
+ def visit_Arel_Nodes_SelectCore(o, collector)
116
+ collector = super
117
+ maybe_visit o.optimizer_hints, collector
118
+ end
119
+
120
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
121
+ hints = o.expr.map { |v| sanitize_as_option_clause(v) }.join(", ")
122
+ collector << "OPTION (#{hints})"
123
+ end
124
+
125
+ def visit_Arel_Table(o, collector)
84
126
  # Apparently, o.engine.connection can actually be a different adapter
85
127
  # than sqlserver. Can be removed if fixed in ActiveRecord. See:
86
128
  # github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/450
@@ -102,7 +144,7 @@ module Arel
102
144
  end
103
145
  end
104
146
 
105
- def visit_Arel_Nodes_JoinSource o, collector
147
+ def visit_Arel_Nodes_JoinSource(o, collector)
106
148
  if o.left
107
149
  collector = visit o.left, collector
108
150
  collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector
@@ -114,39 +156,53 @@ module Arel
114
156
  collector
115
157
  end
116
158
 
117
- def visit_Arel_Nodes_InnerJoin o, collector
118
- collector << "INNER JOIN "
119
- collector = visit o.left, collector
120
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
121
- if o.right
122
- collector << " "
123
- visit(o.right, collector)
159
+ def visit_Arel_Nodes_InnerJoin(o, collector)
160
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
161
+ collector << "CROSS "
162
+ visit o.left, collector
124
163
  else
125
- collector
164
+ collector << "INNER JOIN "
165
+ collector = visit o.left, collector
166
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
167
+ if o.right
168
+ collector << " "
169
+ visit(o.right, collector)
170
+ else
171
+ collector
172
+ end
126
173
  end
127
174
  end
128
175
 
129
- def visit_Arel_Nodes_OuterJoin o, collector
130
- collector << "LEFT OUTER JOIN "
131
- collector = visit o.left, collector
132
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
133
- collector << " "
134
- visit o.right, collector
176
+ def visit_Arel_Nodes_OuterJoin(o, collector)
177
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
178
+ collector << "OUTER "
179
+ visit o.left, collector
180
+ else
181
+ collector << "LEFT OUTER JOIN "
182
+ collector = visit o.left, collector
183
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
184
+ collector << " "
185
+ visit o.right, collector
186
+ end
135
187
  end
136
188
 
137
- def collect_in_clause(left, right, collector)
138
- if Array === right
139
- right.each { |node| remove_invalid_ordering_from_select_statement(node) }
189
+ def visit_Arel_Nodes_In(o, collector)
190
+ if Array === o.right
191
+ o.right.each { |node| remove_invalid_ordering_from_select_statement(node) }
140
192
  else
141
- remove_invalid_ordering_from_select_statement(right)
193
+ remove_invalid_ordering_from_select_statement(o.right)
142
194
  end
143
195
 
144
196
  super
145
197
  end
146
198
 
199
+ def collect_optimizer_hints(o, collector)
200
+ collector
201
+ end
202
+
147
203
  # SQLServer ToSql/Visitor (Additions)
148
204
 
149
- def visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, options = {}
205
+ def visit_Arel_Nodes_SelectStatement_SQLServer_Lock(collector, options = {})
150
206
  if select_statement_lock?
151
207
  collector = visit @select_statement.lock, collector
152
208
  collector << " " if options[:space]
@@ -154,7 +210,7 @@ module Arel
154
210
  collector
155
211
  end
156
212
 
157
- def visit_Orders_And_Let_Fetch_Happen o, collector
213
+ def visit_Orders_And_Let_Fetch_Happen(o, collector)
158
214
  make_Fetch_Possible_And_Deterministic o
159
215
  unless o.orders.empty?
160
216
  collector << " ORDER BY "
@@ -167,13 +223,25 @@ module Arel
167
223
  collector
168
224
  end
169
225
 
170
- def visit_Make_Fetch_Happen o, collector
226
+ def visit_Make_Fetch_Happen(o, collector)
171
227
  o.offset = Nodes::Offset.new(0) if o.limit && !o.offset
172
228
  collector = visit o.offset, collector if o.offset
173
229
  collector = visit o.limit, collector if o.limit
174
230
  collector
175
231
  end
176
232
 
233
+ def visit_Arel_Nodes_Lateral(o, collector)
234
+ collector << "APPLY"
235
+ collector << " "
236
+ if o.expr.is_a?(Arel::Nodes::SelectStatement)
237
+ collector << "("
238
+ visit(o.expr, collector)
239
+ collector << ")"
240
+ else
241
+ visit(o.expr, collector)
242
+ end
243
+ end
244
+
177
245
  # SQLServer Helpers
178
246
 
179
247
  def node_value(node)
@@ -190,7 +258,7 @@ module Arel
190
258
  @select_statement && @select_statement.lock
191
259
  end
192
260
 
193
- def make_Fetch_Possible_And_Deterministic o
261
+ def make_Fetch_Possible_And_Deterministic(o)
194
262
  return if o.limit.nil? && o.offset.nil?
195
263
 
196
264
  t = table_From_Statement o
@@ -203,7 +271,7 @@ module Arel
203
271
  end
204
272
  end
205
273
 
206
- def distinct_One_As_One_Is_So_Not_Fetch o
274
+ def distinct_One_As_One_Is_So_Not_Fetch(o)
207
275
  core = o.cores.first
208
276
  distinct = Nodes::Distinct === core.set_quantifier
209
277
  oneasone = core.projections.all? { |x| x == ActiveRecord::FinderMethods::ONE_AS_ONE }
@@ -214,7 +282,7 @@ module Arel
214
282
  end
215
283
  end
216
284
 
217
- def table_From_Statement o
285
+ def table_From_Statement(o)
218
286
  core = o.cores.first
219
287
  if Arel::Table === core.from
220
288
  core.from
@@ -225,15 +293,28 @@ module Arel
225
293
  end
226
294
  end
227
295
 
228
- def primary_Key_From_Table t
296
+ def primary_Key_From_Table(t)
229
297
  return unless t
230
298
 
231
- column_name = @connection.schema_cache.primary_keys(t.name) ||
232
- @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
299
+ primary_keys = @connection.schema_cache.primary_keys(t.name)
300
+ column_name = nil
301
+
302
+ case primary_keys
303
+ when NilClass
304
+ column_name = @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
305
+ when String
306
+ column_name = primary_keys
307
+ when Array
308
+ candidate_columns = @connection.schema_cache.columns_hash(t.name).slice(*primary_keys).values
309
+ candidate_column = candidate_columns.find(&:is_identity?)
310
+ candidate_column ||= candidate_columns.first
311
+ column_name = candidate_column.try(:name)
312
+ end
313
+
233
314
  column_name ? t[column_name] : nil
234
315
  end
235
316
 
236
- def remote_server_table_name o
317
+ def remote_server_table_name(o)
237
318
  ActiveRecord::ConnectionAdapters::SQLServer::Utils.extract_identifiers(
238
319
  "#{o.class.engine.connection.database_prefix}#{o.name}"
239
320
  ).quoted
@@ -247,6 +328,10 @@ module Arel
247
328
 
248
329
  node.orders = [] unless node.offset || node.limit
249
330
  end
331
+
332
+ def sanitize_as_option_clause(value)
333
+ value.gsub(%r{OPTION \s* \( (.+) \)}xi, "\\1")
334
+ end
250
335
  end
251
336
  end
252
337
  end