require 'base64' require 'active_record' require 'arel_sqlserver' require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/sqlserver/core_ext/active_record' require 'active_record/connection_adapters/sqlserver/core_ext/explain' require 'active_record/connection_adapters/sqlserver/core_ext/explain_subscriber' require 'active_record/connection_adapters/sqlserver/core_ext/attribute_methods' require 'active_record/connection_adapters/sqlserver/version' require 'active_record/connection_adapters/sqlserver/type' require 'active_record/connection_adapters/sqlserver/database_limits' require 'active_record/connection_adapters/sqlserver/database_statements' require 'active_record/connection_adapters/sqlserver/database_tasks' require 'active_record/connection_adapters/sqlserver/transaction' require 'active_record/connection_adapters/sqlserver/errors' require 'active_record/connection_adapters/sqlserver/schema_cache' require 'active_record/connection_adapters/sqlserver/schema_creation' require 'active_record/connection_adapters/sqlserver/schema_statements' require 'active_record/connection_adapters/sqlserver/showplan' require 'active_record/connection_adapters/sqlserver/table_definition' require 'active_record/connection_adapters/sqlserver/quoting' require 'active_record/connection_adapters/sqlserver/utils' require 'active_record/sqlserver_base' require 'active_record/connection_adapters/sqlserver_column' require 'active_record/tasks/sqlserver_database_tasks' module ActiveRecord module ConnectionAdapters class SQLServerAdapter < AbstractAdapter include SQLServer::Version, SQLServer::Quoting, SQLServer::DatabaseStatements, SQLServer::Showplan, SQLServer::SchemaStatements, SQLServer::DatabaseLimits, SQLServer::DatabaseTasks ADAPTER_NAME = 'SQLServer'.freeze attr_reader :spid cattr_accessor :cs_equality_operator, instance_accessor: false cattr_accessor :use_output_inserted, instance_accessor: false cattr_accessor :lowercase_schema_reflection, :showplan_option self.cs_equality_operator = 'COLLATE Latin1_General_CS_AS_WS' self.use_output_inserted = true def initialize(connection, logger, pool, config) super(connection, logger, pool) # AbstractAdapter Responsibility @schema_cache = SQLServer::SchemaCache.new self @visitor = Arel::Visitors::SQLServer.new self @prepared_statements = true # Our Responsibility @connection_options = config connect @sqlserver_azure = !!(select_value('SELECT @@version', 'SCHEMA') =~ /Azure/i) initialize_dateformatter use_database end # === Abstract Adapter ========================================== # def valid_type?(type) !native_database_types[type].nil? end def schema_creation SQLServer::SchemaCreation.new self end def adapter_name ADAPTER_NAME end def supports_migrations? true end def supports_primary_key? true end def supports_count_distinct? true end def supports_ddl_transactions? true end def supports_bulk_alter? false end def supports_index_sort_order? true end def supports_partial_index? true end def supports_explain? true end def supports_transaction_isolation? true end def supports_views? true end def supports_foreign_keys? true end def disable_referential_integrity tables = tables_with_referential_integrity tables.each { |t| do_execute "ALTER TABLE #{t} NOCHECK CONSTRAINT ALL" } yield ensure tables.each { |t| do_execute "ALTER TABLE #{t} CHECK CONSTRAINT ALL" } end # === Abstract Adapter (Connection Management) ================== # def active? return false unless @connection raw_connection_do 'SELECT 1' true rescue TinyTds::Error, ODBC::Error false end def reconnect! super disconnect! connect end def disconnect! super @spid = nil case @connection_options[:mode] when :dblib @connection.close rescue nil when :odbc @connection.disconnect rescue nil end @connection = nil end def reset! reset_transaction do_execute 'IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION' end # === Abstract Adapter (Misc Support) =========================== # def tables_with_referential_integrity schemas_and_tables = select_rows <<-SQL.strip_heredoc SELECT s.name, o.name FROM sys.foreign_keys i INNER JOIN sys.objects o ON i.parent_object_id = o.OBJECT_ID INNER JOIN sys.schemas s ON o.schema_id = s.schema_id SQL schemas_and_tables.map do |schema_table| schema, table = schema_table "#{SQLServer::Utils.quoted_raw(schema)}.#{SQLServer::Utils.quoted_raw(table)}" end end def pk_and_sequence_for(table_name) pk = primary_key(table_name) pk ? [pk, nil] : nil end def primary_key(table_name) schema_cache.columns(table_name).find(&:is_primary?).try(:name) || identity_column(table_name).try(:name) end # === SQLServer Specific (DB Reflection) ======================== # def sqlserver? true end def sqlserver_azure? @sqlserver_azure end def database_prefix_remote_server? return false if database_prefix.blank? name = SQLServer::Utils.extract_identifiers(database_prefix) name.fully_qualified? && name.object.blank? end def database_prefix @connection_options[:database_prefix] end def version self.class::VERSION end def inspect "#<#{self.class} version: #{version}, mode: #{@connection_options[:mode]}, azure: #{sqlserver_azure?.inspect}>" end protected # === Abstract Adapter (Misc Support) =========================== # def initialize_type_map(m) m.register_type %r{.*}, SQLServer::Type::UnicodeString.new # Exact Numerics register_class_with_limit m, 'bigint(8)', SQLServer::Type::BigInteger m.alias_type 'bigint', 'bigint(8)' register_class_with_limit m, 'int(4)', SQLServer::Type::Integer m.alias_type 'integer', 'int(4)' m.alias_type 'int', 'int(4)' register_class_with_limit m, 'smallint(2)', SQLServer::Type::SmallInteger m.alias_type 'smallint', 'smallint(2)' register_class_with_limit m, 'tinyint(1)', SQLServer::Type::TinyInteger m.alias_type 'tinyint', 'tinyint(1)' m.register_type 'bit', SQLServer::Type::Boolean.new m.register_type %r{\Adecimal}i do |sql_type| scale = extract_scale(sql_type) precision = extract_precision(sql_type) SQLServer::Type::Decimal.new precision: precision, scale: scale end m.alias_type %r{\Anumeric}i, 'decimal' m.register_type 'money', SQLServer::Type::Money.new m.register_type 'smallmoney', SQLServer::Type::SmallMoney.new # Approximate Numerics m.register_type 'float', SQLServer::Type::Float.new m.register_type 'real', SQLServer::Type::Real.new # Date and Time m.register_type 'date', SQLServer::Type::Date.new m.register_type 'datetime', SQLServer::Type::DateTime.new m.register_type %r{\Adatetime2}i do |sql_type| precision = extract_precision(sql_type) SQLServer::Type::DateTime2.new precision: precision end m.register_type %r{\Adatetimeoffset}i do |sql_type| precision = extract_precision(sql_type) SQLServer::Type::DateTimeOffset.new precision: precision end m.register_type 'smalldatetime', SQLServer::Type::SmallDateTime.new m.register_type %r{\Atime}i do |sql_type| scale = extract_scale(sql_type) precision = extract_precision(sql_type) SQLServer::Type::Time.new precision: precision end # Character Strings register_class_with_limit m, %r{\Achar}i, SQLServer::Type::Char register_class_with_limit m, %r{\Avarchar}i, SQLServer::Type::Varchar m.register_type 'varchar(max)', SQLServer::Type::VarcharMax.new m.register_type 'text', SQLServer::Type::Text.new # Unicode Character Strings register_class_with_limit m, %r{\Anchar}i, SQLServer::Type::UnicodeChar register_class_with_limit m, %r{\Anvarchar}i, SQLServer::Type::UnicodeVarchar m.alias_type 'string', 'nvarchar(4000)' m.register_type 'nvarchar(max)', SQLServer::Type::UnicodeVarcharMax.new m.register_type 'ntext', SQLServer::Type::UnicodeText.new # Binary Strings register_class_with_limit m, %r{\Abinary}i, SQLServer::Type::Binary register_class_with_limit m, %r{\Avarbinary}i, SQLServer::Type::Varbinary m.register_type 'varbinary(max)', SQLServer::Type::VarbinaryMax.new # Other Data Types m.register_type 'uniqueidentifier', SQLServer::Type::Uuid.new m.register_type 'timestamp', SQLServer::Type::Timestamp.new end def translate_exception(e, message) case message when /(cannot insert duplicate key .* with unique index) | (violation of unique key constraint)/i RecordNotUnique.new(message, e) when /conflicted with the foreign key constraint/i InvalidForeignKey.new(message, e) when /has been chosen as the deadlock victim/i DeadlockVictim.new(message, e) when /database .* does not exist/i NoDatabaseError.new(message, e) else super end end # === SQLServer Specific (Connection Management) ================ # def connect config = @connection_options @connection = case config[:mode] when :dblib dblib_connect(config) when :odbc odbc_connect(config) end @spid = _raw_select('SELECT @@SPID', fetch: :rows).first.first configure_connection end def dblib_connect(config) TinyTds::Client.new( dataserver: config[:dataserver], host: config[:host], port: config[:port], username: config[:username], password: config[:password], database: config[:database], tds_version: config[:tds_version], appname: config_appname(config), login_timeout: config_login_timeout(config), timeout: config_timeout(config), encoding: config_encoding(config), azure: config[:azure] ).tap do |client| if config[:azure] client.execute('SET ANSI_NULLS ON').do client.execute('SET CURSOR_CLOSE_ON_COMMIT OFF').do client.execute('SET ANSI_NULL_DFLT_ON ON').do client.execute('SET IMPLICIT_TRANSACTIONS OFF').do client.execute('SET ANSI_PADDING ON').do client.execute('SET QUOTED_IDENTIFIER ON').do client.execute('SET ANSI_WARNINGS ON').do else client.execute('SET ANSI_DEFAULTS ON').do client.execute('SET CURSOR_CLOSE_ON_COMMIT OFF').do client.execute('SET IMPLICIT_TRANSACTIONS OFF').do end client.execute('SET TEXTSIZE 2147483647').do client.execute('SET CONCAT_NULL_YIELDS_NULL ON').do end end def odbc_connect(config) if config[:dsn].include?(';') driver = ODBC::Driver.new.tap do |d| d.name = config[:dsn_name] || 'Driver1' d.attrs = config[:dsn].split(';').map { |atr| atr.split('=') }.reject { |kv| kv.size != 2 }.reduce({}) { |a, e| k, v = e ; a[k] = v ; a } end ODBC::Database.new.drvconnect(driver) else ODBC.connect config[:dsn], config[:username], config[:password] end.tap do |c| begin c.use_time = true c.use_utc = ActiveRecord::Base.default_timezone == :utc rescue Exception warn 'Ruby ODBC v0.99992 or higher is required.' end end end def config_appname(config) config[:appname] || configure_application_name || Rails.application.class.name.split('::').first rescue nil end def config_login_timeout(config) config[:login_timeout].present? ? config[:login_timeout].to_i : nil end def config_timeout(config) config[:timeout].present? ? config[:timeout].to_i / 1000 : nil end def config_encoding(config) config[:encoding].present? ? config[:encoding] : nil end def configure_connection ; end def configure_application_name ; end def initialize_dateformatter @database_dateformat = user_options_dateformat a, b, c = @database_dateformat.each_char.to_a [a, b, c].each { |f| f.upcase! if f == 'y' } dateformat = "%#{a}-%#{b}-%#{c}" ::Date::DATE_FORMATS[:_sqlserver_dateformat] = dateformat ::Time::DATE_FORMATS[:_sqlserver_dateformat] = dateformat ::Time::DATE_FORMATS[:_sqlserver_time] = '%H:%M:%S' ::Time::DATE_FORMATS[:_sqlserver_datetime] = "#{dateformat} %H:%M:%S" ::Time::DATE_FORMATS[:_sqlserver_datetimeoffset] = lambda { |time| time.strftime "#{dateformat} %H:%M:%S.%9N #{time.formatted_offset}" } end end end end