diff --git a/lib/arjdbc/abstract/database_statements.rb b/lib/arjdbc/abstract/database_statements.rb index e7748aee6..876f49295 100644 --- a/lib/arjdbc/abstract/database_statements.rb +++ b/lib/arjdbc/abstract/database_statements.rb @@ -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 diff --git a/lib/arjdbc/sqlite3/adapter.rb b/lib/arjdbc/sqlite3/adapter.rb index 9740afba3..b978efe3b 100644 --- a/lib/arjdbc/sqlite3/adapter.rb +++ b/lib/arjdbc/sqlite3/adapter.rb @@ -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" @@ -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", @@ -81,8 +83,6 @@ module SQLite3 } class StatementPool < ConnectionAdapters::StatementPool # :nodoc: - alias reset clear - private def dealloc(stmt) stmt.close unless stmt.closed? @@ -90,10 +90,21 @@ def dealloc(stmt) 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 @@ -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. @@ -178,7 +181,6 @@ def disconnect! @connection.close rescue nil end - def supports_index_sort_order? true end @@ -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 ======================================== @@ -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: @@ -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}" @@ -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." @@ -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 @@ -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) diff --git a/src/java/arjdbc/jdbc/RubyJdbcConnection.java b/src/java/arjdbc/jdbc/RubyJdbcConnection.java index 3efed49ef..588e777b0 100644 --- a/src/java/arjdbc/jdbc/RubyJdbcConnection.java +++ b/src/java/arjdbc/jdbc/RubyJdbcConnection.java @@ -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); diff --git a/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java b/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java index 4e14a9156..35526781f 100644 --- a/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java +++ b/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java @@ -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,