activerecord-jdbc-alt-adapter 50.3.0-java
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 +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +100 -0
- data/.yardopts +4 -0
- data/CONTRIBUTING.md +50 -0
- data/Gemfile +92 -0
- data/History.md +1191 -0
- data/LICENSE.txt +26 -0
- data/README.md +240 -0
- data/RUNNING_TESTS.md +127 -0
- data/Rakefile +336 -0
- data/Rakefile.jdbc +20 -0
- data/activerecord-jdbc-adapter.gemspec +55 -0
- data/activerecord-jdbc-alt-adapter.gemspec +56 -0
- data/lib/active_record/connection_adapters/as400_adapter.rb +2 -0
- data/lib/active_record/connection_adapters/db2_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/derby_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/firebird_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/h2_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/hsqldb_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/informix_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/jdbc_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/jndi_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/mariadb_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/mssql_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1 -0
- data/lib/activerecord-jdbc-adapter.rb +1 -0
- data/lib/arel/visitors/compat.rb +60 -0
- data/lib/arel/visitors/db2.rb +137 -0
- data/lib/arel/visitors/derby.rb +112 -0
- data/lib/arel/visitors/firebird.rb +79 -0
- data/lib/arel/visitors/h2.rb +25 -0
- data/lib/arel/visitors/hsqldb.rb +32 -0
- data/lib/arel/visitors/postgresql_jdbc.rb +6 -0
- data/lib/arel/visitors/sql_server.rb +225 -0
- data/lib/arel/visitors/sql_server/ng42.rb +294 -0
- data/lib/arel/visitors/sqlserver.rb +214 -0
- data/lib/arjdbc.rb +19 -0
- data/lib/arjdbc/abstract/connection_management.rb +35 -0
- data/lib/arjdbc/abstract/core.rb +74 -0
- data/lib/arjdbc/abstract/database_statements.rb +64 -0
- data/lib/arjdbc/abstract/statement_cache.rb +58 -0
- data/lib/arjdbc/abstract/transaction_support.rb +86 -0
- data/lib/arjdbc/db2.rb +4 -0
- data/lib/arjdbc/db2/adapter.rb +789 -0
- data/lib/arjdbc/db2/as400.rb +130 -0
- data/lib/arjdbc/db2/column.rb +167 -0
- data/lib/arjdbc/db2/connection_methods.rb +44 -0
- data/lib/arjdbc/derby.rb +3 -0
- data/lib/arjdbc/derby/active_record_patch.rb +13 -0
- data/lib/arjdbc/derby/adapter.rb +540 -0
- data/lib/arjdbc/derby/connection_methods.rb +20 -0
- data/lib/arjdbc/derby/schema_creation.rb +15 -0
- data/lib/arjdbc/discover.rb +104 -0
- data/lib/arjdbc/firebird.rb +4 -0
- data/lib/arjdbc/firebird/adapter.rb +434 -0
- data/lib/arjdbc/firebird/connection_methods.rb +23 -0
- data/lib/arjdbc/h2.rb +3 -0
- data/lib/arjdbc/h2/adapter.rb +303 -0
- data/lib/arjdbc/h2/connection_methods.rb +27 -0
- data/lib/arjdbc/hsqldb.rb +3 -0
- data/lib/arjdbc/hsqldb/adapter.rb +297 -0
- data/lib/arjdbc/hsqldb/connection_methods.rb +28 -0
- data/lib/arjdbc/hsqldb/explain_support.rb +35 -0
- data/lib/arjdbc/hsqldb/schema_creation.rb +11 -0
- data/lib/arjdbc/informix.rb +5 -0
- data/lib/arjdbc/informix/adapter.rb +162 -0
- data/lib/arjdbc/informix/connection_methods.rb +9 -0
- data/lib/arjdbc/jdbc.rb +59 -0
- data/lib/arjdbc/jdbc/adapter.rb +475 -0
- data/lib/arjdbc/jdbc/adapter_require.rb +46 -0
- data/lib/arjdbc/jdbc/base_ext.rb +15 -0
- data/lib/arjdbc/jdbc/callbacks.rb +53 -0
- data/lib/arjdbc/jdbc/column.rb +97 -0
- data/lib/arjdbc/jdbc/connection.rb +14 -0
- data/lib/arjdbc/jdbc/connection_methods.rb +37 -0
- data/lib/arjdbc/jdbc/error.rb +65 -0
- data/lib/arjdbc/jdbc/extension.rb +59 -0
- data/lib/arjdbc/jdbc/java.rb +13 -0
- data/lib/arjdbc/jdbc/railtie.rb +2 -0
- data/lib/arjdbc/jdbc/rake_tasks.rb +3 -0
- data/lib/arjdbc/jdbc/serialized_attributes_helper.rb +3 -0
- data/lib/arjdbc/jdbc/type_cast.rb +166 -0
- data/lib/arjdbc/jdbc/type_converter.rb +142 -0
- data/lib/arjdbc/mssql.rb +7 -0
- data/lib/arjdbc/mssql/adapter.rb +384 -0
- data/lib/arjdbc/mssql/column.rb +29 -0
- data/lib/arjdbc/mssql/connection_methods.rb +79 -0
- data/lib/arjdbc/mssql/database_statements.rb +134 -0
- data/lib/arjdbc/mssql/errors.rb +6 -0
- data/lib/arjdbc/mssql/explain_support.rb +129 -0
- data/lib/arjdbc/mssql/extensions.rb +36 -0
- data/lib/arjdbc/mssql/limit_helpers.rb +231 -0
- data/lib/arjdbc/mssql/lock_methods.rb +77 -0
- data/lib/arjdbc/mssql/old_adapter.rb +804 -0
- data/lib/arjdbc/mssql/old_column.rb +200 -0
- data/lib/arjdbc/mssql/quoting.rb +101 -0
- data/lib/arjdbc/mssql/schema_creation.rb +31 -0
- data/lib/arjdbc/mssql/schema_definitions.rb +74 -0
- data/lib/arjdbc/mssql/schema_statements.rb +329 -0
- data/lib/arjdbc/mssql/transaction.rb +69 -0
- data/lib/arjdbc/mssql/types.rb +52 -0
- data/lib/arjdbc/mssql/types/binary_types.rb +33 -0
- data/lib/arjdbc/mssql/types/date_and_time_types.rb +134 -0
- data/lib/arjdbc/mssql/types/deprecated_types.rb +40 -0
- data/lib/arjdbc/mssql/types/numeric_types.rb +71 -0
- data/lib/arjdbc/mssql/types/string_types.rb +56 -0
- data/lib/arjdbc/mssql/utils.rb +66 -0
- data/lib/arjdbc/mysql.rb +3 -0
- data/lib/arjdbc/mysql/adapter.rb +140 -0
- data/lib/arjdbc/mysql/connection_methods.rb +166 -0
- data/lib/arjdbc/oracle/adapter.rb +863 -0
- data/lib/arjdbc/postgresql.rb +3 -0
- data/lib/arjdbc/postgresql/adapter.rb +687 -0
- data/lib/arjdbc/postgresql/base/array_decoder.rb +26 -0
- data/lib/arjdbc/postgresql/base/array_encoder.rb +25 -0
- data/lib/arjdbc/postgresql/base/array_parser.rb +95 -0
- data/lib/arjdbc/postgresql/base/pgconn.rb +11 -0
- data/lib/arjdbc/postgresql/column.rb +51 -0
- data/lib/arjdbc/postgresql/connection_methods.rb +67 -0
- data/lib/arjdbc/postgresql/name.rb +24 -0
- data/lib/arjdbc/postgresql/oid_types.rb +266 -0
- data/lib/arjdbc/railtie.rb +11 -0
- data/lib/arjdbc/sqlite3.rb +3 -0
- data/lib/arjdbc/sqlite3/adapter.rb +678 -0
- data/lib/arjdbc/sqlite3/connection_methods.rb +59 -0
- data/lib/arjdbc/sybase.rb +2 -0
- data/lib/arjdbc/sybase/adapter.rb +47 -0
- data/lib/arjdbc/tasks.rb +13 -0
- data/lib/arjdbc/tasks/database_tasks.rb +31 -0
- data/lib/arjdbc/tasks/databases.rake +48 -0
- data/lib/arjdbc/tasks/db2_database_tasks.rb +104 -0
- data/lib/arjdbc/tasks/derby_database_tasks.rb +95 -0
- data/lib/arjdbc/tasks/h2_database_tasks.rb +31 -0
- data/lib/arjdbc/tasks/hsqldb_database_tasks.rb +70 -0
- data/lib/arjdbc/tasks/jdbc_database_tasks.rb +169 -0
- data/lib/arjdbc/tasks/mssql_database_tasks.rb +46 -0
- data/lib/arjdbc/util/quoted_cache.rb +60 -0
- data/lib/arjdbc/util/serialized_attributes.rb +98 -0
- data/lib/arjdbc/util/table_copier.rb +110 -0
- data/lib/arjdbc/version.rb +3 -0
- data/lib/generators/jdbc/USAGE +9 -0
- data/lib/generators/jdbc/jdbc_generator.rb +17 -0
- data/lib/jdbc_adapter.rb +2 -0
- data/lib/jdbc_adapter/rake_tasks.rb +4 -0
- data/lib/jdbc_adapter/version.rb +4 -0
- data/pom.xml +114 -0
- data/rails_generators/jdbc_generator.rb +15 -0
- data/rails_generators/templates/config/initializers/jdbc.rb +10 -0
- data/rails_generators/templates/lib/tasks/jdbc.rake +11 -0
- data/rakelib/01-tomcat.rake +51 -0
- data/rakelib/02-test.rake +132 -0
- data/rakelib/bundler_ext.rb +11 -0
- data/rakelib/db.rake +75 -0
- data/rakelib/rails.rake +223 -0
- data/src/java/arjdbc/ArJdbcModule.java +276 -0
- data/src/java/arjdbc/db2/DB2Module.java +76 -0
- data/src/java/arjdbc/db2/DB2RubyJdbcConnection.java +126 -0
- data/src/java/arjdbc/derby/DerbyModule.java +178 -0
- data/src/java/arjdbc/derby/DerbyRubyJdbcConnection.java +152 -0
- data/src/java/arjdbc/firebird/FirebirdRubyJdbcConnection.java +174 -0
- data/src/java/arjdbc/h2/H2Module.java +50 -0
- data/src/java/arjdbc/h2/H2RubyJdbcConnection.java +85 -0
- data/src/java/arjdbc/hsqldb/HSQLDBModule.java +73 -0
- data/src/java/arjdbc/informix/InformixRubyJdbcConnection.java +75 -0
- data/src/java/arjdbc/jdbc/AdapterJavaService.java +43 -0
- data/src/java/arjdbc/jdbc/Callable.java +44 -0
- data/src/java/arjdbc/jdbc/ConnectionFactory.java +45 -0
- data/src/java/arjdbc/jdbc/DataSourceConnectionFactory.java +156 -0
- data/src/java/arjdbc/jdbc/DriverConnectionFactory.java +63 -0
- data/src/java/arjdbc/jdbc/DriverWrapper.java +119 -0
- data/src/java/arjdbc/jdbc/JdbcResult.java +130 -0
- data/src/java/arjdbc/jdbc/RubyConnectionFactory.java +61 -0
- data/src/java/arjdbc/jdbc/RubyJdbcConnection.java +3979 -0
- data/src/java/arjdbc/mssql/MSSQLModule.java +90 -0
- data/src/java/arjdbc/mssql/MSSQLRubyJdbcConnection.java +508 -0
- data/src/java/arjdbc/mysql/MySQLModule.java +152 -0
- data/src/java/arjdbc/mysql/MySQLRubyJdbcConnection.java +294 -0
- data/src/java/arjdbc/oracle/OracleModule.java +80 -0
- data/src/java/arjdbc/oracle/OracleRubyJdbcConnection.java +455 -0
- data/src/java/arjdbc/postgresql/ByteaUtils.java +157 -0
- data/src/java/arjdbc/postgresql/PgDateTimeUtils.java +52 -0
- data/src/java/arjdbc/postgresql/PostgreSQLModule.java +77 -0
- data/src/java/arjdbc/postgresql/PostgreSQLResult.java +192 -0
- data/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +948 -0
- data/src/java/arjdbc/sqlite3/SQLite3Module.java +73 -0
- data/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java +525 -0
- data/src/java/arjdbc/util/CallResultSet.java +826 -0
- data/src/java/arjdbc/util/DateTimeUtils.java +699 -0
- data/src/java/arjdbc/util/ObjectSupport.java +65 -0
- data/src/java/arjdbc/util/QuotingUtils.java +137 -0
- data/src/java/arjdbc/util/StringCache.java +63 -0
- data/src/java/arjdbc/util/StringHelper.java +145 -0
- metadata +269 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
# MSSQL specific extensions to column definitions in a table.
|
|
4
|
+
class MSSQLColumn < Column
|
|
5
|
+
def initialize(name, raw_default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil)
|
|
6
|
+
default = extract_default(raw_default)
|
|
7
|
+
|
|
8
|
+
super(name, default, sql_type_metadata, null, table_name, default_function, collation, comment: comment)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def extract_default(value)
|
|
12
|
+
# return nil if default does not match the patterns to avoid
|
|
13
|
+
# any unexpected errors.
|
|
14
|
+
return unless value =~ /^\(N?'(.*)'\)$/m || value =~ /^\(\(?(.*?)\)?\)$/
|
|
15
|
+
|
|
16
|
+
unquote_string(Regexp.last_match[1])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unquote_string(string)
|
|
20
|
+
string.to_s.gsub("''", "'")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def identity?
|
|
24
|
+
sql_type.downcase.include? 'identity'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
ArJdbc::ConnectionMethods.module_eval do
|
|
2
|
+
|
|
3
|
+
# Default connection method for MS-SQL adapter (`adapter: mssql`),
|
|
4
|
+
# uses the (open-source) jTDS driver.
|
|
5
|
+
# If you'd like to use the "official" MS's SQL-JDBC driver, it's preferable
|
|
6
|
+
# to use the {#sqlserver_connection} method (set `adapter: sqlserver`).
|
|
7
|
+
def mssql_connection(config)
|
|
8
|
+
# NOTE: this detection ain't perfect and is only meant as a temporary hack
|
|
9
|
+
# users will get a deprecation eventually to use `adapter: sqlserver` ...
|
|
10
|
+
if config[:driver] =~ /SQLServerDriver$/ || config[:url] =~ /^jdbc:sqlserver:/
|
|
11
|
+
return sqlserver_connection(config)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config[:adapter_spec] ||= ::ArJdbc::MSSQL
|
|
15
|
+
config[:adapter_class] = ActiveRecord::ConnectionAdapters::MSSQLAdapter unless config.key?(:adapter_class)
|
|
16
|
+
|
|
17
|
+
return jndi_connection(config) if jndi_config?(config)
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
require 'jdbc/jtds'
|
|
21
|
+
# NOTE: the adapter has only support for working with the
|
|
22
|
+
# open-source jTDS driver (won't work with MS's driver) !
|
|
23
|
+
::Jdbc::JTDS.load_driver(:require) if defined?(::Jdbc::JTDS.load_driver)
|
|
24
|
+
rescue LoadError => e # assuming driver.jar is on the class-path
|
|
25
|
+
raise e unless e.message.to_s.index('no such file to load')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
config[:host] ||= 'localhost'
|
|
29
|
+
config[:port] ||= 1433
|
|
30
|
+
config[:driver] ||= defined?(::Jdbc::JTDS.driver_name) ? ::Jdbc::JTDS.driver_name : 'net.sourceforge.jtds.jdbc.Driver'
|
|
31
|
+
config[:connection_alive_sql] ||= 'SELECT 1'
|
|
32
|
+
|
|
33
|
+
config[:url] ||= begin
|
|
34
|
+
url = "jdbc:jtds:sqlserver://#{config[:host]}:#{config[:port]}/#{config[:database]}"
|
|
35
|
+
# Instance is often a preferrable alternative to port when dynamic ports are used.
|
|
36
|
+
# If instance is specified then port is essentially ignored.
|
|
37
|
+
url << ";instance=#{config[:instance]}" if config[:instance]
|
|
38
|
+
# This will enable windows domain-based authentication and will require the JTDS native libraries be available.
|
|
39
|
+
url << ";domain=#{config[:domain]}" if config[:domain]
|
|
40
|
+
# AppName is shown in sql server as additional information against the connection.
|
|
41
|
+
url << ";appname=#{config[:appname]}" if config[:appname]
|
|
42
|
+
url
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
unless config[:domain]
|
|
46
|
+
config[:username] ||= 'sa'
|
|
47
|
+
config[:password] ||= ''
|
|
48
|
+
end
|
|
49
|
+
jdbc_connection(config)
|
|
50
|
+
end
|
|
51
|
+
alias_method :jdbcmssql_connection, :mssql_connection
|
|
52
|
+
|
|
53
|
+
# @note Assumes SQLServer SQL-JDBC driver on the class-path.
|
|
54
|
+
def sqlserver_connection(config)
|
|
55
|
+
config[:adapter_spec] ||= ::ArJdbc::MSSQL
|
|
56
|
+
config[:adapter_class] = ActiveRecord::ConnectionAdapters::MSSQLAdapter unless config.key?(:adapter_class)
|
|
57
|
+
|
|
58
|
+
return jndi_connection(config) if jndi_config?(config)
|
|
59
|
+
|
|
60
|
+
config[:host] ||= 'localhost'
|
|
61
|
+
config[:driver] ||= 'com.microsoft.sqlserver.jdbc.SQLServerDriver'
|
|
62
|
+
config[:connection_alive_sql] ||= 'SELECT 1'
|
|
63
|
+
|
|
64
|
+
config[:url] ||= begin
|
|
65
|
+
url = "jdbc:sqlserver://#{config[:host]}"
|
|
66
|
+
url << ( config[:port] ? ":#{config[:port]};" : ';' )
|
|
67
|
+
url << "databaseName=#{config[:database]};" if config[:database]
|
|
68
|
+
url << "instanceName=#{config[:instance]};" if config[:instance]
|
|
69
|
+
app = config[:appname] || config[:application]
|
|
70
|
+
url << "applicationName=#{app};" if app
|
|
71
|
+
isc = config[:integrated_security] # Win only - needs sqljdbc_auth.dll
|
|
72
|
+
url << "integratedSecurity=#{isc};" unless isc.nil?
|
|
73
|
+
url
|
|
74
|
+
end
|
|
75
|
+
jdbc_connection(config)
|
|
76
|
+
end
|
|
77
|
+
alias_method :jdbcsqlserver_connection, :sqlserver_connection
|
|
78
|
+
|
|
79
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module MSSQL
|
|
4
|
+
module DatabaseStatements
|
|
5
|
+
|
|
6
|
+
def exec_proc(proc_name, *variables)
|
|
7
|
+
vars =
|
|
8
|
+
if variables.any? && variables.first.is_a?(Hash)
|
|
9
|
+
variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
|
|
10
|
+
else
|
|
11
|
+
variables.map { |v| quote(v) }
|
|
12
|
+
end.join(', ')
|
|
13
|
+
sql = "EXEC #{proc_name} #{vars}".strip
|
|
14
|
+
log(sql, 'Execute Procedure') do
|
|
15
|
+
result = @connection.execute_query_raw(sql)
|
|
16
|
+
result.map! do |row|
|
|
17
|
+
row = row.is_a?(Hash) ? row.with_indifferent_access : row
|
|
18
|
+
yield(row) if block_given?
|
|
19
|
+
row
|
|
20
|
+
end
|
|
21
|
+
result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
alias_method :execute_procedure, :exec_proc # AR-SQLServer-Adapter naming
|
|
25
|
+
|
|
26
|
+
def execute(sql, name = nil)
|
|
27
|
+
# with identity insert on block
|
|
28
|
+
if insert_sql?(sql)
|
|
29
|
+
table_name_for_identity_insert = identity_insert_table_name(sql)
|
|
30
|
+
|
|
31
|
+
if table_name_for_identity_insert
|
|
32
|
+
with_identity_insert_enabled(table_name_for_identity_insert) do
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
|
|
44
|
+
table_name_for_identity_insert = identity_insert_table_name(sql)
|
|
45
|
+
|
|
46
|
+
if table_name_for_identity_insert
|
|
47
|
+
with_identity_insert_enabled(table_name_for_identity_insert) do
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Implements the truncate method.
|
|
56
|
+
def truncate(table_name, name = nil)
|
|
57
|
+
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Not a rails method, own method to test different isolation
|
|
61
|
+
# levels supported by the mssql adapter.
|
|
62
|
+
def supports_transaction_isolation_level?(level)
|
|
63
|
+
@connection.supports_transaction_isolation?(level)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def transaction_isolation=(value)
|
|
67
|
+
@connection.set_transaction_isolation(value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def transaction_isolation
|
|
71
|
+
@connection.get_transaction_isolation
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def insert_sql?(sql)
|
|
77
|
+
!(sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def identity_insert_table_name(sql)
|
|
81
|
+
table_name = get_table_name(sql)
|
|
82
|
+
id_column = identity_column_name(table_name)
|
|
83
|
+
if id_column && sql.strip =~ /INSERT INTO [^ ]+ ?\((.+?)\)/i
|
|
84
|
+
insert_columns = $1.split(/, */).map{|w| ArJdbc::MSSQL::Utils.unquote_column_name(w)}
|
|
85
|
+
return table_name if insert_columns.include?(id_column)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def identity_column_name(table_name)
|
|
90
|
+
for column in columns(table_name)
|
|
91
|
+
return column.name if column.identity?
|
|
92
|
+
end
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Turns IDENTITY_INSERT ON for table during execution of the block
|
|
97
|
+
# N.B. This sets the state of IDENTITY_INSERT to OFF after the
|
|
98
|
+
# block has been executed without regard to its previous state
|
|
99
|
+
def with_identity_insert_enabled(table_name)
|
|
100
|
+
set_identity_insert(table_name, true)
|
|
101
|
+
yield
|
|
102
|
+
ensure
|
|
103
|
+
set_identity_insert(table_name, false)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_identity_insert(table_name, enable = true)
|
|
107
|
+
if enable
|
|
108
|
+
execute("SET IDENTITY_INSERT #{quote_table_name(table_name)} ON")
|
|
109
|
+
else
|
|
110
|
+
execute("SET IDENTITY_INSERT #{quote_table_name(table_name)} OFF")
|
|
111
|
+
end
|
|
112
|
+
rescue Exception => e
|
|
113
|
+
raise ActiveRecord::ActiveRecordError, "IDENTITY_INSERT could not be turned" +
|
|
114
|
+
" #{enable ? 'ON' : 'OFF'} for table #{table_name} due : #{e.inspect}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def get_table_name(sql, qualified = nil)
|
|
118
|
+
if sql =~ TABLE_NAME_INSERT_UPDATE
|
|
119
|
+
tn = $2 || $3
|
|
120
|
+
qualified ? tn : ArJdbc::MSSQL::Utils.unqualify_table_name(tn)
|
|
121
|
+
elsif sql =~ TABLE_NAME_FROM
|
|
122
|
+
qualified ? $1 : ArJdbc::MSSQL::Utils.unqualify_table_name($1)
|
|
123
|
+
else
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
TABLE_NAME_INSERT_UPDATE = /^\s*(INSERT|EXEC sp_executesql N'INSERT)(?:\s+INTO)?\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
|
129
|
+
|
|
130
|
+
TABLE_NAME_FROM = /\bFROM\s+([^\(\)\s,]+)\s*/i
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'active_support/core_ext/string'
|
|
2
|
+
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
module ConnectionAdapters
|
|
5
|
+
module MSSQL
|
|
6
|
+
# NOTE: the execution plan (explain) is a estimated only for prepared
|
|
7
|
+
# statements similar the jTDS used to provide. The mssql-jdbc driver
|
|
8
|
+
# does not supports explain from prepared statements.
|
|
9
|
+
# more in: https://github.com/Microsoft/mssql-jdbc/issues/778
|
|
10
|
+
#
|
|
11
|
+
module ExplainSupport
|
|
12
|
+
DISABLED = Java::JavaLang::Boolean.getBoolean('arjdbc.mssql.explain_support.disabled')
|
|
13
|
+
|
|
14
|
+
def supports_explain?
|
|
15
|
+
!DISABLED
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def explain(arel, binds = [])
|
|
19
|
+
return if DISABLED
|
|
20
|
+
|
|
21
|
+
# sql = to_sql(arel, binds)
|
|
22
|
+
# result = with_showplan_on { exec_query(sql, 'EXPLAIN', binds) }
|
|
23
|
+
sql = interpolate_sql_statement(arel, binds)
|
|
24
|
+
result = with_showplan_on do
|
|
25
|
+
exec_query(sql, 'EXPLAIN', [])
|
|
26
|
+
end
|
|
27
|
+
PrinterTable.new(result).pp
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
# converting the prepared statements to sql
|
|
33
|
+
def interpolate_sql_statement(arel, binds)
|
|
34
|
+
return arel if binds.empty?
|
|
35
|
+
|
|
36
|
+
sql = if arel.respond_to?(:to_sql)
|
|
37
|
+
arel.to_sql
|
|
38
|
+
else
|
|
39
|
+
arel
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
binds.each do |bind|
|
|
43
|
+
value = quote(bind.value_for_database)
|
|
44
|
+
sql.sub!('?', value)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sql
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_showplan_on
|
|
51
|
+
set_showplan_option(true)
|
|
52
|
+
yield
|
|
53
|
+
ensure
|
|
54
|
+
set_showplan_option(false)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def set_showplan_option(enable = true)
|
|
58
|
+
option = 'SHOWPLAN_ALL'
|
|
59
|
+
execute "SET #{option} #{enable ? 'ON' : 'OFF'}"
|
|
60
|
+
rescue Exception => e
|
|
61
|
+
raise ActiveRecord::ActiveRecordError, "#{option} could not be turned" +
|
|
62
|
+
" #{enable ? 'ON' : 'OFF'} (check SHOWPLAN permissions) due : #{e.inspect}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @private
|
|
66
|
+
class PrinterTable
|
|
67
|
+
|
|
68
|
+
cattr_accessor :max_column_width, :cell_padding
|
|
69
|
+
self.max_column_width = 50
|
|
70
|
+
self.cell_padding = 1
|
|
71
|
+
|
|
72
|
+
attr_reader :result
|
|
73
|
+
|
|
74
|
+
def initialize(result)
|
|
75
|
+
@result = result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def pp
|
|
79
|
+
@widths = compute_column_widths
|
|
80
|
+
@separator = build_separator
|
|
81
|
+
pp = []
|
|
82
|
+
pp << @separator
|
|
83
|
+
pp << build_cells(result.columns)
|
|
84
|
+
pp << @separator
|
|
85
|
+
result.rows.each do |row|
|
|
86
|
+
pp << build_cells(row)
|
|
87
|
+
end
|
|
88
|
+
pp << @separator
|
|
89
|
+
pp.join("\n") << "\n"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def compute_column_widths
|
|
95
|
+
[].tap do |computed_widths|
|
|
96
|
+
result.columns.each_with_index do |column, i|
|
|
97
|
+
cells_in_column = [column] + result.rows.map { |r| cast_item(r[i]) }
|
|
98
|
+
computed_width = cells_in_column.map(&:length).max
|
|
99
|
+
final_width = computed_width > max_column_width ? max_column_width : computed_width
|
|
100
|
+
computed_widths << final_width
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_separator
|
|
106
|
+
'+' << @widths.map {|w| '-' * (w + (cell_padding * 2))}.join('+') << '+'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_cells(items)
|
|
110
|
+
cells = []
|
|
111
|
+
items.each_with_index do |item, i|
|
|
112
|
+
cells << cast_item(item).ljust(@widths[i])
|
|
113
|
+
end
|
|
114
|
+
"| #{cells.join(' | ')} |"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def cast_item(item)
|
|
118
|
+
case item
|
|
119
|
+
when NilClass then 'NULL'
|
|
120
|
+
when Float then item.to_s.to(9)
|
|
121
|
+
else item.to_s.truncate(max_column_width)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# This file contains extensions, overrides, and monkey patches to core parts
|
|
2
|
+
# of active record to allow SQL Server work properly.
|
|
3
|
+
#
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
module ConnectionAdapters
|
|
6
|
+
module MSSQL
|
|
7
|
+
module AttributeMethods
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# Overrides the original attributes_for_update merthod to reject
|
|
12
|
+
# primary keys because SQL Server does not allow updates
|
|
13
|
+
# of identity columns.
|
|
14
|
+
# NOTE: rails 4.1 used to reject primary keys but later changes broke
|
|
15
|
+
# this behaviour, even the current comments for that method says that
|
|
16
|
+
# it rejects primary key but it doesn't (maybe a rails bug?)
|
|
17
|
+
def attributes_for_update(attribute_names)
|
|
18
|
+
attribute_names.reject do |name|
|
|
19
|
+
# It seems is only required to check if column in identity or not.
|
|
20
|
+
# This allows to update rails custom primary keys
|
|
21
|
+
next true if readonly_attribute?(name)
|
|
22
|
+
|
|
23
|
+
column = self.class.columns_hash[name]
|
|
24
|
+
column && column.identity?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ActiveRecord
|
|
33
|
+
class Base
|
|
34
|
+
include ActiveRecord::ConnectionAdapters::MSSQL::AttributeMethods
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
module ArJdbc
|
|
2
|
+
module MSSQL
|
|
3
|
+
module LimitHelpers
|
|
4
|
+
|
|
5
|
+
# @private
|
|
6
|
+
FIND_SELECT = /\b(SELECT(\s+DISTINCT)?)\b(.*)/mi
|
|
7
|
+
# @private
|
|
8
|
+
FIND_AGGREGATE_FUNCTION = /(AVG|COUNT|COUNT_BIG|MAX|MIN|SUM|STDDEV|STDEVP|VAR|VARP)\(/i
|
|
9
|
+
|
|
10
|
+
# @private
|
|
11
|
+
module SqlServerReplaceLimitOffset
|
|
12
|
+
|
|
13
|
+
GROUP_BY = 'GROUP BY'
|
|
14
|
+
ORDER_BY = 'ORDER BY'
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def replace_limit_offset!(sql, limit, offset, order)
|
|
19
|
+
offset ||= 0
|
|
20
|
+
|
|
21
|
+
if match = FIND_SELECT.match(sql)
|
|
22
|
+
select, distinct, rest_of_query = match[1], match[2], match[3]
|
|
23
|
+
rest_of_query.strip!
|
|
24
|
+
end
|
|
25
|
+
rest_of_query[0] = '*' if rest_of_query[0...1] == '1' && rest_of_query !~ /1 AS/i
|
|
26
|
+
if rest_of_query[0...1] == '*'
|
|
27
|
+
from_table = Utils.get_table_name(rest_of_query, true)
|
|
28
|
+
rest_of_query = "#{from_table}.#{rest_of_query}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Ensure correct queries if the rest_of_query contains a 'GROUP BY'. Otherwise the following error occurs:
|
|
32
|
+
# ActiveRecord::StatementInvalid: ActiveRecord::JDBCError: Column 'users.id' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
|
|
33
|
+
# SELECT t.* FROM ( SELECT ROW_NUMBER() OVER(ORDER BY users.id) AS _row_num, [users].[lft], COUNT([users].[lft]) FROM [users] GROUP BY [users].[lft] HAVING COUNT([users].[lft]) > 1 ) AS t WHERE t._row_num BETWEEN 1 AND 1
|
|
34
|
+
if i = ( rest_of_query.rindex(GROUP_BY) || rest_of_query.rindex('group by') )
|
|
35
|
+
# Do not catch 'GROUP BY' statements from sub-selects, indicated
|
|
36
|
+
# by more closing than opening brackets after the last group by.
|
|
37
|
+
rest_after_last_group_by = rest_of_query[i..-1]
|
|
38
|
+
opening_brackets_count = rest_after_last_group_by.count('(')
|
|
39
|
+
closing_brackets_count = rest_after_last_group_by.count(')')
|
|
40
|
+
|
|
41
|
+
if opening_brackets_count == closing_brackets_count
|
|
42
|
+
order_start = order.strip[0, 8]; order_start.upcase!
|
|
43
|
+
if order_start == ORDER_BY && order.match(FIND_AGGREGATE_FUNCTION)
|
|
44
|
+
# do nothing
|
|
45
|
+
elsif order.count(',') == 0
|
|
46
|
+
order.gsub!(/ORDER +BY +([^\s]+)(\s+ASC|\s+DESC)?/i, 'ORDER BY MIN(\1)\2')
|
|
47
|
+
else
|
|
48
|
+
raise("can not handle multiple order conditions (#{order.inspect}) in #{sql.inspect}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if distinct # select =~ /DISTINCT/i
|
|
54
|
+
order = order.gsub(/(\[[a-z0-9_]+\]|[a-z0-9_]+)\./, 't.')
|
|
55
|
+
new_sql = "SELECT t.* FROM "
|
|
56
|
+
new_sql << "( SELECT ROW_NUMBER() OVER(#{order}) AS _row_num, t.* FROM (#{select} #{rest_of_query}) AS t ) AS t"
|
|
57
|
+
append_limit_row_num_clause(new_sql, limit, offset)
|
|
58
|
+
else
|
|
59
|
+
select_columns_before_from = rest_of_query.gsub(/FROM.*/, '').strip
|
|
60
|
+
only_one_column = !select_columns_before_from.include?(',')
|
|
61
|
+
only_one_id_column = only_one_column && (select_columns_before_from.ends_with?('.id') || select_columns_before_from.ends_with?('.[id]'))
|
|
62
|
+
|
|
63
|
+
if only_one_id_column
|
|
64
|
+
# If there's only one id column a subquery will be created which only contains this column
|
|
65
|
+
new_sql = "#{select} t.id FROM "
|
|
66
|
+
else
|
|
67
|
+
# All selected columns are used
|
|
68
|
+
new_sql = "#{select} t.* FROM "
|
|
69
|
+
end
|
|
70
|
+
new_sql << "( SELECT ROW_NUMBER() OVER(#{order}) AS _row_num, #{rest_of_query} ) AS t"
|
|
71
|
+
append_limit_row_num_clause(new_sql, limit, offset)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sql.replace new_sql
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def append_limit_row_num_clause(sql, limit, offset)
|
|
78
|
+
if limit
|
|
79
|
+
start_row = offset + 1; end_row = offset + limit.to_i
|
|
80
|
+
sql << " WHERE t._row_num BETWEEN #{start_row} AND #{end_row}"
|
|
81
|
+
else
|
|
82
|
+
sql << " WHERE t._row_num > #{offset}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @private
|
|
89
|
+
module SqlServer2000ReplaceLimitOffset
|
|
90
|
+
|
|
91
|
+
module_function
|
|
92
|
+
|
|
93
|
+
def replace_limit_offset!(sql, limit, offset, order)
|
|
94
|
+
if limit
|
|
95
|
+
offset ||= 0
|
|
96
|
+
start_row = offset + 1
|
|
97
|
+
end_row = offset + limit.to_i
|
|
98
|
+
|
|
99
|
+
if match = FIND_SELECT.match(sql)
|
|
100
|
+
select, distinct, rest_of_query = match[1], match[2], match[3]
|
|
101
|
+
end
|
|
102
|
+
#need the table name for avoiding amiguity
|
|
103
|
+
table_name = Utils.get_table_name(sql, true)
|
|
104
|
+
primary_key = get_primary_key(order, table_name)
|
|
105
|
+
|
|
106
|
+
#I am not sure this will cover all bases. but all the tests pass
|
|
107
|
+
if order[/ORDER/].nil?
|
|
108
|
+
new_order = "ORDER BY #{order}, [#{table_name}].[#{primary_key}]" if order.index("#{table_name}.#{primary_key}").nil?
|
|
109
|
+
else
|
|
110
|
+
new_order ||= order
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if (start_row == 1) && (end_row ==1)
|
|
114
|
+
new_sql = "#{select} TOP 1 #{rest_of_query} #{new_order}"
|
|
115
|
+
sql.replace(new_sql)
|
|
116
|
+
else
|
|
117
|
+
# We are in deep trouble here. SQL Server does not have any kind of OFFSET build in.
|
|
118
|
+
# Only remaining solution is adding a where condition to be sure that the ID is not in SELECT TOP OFFSET FROM SAME_QUERY.
|
|
119
|
+
# To do so we need to extract each part of the query to insert our additional condition in the right place.
|
|
120
|
+
query_without_select = rest_of_query[/FROM/i=~ rest_of_query.. -1]
|
|
121
|
+
additional_condition = "#{table_name}.#{primary_key} NOT IN (#{select} TOP #{offset} #{table_name}.#{primary_key} #{query_without_select} #{new_order})"
|
|
122
|
+
|
|
123
|
+
# Extract the different parts of the query
|
|
124
|
+
having, group_by, where, from, selection = split_sql(rest_of_query, /having/i, /group by/i, /where/i, /from/i)
|
|
125
|
+
|
|
126
|
+
# Update the where part to add our additional condition
|
|
127
|
+
if where.blank?
|
|
128
|
+
where = "WHERE #{additional_condition}"
|
|
129
|
+
else
|
|
130
|
+
where = "#{where} AND #{additional_condition}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Replace the query to be our new customized query
|
|
134
|
+
sql.replace("#{select} TOP #{limit} #{selection} #{from} #{where} #{group_by} #{having} #{new_order}")
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
sql
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Split the rest_of_query into chunks based on regexs (applied from end of string to the beginning)
|
|
141
|
+
# The result is an array of regexs.size+1 elements (the last one being the remaining once everything was chopped away)
|
|
142
|
+
def split_sql(rest_of_query, *regexs)
|
|
143
|
+
results = Array.new
|
|
144
|
+
|
|
145
|
+
regexs.each do |regex|
|
|
146
|
+
if position = (regex =~ rest_of_query)
|
|
147
|
+
# Extract the matched string and chop the rest_of_query
|
|
148
|
+
matched = rest_of_query[position..-1]
|
|
149
|
+
rest_of_query = rest_of_query[0...position]
|
|
150
|
+
else
|
|
151
|
+
matched = nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
results << matched
|
|
155
|
+
end
|
|
156
|
+
results << rest_of_query
|
|
157
|
+
|
|
158
|
+
results
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def get_primary_key(order, table_name) # table_name might be quoted
|
|
162
|
+
if order =~ /(\w*id\w*)/i
|
|
163
|
+
$1
|
|
164
|
+
else
|
|
165
|
+
unquoted_name = Utils.unquote_table_name(table_name)
|
|
166
|
+
model = descendants.find { |m| m.table_name == table_name || m.table_name == unquoted_name }
|
|
167
|
+
model ? model.primary_key : 'id'
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
if ActiveRecord::VERSION::MAJOR >= 3
|
|
174
|
+
def descendants; ::ActiveRecord::Base.descendants; end
|
|
175
|
+
else
|
|
176
|
+
def descendants; ::ActiveRecord::Base.send(:subclasses) end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
if ::ActiveRecord::VERSION::MAJOR < 3
|
|
184
|
+
|
|
185
|
+
def setup_limit_offset!(version = nil)
|
|
186
|
+
if version.to_s == '2000' || sqlserver_2000?
|
|
187
|
+
extend SqlServer2000AddLimitOffset
|
|
188
|
+
else
|
|
189
|
+
extend SqlServerAddLimitOffset
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
else
|
|
194
|
+
|
|
195
|
+
def setup_limit_offset!(version = nil); end
|
|
196
|
+
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# @private
|
|
200
|
+
module SqlServerAddLimitOffset
|
|
201
|
+
|
|
202
|
+
# @note Only needed with (non-AREL) ActiveRecord **2.3**.
|
|
203
|
+
# @see Arel::Visitors::SQLServer
|
|
204
|
+
def add_limit_offset!(sql, options)
|
|
205
|
+
if options[:limit]
|
|
206
|
+
order = "ORDER BY #{options[:order] || determine_order_clause(sql)}"
|
|
207
|
+
sql.sub!(/ ORDER BY.*$/i, '')
|
|
208
|
+
SqlServerReplaceLimitOffset.replace_limit_offset!(sql, options[:limit], options[:offset], order)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
end if ::ActiveRecord::VERSION::MAJOR < 3
|
|
213
|
+
|
|
214
|
+
# @private
|
|
215
|
+
module SqlServer2000AddLimitOffset
|
|
216
|
+
|
|
217
|
+
# @note Only needed with (non-AREL) ActiveRecord **2.3**.
|
|
218
|
+
# @see Arel::Visitors::SQLServer
|
|
219
|
+
def add_limit_offset!(sql, options)
|
|
220
|
+
if options[:limit]
|
|
221
|
+
order = "ORDER BY #{options[:order] || determine_order_clause(sql)}"
|
|
222
|
+
sql.sub!(/ ORDER BY.*$/i, '')
|
|
223
|
+
SqlServer2000ReplaceLimitOffset.replace_limit_offset!(sql, options[:limit], options[:offset], order)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
end if ::ActiveRecord::VERSION::MAJOR < 3
|
|
228
|
+
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|