class ActiveRecord::ConnectionAdapters::SQLite3Adapter

Active Record SQLite3 Adapter

The SQLite3 adapter works with the sqlite3-ruby drivers (available as gem from rubygems.org/gems/sqlite3).

Options:

Constants

ADAPTER_NAME
COLLATE_REGEX
DEFAULT_PRAGMAS
DEFERRABLE_REGEX
EXTENDED_TYPE_MAPS
FINAL_CLOSE_PARENS_REGEX
FK_REGEX
GENERATED_ALWAYS_AS_REGEX
NATIVE_DATABASE_TYPES
PRIMARY_KEY_AUTOINCREMENT_REGEX
TYPE_MAP
UNQUOTED_OPEN_PARENS_REGEX
VIRTUAL_TABLE_REGEX

Public Class Methods

dbconsole(config, options = {}) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 44
def dbconsole(config, options = {})
  args = []

  args << "-#{options[:mode]}" if options[:mode]
  args << "-header" if options[:header]
  args << File.expand_path(config.database, Rails.respond_to?(:root) ? Rails.root : nil)

  find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
end
new(...) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 102
def initialize(...)
  super

  @memory_database = false
  case @config[:database].to_s
  when ""
    raise ArgumentError, "No database file specified. Missing argument: database"
  when ":memory:"
    @memory_database = true
  when /\Afile:/
  else
    # Otherwise we have a path relative to Rails.root
    @config[:database] = File.expand_path(@config[:database], Rails.root) if defined?(Rails.root)
    dirname = File.dirname(@config[:database])
    unless File.directory?(dirname)
      begin
        FileUtils.mkdir_p(dirname)
      rescue SystemCallError
        raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
      end
    end
  end

  @last_affected_rows = nil
  @previous_read_uncommitted = nil
  @config[:strict] = ConnectionAdapters::SQLite3Adapter.strict_strings_by_default unless @config.key?(:strict)
  @connection_parameters = @config.merge(
    database: @config[:database].to_s,
    results_as_hash: true,
    default_transaction_mode: :immediate,
  )
end
new_client(config) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 34
def new_client(config)
  ::SQLite3::Database.new(config[:database].to_s, config)
rescue Errno::ENOENT => error
  if error.message.include?("No such file or directory")
    raise ActiveRecord::NoDatabaseError
  else
    raise
  end
end
strict_strings_by_default() click to toggle source

Configure the SQLite3Adapter to be used in a strict strings mode. This will disable double-quoted string literals, because otherwise typos can silently go unnoticed. For example, it is possible to create an index for a non existing column. If you wish to enable this mode you can add the following line to your application.rb file:

config.active_record.sqlite3_adapter_strict_strings_by_default = true
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 67
class_attribute :strict_strings_by_default, default: false

Private Class Methods

initialize_type_map(m) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 494
def initialize_type_map(m)
  super
  register_class_with_limit m, %r(int)i, SQLite3Integer
end

Public Instance Methods

active?()
Alias for: connected?
add_timestamps(table_name, **options) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 392
def add_timestamps(table_name, **options)
  options[:null] = false if options[:null].nil?

  if !options.key?(:precision)
    options[:precision] = 6
  end

  alter_table(table_name) do |definition|
    definition.column :created_at, :datetime, **options
    definition.column :updated_at, :datetime, **options
  end
end
connected?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 206
def connected?
  !(@raw_connection.nil? || @raw_connection.closed?)
end
Also aliased as: active?
create_virtual_table(table_name, module_name, values) click to toggle source

Creates a virtual table

Example:

create_virtual_table :emails, :fts5, ['sender', 'title',' body']
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 308
def create_virtual_table(table_name, module_name, values)
  exec_query "CREATE VIRTUAL TABLE IF NOT EXISTS #{table_name} USING #{module_name} (#{values.join(", ")})"
end
database_exists?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 135
def database_exists?
  @config[:database] == ":memory:" || File.exist?(@config[:database].to_s)
end
disconnect!() click to toggle source

Disconnects from the database if already connected. Otherwise, this method does nothing.

# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 216
def disconnect!
  super

  @raw_connection&.close rescue nil
  @raw_connection = nil
end
drop_virtual_table(table_name, module_name, values, **options) click to toggle source

Drops a virtual table

Although this command ignores module_name and values, it can be helpful to provide these in a migration’s change method so it can be reverted. In that case, module_name, values and options will be used by create_virtual_table.

# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 317
def drop_virtual_table(table_name, module_name, values, **options)
  drop_table(table_name)
end
encoding() click to toggle source

Returns the current database encoding format as a string, e.g. ‘UTF-8’

# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 232
def encoding
  any_raw_connection.encoding.to_s
end
foreign_keys(table_name) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 412
def foreign_keys(table_name)
  # SQLite returns 1 row for each column of composite foreign keys.
  fk_info = internal_exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA")
  # Deferred or immediate foreign keys can only be seen in the CREATE TABLE sql
  fk_defs = table_structure_sql(table_name)
              .select do |column_string|
                column_string.start_with?("CONSTRAINT") &&
                column_string.include?("FOREIGN KEY")
              end
              .to_h do |fk_string|
                _, from, table, to = fk_string.match(FK_REGEX).to_a
                _, mode = fk_string.match(DEFERRABLE_REGEX).to_a
                deferred = mode&.downcase&.to_sym || false
                [[table, from, to], deferred]
              end

  grouped_fk = fk_info.group_by { |row| row["id"] }.values.each { |group| group.sort_by! { |row| row["seq"] } }
  grouped_fk.map do |group|
    row = group.first
    options = {
      on_delete: extract_foreign_key_action(row["on_delete"]),
      on_update: extract_foreign_key_action(row["on_update"]),
      deferrable: fk_defs[[row["table"], row["from"], row["to"]]]
    }

    if group.one?
      options[:column] = row["from"]
      options[:primary_key] = row["to"]
    else
      options[:column] = group.map { |row| row["from"] }
      options[:primary_key] = group.map { |row| row["to"] }
    end
    ForeignKeyDefinition.new(table_name, row["table"], options)
  end
end
rename_table(table_name, new_name, **options) click to toggle source

Renames a table.

Example:

rename_table('octopuses', 'octopi')
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 325
def rename_table(table_name, new_name, **options)
  validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
  schema_cache.clear_data_source_cache!(table_name.to_s)
  schema_cache.clear_data_source_cache!(new_name.to_s)
  exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
  rename_table_indexes(table_name, new_name, **options)
end
requires_reloading?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 159
def requires_reloading?
  true
end
supports_check_constraints?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 167
def supports_check_constraints?
  true
end
supports_common_table_expressions?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 183
def supports_common_table_expressions?
  database_version >= "3.8.3"
end
supports_concurrent_connections?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 198
def supports_concurrent_connections?
  !@memory_database
end
supports_datetime_with_precision?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 175
def supports_datetime_with_precision?
  true
end
supports_ddl_transactions?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 139
def supports_ddl_transactions?
  true
end
supports_deferrable_constraints?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 244
def supports_deferrable_constraints?
  true
end
supports_explain?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 236
def supports_explain?
  true
end
supports_expression_index?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 155
def supports_expression_index?
  database_version >= "3.9.0"
end
supports_foreign_keys?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 163
def supports_foreign_keys?
  true
end
supports_index_sort_order?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 223
def supports_index_sort_order?
  true
end
supports_insert_conflict_target?()
supports_insert_on_conflict?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 191
def supports_insert_on_conflict?
  database_version >= "3.24.0"
end
supports_insert_on_duplicate_skip?()
supports_insert_on_duplicate_update?()
supports_insert_returning?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 187
def supports_insert_returning?
  database_version >= "3.35.0"
end
supports_json?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 179
def supports_json?
  true
end
supports_lazy_transactions?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 240
def supports_lazy_transactions?
  true
end
supports_partial_index?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 151
def supports_partial_index?
  true
end
supports_savepoints?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 143
def supports_savepoints?
  true
end
supports_transaction_isolation?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 147
def supports_transaction_isolation?
  true
end
supports_views?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 171
def supports_views?
  true
end
supports_virtual_columns?() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 202
def supports_virtual_columns?
  database_version >= "3.31.0"
end
virtual_tables() click to toggle source

Returns a list of defined virtual tables

# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 292
      def virtual_tables
        query = <<~SQL
          SELECT name, sql FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL %';
        SQL

        exec_query(query, "SCHEMA").cast_values.each_with_object({}) do |row, memo|
          table_name, sql = row[0], row[1]
          _, module_name, arguments = sql.match(VIRTUAL_TABLE_REGEX).to_a
          memo[table_name] = [module_name, arguments]
        end.to_a
      end

Private Instance Methods

alter_table( table_name, foreign_keys = foreign_keys(table_name), check_constraints = check_constraints(table_name), **options ) { |definition| ... } click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 556
def alter_table(
  table_name,
  foreign_keys = foreign_keys(table_name),
  check_constraints = check_constraints(table_name),
  **options
)
  altered_table_name = "a#{table_name}"

  caller = lambda do |definition|
    rename = options[:rename] || {}
    foreign_keys.each do |fk|
      if column = rename[fk.options[:column]]
        fk.options[:column] = column
      end
      to_table = strip_table_name_prefix_and_suffix(fk.to_table)
      definition.foreign_key(to_table, **fk.options)
    end

    check_constraints.each do |chk|
      definition.check_constraint(chk.expression, **chk.options)
    end

    yield definition if block_given?
  end

  transaction do
    disable_referential_integrity do
      move_table(table_name, altered_table_name, options.merge(temporary: true))
      move_table(altered_table_name, table_name, &caller)
    end
  end
end
arel_visitor() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 793
def arel_visitor
  Arel::Visitors::SQLite.new(self)
end
bind_params_length() click to toggle source

See www.sqlite.org/limits.html, the default value is 999 when not configured.

# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 506
def bind_params_length
  999
end
build_statement_pool() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 797
def build_statement_pool
  StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
end
column_definitions(table_name)
Alias for: table_structure
configure_connection() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 815
        def configure_connection
          if @config[:timeout] && @config[:retries]
            raise ArgumentError, "Cannot specify both timeout and retries arguments"
          elsif @config[:timeout]
            timeout = self.class.type_cast_config_to_integer(@config[:timeout])
            raise TypeError, "timeout must be integer, not #{timeout}" unless timeout.is_a?(Integer)
            @raw_connection.busy_handler_timeout = timeout
          elsif @config[:retries]
            ActiveRecord.deprecator.warn(<<~MSG)
              The retries option is deprecated and will be removed in Rails 8.1. Use timeout instead.
            MSG
            retries = self.class.type_cast_config_to_integer(@config[:retries])
            raw_connection.busy_handler { |count| count <= retries }
          end

          super

          pragmas = @config.fetch(:pragmas, {}).stringify_keys
          DEFAULT_PRAGMAS.merge(pragmas).each do |pragma, value|
            if ::SQLite3::Pragmas.method_defined?("#{pragma}=")
              @raw_connection.public_send("#{pragma}=", value)
            else
              warn "Unknown SQLite pragma: #{pragma}"
            end
          end
        end
connect() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 801
def connect
  @raw_connection = self.class.new_client(@connection_parameters)
rescue ConnectionNotEstablished => ex
  raise ex.set_pool(@pool)
end
copy_table(from, to, options = {}) { |definition| ... } click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 594
def copy_table(from, to, options = {})
  from_primary_key = primary_key(from)
  options[:id] = false
  create_table(to, **options) do |definition|
    @definition = definition
    if from_primary_key.is_a?(Array)
      @definition.primary_keys from_primary_key
    end

    columns(from).each do |column|
      column_name = options[:rename] ?
        (options[:rename][column.name] ||
         options[:rename][column.name.to_sym] ||
         column.name) : column.name

      column_options = {
        limit: column.limit,
        precision: column.precision,
        scale: column.scale,
        null: column.null,
        collation: column.collation,
        primary_key: column_name == from_primary_key
      }

      if column.virtual?
        column_options[:as] = column.default_function
        column_options[:stored] = column.virtual_stored?
        column_options[:type] = column.type
      elsif column.has_default?
        type = lookup_cast_type_from_column(column)
        default = type.deserialize(column.default)
        default = -> { column.default_function } if default.nil?

        unless column.auto_increment?
          column_options[:default] = default
        end
      end

      column_type = column.virtual? ? :virtual : (column.bigint? ? :bigint : column.type)
      @definition.column(column_name, column_type, **column_options)
    end

    yield @definition if block_given?
  end
  copy_table_indexes(from, to, options[:rename] || {})

  columns_to_copy = @definition.columns.reject { |col| col.options.key?(:as) }.map(&:name)
  copy_table_contents(from, to,
    columns_to_copy,
    options[:rename] || {})
end
copy_table_contents(from, to, columns, rename = {}) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 674
def copy_table_contents(from, to, columns, rename = {})
  column_mappings = Hash[columns.map { |name| [name, name] }]
  rename.each { |a| column_mappings[a.last] = a.first }
  from_columns = columns(from).collect(&:name)
  columns = columns.find_all { |col| from_columns.include?(column_mappings[col]) }
  from_columns_to_copy = columns.map { |col| column_mappings[col] }
  quoted_columns = columns.map { |col| quote_column_name(col) } * ","
  quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ","

  internal_exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns})
             SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}")
end
copy_table_indexes(from, to, rename = {}) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 646
def copy_table_indexes(from, to, rename = {})
  indexes(from).each do |index|
    name = index.name
    if to == "a#{from}"
      name = "t#{name}"
    elsif from == "a#{to}"
      name = name[1..-1]
    end

    columns = index.columns
    if columns.is_a?(Array)
      to_column_names = columns(to).map(&:name)
      columns = columns.map { |c| rename[c] || c }.select do |column|
        to_column_names.include?(column)
      end
    end

    unless columns.empty?
      # index name can't be the same
      options = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
      options[:unique] = true if index.unique
      options[:where] = index.where if index.where
      options[:order] = index.orders if index.orders
      add_index(to, columns, **options)
    end
  end
end
extract_default_function(default_value, default) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 540
def extract_default_function(default_value, default)
  default if has_default_function?(default_value, default)
end
extract_value_from_default(default) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 517
def extract_value_from_default(default)
  case default
  when /^null$/i
    nil
  # Quoted types
  when /^'([^|]*)'$/m
    $1.gsub("''", "'")
  # Quoted types
  when /^"([^|]*)"$/m
    $1.gsub('""', '"')
  # Numeric types
  when /\A-?\d+(\.\d*)?\z/
    $&
  # Binary columns
  when /x'(.*)'/
    [ $1 ].pack("H*")
  else
    # Anything else is blank or some function
    # and we can't know the value of that, so return nil.
    nil
  end
end
has_default_function?(default_value, default) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 544
def has_default_function?(default_value, default)
  !default_value && %r{\w+\(.*\)|CURRENT_TIME|CURRENT_DATE|CURRENT_TIMESTAMP|\|\|}.match?(default)
end
invalid_alter_table_type?(type, options) click to toggle source

See: www.sqlite.org/lang_altertable.html SQLite has an additional restriction on the ALTER TABLE statement

# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 550
def invalid_alter_table_type?(type, options)
  type == :primary_key || options[:primary_key] ||
    options[:null] == false && options[:default].nil? ||
    (type == :virtual && options[:stored])
end
move_table(from, to, options = {}, &block) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 589
def move_table(from, to, options = {}, &block)
  copy_table(from, to, options, &block)
  drop_table(from)
end
reconnect() click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 807
def reconnect
  if active?
    @raw_connection.rollback rescue nil
  else
    connect
  end
end
table_info(table_name) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 785
def table_info(table_name)
  if supports_virtual_columns?
    internal_exec_query("PRAGMA table_xinfo(#{quote_table_name(table_name)})", "SCHEMA")
  else
    internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
  end
end
table_structure(table_name) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 510
def table_structure(table_name)
  structure = table_info(table_name)
  raise ActiveRecord::StatementInvalid.new("Could not find table '#{table_name}'", connection_pool: @pool) if structure.empty?
  table_structure_with_collation(table_name, structure)
end
Also aliased as: column_definitions
table_structure_sql(table_name, column_names = nil) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 752
        def table_structure_sql(table_name, column_names = nil)
          unless column_names
            column_info = table_info(table_name)
            column_names = column_info.map { |column| column["name"] }
          end

          sql = <<~SQL
            SELECT sql FROM
              (SELECT * FROM sqlite_master UNION ALL
               SELECT * FROM sqlite_temp_master)
            WHERE type = 'table' AND name = #{quote(table_name)}
          SQL

          # Result will have following sample string
          # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
          #                       "password_digest" varchar COLLATE "NOCASE",
          #                       "o_id" integer,
          #                       CONSTRAINT "fk_rails_78146ddd2e" FOREIGN KEY ("o_id") REFERENCES "os" ("id"));
          result = query_value(sql, "SCHEMA")

          return [] unless result

          # Splitting with left parentheses and discarding the first part will return all
          # columns separated with comma(,).
          result.partition(UNQUOTED_OPEN_PARENS_REGEX)
                .last
                .sub(FINAL_CLOSE_PARENS_REGEX, "")
                # column definitions can have a comma in them, so split on commas followed
                # by a space and a column name in quotes or followed by the keyword CONSTRAINT
                .split(/,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})"))/i)
                .map(&:strip)
        end
table_structure_with_collation(table_name, basic_structure) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 711
def table_structure_with_collation(table_name, basic_structure)
  collation_hash = {}
  auto_increments = {}
  generated_columns = {}

  column_strings = table_structure_sql(table_name, basic_structure.map { |column| column["name"] })

  if column_strings.any?
    column_strings.each do |column_string|
      # This regex will match the column name and collation type and will save
      # the value in $1 and $2 respectively.
      collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string
      auto_increments[$1] = true if PRIMARY_KEY_AUTOINCREMENT_REGEX =~ column_string
      generated_columns[$1] = $2 if GENERATED_ALWAYS_AS_REGEX =~ column_string
    end

    basic_structure.map do |column|
      column_name = column["name"]

      if collation_hash.has_key? column_name
        column["collation"] = collation_hash[column_name]
      end

      if auto_increments.has_key?(column_name)
        column["auto_increment"] = true
      end

      if generated_columns.has_key?(column_name)
        column["dflt_value"] = generated_columns[column_name]
      end

      column
    end
  else
    basic_structure.to_a
  end
end
translate_exception(exception, message:, sql:, binds:) click to toggle source
# File lib/active_record/connection_adapters/sqlite3_adapter.rb, line 687
def translate_exception(exception, message:, sql:, binds:)
  # SQLite 3.8.2 returns a newly formatted error message:
  #   UNIQUE constraint failed: *table_name*.*column_name*
  # Older versions of SQLite return:
  #   column *column_name* is not unique
  if exception.message.match?(/(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)/i)
    RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
  elsif exception.message.match?(/(.* may not be NULL|NOT NULL constraint failed: .*)/i)
    NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
  elsif exception.message.match?(/FOREIGN KEY constraint failed/i)
    InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
  elsif exception.message.match?(/called on a closed database/i)
    ConnectionNotEstablished.new(exception, connection_pool: @pool)
  elsif exception.is_a?(::SQLite3::BusyException)
    StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
  else
    super
  end
end