module ArJdbc module MSSQL # @see ActiveRecord::ConnectionAdapters::JdbcColumn#column_types def self.column_selector [ /sqlserver|tds|Microsoft SQL/i, lambda { |config, column| column.extend(ColumnMethods) } ] end # @private # @see ActiveRecord::ConnectionAdapters::JdbcColumn module ColumnMethods def self.included(base) # NOTE: assumes a standalone MSSQLColumn class class << base; include Cast; end end include LockMethods unless AR42 # @override def simplified_type(field_type) case field_type when /int|bigint|smallint|tinyint/i then :integer when /numeric/i then (@scale.nil? || @scale == 0) ? :integer : :decimal when /float|double|money|real|smallmoney/i then :decimal when /datetime|smalldatetime/i then :datetime when /timestamp/i then :timestamp when /time/i then :time when /date/i then :date when /text|ntext|xml/i then :text when /binary|image|varbinary/i then :binary when /char|nchar|nvarchar|string|varchar/i then (@limit == 1073741823 ? (@limit = nil; :text) : :string) when /bit/i then :boolean when /uniqueidentifier/i then :string else super end end # @override def default_value(value) return $1 if value =~ /^\(N?'(.*)'\)$/ || value =~ /^\(\(?(.*?)\)?\)$/ value end # @override def type_cast(value) return nil if value.nil? case type when :integer then ( value.is_a?(String) ? unquote(value) : (value || 0) ).to_i when :primary_key then value.respond_to?(:to_i) ? value.to_i : ((value && 1) || 0) when :decimal then self.class.value_to_decimal(unquote(value)) when :date then self.class.string_to_date(value) when :datetime then self.class.string_to_time(value) when :timestamp then self.class.string_to_time(value) when :time then self.class.string_to_dummy_time(value) when :boolean then value == true || (value =~ /^t(rue)?$/i) == 0 || unquote(value) == '1' when :binary then unquote(value) when :string then (value == 'null' ? '' : value) when :text then (value == 'null' ? '' : value) else value end end # @override def extract_limit(sql_type) case sql_type when /^smallint/i 2 when /^int/i 4 when /^bigint/i 8 when /\(max\)/, /decimal/, /numeric/ nil when /text|ntext|xml|binary|image|varbinary|bit/ nil else super end end # #primary replacement that works on 4.2 as well # #columns will set @primary even when on AR 4.2 def primary?; @primary end alias_method :is_primary, :primary? def identity? !! sql_type.downcase.index('identity') end # @deprecated alias_method :identity, :identity? alias_method :is_identity, :identity? # NOTE: these do not handle = equality as expected # see {#repair_special_columns} # (TEXT, NTEXT, and IMAGE data types are deprecated) # @private def special? unless defined? @special # /text|ntext|image|xml/i sql_type = @sql_type.downcase @special = !! ( sql_type.index('text') || sql_type.index('image') || sql_type.index('xml') ) end @special end # @deprecated alias_method :special, :special? alias_method :is_special, :special? def is_utf8? !!( sql_type =~ /nvarchar|ntext|nchar/i ) end private def unquote(value) value.to_s.sub(/\A\([\(\']?/, "").sub(/[\'\)]?\)\Z/, "") end module Cast def string_to_date(value) return value unless value.is_a?(String) return nil if value.empty? if value.include? "/" date = Date.strptime(value, "%m/%d/%Y") else date = fast_string_to_date(value) date ? date : Date.parse(value) rescue nil end end def string_to_time(value) return value unless value.is_a?(String) return nil if value.empty? if value.include? "/" result = DateTime.strptime(value+' '+Time.now.zone, "%m/%d/%Y %Z") elsif value =~ ActiveRecord::ConnectionAdapters::Column::Format::ISO_DATE result = DateTime.parse(value+'T00:00:00'+Time.now.formatted_offset) elsif value =~ ActiveRecord::ConnectionAdapters::Column::Format::ISO_DATETIME result = fast_string_to_time(value) else #It looks like there is a bug in the datetime string logic that lives upstream of this method #getting called. The fractional seconds are getting thrown out of values stored in the db such #as: #2016-12-24 11:09:43.097 (by examination in db visualizer) #which end up as (by the time the value gets to this method): #2016-12-24 11:09:43. #without the zero the ISO_DATETIME format check fails and we are getting conversion done #by the DateTime.parse which ignores timezone. This ends up returning dates erroneously. #It is tempting to make the else condition below raise an exception because we really don't #want timezone to be ignored in any situation. My level of confidence is not high enough to #change now. check_missing_zero = value + '0' if check_missing_zero =~ ActiveRecord::ConnectionAdapters::Column::Format::ISO_DATETIME result = fast_string_to_time(check_missing_zero) else result = DateTime.parse(value).to_time rescue nil end end result end ISO_TIME = /\A(\d\d)\:(\d\d)\:(\d\d)(\.\d+)?\z/ def string_to_dummy_time(value) return value unless value.is_a?(String) return nil if value.empty? if value =~ ISO_TIME # "12:34:56.1234560" microsec = ($4.to_f * 1_000_000).round.to_i new_time 2000, 1, 1, $1.to_i, $2.to_i, $3.to_i, microsec else super(value) end end def string_to_binary(value) # this will only allow the adapter to insert binary data with a length # of 7K or less because of a SQL Server statement length policy ... "0x#{value.unpack("H*")}" # "0x#{value.unpack("H*")[0]}" end def binary_to_string(value) if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT value = value.force_encoding(Encoding::ASCII_8BIT) end value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*') end end end def self.const_missing(name) if name.to_sym == :Column ArJdbc.deprecate("#{self.name}::Column will change to refer to the actual column class, please use ColumnMethods instead", :once) return ColumnMethods end super end end end