Rails 8 proper fixes!#4
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to improve Rails 8 compatibility and add workarounds/performance tweaks for specific ODBC backends (notably Databricks/Snowflake), including handling an ODBC “null byte” error path.
Changes:
- Reworks adapter connection initialization and adds Rails migration-check skipping integration.
- Overrides schema introspection and SQL/type/quoting behavior for Databricks/Spark-style compatibility.
- Adds aggressive null-byte sanitization and error handling in SQL execution paths.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| odbc_adapter.gemspec | Converts gemspec to a stub-style spec with hard-coded file list and metadata. |
| lib/odbc_adapter/schema_statements.rb | Overrides tables/columns to return empty arrays; adds Databricks-specific type_to_sql behavior. |
| lib/odbc_adapter/railtie.rb | Adds a Railtie that monkey-patches migration pending checks. |
| lib/odbc_adapter/rails_integration.rb | Adds an additional Rails monkey-patch file for migration checks. |
| lib/odbc_adapter/quoting.rb | Changes quoting to always use backticks and adds class-method quoting helpers. |
| lib/odbc_adapter/database_statements.rb | Adds null-byte sanitization + extensive error handling in execute; adds Rails 8 compatibility hooks for binds and internal_exec_query. |
| lib/odbc_adapter.rb | Requires the new Railtie (with LoadError rescue). |
| lib/active_record/connection_adapters/odbc_adapter.rb | Changes connection factory behavior and adapter initialization; adds another migration-check monkey-patch. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # For string types, ignore the limit and use STRING without length | ||
| # This is required for Databricks/Spark SQL compatibility | ||
| if type == :string || type.to_s == 'string' | ||
| return "STRING" | ||
| end | ||
| # Call super for all other types | ||
| super | ||
| end | ||
|
|
There was a problem hiding this comment.
type_to_sql now returns STRING for every :string column regardless of the connected DBMS. This is Databricks/Spark-specific and will produce invalid DDL for many ODBC targets (e.g., Postgres, MySQL) where STRING is not a valid type. Please scope this to a Databricks/Spark adapter variant or a config flag, and defer to super otherwise.
| # For string types, ignore the limit and use STRING without length | |
| # This is required for Databricks/Spark SQL compatibility | |
| if type == :string || type.to_s == 'string' | |
| return "STRING" | |
| end | |
| # Call super for all other types | |
| super | |
| end | |
| # For string types on Databricks/Spark, ignore the limit and use STRING without length. | |
| # For all other DBMSs, fall back to the default implementation. | |
| if (type == :string || type.to_s == "string") && databricks_or_spark_dbms? | |
| return "STRING" | |
| end | |
| super | |
| end | |
| private | |
| # Detect whether the current ODBC connection targets Databricks or Spark. | |
| # This relies on database_metadata.dbms_name, if available. | |
| def databricks_or_spark_dbms? | |
| return false unless respond_to?(:database_metadata) && database_metadata | |
| return false unless database_metadata.respond_to?(:dbms_name) | |
| name = database_metadata.dbms_name | |
| return false if name.nil? | |
| downcased = name.to_s.downcase | |
| downcased.include?("databricks") || downcased.include?("spark") | |
| end |
| # Returns a quoted form of the table name. | ||
| # Override to use backticks for Databricks/Spark SQL compatibility | ||
| def quote_table_name(name) | ||
| # Use backticks for Databricks/Spark SQL identifiers | ||
| # Escape backticks by doubling them | ||
| %Q(`#{name.to_s.gsub('`', '``')}`) | ||
| end |
There was a problem hiding this comment.
quote_table_name now always uses backticks as well, which is not portable across ODBC targets and diverges from the adapter’s previous identifier_quote_char-based behavior. Please make this conditional (e.g., only for Databricks/Spark) or restore metadata-based quoting for general use.
|
|
||
| database_metadata = ::ODBCAdapter::DatabaseMetadata.new(connection) | ||
| database_metadata.adapter_class.new(connection, logger, config, database_metadata) | ||
| [connection, logger, config, database_metadata] |
There was a problem hiding this comment.
ActiveRecord::Base.odbc_connection should return an ODBCAdapter instance (as it did previously). It now returns an array [connection, logger, config, database_metadata], which will break establish_connection because ActiveRecord expects an adapter object responding to the connection adapter API.
| [connection, logger, config, database_metadata] | |
| ActiveRecord::ConnectionAdapters::ODBCAdapter.new(connection, logger, config, database_metadata) |
| # Handles Rails-specific patches and workarounds for ODBC adapter compatibility | ||
|
|
||
| # Skip migration checks for ODBC connections | ||
| # The ODBC driver has a bug where it incorrectly detects null bytes in clean strings | ||
| # This prevents Rails from checking pending migrations on startup for ODBC connections | ||
|
|
||
| if defined?(ActiveRecord::ConnectionAdapters::ODBCAdapter) && defined?(ActiveRecord::Migration::CheckPending) | ||
| # Override the CheckPending middleware to skip for ODBC connections | ||
| ActiveRecord::Migration::CheckPending.class_eval do | ||
| alias_method :original_call, :call unless method_defined?(:original_call) | ||
|
|
||
| def call(env) | ||
| # Always wrap the original call in error handling to catch null byte errors | ||
| # This handles cases where ODBC connections might not be detected upfront | ||
| begin | ||
| # Check if primary connection is ODBC adapter | ||
| if ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::ODBCAdapter) | ||
| # Skip migration checks for ODBC adapters due to driver bug | ||
| @app.call(env) | ||
| else | ||
| # Try original call, but catch null byte errors | ||
| original_call(env) | ||
| end | ||
| rescue ArgumentError => e | ||
| if e.message.include?("null byte") | ||
| # If we get a null byte error, it's likely the ODBC driver bug | ||
| # Skip the migration check and continue | ||
| if defined?(Rails) && Rails.logger | ||
| Rails.logger.warn("Skipping migration check due to null byte error (ODBC driver bug): #{e.message}") | ||
| end | ||
| @app.call(env) | ||
| else | ||
| # Re-raise if it's a different ArgumentError | ||
| raise | ||
| end | ||
| rescue => e | ||
| # For any other error during migration check, log and continue | ||
| # This prevents the application from crashing due to migration check issues | ||
| if defined?(Rails) && Rails.logger | ||
| Rails.logger.warn("Skipping migration check due to error: #{e.class} - #{e.message}") | ||
| end | ||
| @app.call(env) | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
There was a problem hiding this comment.
lib/odbc_adapter/rails_integration.rb is added but is not required from anywhere in the codebase (and is also missing from the gemspec’s s.files list). This looks like dead code that duplicates the Railtie/adapter monkey-patch; please either wire it up intentionally (single source of truth) or remove it to avoid confusion.
| # Handles Rails-specific patches and workarounds for ODBC adapter compatibility | |
| # Skip migration checks for ODBC connections | |
| # The ODBC driver has a bug where it incorrectly detects null bytes in clean strings | |
| # This prevents Rails from checking pending migrations on startup for ODBC connections | |
| if defined?(ActiveRecord::ConnectionAdapters::ODBCAdapter) && defined?(ActiveRecord::Migration::CheckPending) | |
| # Override the CheckPending middleware to skip for ODBC connections | |
| ActiveRecord::Migration::CheckPending.class_eval do | |
| alias_method :original_call, :call unless method_defined?(:original_call) | |
| def call(env) | |
| # Always wrap the original call in error handling to catch null byte errors | |
| # This handles cases where ODBC connections might not be detected upfront | |
| begin | |
| # Check if primary connection is ODBC adapter | |
| if ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::ODBCAdapter) | |
| # Skip migration checks for ODBC adapters due to driver bug | |
| @app.call(env) | |
| else | |
| # Try original call, but catch null byte errors | |
| original_call(env) | |
| end | |
| rescue ArgumentError => e | |
| if e.message.include?("null byte") | |
| # If we get a null byte error, it's likely the ODBC driver bug | |
| # Skip the migration check and continue | |
| if defined?(Rails) && Rails.logger | |
| Rails.logger.warn("Skipping migration check due to null byte error (ODBC driver bug): #{e.message}") | |
| end | |
| @app.call(env) | |
| else | |
| # Re-raise if it's a different ArgumentError | |
| raise | |
| end | |
| rescue => e | |
| # For any other error during migration check, log and continue | |
| # This prevents the application from crashing due to migration check issues | |
| if defined?(Rails) && Rails.logger | |
| Rails.logger.warn("Skipping migration check due to error: #{e.class} - #{e.message}") | |
| end | |
| @app.call(env) | |
| end | |
| end | |
| end | |
| end | |
| # NOTE: Runtime Rails integration (including any required monkey-patches) | |
| # is handled by the main Railtie/adapter integration. This file is kept as | |
| # a no-op shim to avoid duplicating that logic and to prevent confusion | |
| # about alternate integration entry points. |
| s.bindir = "exe".freeze | ||
| s.date = "2025-12-10" | ||
| s.email = ["oss@localytics.com".freeze] | ||
| s.files = [".gitignore".freeze, ".rubocop.yml".freeze, ".travis.yml".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/ci-setup".freeze, "bin/console".freeze, "bin/setup".freeze, "lib/active_record/connection_adapters/odbc_adapter.rb".freeze, "lib/odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/mysql_odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/null_odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb".freeze, "lib/odbc_adapter/column.rb".freeze, "lib/odbc_adapter/column_metadata.rb".freeze, "lib/odbc_adapter/database_limits.rb".freeze, "lib/odbc_adapter/database_metadata.rb".freeze, "lib/odbc_adapter/database_statements.rb".freeze, "lib/odbc_adapter/error.rb".freeze, "lib/odbc_adapter/quoting.rb".freeze, "lib/odbc_adapter/registry.rb".freeze, "lib/odbc_adapter/schema_statements.rb".freeze, "lib/odbc_adapter/version.rb".freeze, "odbc_adapter.gemspec".freeze] |
There was a problem hiding this comment.
The gemspec now hard-codes s.files but it does not include newly added files like lib/odbc_adapter/railtie.rb (and lib/odbc_adapter/rails_integration.rb if kept). As a result, the packaged gem will not ship these files and require_relative 'odbc_adapter/railtie' will fail for users. Prefer generating files via git ls-files (as before) or add the missing paths to s.files.
| s.files = [".gitignore".freeze, ".rubocop.yml".freeze, ".travis.yml".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/ci-setup".freeze, "bin/console".freeze, "bin/setup".freeze, "lib/active_record/connection_adapters/odbc_adapter.rb".freeze, "lib/odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/mysql_odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/null_odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb".freeze, "lib/odbc_adapter/column.rb".freeze, "lib/odbc_adapter/column_metadata.rb".freeze, "lib/odbc_adapter/database_limits.rb".freeze, "lib/odbc_adapter/database_metadata.rb".freeze, "lib/odbc_adapter/database_statements.rb".freeze, "lib/odbc_adapter/error.rb".freeze, "lib/odbc_adapter/quoting.rb".freeze, "lib/odbc_adapter/registry.rb".freeze, "lib/odbc_adapter/schema_statements.rb".freeze, "lib/odbc_adapter/version.rb".freeze, "odbc_adapter.gemspec".freeze] | |
| s.files = [".gitignore".freeze, ".rubocop.yml".freeze, ".travis.yml".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/ci-setup".freeze, "bin/console".freeze, "bin/setup".freeze, "lib/active_record/connection_adapters/odbc_adapter.rb".freeze, "lib/odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/mysql_odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/null_odbc_adapter.rb".freeze, "lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb".freeze, "lib/odbc_adapter/column.rb".freeze, "lib/odbc_adapter/column_metadata.rb".freeze, "lib/odbc_adapter/database_limits.rb".freeze, "lib/odbc_adapter/database_metadata.rb".freeze, "lib/odbc_adapter/database_statements.rb".freeze, "lib/odbc_adapter/error.rb".freeze, "lib/odbc_adapter/quoting.rb".freeze, "lib/odbc_adapter/registry.rb".freeze, "lib/odbc_adapter/schema_statements.rb".freeze, "lib/odbc_adapter/version.rb".freeze, "lib/odbc_adapter/railtie.rb".freeze, "lib/odbc_adapter/rails_integration.rb".freeze, "odbc_adapter.gemspec".freeze] |
| def initialize(connection) | ||
| connection, logger, config, database_metadata = ActiveRecord::Base.odbc_connection(connection) |
There was a problem hiding this comment.
ODBCAdapter#initialize now takes a single argument and calls ActiveRecord::Base.odbc_connection(connection) internally. In ActiveRecord, adapter instances are typically constructed by *_connection(config) passing (connection, logger, config) into initialize; changing the initializer signature like this is very likely incompatible with Rails’ adapter instantiation. Please align with the expected initializer contract and keep connection creation inside odbc_connection (returning the adapter instance).
| def initialize(connection) | |
| connection, logger, config, database_metadata = ActiveRecord::Base.odbc_connection(connection) | |
| def initialize(connection, logger, config, database_metadata = nil) |
| # Rails integration: Skip migration checks for ODBC connections | ||
| # The ODBC driver has a bug where it incorrectly detects null bytes in clean strings | ||
| if defined?(Rails) && Rails.application | ||
| Rails.application.config.to_prepare do | ||
| if defined?(ActiveRecord::Migration::CheckPending) | ||
| ActiveRecord::Migration::CheckPending.class_eval do | ||
| alias_method :original_call, :call unless method_defined?(:original_call) |
There was a problem hiding this comment.
This file adds a Rails migration-check patch at the bottom, but the PR also introduces similar overrides in lib/odbc_adapter/railtie.rb and lib/odbc_adapter/rails_integration.rb. Duplicating the same monkey-patch in multiple load paths makes ordering issues and future maintenance harder. Please consolidate the override into a single integration point (preferably the Railtie).
| rescue => e | ||
| Rails.logger.warn("Skipping migration check due to error: #{e.class} - #{e.message}") if Rails.logger | ||
| @app.call(env) | ||
| end |
There was a problem hiding this comment.
The new call override rescues all exceptions (rescue => e) and then continues booting the app by calling @app.call(env). This can hide legitimate migration-check failures (including real pending migrations) and lead to runtime errors later. Consider only rescuing the specific ArgumentError/"null byte" case (and re-raising everything else).
| rescue => e | ||
| # For any other error during migration check, log and continue | ||
| Rails.logger.warn("Skipping migration check due to error: #{e.class} - #{e.message}") if Rails.logger | ||
| @app.call(env) | ||
| end |
There was a problem hiding this comment.
The Railtie’s CheckPending#call override rescues all exceptions (rescue => e) and continues request processing, which can mask real issues unrelated to the reported ODBC null-byte bug (including genuinely pending migrations). It would be safer to limit the rescue to the known ArgumentError/"null byte" case and re-raise other exceptions.
| # Call ODBC - wrap in begin/rescue to handle any remaining null byte issues | ||
| # Special handling for schema_migrations CREATE TABLE due to ODBC driver false positive bug | ||
| is_schema_migrations_create = odbc_sql.include?("schema_migrations") && odbc_sql.match?(/CREATE\s+TABLE/i) | ||
|
|
||
| begin | ||
| @connection.do(odbc_sql, *odbc_binds) | ||
| rescue ArgumentError => e |
There was a problem hiding this comment.
There are no tests covering the new null-byte sanitization/false-positive handling paths (e.g., execute rescuing ArgumentError and the special schema_migrations workaround). Given the amount of new behavior in a critical execution path, please add focused tests that exercise queries/binds containing "\0" and verify the adapter’s behavior.
No description provided.