File: ado.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (283 lines) | stat: -rw-r--r-- 9,102 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# frozen-string-literal: true

require 'win32ole'

module Sequel
  # The ADO adapter provides connectivity to ADO databases in Windows.
  module ADO
    # ADO constants (DataTypeEnum)
    # Source: https://msdn.microsoft.com/en-us/library/ms675318(v=vs.85).aspx
    AdBigInt           = 20
    AdBinary           = 128
    #AdBoolean          = 11
    #AdBSTR             = 8
    #AdChapter          = 136
    #AdChar             = 129
    #AdCurrency         = 6
    #AdDate             = 7
    AdDBDate           = 133
    #AdDBTime           = 134
    AdDBTimeStamp      = 135
    #AdDecimal          = 14
    #AdDouble           = 5
    #AdEmpty            = 0
    #AdError            = 10
    #AdFileTime         = 64
    #AdGUID             = 72
    #AdIDispatch        = 9
    #AdInteger          = 3
    #AdIUnknown         = 13
    AdLongVarBinary    = 205
    #AdLongVarChar      = 201
    #AdLongVarWChar     = 203
    AdNumeric          = 131
    #AdPropVariant      = 138
    #AdSingle           = 4
    #AdSmallInt         = 2
    #AdTinyInt          = 16
    #AdUnsignedBigInt   = 21
    #AdUnsignedInt      = 19
    #AdUnsignedSmallInt = 18
    #AdUnsignedTinyInt  = 17
    #AdUserDefined      = 132
    AdVarBinary        = 204
    #AdVarChar          = 200
    #AdVariant          = 12
    AdVarNumeric       = 139
    #AdVarWChar         = 202
    #AdWChar            = 130

    bigint = Object.new
    def bigint.call(v)
      v.to_i
    end

    numeric = Object.new
    def numeric.call(v)
      if v.include?(',')
        BigDecimal(v.tr(',', '.'))
      else
        BigDecimal(v)
      end
    end

    binary = Object.new
    def binary.call(v)
      Sequel.blob(v.pack('c*'))
    end

    date = Object.new
    def date.call(v)
      Date.new(v.year, v.month, v.day)
    end

    CONVERSION_PROCS = {}
    [
      [bigint, AdBigInt],
      [numeric, AdNumeric, AdVarNumeric],
      [date, AdDBDate],
      [binary, AdBinary, AdVarBinary, AdLongVarBinary]
    ].each do |callable, *types|
      callable.freeze
      types.each do |i|
        CONVERSION_PROCS[i] = callable
      end
    end
    CONVERSION_PROCS.freeze

    class Database < Sequel::Database
      set_adapter_scheme :ado

      attr_reader :conversion_procs

      # In addition to the usual database options,
      # the following options have an effect:
      #
      # :command_timeout :: Sets the time in seconds to wait while attempting
      #                     to execute a command before cancelling the attempt and generating
      #                     an error. Specifically, it sets the ADO CommandTimeout property.
      # :driver :: The driver to use in the ADO connection string.  If not provided, a default
      #            of "SQL Server" is used.
      # :conn_string :: The full ADO connection string.  If this is provided,
      #                 the usual options are ignored.
      # :provider :: Sets the Provider of this ADO connection (for example, "SQLOLEDB").
      #              If you don't specify a provider, the default one used by WIN32OLE
      #              has major problems, such as creating a new native database connection
      #              for every query, which breaks things such as temporary tables.
      #
      # Pay special attention to the :provider option, as without specifying a provider,
      # many things will be broken.  The SQLNCLI10 provider appears to work well if you
      # are connecting to Microsoft SQL Server, but it is not the default as that is not
      # always available and would break backwards compatability.
      def connect(server)
        opts = server_opts(server)
        s = opts[:conn_string] || "driver=#{opts[:driver]};server=#{opts[:host]};database=#{opts[:database]}#{";uid=#{opts[:user]};pwd=#{opts[:password]}" if opts[:user]}"
        handle = WIN32OLE.new('ADODB.Connection')
        handle.CommandTimeout = opts[:command_timeout] if opts[:command_timeout]
        handle.Provider = opts[:provider] if opts[:provider]
        handle.Open(s)
        handle
      end
      
      def disconnect_connection(conn)
        conn.Close
      rescue WIN32OLERuntimeError
        nil
      end

      def freeze
        @conversion_procs.freeze
        super
      end

      # Just execute so it doesn't attempt to return the number of rows modified.
      def execute_ddl(sql, opts=OPTS)
        execute(sql, opts)
      end

      # Just execute so it doesn't attempt to return the number of rows modified.
      def execute_insert(sql, opts=OPTS)
        execute(sql, opts)
      end
      
      # Use pass by reference in WIN32OLE to get the number of affected rows,
      # unless is a provider is in use (since some providers don't seem to
      # return the number of affected rows, but the default provider appears
      # to).
      def execute_dui(sql, opts=OPTS)
        return super if opts[:provider]
        synchronize(opts[:server]) do |conn|
          begin
            log_connection_yield(sql, conn){conn.Execute(sql, 1)}
            WIN32OLE::ARGV[1]
          rescue ::WIN32OLERuntimeError => e
            raise_error(e)
          end
        end
      end

      def execute(sql, opts=OPTS)
        synchronize(opts[:server]) do |conn|
          begin
            r = log_connection_yield(sql, conn){conn.Execute(sql)}
            begin
              yield r if defined?(yield)
            ensure
              begin
                r.close
              rescue ::WIN32OLERuntimeError
              end
            end
          rescue ::WIN32OLERuntimeError => e
            raise_error(e)
          end
        end
        nil
      end

      private
      
      def adapter_initialize
        case @opts[:conn_string]
        when /Microsoft\.(Jet|ACE)\.OLEDB/io
          require_relative 'ado/access'
          extend Sequel::ADO::Access::DatabaseMethods
          self.dataset_class = ADO::Access::Dataset
        else
          @opts[:driver] ||= 'SQL Server'
          case @opts[:driver]
          when 'SQL Server'
            require_relative 'ado/mssql'
            extend Sequel::ADO::MSSQL::DatabaseMethods
            self.dataset_class = ADO::MSSQL::Dataset
            set_mssql_unicode_strings
          end
        end

        @conversion_procs = CONVERSION_PROCS.dup
        @conversion_procs[AdDBTimeStamp] = method(:adb_timestamp_to_application_timestamp)

        super
      end

      def adb_timestamp_to_application_timestamp(v)
        # This hard codes a timestamp_precision of 6 when converting.
        # That is the default timestamp_precision, but the ado/mssql adapter uses a timestamp_precision
        # of 3.  However, timestamps returned by ado/mssql have nsec values that end up rounding to a
        # the same value as if a timestamp_precision of 3 was hard coded (either xxx999yzz, where y is
        # 5-9 or xxx000yzz where y is 0-4).
        #
        # ADO subadapters should override this they would like a different timestamp precision and the
        # this code does not work for them (for example, if they provide full nsec precision).
        #
        # Note that fractional second handling for WIN32OLE objects is not correct on ruby <2.2
        to_application_timestamp([v.year, v.month, v.day, v.hour, v.min, v.sec, (v.nsec/1000.0).round * 1000])
      end

      def dataset_class_default
        Dataset
      end

      # The ADO adapter's default provider doesn't support transactions, since it 
      # creates a new native connection for each query.  So Sequel only attempts
      # to use transactions if an explicit :provider is given.
      def begin_transaction(conn, opts=OPTS)
        super if @opts[:provider]
      end

      def commit_transaction(conn, opts=OPTS)
        super if @opts[:provider]
      end

      def database_error_classes
        [::WIN32OLERuntimeError]
      end

      def disconnect_error?(e, opts)
        super || (e.is_a?(::WIN32OLERuntimeError) && e.message =~ /Communication link failure/)
      end

      def rollback_transaction(conn, opts=OPTS)
        super if @opts[:provider]
      end
    end
    
    class Dataset < Sequel::Dataset
      def fetch_rows(sql)
        execute(sql) do |recordset|
          cols = []
          conversion_procs = db.conversion_procs

          recordset.Fields.each do |field|
            cols << [output_identifier(field.Name), conversion_procs[field.Type]]
          end

          self.columns = cols.map(&:first)
          return if recordset.EOF
          max = cols.length

          recordset.GetRows.transpose.each do |field_values|
            h = {}

            i = -1
            while (i += 1) < max
              name, cp = cols[i]
              h[name] = if (v = field_values[i]) && cp
                cp.call(v)
              else
                v
              end
            end
            
            yield h
          end
        end
      end
      
      # ADO can return for for delete and update statements, depending on the provider.
      def provides_accurate_rows_matched?
        false
      end
    end
  end
end