Skip to content

Conversation

eddietejeda
Copy link

  • Update database_statements.rb transaction support and query execution, default to using @raw_connection, and add support for binding params
  • Add schema_statements.rb DDL operations (CREATE/ALTER/DROP table support)
  • BREAKING: Remove legacy Railtie approach and use modern ActiveRecord::ConnectionAdapters.register approach for registering gem
  • Add YARD documentation to make it clear which files were implementing necessary methods
  • Default to writing DuckDB file to disk
  • Validated lifecycle of creating and deploying databases and tested bundle exec rails db:drop db:create db:migrate db:seed db:reset
  • Use DuckDB's information_schema for accessing meta data
  • Default primary ids as bigints

- Update database_statements.rb transaction support and query execution, default to using @raw_connection, and add support for binding params
- Add schema_statements.rb DDL operations (CREATE/ALTER/DROP table support)
- BREAKING: Remove legacy Railtie approach and use modern ActiveRecord::ConnectionAdapters.register approach for registering gem
- Add YARD documentation to make it clear which files were implementing necessary methods
- Default to writing DuckDB file to disk
- Validated lifecycle of creating and deploying databases and tested bundle exec rails db:drop db:create db:migrate db:seed db:reset
- Use DuckDB's information_schema for accessing meta data
- Default primary ids as bigints
@eddietejeda eddietejeda changed the title Update core DuckDB adapter implementation Feature / Update core DuckDB adapter implementation Jun 23, 2025
@eddietejeda eddietejeda changed the title Feature / Update core DuckDB adapter implementation Feature/Update core DuckDB adapter implementation Jun 23, 2025
# @param [Boolean] materialize_transactions Whether to materialize transactions
# @return [Object] Query result
def internal_execute(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, &block)
raw_execute(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions, &block)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that raw_execute is used only in this method.
How about inlining the raw_execute implementation here and remove raw_execute?

BTW, can we use the default implementations of internal_execute and raw_execute in Active Record? If we can use them, we can reduce maintenance cost.

Comment on lines +121 to +122
result = internal_execute(sql, name)
result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
result = internal_execute(sql, name)
result
internal_execute(sql, name)

def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil)
if pk && supports_insert_returning?
# Use INSERT...RETURNING to get the inserted ID
returning_sql = sql.sub(/\bINSERT\b/i, "INSERT").concat(" RETURNING #{quote_column_name(pk)}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we don't need the sub here:

Suggested change
returning_sql = sql.sub(/\bINSERT\b/i, "INSERT").concat(" RETURNING #{quote_column_name(pk)}")
returning_sql = "#{sql} RETURNING #{quote_column_name(pk)}"

BTW, do we need to override exec_insert? It seems that we can use the default sql_for_insert and exec_insert implementations in Active Record.

Comment on lines +173 to +183
# @note convert Arel to SQL string
# @param [Object] arel Arel object or SQL string
# @param [Array] binds Bind parameters (unused)
# @return [String] SQL string
def to_sql(arel, binds = [])
if arel.respond_to?(:to_sql)
arel.to_sql
else
arel.to_s
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Can we use the default implementation in Active Record?

Comment on lines +200 to +229
# Our simplified implementation for basic cases
if arel_or_sql_string.respond_to?(:ast)
# For Arel objects, visit the AST to get SQL and collect binds
visitor = arel_visitor
collector = Arel::Collectors::SQLString.new
visitor.accept(arel_or_sql_string.ast, collector)
sql = collector.value

# Extract binds from the visitor if it collected them
visitor_binds = if visitor.respond_to?(:binds)
visitor.binds
else
[]
end

result = [sql, binds + visitor_binds]
# Add any additional args back to maintain signature compatibility
args.each { |arg| result << arg }
result
elsif arel_or_sql_string.respond_to?(:to_sql)
# For objects with to_sql method, use it directly
result = [arel_or_sql_string.to_sql, binds]
args.each { |arg| result << arg }
result
else
# For plain strings, return as-is
result = [arel_or_sql_string.to_s, binds]
args.each { |arg| result << arg }
result
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the custom implementation for this? If we can use the default implementation, we can reduce maintenance cost.

Comment on lines +151 to 156
# @override
# @note Implements AbstractAdapter interface method
# @return [String] The adapter name
def adapter_name # :nodoc:
"DuckDB"
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the default implementation.

Suggested change
# @override
# @note Implements AbstractAdapter interface method
# @return [String] The adapter name
def adapter_name # :nodoc:
"DuckDB"
end

Comment on lines +233 to +241
# @override
# @note Implements AbstractAdapter interface method
# @param [String] table_name Name of the table
# @return [Array<ActiveRecord::ConnectionAdapters::Column>] Array of column objects
def columns(table_name) # :nodoc:
column_definitions(table_name).map do |field|
new_column_from_field(table_name, field)
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the default implementation?

# @param [Array] binds Bind parameters
# @param [Boolean] prepare Whether to prepare statement
# @param [Boolean] async Whether to execute asynchronously
# @return [Object] Query result
def execute_and_clear(sql, name, binds, prepare: false, async: false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this isn't used now. Can we remove this?

each_hash(result)
end
end
# @note used by columns() method
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# @note used by columns() method
# @note used by columns() method

ActiveRecord::Relation::QueryAttribute.new("table_name", table_name, ActiveRecord::Type::String.new)
]

result = internal_exec_query(sql, "SCHEMA", binds)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use query instead of internal_exec_query here?

@kou kou requested a review from Copilot June 23, 2025 03:12
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces major updates to the core DuckDB adapter implementation for ActiveRecord, adding support for binding parameters, DDL operations, and improved connection handling while removing legacy Railtie support.

  • Update version information and remove deprecated Railtie registration
  • Add new registration via ActiveRecord::ConnectionAdapters.register and include enhanced methods for connecting, querying, and schema operations
  • Include comprehensive YARD documentation and enhanced error handling for file-based databases

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/activerecord_duckdb_adapter/version.rb Version updated from 0.1.0 to 0.2.0
lib/activerecord_duckdb_adapter.rb Legacy Railtie removed
lib/activerecord-duckdb-adapter.rb New adapter registration mechanism introduced
lib/active_record/connection_adapters/duckdb_adapter.rb Core adapter updated for connection, quoting, and query execution
lib/active_record/connection_adapters/duckdb/schema_statements.rb Added DDL operations and table creation enhancements
lib/active_record/connection_adapters/duckdb/database_statements.rb Expanded query execution with parameter binding and detailed explanation support
Comments suppressed due to low confidence (1)

lib/active_record/connection_adapters/duckdb_adapter.rb:122

  • [nitpick] The 'reconnect' method currently only returns the existing connection without re-establishing it; if this behavior is intentional, consider renaming it to reflect that or adding reconnection logic for clarity.
      def reconnect

Comment on lines +90 to +91
rescue SystemCallError
raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
Copy link
Preview

Copilot AI Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider capturing and including the original SystemCallError details when directory creation fails, so that the raised ActiveRecord::NoDatabaseError provides more context for debugging.

Suggested change
rescue SystemCallError
raise ActiveRecord::NoDatabaseError.new(connection_pool: @pool)
rescue SystemCallError => e
raise ActiveRecord::NoDatabaseError.new(
"Failed to create directory '#{dirname}': #{e.message}",
connection_pool: @pool,
cause: e
)

Copilot uses AI. Check for mistakes.

# @param [Array] binds Bind parameters
# @param [Array] args Additional arguments
# @return [Array] Array containing SQL string and bind parameters
def to_sql_and_binds(arel_or_sql_string, binds = [], *args)
Copy link
Preview

Copilot AI Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider refactoring 'to_sql_and_binds' to reduce duplication between handling Arel objects and plain SQL strings, which may improve clarity and maintainability.

Copilot uses AI. Check for mistakes.

@eddietejeda
Copy link
Author

FYI: I needed to step away from this, but updates are coming later this month.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants