Skip to content

Commit

Permalink
Add execute_batch2 for sqlite3 and closed? for all adapters.
Browse files Browse the repository at this point in the history
Our execute_batch2 is same name as cext method but it works with
an array instead of a semicolon-delimeted string.
  • Loading branch information
enebo committed May 5, 2022
1 parent 304ea13 commit 781b792
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 80 deletions.
2 changes: 1 addition & 1 deletion lib/arjdbc/abstract/database_statements.rb
Expand Up @@ -30,7 +30,7 @@ def exec_insert(sql, name = nil, binds = NO_BINDS, pk = nil, sequence_name = nil

# It appears that at this point (AR 5.0) "prepare" should only ever be true
# if prepared statements are enabled
def exec_query(sql, name = nil, binds = NO_BINDS, prepare: false)
def exec_query(sql, name = nil, binds = NO_BINDS, prepare: false, async: false)
if preventing_writes? && write_query?(sql)
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
end
Expand Down
110 changes: 31 additions & 79 deletions lib/arjdbc/sqlite3/adapter.rb
Expand Up @@ -10,6 +10,7 @@
require "active_record/connection_adapters/statement_pool"
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"
Expand Down Expand Up @@ -64,6 +65,7 @@ module SQLite3
# 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",
Expand All @@ -81,19 +83,28 @@ module SQLite3
}

class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
alias reset clear

private
def dealloc(stmt)
stmt.close unless stmt.closed?
end
end

def initialize(connection, logger, connection_options, config)
@memory_database = config[:database] == ":memory:"
super(connection, logger, config)
configure_connection
end

def self.database_exists?(config)
config = config.symbolize_keys
if config[:database] == ":memory:"
true
else
database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database]
File.exist?(database_file)
end
end

def supports_ddl_transactions?
true
end
Expand Down Expand Up @@ -158,18 +169,10 @@ def active?
!@raw_connection.closed?
end

def reconnect!(restore_transactions: false)
@lock.synchronize do
if active?
@connection.rollback rescue nil
else
connect
end

super
end
def reconnect!
super
connect if @connection.closed?
end
alias :reset! :reconnect!

# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
Expand All @@ -178,7 +181,6 @@ def disconnect!
@connection.close rescue nil
end


def supports_index_sort_order?
true
end
Expand Down Expand Up @@ -216,48 +218,8 @@ def disable_referential_integrity # :nodoc:
end
end

#--
# DATABASE STATEMENTS ======================================
#++

READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
:pragma
) # :nodoc:
private_constant :READ_QUERY

def write_query?(sql) # :nodoc:
!READ_QUERY.match?(sql)
end

def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
# DIFFERENCE: FQN
::ActiveRecord::ConnectionAdapters::SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
end

# DIFFERENCE: implemented in ArJdbc::Abstract::DatabaseStatements
#def exec_query(sql, name = nil, binds = [], prepare: false)

# DIFFERENCE: implemented in ArJdbc::Abstract::DatabaseStatements
#def exec_delete(sql, name = "SQL", binds = [])

def last_inserted_id(result)
@connection.last_insert_row_id
end

# DIFFERENCE: implemented in ArJdbc::Abstract::DatabaseStatements
#def execute(sql, name = nil) #:nodoc:

def begin_db_transaction #:nodoc:
log("begin transaction", 'TRANSACTION') { @connection.transaction }
end

def commit_db_transaction #:nodoc:
log("commit transaction", 'TRANSACTION') { @connection.commit }
end

def exec_rollback_db_transaction #:nodoc:
log("rollback transaction", 'TRANSACTION') { @connection.rollback }
def all_foreign_keys_valid? # :nodoc:
execute("PRAGMA foreign_key_check").blank?
end

# SCHEMA STATEMENTS ========================================
Expand All @@ -275,6 +237,7 @@ def remove_index(table_name, column_name = nil, **options) # :nodoc:
exec_query "DROP INDEX #{quote_column_name(index_name)}"
end


# Renames a table.
#
# Example:
Expand Down Expand Up @@ -366,18 +329,6 @@ def foreign_keys(table_name)
end
end

def insert_fixtures_set(fixture_set, tables_to_delete = [])
disable_referential_integrity do
transaction(requires_new: true) do
tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }

fixture_set.each do |table_name, rows|
rows.each { |row| insert_fixture(row, table_name) }
end
end
end
end

def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"

Expand All @@ -392,18 +343,14 @@ def build_insert_sql(insert) # :nodoc:
sql
end

def shared_cache?
config[:properties] && config[:properties][:shared_cache] == true
def shared_cache? # :nodoc:
@config.fetch(:flags, 0).anybits?(::SQLite3::Constants::Open::SHAREDCACHE)
end

def get_database_version # :nodoc:
SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)"))
end

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

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."
Expand Down Expand Up @@ -553,6 +500,7 @@ def copy_table_indexes(from, to, rename = {})
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
Expand Down Expand Up @@ -803,11 +751,15 @@ def self.jdbc_connection_class

# because the JDBC driver doesn't like multiple SQL statements in one JDBC statement
def combine_multi_statements(total_sql)
if total_sql.length == 1
total_sql.first
else
total_sql
end
total_sql
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

def initialize_type_map(m = type_map)
Expand Down
15 changes: 15 additions & 0 deletions src/java/arjdbc/jdbc/RubyJdbcConnection.java
Expand Up @@ -720,6 +720,21 @@ public IRubyObject open_p(final ThreadContext context) {
}
}

@JRubyMethod(name = "closed?")
public IRubyObject closed_p(ThreadContext context) {
try {
final Connection connection = getConnectionInternal(false);

if (connection == null) return context.fals;

// NOTE: isClosed method generally cannot be called to determine
// whether a connection to a database is valid or invalid ...
return context.runtime.newBoolean(connection.isClosed());
} catch (SQLException e) {
return handleException(context, e);
}
}

@JRubyMethod(name = "close")
public IRubyObject close(final ThreadContext context) {
final Connection connection = getConnection(false);
Expand Down
28 changes: 28 additions & 0 deletions src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java
Expand Up @@ -460,6 +460,34 @@ public IRubyObject readonly_p(final ThreadContext context) throws SQLException {
return context.runtime.newBoolean(connection.isReadOnly());
}

// note: sqlite3 cext uses this same method but we do not combine all our statements
// into a single ; delimited string but leave it as an array of statements. This is
// because the JDBC way of handling batches is to use addBatch().
@JRubyMethod(name = "execute_batch2")
public IRubyObject execute_batch2(ThreadContext context, IRubyObject statementsArg) {
// Assume we will only call this with an array.
final RubyArray statements = (RubyArray) statementsArg;
return withConnection(context, connection -> {
Statement statement = null;
try {
statement = createStatement(context, connection);

int length = statements.getLength();
for (int i = 0; i < length; i++) {
statement.addBatch(sqlString(statements.eltOk(i)));
}
statement.executeBatch();
return context.nil;
} catch (final SQLException e) {
// Generate list semicolon list of statements which should match AR error formatting more.
debugErrorSQL(context, sqlString(statements.join(context, context.runtime.newString(";\n"))));
throw e;
} finally {
close(statement);
}
});
}

@Override
protected void setDecimalParameter(final ThreadContext context,
final Connection connection, final PreparedStatement statement,
Expand Down

0 comments on commit 781b792

Please sign in to comment.