aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
blob: 39118583bd2c82189ba2ac397575501aea6751f1 (plain) (blame)
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
module ActiveRecord
  module ConnectionAdapters # :nodoc:
    module DatabaseStatements
      # Returns an array of record hashes with the column names as keys and
      # column values as values.
      def select_all(sql, name = nil)
        select(sql, name)
      end

      # Returns a record hash with the column names as keys and column values
      # as values.
      def select_one(sql, name = nil)
        result = select_all(sql, name)
        result.first if result
      end

      # Returns a single value from a record
      def select_value(sql, name = nil)
        if result = select_one(sql, name)
          result.values.first
        end
      end

      # Returns an array of the values of the first column in a select:
      #   select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
      def select_values(sql, name = nil)
        result = select_rows(sql, name)
        result.map { |v| v[0] }
      end

      # Returns an array of arrays containing the field values.
      # Order is the same as that returned by +columns+.
      def select_rows(sql, name = nil)
      end
      undef_method :select_rows

      # Executes the SQL statement in the context of this connection.
      def execute(sql, name = nil, skip_logging = false)
      end
      undef_method :execute

      # Returns the last auto-generated ID from the affected table.
      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
        insert_sql(sql, name, pk, id_value, sequence_name)
      end

      # Executes the update statement and returns the number of rows affected.
      def update(sql, name = nil)
        update_sql(sql, name)
      end

      # Executes the delete statement and returns the number of rows affected.
      def delete(sql, name = nil)
        delete_sql(sql, name)
      end
      
      # Checks whether there is currently no transaction active. This is done
      # by querying the database driver, and does not use the transaction
      # house-keeping information recorded by #increment_open_transactions and
      # friends.
      #
      # Returns true if there is no transaction active, false if there is a
      # transaction active, and nil if this information is unknown.
      #
      # Not all adapters supports transaction state introspection. Currently,
      # only the PostgreSQL adapter supports this.
      def outside_transaction?
        nil
      end

      # Runs the given block in a database transaction, and returns the result
      # of the block.
      #
      # == Nested transactions support
      #
      # Most databases don't support true nested transactions. At the time of
      # writing, the only database that supports true nested transactions that
      # we're aware of, is MS-SQL.
      #
      # In order to get around this problem, #transaction will emulate the effect
      # of nested transactions, by using savepoints:
      # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
      # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
      #
      # It is safe to call this method if a database transaction is already open,
      # i.e. if #transaction is called within another #transaction block. In case
      # of a nested call, #transaction will behave as follows:
      #
      # - The block will be run without doing anything. All database statements
      #   that happen within the block are effectively appended to the already
      #   open database transaction.
      # - However, if +requires_new+ is set, the block will be wrapped in a
      #   database savepoint acting as a sub-transaction.
      #
      # === Caveats
      #
      # MySQL doesn't support DDL transactions. If you perform a DDL operation,
      # then any created savepoints will be automatically released. For example,
      # if you've created a savepoint, then you execute a CREATE TABLE statement,
      # then the savepoint that was created will be automatically released.
      #
      # This means that, on MySQL, you shouldn't execute DDL operations inside
      # a #transaction call that you know might create a savepoint. Otherwise,
      # #transaction will raise exceptions when it tries to release the
      # already-automatically-released savepoints:
      #
      #   Model.connection.transaction do  # BEGIN
      #     Model.connection.transaction(:requires_new => true) do  # CREATE SAVEPOINT active_record_1
      #       Model.connection.create_table(...)
      #       # active_record_1 now automatically released
      #     end  # RELEASE SAVEPOINT active_record_1  <--- BOOM! database error!
      #   end
      def transaction(options = {})
        options.assert_valid_keys :requires_new, :joinable

        last_transaction_joinable, @transaction_joinable =
          @transaction_joinable, options[:joinable] || true
        requires_new = options[:requires_new] || !last_transaction_joinable

        transaction_open = false
        begin
          if block_given?
            if requires_new || open_transactions == 0
              if open_transactions == 0
                begin_db_transaction
              elsif requires_new
                create_savepoint
              end
              increment_open_transactions
              transaction_open = true
            end
            yield
          end
        rescue Exception => database_transaction_rollback
          if transaction_open && !outside_transaction?
            transaction_open = false
            decrement_open_transactions
            if open_transactions == 0
              rollback_db_transaction
            else
              rollback_to_savepoint
            end
          end
          raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback
        end
      ensure
        @transaction_joinable = last_transaction_joinable

        if outside_transaction?
          @open_transactions = 0
        elsif transaction_open
          decrement_open_transactions
          begin
            if open_transactions == 0
              commit_db_transaction
            else
              release_savepoint
            end
          rescue Exception => database_transaction_rollback
            if open_transactions == 0
              rollback_db_transaction
            else
              rollback_to_savepoint
            end
            raise
          end
        end
      end
      
      # Begins the transaction (and turns off auto-committing).
      def begin_db_transaction()    end

      # Commits the transaction (and turns on auto-committing).
      def commit_db_transaction()   end

      # Rolls back the transaction (and turns on auto-committing). Must be
      # done if the transaction block raises an exception or returns false.
      def rollback_db_transaction() end

      # Alias for <tt>add_limit_offset!</tt>.
      def add_limit!(sql, options)
        add_limit_offset!(sql, options) if options
      end

      # Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
      # fragment that has the same semantics as LIMIT and OFFSET.
      #
      # +options+ must be a Hash which contains a +:limit+ option (required)
      # and an +:offset+ option (optional).
      #
      # This method *modifies* the +sql+ parameter.
      #
      # ===== Examples
      #  add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
      # generates
      #  SELECT * FROM suppliers LIMIT 10 OFFSET 50
      def add_limit_offset!(sql, options)
        if limit = options[:limit]
          sql << " LIMIT #{sanitize_limit(limit)}"
          if offset = options[:offset]
            sql << " OFFSET #{offset.to_i}"
          end
        end
        sql
      end

      # Appends a locking clause to an SQL statement.
      # This method *modifies* the +sql+ parameter.
      #   # SELECT * FROM suppliers FOR UPDATE
      #   add_lock! 'SELECT * FROM suppliers', :lock => true
      #   add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
      def add_lock!(sql, options)
        case lock = options[:lock]
          when true;   sql << ' FOR UPDATE'
          when String; sql << " #{lock}"
        end
      end

      def default_sequence_name(table, column)
        nil
      end

      # Set the sequence to the max value of the table's column.
      def reset_sequence!(table, column, sequence = nil)
        # Do nothing by default.  Implement for PostgreSQL, Oracle, ...
      end

      # Inserts the given fixture into the table. Overridden in adapters that require
      # something beyond a simple insert (eg. Oracle).
      def insert_fixture(fixture, table_name)
        execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
      end

      def empty_insert_statement(table_name)
        "INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
      end

      def case_sensitive_equality_operator
        "="
      end

      def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
        "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
      end

      protected
        # Returns an array of record hashes with the column names as keys and
        # column values as values.
        def select(sql, name = nil)
        end
        undef_method :select

        # Returns the last auto-generated ID from the affected table.
        def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
          execute(sql, name)
          id_value
        end

        # Executes the update statement and returns the number of rows affected.
        def update_sql(sql, name = nil)
          execute(sql, name)
        end

        # Executes the delete statement and returns the number of rows affected.
        def delete_sql(sql, name = nil)
          update_sql(sql, name)
        end

        # Sanitizes the given LIMIT parameter in order to prevent SQL injection.
        #
        # +limit+ may be anything that can evaluate to a string via #to_s. It
        # should look like an integer, or a comma-delimited list of integers.
        #
        # Returns the sanitized limit parameter, either as an integer, or as a
        # string which contains a comma-delimited list of integers.
        def sanitize_limit(limit)
          if limit.to_s =~ /,/
            limit.to_s.split(',').map{ |i| i.to_i }.join(',')
          else
            limit.to_i
          end
        end
    end
  end
end