Skip to content

Instantly share code, notes, and snippets.

@headius
Created July 11, 2024 16:32
Show Gist options
  • Save headius/423138b66fdad9012088358ae623b339 to your computer and use it in GitHub Desktop.
Save headius/423138b66fdad9012088358ae623b339 to your computer and use it in GitHub Desktop.
A diff of the ActiveRecord SQLite3 adapter and the same in ActiveRecord-JDBC-Adapter
--- ../rails/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb 2024-07-11 11:27:35
+++ ../activerecord-jdbc-adapter/lib/arjdbc/sqlite3/adapter.rb 2024-07-11 11:27:27
@@ -1,753 +1,884 @@
# frozen_string_literal: true
+ArJdbc.load_java_part :SQLite3
+
+require "arjdbc/abstract/core"
+require "arjdbc/abstract/database_statements"
+require 'arjdbc/abstract/statement_cache'
+require "arjdbc/abstract/transaction_support"
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
-require "active_record/connection_adapters/sqlite3/column"
require "active_record/connection_adapters/sqlite3/explain_pretty_printer"
require "active_record/connection_adapters/sqlite3/quoting"
require "active_record/connection_adapters/sqlite3/database_statements"
require "active_record/connection_adapters/sqlite3/schema_creation"
require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
require "active_record/connection_adapters/sqlite3/schema_statements"
+require "active_support/core_ext/class/attribute"
+require "arjdbc/sqlite3/column"
-gem "sqlite3", "~> 1.4"
-require "sqlite3"
-
-module ActiveRecord
- module ConnectionHandling # :nodoc:
- def sqlite3_adapter_class
- ConnectionAdapters::SQLite3Adapter
+module SQLite3
+ module Constants
+ module Open
+ READONLY = 0x00000001
+ READWRITE = 0x00000002
+ CREATE = 0x00000004
+ DELETEONCLOSE = 0x00000008
+ EXCLUSIVE = 0x00000010
+ AUTOPROXY = 0x00000020
+ URI = 0x00000040
+ MEMORY = 0x00000080
+ MAIN_DB = 0x00000100
+ TEMP_DB = 0x00000200
+ TRANSIENT_DB = 0x00000400
+ MAIN_JOURNAL = 0x00000800
+ TEMP_JOURNAL = 0x00001000
+ SUBJOURNAL = 0x00002000
+ MASTER_JOURNAL = 0x00004000
+ NOMUTEX = 0x00008000
+ FULLMUTEX = 0x00010000
+ SHAREDCACHE = 0x00020000
+ PRIVATECACHE = 0x00040000
+ WAL = 0x00080000
end
-
- def sqlite3_connection(config)
- sqlite3_adapter_class.new(config)
end
end
- module ConnectionAdapters # :nodoc:
- # = Active Record SQLite3 Adapter
- #
- # The SQLite3 adapter works with the sqlite3-ruby drivers
- # (available as gem from https://rubygems.org/gems/sqlite3).
- #
- # Options:
- #
- # * <tt>:database</tt> - Path to the database file.
- class SQLite3Adapter < AbstractAdapter
- ADAPTER_NAME = "SQLite"
+module ArJdbc
+ # All the code in this module is a copy of ConnectionAdapters::SQLite3Adapter from active_record 5.
+ # The constants at the front of this file are to allow the rest of the file to remain with no modifications
+ # from its original source. If you hack on this file try not to modify this module and instead try and
+ # put those overrides in SQL3Adapter below. We try and keep a copy of the Rails this adapter supports
+ # with the current goal of being able to diff changes easily over time and to also eventually remove
+ # this module from ARJDBC altogether.
+ module SQLite3
+ # DIFFERENCE: Some common constant names to reduce differences in rest of this module from AR5 version
+ ConnectionAdapters = ::ActiveRecord::ConnectionAdapters
+ IndexDefinition = ::ActiveRecord::ConnectionAdapters::IndexDefinition
+ Quoting = ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
+ RecordNotUnique = ::ActiveRecord::RecordNotUnique
+ SchemaCreation = ConnectionAdapters::SQLite3::SchemaCreation
+ SQLite3Adapter = ConnectionAdapters::AbstractAdapter
- class << self
- 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
+ ADAPTER_NAME = 'SQLite'
- 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("sqlite3", *args)
- end
- end
-
- include SQLite3::Quoting
- include SQLite3::SchemaStatements
- include SQLite3::DatabaseStatements
-
- ##
- # :singleton-method:
- # 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
- class_attribute :strict_strings_by_default, default: false
+ # DIFFERENCE: FQN
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::Quoting
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
+ include ::ActiveRecord::ConnectionAdapters::SQLite3::DatabaseStatements
NATIVE_DATABASE_TYPES = {
primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
string: { name: "varchar" },
text: { name: "text" },
integer: { name: "integer" },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
boolean: { name: "boolean" },
json: { name: "json" },
}
class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
- alias reset clear
-
private
def dealloc(stmt)
stmt.close unless stmt.closed?
end
end
- 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
- Dir.mkdir(dirname)
- rescue Errno::ENOENT => error
- if error.message.include?("No such file or directory")
- raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
- else
- raise
- end
- end
- end
- end
-
- @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)
- @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
- end
-
- def database_exists?
+ def self.database_exists?(config)
@config[:database] == ":memory:" || File.exist?(@config[:database].to_s)
end
def supports_ddl_transactions?
true
end
def supports_savepoints?
true
end
def supports_transaction_isolation?
true
end
def supports_partial_index?
true
end
def supports_expression_index?
database_version >= "3.9.0"
end
def requires_reloading?
true
end
def supports_foreign_keys?
true
end
def supports_check_constraints?
true
end
def supports_views?
true
end
def supports_datetime_with_precision?
true
end
def supports_json?
true
end
def supports_common_table_expressions?
database_version >= "3.8.3"
end
def supports_insert_returning?
database_version >= "3.35.0"
end
def supports_insert_on_conflict?
database_version >= "3.24.0"
end
alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
alias supports_insert_conflict_target? supports_insert_on_conflict?
+ # DIFFERENCE: active?, reconnect!, disconnect! handles by arjdbc core
def supports_concurrent_connections?
!@memory_database
end
def active?
@raw_connection && !@raw_connection.closed?
end
def return_value_after_insert?(column) # :nodoc:
column.auto_populated?
end
- alias :reset! :reconnect!
+ # MISSING: alias :reset! :reconnect!
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
super
@raw_connection&.close rescue nil
@raw_connection = nil
end
def supports_index_sort_order?
true
end
def native_database_types # :nodoc:
NATIVE_DATABASE_TYPES
end
- # Returns the current database encoding format as a string, e.g. 'UTF-8'
+ # Returns the current database encoding format as a string, eg: 'UTF-8'
def encoding
any_raw_connection.encoding.to_s
end
def supports_explain?
true
end
def supports_lazy_transactions?
true
end
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity # :nodoc:
old_foreign_keys = query_value("PRAGMA foreign_keys")
old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys")
begin
execute("PRAGMA defer_foreign_keys = ON")
execute("PRAGMA foreign_keys = OFF")
yield
ensure
execute("PRAGMA defer_foreign_keys = #{old_defer_foreign_keys}")
execute("PRAGMA foreign_keys = #{old_foreign_keys}")
end
end
def check_all_foreign_keys_valid! # :nodoc:
sql = "PRAGMA foreign_key_check"
result = execute(sql)
unless result.blank?
tables = result.map { |row| row["table"] }
raise ActiveRecord::StatementInvalid.new("Foreign key violations found: #{tables.join(", ")}", sql: sql)
end
end
# SCHEMA STATEMENTS ========================================
def primary_keys(table_name) # :nodoc:
pks = table_structure(table_name).select { |f| f["pk"] > 0 }
pks.sort_by { |f| f["pk"] }.map { |f| f["name"] }
end
def remove_index(table_name, column_name = nil, **options) # :nodoc:
return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
index_name = index_name_for_remove(table_name, column_name, options)
- exec_query "DROP INDEX #{quote_column_name(index_name)}"
+ internal_exec_query "DROP INDEX #{quote_column_name(index_name)}"
end
+
# Renames a table.
#
# Example:
# rename_table('octopuses', 'octopi')
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)}"
+ internal_exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
rename_table_indexes(table_name, new_name)
end
def add_column(table_name, column_name, type, **options) # :nodoc:
if invalid_alter_table_type?(type, options)
alter_table(table_name) do |definition|
definition.column(column_name, type, **options)
end
else
super
end
end
def remove_column(table_name, column_name, type = nil, **options) # :nodoc:
alter_table(table_name) do |definition|
definition.remove_column column_name
definition.foreign_keys.delete_if { |fk| fk.column == column_name.to_s }
end
end
def remove_columns(table_name, *column_names, type: nil, **options) # :nodoc:
alter_table(table_name) do |definition|
column_names.each do |column_name|
definition.remove_column column_name
end
column_names = column_names.map(&:to_s)
definition.foreign_keys.delete_if { |fk| column_names.include?(fk.column) }
end
end
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
default = extract_new_default_value(default_or_changes)
alter_table(table_name) do |definition|
definition[column_name].default = default
end
end
def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
validate_change_column_null_argument!(null)
unless null || default.nil?
internal_exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
alter_table(table_name) do |definition|
definition[column_name].null = null
end
end
def change_column(table_name, column_name, type, **options) # :nodoc:
alter_table(table_name) do |definition|
definition.change_column(column_name, type, **options)
end
end
def rename_column(table_name, column_name, new_column_name) # :nodoc:
column = column_for(table_name, column_name)
alter_table(table_name, rename: { column.name => new_column_name.to_s })
rename_column_indexes(table_name, column.name, new_column_name)
end
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
def add_reference(table_name, ref_name, **options) # :nodoc:
super(table_name, ref_name, type: :integer, **options)
end
alias :add_belongs_to :add_reference
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")
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"])
}
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)
+ # DIFFERENCE: FQN
+ ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, row["table"], options)
end
end
def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
if insert.skip_duplicates?
sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
elsif insert.update_duplicates?
sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
if insert.raw_update_sql?
sql << insert.raw_update_sql
else
sql << insert.touch_model_timestamps_unless { |column| "#{column} IS excluded.#{column}" }
sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
end
end
sql << " RETURNING #{insert.returning}" if insert.returning
sql
end
def shared_cache? # :nodoc:
@config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
end
def use_insert_returning?
@use_insert_returning
end
def get_database_version # :nodoc:
SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)", "SCHEMA"))
end
- def check_version # :nodoc:
+ def check_version
if database_version < "3.8.0"
raise "Your version of SQLite (#{database_version}) is too old. Active Record supports SQLite >= 3.8."
end
end
- class SQLite3Integer < Type::Integer # :nodoc:
- private
- def _limit
- # INTEGER storage class can be stored 8 bytes value.
- # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
- limit || 8
- end
- end
+ # DIFFERENCE: here to
+ def new_column_from_field(table_name, field, definitions)
+ default = field["dflt_value"]
- ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3)
+ type_metadata = fetch_type_metadata(field["type"])
+ default_value = extract_value_from_default(default)
+ default_function = extract_default_function(default_value, default)
+ rowid = is_column_the_rowid?(field, definitions)
- class << self
- private
- def initialize_type_map(m)
- super
- register_class_with_limit m, %r(int)i, SQLite3Integer
+ ActiveRecord::ConnectionAdapters::SQLite3Column.new(
+ field["name"],
+ default_value,
+ type_metadata,
+ field["notnull"].to_i == 0,
+ default_function,
+ collation: field["collation"],
+ auto_increment: field["auto_increment"],
+ rowid: rowid
+ )
end
- end
- TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
- EXTENDED_TYPE_MAPS = Concurrent::Map.new
-
private
# See https://www.sqlite.org/limits.html,
# the default value is 999 when not configured.
def bind_params_length
999
end
def table_structure(table_name)
structure = internal_exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
table_structure_with_collation(table_name, structure)
end
alias column_definitions table_structure
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
def extract_default_function(default_value, default)
default if has_default_function?(default_value, default)
end
def has_default_function?(default_value, default)
!default_value && %r{\w+\(.*\)|CURRENT_TIME|CURRENT_DATE|CURRENT_TIMESTAMP|\|\|}.match?(default)
end
# See: https://www.sqlite.org/lang_altertable.html
# SQLite has an additional restriction on the ALTER TABLE statement
def invalid_alter_table_type?(type, options)
type.to_sym == :primary_key || options[:primary_key] ||
options[:null] == false && options[:default].nil?
end
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
def move_table(from, to, options = {}, &block)
copy_table(from, to, options, &block)
drop_table(from)
end
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
if column.has_default?
type = lookup_cast_type_from_column(column)
default = type.deserialize(column.default)
default = -> { column.default_function } if default.nil?
end
column_options = {
limit: column.limit,
precision: column.precision,
scale: column.scale,
null: column.null,
collation: column.collation,
primary_key: column_name == from_primary_key
}
unless column.auto_increment?
column_options[:default] = default
end
column_type = 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] || {})
copy_table_contents(from, to,
@definition.columns.map(&:name),
options[:rename] || {})
end
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
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
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)
+ # DIFFERENCE: FQN
+ ::ActiveRecord::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)
+ # DIFFERENCE: FQN
+ ::ActiveRecord::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)
+ # DIFFERENCE: FQN
+ ::ActiveRecord::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)
+ # DIFFERENCE: FQN
+ ::ActiveRecord::ConnectionNotEstablished.new(exception, connection_pool: @pool)
+ elsif exception.message.match?(/sql error/i)
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
+ elsif exception.message.match?(/write a readonly database/i)
+ message = message.sub('org.sqlite.SQLiteException', 'SQLite3::ReadOnlyException')
+ ::ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds, connection_pool: @pool)
else
super
end
end
- COLLATE_REGEX = /.*"(\w+)".*collate\s+"(\w+)".*/i
- PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*"(\w+)".+PRIMARY KEY AUTOINCREMENT/i
+ COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze
+ PRIMARY_KEY_AUTOINCREMENT_REGEX = /.*\"(\w+)\".+PRIMARY KEY AUTOINCREMENT/i
def table_structure_with_collation(table_name, basic_structure)
collation_hash = {}
auto_increments = {}
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");
result = query_value(sql, "SCHEMA")
if result
# Splitting with left parentheses and discarding the first part will return all
# columns separated with comma(,).
columns_string = result.split("(", 2).last
columns_string.split(",").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
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
column
end
else
basic_structure.to_a
end
end
def arel_visitor
Arel::Visitors::SQLite.new(self)
end
def build_statement_pool
StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
end
- def connect
- @raw_connection = self.class.new_client(@connection_parameters)
- rescue ConnectionNotEstablished => ex
- raise ex.set_pool(@pool)
+ def configure_connection
+ if @config[:timeout] && @config[:retries]
+ raise ArgumentError, "Cannot specify both timeout and retries arguments"
+ elsif @config[:timeout]
+ # FIXME: missing from adapter
+ # @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
+ elsif @config[:retries]
+ retries = self.class.type_cast_config_to_integer(@config[:retries])
+ raw_connection.busy_handler do |count|
+ count <= retries
end
+ end
- def reconnect
- if active?
- @raw_connection.rollback rescue nil
- else
- connect
+ # Enforce foreign key constraints
+ # https://www.sqlite.org/pragma.html#pragma_foreign_keys
+ # https://www.sqlite.org/foreignkeys.html
+ raw_execute("PRAGMA foreign_keys = ON", "SCHEMA")
+ unless @memory_database
+ # Journal mode WAL allows for greater concurrency (many readers + one writer)
+ # https://www.sqlite.org/pragma.html#pragma_journal_mode
+ raw_execute("PRAGMA journal_mode = WAL", "SCHEMA")
+ # Set more relaxed level of database durability
+ # 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
+ # https://www.sqlite.org/pragma.html#pragma_synchronous
+ raw_execute("PRAGMA synchronous = NORMAL", "SCHEMA")
+ # Set the global memory map so all processes can share some data
+ # https://www.sqlite.org/pragma.html#pragma_mmap_size
+ # https://www.sqlite.org/mmap.html
+ raw_execute("PRAGMA mmap_size = #{128.megabytes}", "SCHEMA")
end
+ # Impose a limit on the WAL file to prevent unlimited growth
+ # https://www.sqlite.org/pragma.html#pragma_journal_size_limit
+ raw_execute("PRAGMA journal_size_limit = #{64.megabytes}", "SCHEMA")
+ # Set the local connection cache to 2000 pages
+ # https://www.sqlite.org/pragma.html#pragma_cache_size
+ raw_execute("PRAGMA cache_size = 2000", "SCHEMA")
end
def configure_connection
if @config[:timeout] && @config[:retries]
raise ArgumentError, "Cannot specify both timeout and retries arguments"
elsif @config[:timeout]
- @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
+ # FIXME:
+# @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
elsif @config[:retries]
retries = self.class.type_cast_config_to_integer(@config[:retries])
raw_connection.busy_handler do |count|
count <= retries
end
end
# Enforce foreign key constraints
# https://www.sqlite.org/pragma.html#pragma_foreign_keys
# https://www.sqlite.org/foreignkeys.html
raw_execute("PRAGMA foreign_keys = ON", "SCHEMA")
unless @memory_database
# Journal mode WAL allows for greater concurrency (many readers + one writer)
# https://www.sqlite.org/pragma.html#pragma_journal_mode
raw_execute("PRAGMA journal_mode = WAL", "SCHEMA")
# Set more relaxed level of database durability
# 2 = "FULL" (sync on every write), 1 = "NORMAL" (sync every 1000 written pages) and 0 = "NONE"
# https://www.sqlite.org/pragma.html#pragma_synchronous
raw_execute("PRAGMA synchronous = NORMAL", "SCHEMA")
# Set the global memory map so all processes can share some data
# https://www.sqlite.org/pragma.html#pragma_mmap_size
# https://www.sqlite.org/mmap.html
raw_execute("PRAGMA mmap_size = #{128.megabytes}", "SCHEMA")
end
# Impose a limit on the WAL file to prevent unlimited growth
# https://www.sqlite.org/pragma.html#pragma_journal_size_limit
raw_execute("PRAGMA journal_size_limit = #{64.megabytes}", "SCHEMA")
# Set the local connection cache to 2000 pages
# https://www.sqlite.org/pragma.html#pragma_cache_size
raw_execute("PRAGMA cache_size = 2000", "SCHEMA")
end
end
- ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
+ # DIFFERENCE: A registration here is moved down to concrete class so we are not registering part of an adapter.
end
+
+module ActiveRecord::ConnectionAdapters
+
+ remove_const(:SQLite3Adapter) if const_defined?(:SQLite3Adapter)
+
+ # Currently our adapter is named the same as what AR5 names its adapter. We will need to get
+ # this changed at some point so this can be a unique name and we can extend activerecord
+ # ActiveRecord::ConnectionAdapters::SQLite3Adapter. Once we can do that we can remove the
+ # module SQLite3 above and remove a majority of this file.
+ class SQLite3Adapter < AbstractAdapter
+ include ArJdbc::Abstract::Core
+ include ArJdbc::SQLite3
+ include ArJdbc::Abstract::ConnectionManagement
+ include ArJdbc::Abstract::DatabaseStatements
+ include ArJdbc::Abstract::StatementCache
+ include ArJdbc::Abstract::TransactionSupport
+
+ ##
+ # :singleton-method:
+ # 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
+ class_attribute :strict_strings_by_default, default: false # Does not actually do anything right now
+
+ def self.represent_boolean_as_integer=(value) # :nodoc:
+ if value == false
+ raise "`.represent_boolean_as_integer=` is now always true, so make sure your application can work with it and remove this settings."
end
+
+ ActiveSupport::Deprecation.warn(
+ "`.represent_boolean_as_integer=` is now always true, so setting this is deprecated and will be removed in Rails 6.1."
+ )
+ end
+
+ def self.database_exists?(config)
+ config = config.symbolize_keys
+ if config[:database] == ":memory:"
+ return true
+ else
+ database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database]
+ File.exist?(database_file)
+ end
+ end
+
+
+ def supports_transaction_isolation?
+ false
+ end
+
+ def begin_isolated_db_transaction(isolation)
+ raise ActiveRecord::TransactionIsolationError, "SQLite3 only supports the `read_uncommitted` transaction isolation level" if isolation != :read_uncommitted
+ raise StandardError, "You need to enable the shared-cache mode in SQLite mode before attempting to change the transaction isolation level" unless shared_cache?
+ super
+ end
+
+ # SQLite driver doesn't support all types of insert statements with executeUpdate so
+ # make it act like a regular query and the ids will be returned from #last_inserted_id
+ # example: INSERT INTO "aircraft" DEFAULT VALUES
+ def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
+ sql, binds = sql_for_insert(sql, pk, binds, returning)
+ internal_exec_query(sql, name, binds)
+ end
+
+ def jdbc_column_class
+ ::ActiveRecord::ConnectionAdapters::SQLite3Column
+ end
+
+ def jdbc_connection_class(spec)
+ self.class.jdbc_connection_class
+ end
+
+ # @see ActiveRecord::ConnectionAdapters::JdbcAdapter#jdbc_connection_class
+ def self.jdbc_connection_class
+ ::ActiveRecord::ConnectionAdapters::SQLite3JdbcConnection
+ end
+
+ # Note: This is not an override of ours but a moved line from AR Sqlite3Adapter to register ours vs our copied module (which would be their class).
+# ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
+
+ # DIFFERENCE: FQN
+ class SQLite3Integer < ::ActiveRecord::Type::Integer # :nodoc:
+ private
+ def _limit
+ # INTEGER storage class can be stored 8 bytes value.
+ # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
+ limit || 8
+ end
+ end
+
+ # DIFFERENCE: FQN
+ ::ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3)
+
+ class << self
+ def dbconsole(config, options = {})
+ args = []
+
+ args << "-#{options[:mode]}" if options[:mode]
+ args << "-header" if options[:header]
+ args << File.expand_path(config.database, const_defined?(:Rails) && Rails.respond_to?(:root) ? Rails.root : nil)
+
+ find_cmd_and_exec("sqlite3", *args)
+ end
+
+ private
+ def initialize_type_map(m)
+ super
+ register_class_with_limit m, %r(int)i, SQLite3Integer
+ end
+ end
+
+ TYPE_MAP = ActiveRecord::Type::TypeMap.new.tap { |m| initialize_type_map(m) }
+
+ private
+
+ # because the JDBC driver doesn't like multiple SQL statements in one JDBC statement
+ def combine_multi_statements(total_sql)
+ total_sql
+ end
+
+ def type_map
+ TYPE_MAP
+ end
+
+ # combine
+ def write_query?(sql) # :nodoc:
+ return sql.any? { |stmt| super(stmt) } if sql.kind_of? Array
+ !READ_QUERY.match?(sql)
+ rescue ArgumentError # Invalid encoding
+ !READ_QUERY.match?(sql.b)
+ end
+ end
+end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment