File: prepared_statements.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 (429 lines) | stat: -rw-r--r-- 14,723 bytes parent folder | download | duplicates (3)
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# frozen-string-literal: true

module Sequel 
  class Dataset
    # ---------------------
    # :section: 8 - Methods related to prepared statements or bound variables
    # On some adapters, these use native prepared statements and bound variables, on others
    # support is emulated.  For details, see the {"Prepared Statements/Bound Variables" guide}[rdoc-ref:doc/prepared_statements.rdoc].
    # ---------------------
    
    PREPARED_ARG_PLACEHOLDER = LiteralString.new('?').freeze

    DEFAULT_PREPARED_STATEMENT_MODULE_METHODS = %w'execute execute_dui execute_insert'.freeze.each(&:freeze)
    PREPARED_STATEMENT_MODULE_CODE = {
      :bind => "opts = Hash[opts]; opts[:arguments] = bind_arguments".freeze,
      :prepare => "sql = prepared_statement_name".freeze,
      :prepare_bind => "sql = prepared_statement_name; opts = Hash[opts]; opts[:arguments] = bind_arguments".freeze
    }.freeze

    def self.prepared_statements_module(code, mods, meths=DEFAULT_PREPARED_STATEMENT_MODULE_METHODS, &block)
      code = PREPARED_STATEMENT_MODULE_CODE[code] || code

      Module.new do
        Array(mods).each do |mod|
          include mod
        end

        if block
          module_eval(&block)
        end

        meths.each do |meth|
          module_eval("def #{meth}(sql, opts=Sequel::OPTS) #{code}; super end", __FILE__, __LINE__)
        end
        private(*meths)
      end
    end
    private_class_method :prepared_statements_module

    # Default implementation of the argument mapper to allow
    # native database support for bind variables and prepared
    # statements (as opposed to the emulated ones used by default).
    module ArgumentMapper
      # The name of the prepared statement, if any.
      def prepared_statement_name
        @opts[:prepared_statement_name]
      end

      # The bind arguments to use for running this prepared statement
      def bind_arguments
        @opts[:bind_arguments]
      end

      # Set the bind arguments based on the hash and call super.
      def call(bind_vars=OPTS, &block)
        sql = prepared_sql
        prepared_args.freeze
        ps = bind(bind_vars)
        ps.clone(:bind_arguments=>ps.map_to_prepared_args(ps.opts[:bind_vars]), :sql=>sql, :prepared_sql=>sql).run(&block)
      end
        
      # Override the given *_sql method based on the type, and
      # cache the result of the sql.
      def prepared_sql
        if sql = @opts[:prepared_sql] || cache_get(:_prepared_sql)
          return sql
        end
        cache_set(:_prepared_sql, super)
      end

      private

      # Report that prepared statements are not emulated, since
      # all adapters that use this use native prepared statements.
      def emulate_prepared_statements?
        false
      end
    end

    # Backbone of the prepared statement support.  Grafts bind variable
    # support into datasets by hijacking #literal and using placeholders.
    # By default, emulates prepared statements and bind variables by
    # taking the hash of bind variables and directly substituting them
    # into the query, which works on all databases, as it is no different
    # from using the dataset without bind variables.
    module PreparedStatementMethods
      # Whether to log the full SQL query.  By default, just the prepared statement
      # name is generally logged on adapters that support native prepared statements.
      def log_sql
        @opts[:log_sql]
      end
      
      # The type of prepared statement, should be one of :select, :first,
      # :insert, :update, :delete, or :single_value
      def prepared_type
        @opts[:prepared_type]
      end
      
      # The array/hash of bound variable placeholder names.
      def prepared_args
        @opts[:prepared_args]
      end
      
      # The dataset that created this prepared statement.
      def orig_dataset
        @opts[:orig_dataset]
      end
      
      # The argument to supply to insert and update, which may use
      # placeholders specified by prepared_args
      def prepared_modify_values
        @opts[:prepared_modify_values]
      end

      # Sets the prepared_args to the given hash and runs the
      # prepared statement.
      def call(bind_vars=OPTS, &block)
        bind(bind_vars).run(&block)
      end

      # Raise an error if attempting to call prepare on an already
      # prepared statement.
      def prepare(*)
        raise Error, "cannot prepare an already prepared statement" unless allow_preparing_prepared_statements?
        super
      end

      # Send the columns to the original dataset, as calling it
      # on the prepared statement can cause problems.
      def columns
        orig_dataset.columns
      end
      
      # Disallow use of delayed evaluations in prepared statements.
      def delayed_evaluation_sql_append(sql, delay)
        raise Error, "delayed evaluations cannot be used in prepared statements" if @opts[:no_delayed_evaluations]
        super
      end

      # Returns the SQL for the prepared statement, depending on
      # the type of the statement and the prepared_modify_values.
      def prepared_sql
        case prepared_type
        when :select, :all, :each
          # Most common scenario, so listed first.
          select_sql
        when :first, :single_value
          clone(:limit=>1).select_sql
        when :insert_select
          insert_select_sql(*prepared_modify_values)
        when :insert, :insert_pk
          insert_sql(*prepared_modify_values)
        when :update
          update_sql(*prepared_modify_values)
        when :delete
          delete_sql
        else
          select_sql
        end
      end
      
      # Changes the values of symbols if they start with $ and
      # prepared_args is present.  If so, they are considered placeholders,
      # and they are substituted using prepared_arg.
      def literal_symbol_append(sql, v)
        if @opts[:bind_vars] && /\A\$(.*)\z/ =~ v
          literal_append(sql, prepared_arg($1.to_sym))
        else
          super
        end
      end
      
      # Programmer friendly string showing this is a prepared statement,
      # with the prepared SQL it represents (which in general won't have
      # substituted variables).
      def inspect
        "<#{visible_class_name}/PreparedStatement #{prepared_sql.inspect}>"
      end
      
      protected
      
      # Run the method based on the type of prepared statement.
      def run(&block)
        case prepared_type
        when :select, :all
          all(&block)
        when :each
          each(&block)
        when :insert_select
          with_sql(prepared_sql).first
        when :first
          first
        when :insert, :update, :delete
          if opts[:returning] && supports_returning?(prepared_type)
            returning_fetch_rows(prepared_sql)
          elsif prepared_type == :delete
            delete
          else
            public_send(prepared_type, *prepared_modify_values)
          end
        when :insert_pk
          fetch_rows(prepared_sql){|r| return r.values.first}
        when Array
          # :nocov:
          case prepared_type[0]
          # :nocov:
          when :map, :as_hash, :to_hash, :to_hash_groups
            public_send(*prepared_type, &block) 
          end
        when :single_value
          single_value
        else
          raise Error, "unsupported prepared statement type used: #{prepared_type.inspect}"
        end
      end
      
      private
      
      # Returns the value of the prepared_args hash for the given key.
      def prepared_arg(k)
        @opts[:bind_vars][k]
      end

      # The symbol cache should always be skipped, since placeholders are symbols.
      def skip_symbol_cache?
        true
      end

      # Use a clone of the dataset extended with prepared statement
      # support and using the same argument hash so that you can use
      # bind variables/prepared arguments in subselects.
      def subselect_sql_append(sql, ds)
        subselect_sql_dataset(sql, ds).prepared_sql
      end

      def subselect_sql_dataset(sql, ds)
        super.clone(:prepared_args=>prepared_args, :bind_vars=>@opts[:bind_vars]).
          send(:to_prepared_statement, :select, nil, :extend=>prepared_statement_modules)
      end
    end
    
    # Default implementation for an argument mapper that uses
    # unnumbered SQL placeholder arguments.  Keeps track of which
    # arguments have been used, and allows arguments to
    # be used more than once.
    module UnnumberedArgumentMapper
      include ArgumentMapper
      
      protected
      
      # Returns a single output array mapping the values of the input hash.
      # Keys in the input hash that are used more than once in the query
      # have multiple entries in the output array.
      def map_to_prepared_args(bind_vars)
        prepared_args.map{|v| bind_vars[v]}
      end
      
      private
      
      # Associates the argument with name k with the next position in
      # the output array.
      def prepared_arg(k)
        prepared_args << k
        prepared_arg_placeholder
      end
    end
    
    # Prepared statements emulation support for adapters that don't
    # support native prepared statements.  Uses a placeholder
    # literalizer to hold the prepared sql with the ability to
    # interpolate arguments to prepare the final SQL string.
    module EmulatePreparedStatementMethods
      include UnnumberedArgumentMapper

      def run(&block)
        if @opts[:prepared_sql_frags]
          sql = literal(Sequel::SQL::PlaceholderLiteralString.new(@opts[:prepared_sql_frags], @opts[:bind_arguments], false))
          clone(:prepared_sql_frags=>nil, :sql=>sql, :prepared_sql=>sql).run(&block)
        else
          super
        end
      end

      private
      
      # Turn emulation of prepared statements back on, since ArgumentMapper
      # turns it off.
      def emulate_prepared_statements?
        true
      end
        
      def emulated_prepared_statement(type, name, values)
        prepared_sql, frags = Sequel::Dataset::PlaceholderLiteralizer::Recorder.new.send(:prepared_sql_and_frags, self, prepared_args) do |pl, ds|
          ds = ds.clone(:recorder=>pl)

          case type
          when :first, :single_value
            ds.limit(1)
          when :update, :insert, :insert_select, :delete
            ds.with_sql(:"#{type}_sql", *values)
          when :insert_pk
            ds.with_sql(:insert_sql, *values)
          else
            ds
          end
        end

        prepared_args.freeze
        clone(:prepared_sql_frags=>frags, :prepared_sql=>prepared_sql, :sql=>prepared_sql)
      end

      # Associates the argument with name k with the next position in
      # the output array.
      def prepared_arg(k)
        prepared_args << k
        @opts[:recorder].arg
      end

      def subselect_sql_dataset(sql, ds)
        super.clone(:recorder=>@opts[:recorder]).
          with_extend(EmulatePreparedStatementMethods)
      end
    end
    
    # Set the bind variables to use for the call.  If bind variables have
    # already been set for this dataset, they are updated with the contents
    # of bind_vars.
    #
    #   DB[:table].where(id: :$id).bind(id: 1).call(:first)
    #   # SELECT * FROM table WHERE id = ? LIMIT 1 -- (1)
    #   # => {:id=>1}
    def bind(bind_vars=OPTS)
      bind_vars = if bv = @opts[:bind_vars]
        bv.merge(bind_vars).freeze
      else
        if bind_vars.frozen?
          bind_vars
        else
          Hash[bind_vars]
        end
      end

      clone(:bind_vars=>bind_vars)
    end
    
    # For the given type (:select, :first, :insert, :insert_select, :update, :delete, or :single_value),
    # run the sql with the bind variables specified in the hash.  +values+ is a hash passed to
    # insert or update (if one of those types is used), which may contain placeholders.
    #
    #   DB[:table].where(id: :$id).call(:first, id: 1)
    #   # SELECT * FROM table WHERE id = ? LIMIT 1 -- (1)
    #   # => {:id=>1}
    def call(type, bind_variables=OPTS, *values, &block)
      to_prepared_statement(type, values, :extend=>bound_variable_modules).call(bind_variables, &block)
    end
    
    # Prepare an SQL statement for later execution.  Takes a type similar to #call,
    # and the +name+ symbol of the prepared statement.
    #
    # This returns a clone of the dataset extended with PreparedStatementMethods,
    # which you can +call+ with the hash of bind variables to use.
    # The prepared statement is also stored in
    # the associated Database, where it can be called by name.
    # The following usage is identical:
    #
    #   ps = DB[:table].where(name: :$name).prepare(:first, :select_by_name)
    #
    #   ps.call(name: 'Blah')
    #   # SELECT * FROM table WHERE name = ? -- ('Blah')
    #   # => {:id=>1, :name=>'Blah'}
    #
    #   DB.call(:select_by_name, name: 'Blah') # Same thing
    def prepare(type, name, *values)
      ps = to_prepared_statement(type, values, :name=>name, :extend=>prepared_statement_modules, :no_delayed_evaluations=>true)

      ps = if ps.send(:emulate_prepared_statements?)
        ps = ps.with_extend(EmulatePreparedStatementMethods)
        ps.send(:emulated_prepared_statement, type, name, values)
      else
        sql = ps.prepared_sql
        ps.prepared_args.freeze
        ps.clone(:prepared_sql=>sql, :sql=>sql)
      end

      db.set_prepared_statement(name, ps)
      ps
    end
    
    protected
    
    # Return a cloned copy of the current dataset extended with
    # PreparedStatementMethods, setting the type and modify values.
    def to_prepared_statement(type, values=nil, opts=OPTS)
      mods = opts[:extend] || []
      mods += [PreparedStatementMethods]

      bind.
        clone(:prepared_statement_name=>opts[:name], :prepared_type=>type, :prepared_modify_values=>values, :orig_dataset=>self, :no_cache_sql=>true, :prepared_args=>@opts[:prepared_args]||[], :no_delayed_evaluations=>opts[:no_delayed_evaluations]).
        with_extend(*mods)
    end

    private
    
    # Don't allow preparing prepared statements by default.
    def allow_preparing_prepared_statements?
      false
    end

    def bound_variable_modules
      prepared_statement_modules
    end

    # Whether prepared statements should be emulated.  True by
    # default so that adapters have to opt in.
    def emulate_prepared_statements?
      true
    end

    def prepared_statement_modules
      []
    end

    # The argument placeholder.  Most databases used unnumbered
    # arguments with question marks, so that is the default.
    def prepared_arg_placeholder
      PREPARED_ARG_PLACEHOLDER
    end
  end
end