Repository: rails/rails
File: activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb (Ruby)

Code before:
              result = if stmt.column_count.zero? # No return
                stmt.step
                ActiveRecord::Result.empty
              else
                types = {}
                stmt.columns.zip(stmt.types).each_with_index do |(c, t), i|
                  types[c] = types[i] = type_map.lookup(t)
                end
                ActiveRecord::Result.new(stmt.columns, stmt.to_a, types.freeze)
              end
            else
              # Don't cache statements if they are not prepared.
              stmt = raw_connection.prepare(sql)
              begin
                unless binds.nil? || binds.empty?
                  stmt.bind_params(type_casted_binds)
                end
                result = if stmt.column_count.zero? # No return
                  stmt.step
                  ActiveRecord::Result.empty
                else
                  types = {}
                  stmt.columns.zip(stmt.types).each_with_index do |(c, t), i|
                    types[c] = types[i] = type_map.lookup(t)
                  end
                  ActiveRecord::Result.new(stmt.columns, stmt.to_a, types.freeze)
                end
              ensure
                stmt.close
              end
            end
            @last_affected_rows = raw_connection.changes
            verified!

            notification_payload[:affected_rows] = @last_affected_rows
            notification_payload[:row_count] = result&.length || 0
            result
          end

          def cast_result(result)
            # Given that SQLite3 doesn't really a Result type, raw_execute already return an ActiveRecord::Result
            # and we have nothing to cast here.
            result
          end

          def affected_rows(result)
            @last_affected_rows
          end

          def execute_batch(statements, name = nil, **kwargs)
            sql = combine_multi_statements(statements)

Review comment (performance):
I get that you copied what was done for PG, but this implementation isn't great performance wise. I probably never noticed it because I never profile with pg.

I'll experiment a few things.

Code after:
              result = if stmt.column_count.zero? # No return
                stmt.step
                ActiveRecord::Result.empty
              else
                ActiveRecord::Result.new(stmt.columns, stmt.to_a, stmt.types.map { |t| type_map.lookup(t) })
              end
            else
              # Don't cache statements if they are not prepared.
              stmt = raw_connection.prepare(sql)
              begin
                unless binds.nil? || binds.empty?
                  stmt.bind_params(type_casted_binds)
                end
                result = if stmt.column_count.zero? # No return
                  stmt.step
                  ActiveRecord::Result.empty
                else
                  ActiveRecord::Result.new(stmt.columns, stmt.to_a, stmt.types.map { |t| type_map.lookup(t) })
                end
              ensure
                stmt.close
              end
            end
            @last_affected_rows = raw_connection.changes
            verified!

            notification_payload[:affected_rows] = @last_affected_rows
            notification_payload[:row_count] = result&.length || 0
            result
          end

          def cast_result(result)
            # Given that SQLite3 doesn't really a Result type, raw_execute already return an ActiveRecord::Result
            # and we have nothing to cast here.
            result
          end

          def affected_rows(result)
            @last_affected_rows
          end

          def execute_batch(statements, name = nil, **kwargs)
            sql = combine_multi_statements(statements)
            raw_execute(sql, name, batch: true, **kwargs)
          end

          def build_truncate_statement(table_name)
            "DELETE FROM #{quote_table_name(table_name)}"
          end

          def returning_column_values(result)