sequel 3.39.0 → 3.40.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.
- data/CHANGELOG +30 -0
- data/README.rdoc +4 -3
- data/doc/active_record.rdoc +1 -1
- data/doc/opening_databases.rdoc +7 -0
- data/doc/release_notes/3.40.0.txt +73 -0
- data/lib/sequel/adapters/ado.rb +29 -3
- data/lib/sequel/adapters/ado/access.rb +334 -0
- data/lib/sequel/adapters/ado/mssql.rb +0 -6
- data/lib/sequel/adapters/cubrid.rb +143 -0
- data/lib/sequel/adapters/jdbc.rb +26 -18
- data/lib/sequel/adapters/jdbc/cubrid.rb +52 -0
- data/lib/sequel/adapters/jdbc/derby.rb +7 -7
- data/lib/sequel/adapters/jdbc/hsqldb.rb +5 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +9 -4
- data/lib/sequel/adapters/mysql.rb +0 -3
- data/lib/sequel/adapters/mysql2.rb +0 -3
- data/lib/sequel/adapters/oracle.rb +4 -1
- data/lib/sequel/adapters/postgres.rb +4 -4
- data/lib/sequel/adapters/shared/access.rb +205 -3
- data/lib/sequel/adapters/shared/cubrid.rb +216 -0
- data/lib/sequel/adapters/shared/db2.rb +7 -2
- data/lib/sequel/adapters/shared/mssql.rb +3 -34
- data/lib/sequel/adapters/shared/mysql.rb +4 -33
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +11 -0
- data/lib/sequel/adapters/shared/oracle.rb +5 -0
- data/lib/sequel/adapters/shared/postgres.rb +2 -1
- data/lib/sequel/adapters/utils/split_alter_table.rb +36 -0
- data/lib/sequel/database/connecting.rb +1 -1
- data/lib/sequel/database/query.rb +30 -7
- data/lib/sequel/database/schema_methods.rb +7 -2
- data/lib/sequel/dataset/query.rb +9 -10
- data/lib/sequel/dataset/sql.rb +14 -26
- data/lib/sequel/extensions/pg_hstore.rb +19 -0
- data/lib/sequel/extensions/pg_row.rb +5 -5
- data/lib/sequel/plugins/association_pks.rb +121 -18
- data/lib/sequel/plugins/json_serializer.rb +19 -0
- data/lib/sequel/sql.rb +11 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +42 -0
- data/spec/core/database_spec.rb +17 -0
- data/spec/core/dataset_spec.rb +11 -0
- data/spec/core/expression_filters_spec.rb +13 -0
- data/spec/extensions/association_pks_spec.rb +163 -3
- data/spec/extensions/pg_hstore_spec.rb +6 -0
- data/spec/extensions/pg_row_spec.rb +17 -0
- data/spec/integration/associations_test.rb +1 -1
- data/spec/integration/dataset_test.rb +13 -13
- data/spec/integration/plugin_test.rb +232 -7
- data/spec/integration/schema_test.rb +8 -12
- data/spec/integration/spec_helper.rb +1 -1
- data/spec/integration/type_test.rb +6 -0
- metadata +9 -2
@@ -0,0 +1,216 @@
|
|
1
|
+
Sequel.require 'adapters/utils/split_alter_table'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
module Cubrid
|
5
|
+
module DatabaseMethods
|
6
|
+
include Sequel::Database::SplitAlterTable
|
7
|
+
|
8
|
+
AUTOINCREMENT = 'AUTO_INCREMENT'.freeze
|
9
|
+
COLUMN_DEFINITION_ORDER = [:auto_increment, :default, :null, :unique, :primary_key, :references]
|
10
|
+
|
11
|
+
def database_type
|
12
|
+
:cubrid
|
13
|
+
end
|
14
|
+
|
15
|
+
def indexes(table, opts={})
|
16
|
+
m = output_identifier_meth
|
17
|
+
m2 = input_identifier_meth
|
18
|
+
indexes = {}
|
19
|
+
metadata_dataset.
|
20
|
+
from(:db_index___i).
|
21
|
+
join(:db_index_key___k, :index_name=>:index_name, :class_name=>:class_name).
|
22
|
+
where(:i__class_name=>m2.call(table), :is_primary_key=>'NO').
|
23
|
+
order(:k__key_order).
|
24
|
+
select(:i__index_name, :k__key_attr_name___column, :is_unique).
|
25
|
+
each do |row|
|
26
|
+
index = indexes[m.call(row[:index_name])] ||= {:columns=>[], :unique=>row[:is_unique]=='YES'}
|
27
|
+
index[:columns] << m.call(row[:column])
|
28
|
+
end
|
29
|
+
indexes
|
30
|
+
end
|
31
|
+
|
32
|
+
def supports_savepoints?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def schema_parse_table(table_name, opts)
|
37
|
+
m = output_identifier_meth(opts[:dataset])
|
38
|
+
m2 = input_identifier_meth(opts[:dataset])
|
39
|
+
|
40
|
+
pks = metadata_dataset.
|
41
|
+
from(:db_index___i).
|
42
|
+
join(:db_index_key___k, :index_name=>:index_name, :class_name=>:class_name).
|
43
|
+
where(:i__class_name=>m2.call(table_name), :is_primary_key=>'YES').
|
44
|
+
order(:k__key_order).
|
45
|
+
select_map(:k__key_attr_name).
|
46
|
+
map{|c| m.call(c)}
|
47
|
+
|
48
|
+
metadata_dataset.
|
49
|
+
from(:db_attribute).
|
50
|
+
where(:class_name=>m2.call(table_name)).
|
51
|
+
order(:def_order).
|
52
|
+
select(:attr_name, :data_type___db_type, :default_value___default, :is_nullable___allow_null).
|
53
|
+
map do |row|
|
54
|
+
name = m.call(row.delete(:attr_name))
|
55
|
+
row[:allow_null] = row[:allow_null] == 'YES'
|
56
|
+
row[:primary_key] = pks.include?(name)
|
57
|
+
row[:type] = schema_column_type(row[:db_type])
|
58
|
+
[name, row]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def tables(opts={})
|
63
|
+
_tables('CLASS')
|
64
|
+
end
|
65
|
+
|
66
|
+
def views(opts={})
|
67
|
+
_tables('VCLASS')
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def _tables(type)
|
73
|
+
m = output_identifier_meth
|
74
|
+
metadata_dataset.
|
75
|
+
from(:db_class).
|
76
|
+
where(:is_system_class=>'NO', :class_type=>type).
|
77
|
+
select_map(:class_name).
|
78
|
+
map{|c| m.call(c)}
|
79
|
+
end
|
80
|
+
|
81
|
+
def alter_table_op_sql(table, op)
|
82
|
+
case op[:op]
|
83
|
+
when :rename_column
|
84
|
+
"RENAME COLUMN #{quote_identifier(op[:name])} AS #{quote_identifier(op[:new_name])}"
|
85
|
+
when :set_column_type, :set_column_null, :set_column_default
|
86
|
+
o = op[:op]
|
87
|
+
opts = schema(table).find{|x| x.first == op[:name]}
|
88
|
+
opts = opts ? opts.last.dup : {}
|
89
|
+
opts[:name] = o == :rename_column ? op[:new_name] : op[:name]
|
90
|
+
opts[:type] = o == :set_column_type ? op[:type] : opts[:db_type]
|
91
|
+
opts[:null] = o == :set_column_null ? op[:null] : opts[:allow_null]
|
92
|
+
opts[:default] = o == :set_column_default ? op[:default] : opts[:ruby_default]
|
93
|
+
opts.delete(:default) if opts[:default] == nil
|
94
|
+
"CHANGE COLUMN #{quote_identifier(op[:name])} #{column_definition_sql(op.merge(opts))}"
|
95
|
+
else
|
96
|
+
super
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def alter_table_sql(table, op)
|
101
|
+
case op[:op]
|
102
|
+
when :drop_index
|
103
|
+
"ALTER TABLE #{quote_schema_table(table)} #{drop_index_sql(table, op)}"
|
104
|
+
else
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def auto_increment_sql
|
110
|
+
AUTOINCREMENT
|
111
|
+
end
|
112
|
+
|
113
|
+
# CUBRID requires auto increment before primary key
|
114
|
+
def column_definition_order
|
115
|
+
COLUMN_DEFINITION_ORDER
|
116
|
+
end
|
117
|
+
|
118
|
+
# CUBRID requires FOREIGN KEY keywords before a column reference
|
119
|
+
def column_references_sql(column)
|
120
|
+
sql = super
|
121
|
+
sql = " FOREIGN KEY#{sql}" unless column[:columns]
|
122
|
+
sql
|
123
|
+
end
|
124
|
+
|
125
|
+
def connection_execute_method
|
126
|
+
:query
|
127
|
+
end
|
128
|
+
|
129
|
+
# CUBRID is case insensitive, so don't modify identifiers
|
130
|
+
def identifier_input_method_default
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# CUBRID is case insensitive, so don't modify identifiers
|
135
|
+
def identifier_output_method_default
|
136
|
+
nil
|
137
|
+
end
|
138
|
+
|
139
|
+
# CUBRID doesn't support booleans, it recommends using smallint.
|
140
|
+
def type_literal_generic_trueclass(column)
|
141
|
+
:smallint
|
142
|
+
end
|
143
|
+
|
144
|
+
# CUBRID uses clob for text types.
|
145
|
+
def uses_clob_for_text?
|
146
|
+
true
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
module DatasetMethods
|
151
|
+
SELECT_CLAUSE_METHODS = Sequel::Dataset.clause_methods(:select, %w'select distinct columns from join where group having compounds order limit')
|
152
|
+
LIMIT = Sequel::Dataset::LIMIT
|
153
|
+
COMMA = Sequel::Dataset::COMMA
|
154
|
+
BOOL_FALSE = '0'.freeze
|
155
|
+
BOOL_TRUE = '1'.freeze
|
156
|
+
|
157
|
+
def complex_expression_sql_append(sql, op, args)
|
158
|
+
case op
|
159
|
+
when :ILIKE
|
160
|
+
super(sql, :LIKE, [SQL::Function.new(:upper, args.at(0)), SQL::Function.new(:upper, args.at(1))])
|
161
|
+
when :"NOT ILIKE"
|
162
|
+
super(sql, :"NOT LIKE", [SQL::Function.new(:upper, args.at(0)), SQL::Function.new(:upper, args.at(1))])
|
163
|
+
else
|
164
|
+
super
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def supports_join_using?
|
169
|
+
false
|
170
|
+
end
|
171
|
+
|
172
|
+
def supports_multiple_column_in?
|
173
|
+
false
|
174
|
+
end
|
175
|
+
|
176
|
+
def supports_timestamp_usecs?
|
177
|
+
false
|
178
|
+
end
|
179
|
+
|
180
|
+
# CUBRID supposedly supports TRUNCATE, but it appears not to work in my testing.
|
181
|
+
# Fallback to using DELETE.
|
182
|
+
def truncate
|
183
|
+
delete
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def literal_false
|
190
|
+
BOOL_FALSE
|
191
|
+
end
|
192
|
+
|
193
|
+
def literal_true
|
194
|
+
BOOL_TRUE
|
195
|
+
end
|
196
|
+
|
197
|
+
# CUBRID doesn't support CTEs or FOR UPDATE.
|
198
|
+
def select_clause_methods
|
199
|
+
SELECT_CLAUSE_METHODS
|
200
|
+
end
|
201
|
+
|
202
|
+
# CUBRID requires a limit to use an offset,
|
203
|
+
# and requires a FROM table if a limit is used.
|
204
|
+
def select_limit_sql(sql)
|
205
|
+
if @opts[:from] && (l = @opts[:limit])
|
206
|
+
sql << LIMIT
|
207
|
+
if o = @opts[:offset]
|
208
|
+
literal_append(sql, o)
|
209
|
+
sql << COMMA
|
210
|
+
end
|
211
|
+
literal_append(sql, l)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -123,10 +123,10 @@ module Sequel
|
|
123
123
|
end
|
124
124
|
|
125
125
|
# Supply columns with NOT NULL if they are part of a composite
|
126
|
-
# primary
|
126
|
+
# primary key or unique constraint
|
127
127
|
def column_list_sql(g)
|
128
128
|
ks = []
|
129
|
-
g.constraints.each{|c| ks = c[:columns] if [:primary_key, :
|
129
|
+
g.constraints.each{|c| ks = c[:columns] if [:primary_key, :unique].include?(c[:type])}
|
130
130
|
g.columns.each{|c| c[:null] = false if ks.include?(c[:name]) }
|
131
131
|
super
|
132
132
|
end
|
@@ -189,6 +189,11 @@ module Sequel
|
|
189
189
|
:smallint
|
190
190
|
end
|
191
191
|
alias type_literal_generic_falseclass type_literal_generic_trueclass
|
192
|
+
|
193
|
+
# DB2 uses clob for text types.
|
194
|
+
def uses_clob_for_text?
|
195
|
+
true
|
196
|
+
end
|
192
197
|
end
|
193
198
|
|
194
199
|
module DatasetMethods
|
@@ -1,4 +1,4 @@
|
|
1
|
-
Sequel.require 'adapters/utils/
|
1
|
+
Sequel.require %w'emulate_offset_with_row_number split_alter_table', 'adapters/utils/'
|
2
2
|
|
3
3
|
module Sequel
|
4
4
|
Dataset::NON_SQL_OPTIONS << :disable_insert_output
|
@@ -13,6 +13,8 @@ module Sequel
|
|
13
13
|
SQL_ROLLBACK_TO_SAVEPOINT = 'IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION autopoint_%d'.freeze
|
14
14
|
SQL_SAVEPOINT = 'SAVE TRANSACTION autopoint_%d'.freeze
|
15
15
|
MSSQL_DEFAULT_RE = /\A(?:\(N?('.*')\)|\(\((-?\d+(?:\.\d+)?)\)\))\z/
|
16
|
+
|
17
|
+
include Sequel::Database::SplitAlterTable
|
16
18
|
|
17
19
|
# Whether to use N'' to quote strings, which allows unicode characters inside the
|
18
20
|
# strings. True by default for compatibility, can be set to false for a possible
|
@@ -110,39 +112,6 @@ module Sequel
|
|
110
112
|
AUTO_INCREMENT
|
111
113
|
end
|
112
114
|
|
113
|
-
# Preprocess the array of operations. If it looks like some operations depend
|
114
|
-
# on results of earlier operations and may require reloading the schema to
|
115
|
-
# work correctly, split those operations into separate lists, and between each
|
116
|
-
# list, remove the cached schema so that the later operations deal with the
|
117
|
-
# then current table schema.
|
118
|
-
def apply_alter_table(name, ops)
|
119
|
-
modified_columns = []
|
120
|
-
op_groups = [[]]
|
121
|
-
ops.each do |op|
|
122
|
-
case op[:op]
|
123
|
-
when :add_column, :set_column_type, :set_column_null
|
124
|
-
if modified_columns.include?(op[:name])
|
125
|
-
op_groups << []
|
126
|
-
else
|
127
|
-
modified_columns << op[:name]
|
128
|
-
end
|
129
|
-
when :rename_column
|
130
|
-
if modified_columns.include?(op[:name]) || modified_columns.include?(op[:new_name])
|
131
|
-
op_groups << []
|
132
|
-
end
|
133
|
-
modified_columns << op[:name] unless modified_columns.include?(op[:name])
|
134
|
-
modified_columns << op[:new_name] unless modified_columns.include?(op[:new_name])
|
135
|
-
end
|
136
|
-
op_groups.last << op
|
137
|
-
end
|
138
|
-
|
139
|
-
op_groups.each do |ops|
|
140
|
-
next if ops.empty?
|
141
|
-
alter_table_sql_list(name, ops).each{|sql| execute_ddl(sql)}
|
142
|
-
remove_cached_schema(name)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
115
|
# MSSQL specific syntax for altering tables.
|
147
116
|
def alter_table_sql(table, op)
|
148
117
|
case op[:op]
|
@@ -1,3 +1,5 @@
|
|
1
|
+
Sequel.require 'adapters/utils/split_alter_table'
|
2
|
+
|
1
3
|
module Sequel
|
2
4
|
Dataset::NON_SQL_OPTIONS << :insert_ignore
|
3
5
|
Dataset::NON_SQL_OPTIONS << :update_ignore
|
@@ -34,6 +36,8 @@ module Sequel
|
|
34
36
|
COLUMN_DEFINITION_ORDER = [:collate, :null, :default, :unique, :primary_key, :auto_increment, :references]
|
35
37
|
PRIMARY = 'PRIMARY'.freeze
|
36
38
|
MYSQL_TIMESTAMP_RE = /\ACURRENT_(?:DATE|TIMESTAMP)?\z/
|
39
|
+
|
40
|
+
include Sequel::Database::SplitAlterTable
|
37
41
|
|
38
42
|
# MySQL's cast rules are restrictive in that you can't just cast to any possible
|
39
43
|
# database type.
|
@@ -170,39 +174,6 @@ module Sequel
|
|
170
174
|
|
171
175
|
private
|
172
176
|
|
173
|
-
# Preprocess the array of operations. If it looks like some operations depend
|
174
|
-
# on results of earlier operations and may require reloading the schema to
|
175
|
-
# work correctly, split those operations into separate lists, and between each
|
176
|
-
# list, remove the cached schema so that the later operations deal with the
|
177
|
-
# then current table schema.
|
178
|
-
def apply_alter_table(name, ops)
|
179
|
-
modified_columns = []
|
180
|
-
op_groups = [[]]
|
181
|
-
ops.each do |op|
|
182
|
-
case op[:op]
|
183
|
-
when :add_column, :set_column_type, :set_column_null, :set_column_default
|
184
|
-
if modified_columns.include?(op[:name])
|
185
|
-
op_groups << []
|
186
|
-
else
|
187
|
-
modified_columns << op[:name]
|
188
|
-
end
|
189
|
-
when :rename_column
|
190
|
-
if modified_columns.include?(op[:name]) || modified_columns.include?(op[:new_name])
|
191
|
-
op_groups << []
|
192
|
-
end
|
193
|
-
modified_columns << op[:name] unless modified_columns.include?(op[:name])
|
194
|
-
modified_columns << op[:new_name] unless modified_columns.include?(op[:new_name])
|
195
|
-
end
|
196
|
-
op_groups.last << op
|
197
|
-
end
|
198
|
-
|
199
|
-
op_groups.each do |ops|
|
200
|
-
next if ops.empty?
|
201
|
-
alter_table_sql_list(name, ops).each{|sql| execute_ddl(sql)}
|
202
|
-
remove_cached_schema(name)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
177
|
# Use MySQL specific syntax for some alter table operations.
|
207
178
|
def alter_table_op_sql(table, op)
|
208
179
|
case op[:op]
|
@@ -6,6 +6,17 @@ module Sequel
|
|
6
6
|
# prepared statements and stored procedures.
|
7
7
|
module PreparedStatements
|
8
8
|
module DatabaseMethods
|
9
|
+
disconnect_errors = <<-END.split("\n").map{|l| l.strip}
|
10
|
+
Commands out of sync; you can't run this command now
|
11
|
+
Can't connect to local MySQL server through socket
|
12
|
+
MySQL server has gone away
|
13
|
+
Lost connection to MySQL server during query
|
14
|
+
This connection is still waiting for a result, try again once you have the result
|
15
|
+
closed MySQL connection
|
16
|
+
END
|
17
|
+
# Error messages for mysql and mysql2 that indicate the current connection should be disconnected
|
18
|
+
MYSQL_DATABASE_DISCONNECT_ERRORS = /\A#{Regexp.union(disconnect_errors)}/o
|
19
|
+
|
9
20
|
# Support stored procedures on MySQL
|
10
21
|
def call_sproc(name, opts={}, &block)
|
11
22
|
args = opts[:args] || []
|
@@ -369,7 +369,7 @@ module Sequel
|
|
369
369
|
# :server :: The server to which to send the NOTIFY statement, if the sharding support
|
370
370
|
# is being used.
|
371
371
|
def notify(channel, opts={})
|
372
|
-
execute_ddl("NOTIFY #{channel}#{", #{literal(opts[:payload].to_s)}" if opts[:payload]}", opts)
|
372
|
+
execute_ddl("NOTIFY #{dataset.send(:table_ref, channel)}#{", #{literal(opts[:payload].to_s)}" if opts[:payload]}", opts)
|
373
373
|
end
|
374
374
|
|
375
375
|
# Return primary key for the given table.
|
@@ -735,6 +735,7 @@ module Sequel
|
|
735
735
|
def initialize_postgres_adapter
|
736
736
|
@primary_keys = {}
|
737
737
|
@primary_key_sequences = {}
|
738
|
+
@conversion_procs = PG_TYPES.dup
|
738
739
|
reset_conversion_procs
|
739
740
|
end
|
740
741
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Sequel::Database::SplitAlterTable
|
2
|
+
private
|
3
|
+
|
4
|
+
# Preprocess the array of operations. If it looks like some operations depend
|
5
|
+
# on results of earlier operations and may require reloading the schema to
|
6
|
+
# work correctly, split those operations into separate lists, and between each
|
7
|
+
# list, remove the cached schema so that the later operations deal with the
|
8
|
+
# then current table schema.
|
9
|
+
def apply_alter_table(name, ops)
|
10
|
+
modified_columns = []
|
11
|
+
op_groups = [[]]
|
12
|
+
ops.each do |op|
|
13
|
+
case op[:op]
|
14
|
+
when :add_column, :set_column_type, :set_column_null, :set_column_default
|
15
|
+
if modified_columns.include?(op[:name])
|
16
|
+
op_groups << []
|
17
|
+
else
|
18
|
+
modified_columns << op[:name]
|
19
|
+
end
|
20
|
+
when :rename_column
|
21
|
+
if modified_columns.include?(op[:name]) || modified_columns.include?(op[:new_name])
|
22
|
+
op_groups << []
|
23
|
+
end
|
24
|
+
modified_columns << op[:name] unless modified_columns.include?(op[:name])
|
25
|
+
modified_columns << op[:new_name] unless modified_columns.include?(op[:new_name])
|
26
|
+
end
|
27
|
+
op_groups.last << op
|
28
|
+
end
|
29
|
+
|
30
|
+
op_groups.each do |ops|
|
31
|
+
next if ops.empty?
|
32
|
+
alter_table_sql_list(name, ops).each{|sql| execute_ddl(sql)}
|
33
|
+
remove_cached_schema(name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -6,7 +6,7 @@ module Sequel
|
|
6
6
|
# ---------------------
|
7
7
|
|
8
8
|
# Array of supported database adapters
|
9
|
-
ADAPTERS = %w'ado amalgalite db2 dbi do firebird ibmdb informix jdbc mock mysql mysql2 odbc openbase oracle postgres sqlite swift tinytds'.collect{|x| x.to_sym}
|
9
|
+
ADAPTERS = %w'ado amalgalite cubrid db2 dbi do firebird ibmdb informix jdbc mock mysql mysql2 odbc openbase oracle postgres sqlite swift tinytds'.collect{|x| x.to_sym}
|
10
10
|
|
11
11
|
# Whether to use the single threaded connection pool by default
|
12
12
|
@@single_threaded = false
|