0.0
The project is in a healthy, maintained state
A Ruby gem that provides a complete database adapter for the Sequel toolkit to work with DuckDB, enabling Ruby applications to connect to and interact with DuckDB databases through Sequel's comprehensive ORM and database abstraction interface.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

~> 5.0, >= 5.0
 Project Readme

Sequel::DuckDB

A Ruby database adapter that enables Sequel to work with DuckDB databases. This gem provides full integration between Sequel's powerful ORM and query building capabilities with DuckDB's high-performance analytical database engine.

Features

  • Complete Sequel Integration: Full compatibility with Sequel's Database and Dataset APIs
  • Connection Management: Support for both file-based and in-memory DuckDB databases
  • Schema Introspection: Automatic discovery of tables, columns, indexes, and constraints
  • SQL Generation: DuckDB-optimized SQL generation for all standard operations
  • Data Type Mapping: Seamless conversion between Ruby and DuckDB data types
  • Transaction Support: Full transaction handling with commit/rollback capabilities
  • Error Handling: Comprehensive error mapping to appropriate Sequel exceptions
  • Performance Optimized: Leverages DuckDB's columnar storage and parallel processing

Installation

Add this line to your application's Gemfile:

gem 'sequel-duckdb'

And then execute:

bundle install

Or install it yourself as:

gem install sequel-duckdb

Quick Start

Basic Connection

require 'sequel'

# Connect to an in-memory database
db = Sequel.connect('duckdb::memory:')

# Connect to a file database
db = Sequel.connect('duckdb:///path/to/database.duckdb')

# Alternative connection syntax
db = Sequel.connect(
  adapter: 'duckdb',
  database: '/path/to/database.duckdb'
)

Basic Usage

# Create a table
db.create_table :users do
  primary_key :id
  String :name, null: false
  String :email
  Integer :age
  DateTime :created_at
end

# Insert data
users = db[:users]
users.insert(name: 'John Doe', email: 'john@example.com', age: 30, created_at: Time.now)
users.insert(name: 'Jane Smith', email: 'jane@example.com', age: 25, created_at: Time.now)

# Query data
puts users.where(age: 30).first
# => {:id=>1, :name=>"John Doe", :email=>"john@example.com", :age=>30, :created_at=>...}

puts users.where { age > 25 }.all
# => [{:id=>1, :name=>"John Doe", ...}, ...]

# Update data
users.where(name: 'John Doe').update(age: 31)

# Delete data
users.where(age: 25).delete

Schema Management

sequel-duckdb supports DuckDB's schema functionality for organizing database objects into logical namespaces.

Creating Schemas

# Create a basic schema
db.create_schema(:analytics)

# Create schema with IF NOT EXISTS
db.create_schema(:staging, if_not_exists: true)

# Create or replace schema
db.create_schema(:temp, or_replace: true)

Dropping Schemas

# Drop an empty schema
db.drop_schema(:analytics)

# Drop schema with IF EXISTS
db.drop_schema(:staging, if_exists: true)

# Drop schema with all objects using CASCADE
db.drop_schema(:temp, cascade: true)

Listing and Checking Schemas

# List all schemas
db.schemas  # => [:main, :analytics, :staging]

# Check if schema exists
db.schema_exists?(:analytics)  # => true

Using Schemas with Tables

# Create table in custom schema
db.create_table(Sequel[:analytics][:sales]) do
  primary_key :id
  String :product
  column :amount, "DECIMAL(10,2)"
  Date :sale_date
end

# Query tables in custom schema
db.fetch("SELECT * FROM analytics.sales WHERE sale_date > ?", [Date.today - 30]).all

# List tables in specific schema
db.tables(schema: "analytics")  # => [:sales, :metrics, ...]

Schema Management Limitations

DuckDB has some limitations compared to other databases:

  • No Schema Ownership: DuckDB doesn't support schema authorization or ownership
  • No Schema Renaming: ALTER SCHEMA RENAME is not supported
  • View Dependencies: Dependencies for views are not tracked by DuckDB
  • No Database DDL: DuckDB doesn't support CREATE DATABASE or DROP DATABASE commands. Instead, databases are created implicitly when you connect to a file path

For attaching additional database files, use raw SQL:

# Attach another database file
db.run("ATTACH 'other.duckdb' AS other")

# Query across attached databases
db.fetch("SELECT * FROM other.schema_name.table_name").all

# Detach database
db.run("DETACH other")

Development

After checking out the repo, run bin/setup to install dependencies:

git clone https://github.com/aguynamedryan/sequel-duckdb.git
cd sequel-duckdb
bin/setup

Running Tests

The test suite uses Minitest and includes both unit tests (using Sequel's mock database) and integration tests (using real DuckDB databases):

# Run all tests
bundle exec rake test

# Run specific test file
ruby test/database_test.rb

# Run tests with verbose output
ruby test/all.rb -v

Development Console

You can run bin/console for an interactive prompt that will allow you to experiment:

bin/console

This will start an IRB session with the gem loaded and a test database available.

Code Quality

The project uses RuboCop for code style enforcement:

# Check code style
bundle exec rubocop

# Auto-fix style issues
bundle exec rubocop -a

Building and Installing

To install this gem onto your local machine:

bundle exec rake install

To build the gem:

bundle exec rake build

Release Process

To release a new version:

  1. Update the version number in lib/sequel/duckdb/version.rb
  2. Update CHANGELOG.md with the new version details
  3. Run the tests to ensure everything works
  4. Commit the changes
  5. Run bundle exec rake release

This will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/aguynamedryan/sequel-duckdb.

Development Guidelines

  1. Follow TDD: Write tests before implementing features
  2. Code Style: Follow the existing RuboCop configuration
  3. Documentation: Update README and code documentation for new features
  4. Compatibility: Ensure compatibility with supported Ruby and Sequel versions
  5. Performance: Consider performance implications, especially for analytical workloads

Reporting Issues

When reporting issues, please include:

  • Ruby version
  • Sequel version
  • DuckDB version
  • Operating system
  • Minimal code example that reproduces the issue
  • Full error message and stack trace

License

The gem is available as open source under the terms of the MIT License.

Acknowledgments

  • Jeremy Evans for creating and maintaining Sequel
  • The DuckDB team for the excellent database engine and Ruby client
  • Contributors to sequel-hexspace and other Sequel adapters for implementation patterns

Connection Options

Connection Strings

# In-memory database (data lost when connection closes)
db = Sequel.connect('duckdb::memory:')

# File database (persistent storage)
db = Sequel.connect('duckdb:///absolute/path/to/database.duckdb')
db = Sequel.connect('duckdb://relative/path/to/database.duckdb')

# With connection options
db = Sequel.connect('duckdb:///path/to/database.duckdb?readonly=true')

readonly opens the file with DuckDB's access_mode set to READ_ONLY, so writes raise Sequel::DatabaseError. All Sequel::Database objects on one file in one process share one DuckDB instance, so the first one to open the file sets its settings. Opening the same file again with other settings, such as read-write after read-only, raises Sequel::DatabaseConnectionError.

Connection Hash

db = Sequel.connect(
  adapter: 'duckdb',
  database: '/path/to/database.duckdb',
  # DuckDB-specific options
  readonly: false,
  config: {
    threads: 4,
    memory_limit: '1GB'
  }
)

Schema Operations

Table Management

# Create table with various column types
db.create_table :products do
  primary_key :id
  String :name, size: 255, null: false
  Text :description
  Decimal :price, size: [10, 2]
  Integer :stock_quantity
  Boolean :active, default: true
  Date :release_date
  DateTime :created_at
  Time :daily_update_time
  column :metadata, 'JSON'  # DuckDB-specific type
end

# Add columns
db.alter_table :products do
  add_column :category_id, Integer
  add_index :category_id
end

# Drop table
db.drop_table :products

Schema Introspection

# List all tables
db.tables
# => [:users, :products, :orders]

# Get table schema
db.schema(:users)
# => [[:id, {:type=>:integer, :db_type=>"INTEGER", :primary_key=>true, ...}],
#     [:name, {:type=>:string, :db_type=>"VARCHAR", :allow_null=>false, ...}], ...]

# Check if table exists
db.table_exists?(:users)
# => true

# Get indexes
db.indexes(:users)
# => {:users_name_index => {:columns=>[:name], :unique=>false, :primary=>false}}

Data Types

Supported Type Mappings

Ruby Type DuckDB Type Notes
String VARCHAR/TEXT Configurable size
Integer INTEGER/BIGINT Auto-sized based on value
Float REAL/DOUBLE Precision preserved
BigDecimal DECIMAL/NUMERIC Precision and scale supported
TrueClass/FalseClass BOOLEAN Native boolean support
Date DATE Date-only values
Time/DateTime TIMESTAMP Full datetime with timezone
Time (time-only) TIME Time-only values
String (binary) BLOB Binary data storage

Type Conversion Examples

# Automatic type conversion
users.insert(
  name: 'Alice',                    # String -> VARCHAR
  age: 28,                         # Integer -> INTEGER
  salary: BigDecimal('75000.50'),  # BigDecimal -> DECIMAL
  active: true,                    # Boolean -> BOOLEAN
  birth_date: Date.new(1995, 5, 15), # Date -> DATE
  created_at: Time.now,            # Time -> TIMESTAMP
  profile_data: '{"key": "value"}' # String -> JSON (if column defined as JSON)
)

# Retrieved data is automatically converted back to Ruby types
user = users.first
user[:birth_date].class  # => Date
user[:created_at].class  # => Time
user[:active].class      # => TrueClass

Query Building

Basic Queries

users = db[:users]

# SELECT with conditions
users.where(active: true)
users.where { age > 25 }
users.where(Sequel.like(:name, 'John%'))

# Ordering and limiting
users.order(:name).limit(10)
users.order(Sequel.desc(:created_at)).first

# Aggregation
users.count
users.avg(:age)
users.group(:department).select(:department, Sequel.count(:id).as(:user_count))

Advanced Queries

# JOINs
db[:users]
  .join(:orders, user_id: :id)
  .select(:users__name, :orders__total)
  .where { orders__total > 100 }

# Subqueries
high_value_users = db[:orders]
  .group(:user_id)
  .having { sum(:total) > 1000 }
  .select(:user_id)

db[:users].where(id: high_value_users)

# Window functions (DuckDB-specific optimization)
db[:sales]
  .select(
    :product_id,
    :amount,
    Sequel.function(:row_number).over(partition: :product_id, order: :amount).as(:rank)
  )

# Common Table Expressions (CTEs)
db.with(:high_spenders,
  db[:orders].group(:user_id).having { sum(:total) > 1000 }.select(:user_id)
).from(:high_spenders)
 .join(:users, id: :user_id)
 .select(:users__name)

Transactions

Basic Transactions

db.transaction do
  users.insert(name: 'Alice', email: 'alice@example.com')
  orders.insert(user_id: users.max(:id), total: 100.00)
  # Automatically commits if no exceptions
end

# Manual rollback
db.transaction do
  users.insert(name: 'Bob', email: 'bob@example.com')
  raise Sequel::Rollback if some_condition
  # Transaction will be rolled back
end

Error Handling

begin
  db.transaction do
    # Some database operations
    users.insert(name: nil)  # This will fail due to NOT NULL constraint
  end
rescue Sequel::NotNullConstraintViolation => e
  puts "Cannot insert user with null name: #{e.message}"
rescue Sequel::DatabaseError => e
  puts "Database error: #{e.message}"
end

DuckDB-Specific Features

Analytical Queries

# DuckDB excels at analytical workloads
sales_summary = db[:sales]
  .select(
    :product_category,
    Sequel.function(:sum, :amount).as(:total_sales),
    Sequel.function(:avg, :amount).as(:avg_sale),
    Sequel.function(:count, :id).as(:transaction_count)
  )
  .group(:product_category)
  .order(Sequel.desc(:total_sales))

# Window functions for analytics
monthly_trends = db[:sales]
  .select(
    :month,
    :amount,
    Sequel.function(:lag, :amount, 1).over(order: :month).as(:prev_month),
    Sequel.function(:sum, :amount).over(order: :month).as(:running_total)
  )

Performance Optimizations

# Bulk inserts (more efficient than individual inserts)
users_data = [
  {name: 'User 1', email: 'user1@example.com'},
  {name: 'User 2', email: 'user2@example.com'},
  # ... many more records
]

db[:users].multi_insert(users_data)

# Use DuckDB's columnar storage advantages
# Query only needed columns for better performance
db[:large_table].select(:id, :name, :created_at).where(active: true)

Exporting with COPY

Database#copy_to runs a DuckDB COPY that writes a dataset (or a SQL string) to a file. FORMAT comes from the format: option, or else from the file extension. A path with neither, such as a directory for a partitioned export, raises Sequel::Error.

db.copy_to(db[:users].where(active: true), "/data/users.parquet")
db.copy_to(db[:events], "/data/events", format: :parquet, partition_by: [:year, :month])

Dataset#copy_to_sql returns the same COPY statement without running it, for example to write a plan file:

db[:users].copy_to_sql("/data/users.csv", header: true, delimiter: "|")
# => COPY (SELECT * FROM "users") TO '/data/users.csv' (FORMAT CSV, HEADER, DELIMITER '|')

Attaching database files

Database#attach runs a DuckDB ATTACH, and Database#detach runs DETACH DATABASE. The catalog name as: is required. A name that DuckDB derives from the file name can equal a schema name inside the file, and then schema.table is ambiguous. Other DuckDB ATTACH options pass through, for example type: :sqlite.

db.attach("/data/dataset.duckdb", as: :dataset, read_only: true)
db[Sequel[:dataset][:people]].count
db.detach(:dataset, if_exists: true)

db.attach_sql("/data/dataset.duckdb", as: :dataset, read_only: true, if_not_exists: true)
# => ATTACH IF NOT EXISTS '/data/dataset.duckdb' AS "dataset" (READ_ONLY)

An attached catalog belongs to the DuckDB database, which all pool connections of a Sequel::Database share. It lasts until Database#disconnect closes the last connection. After that, attach it again; if_not_exists: true makes a repeat safe.

search_path is different: DuckDB keeps it per connection. Set it on every new connection with Sequel's connect_sqls option. Put the writable catalog first, because CREATE TABLE without a schema goes to the first entry, and a read-only catalog there makes it fail.

db = Sequel.connect("duckdb:///exports/42/work.duckdb",
                    connect_sqls: [ "SET search_path = 'work.main'" ])
db.attach("/data/dataset.duckdb", as: :dataset, read_only: true)
db.create_table(:summary) { Integer :n } # created in work.main

Lists

Sequel writes a Ruby Array as a parenthesized list, such as (1, 2, 3), for IN conditions. DuckDB does not read that as a list value. To build a DuckDB list from expressions, call DuckDB's list_value function:

DB.literal([1, 2, 3])
# => (1, 2, 3)
DB.literal(Sequel.function(:list_value, :a, Sequel[:b] + 1))
# => list_value("a", ("b" + 1))

IS DISTINCT FROM

The adapter declares that DuckDB supports IS DISTINCT FROM. Sequel's is_distinct_from extension then writes it natively. Load the extension on the Database. With only Sequel.extension :is_distinct_from, the builder exists, but the dataset cannot write it and raises NoMethodError. Write IS NOT DISTINCT FROM by negating the expression with ~:

DB.extension :is_distinct_from
DB[:t].where(Sequel.is_distinct_from(:a, :b))
# => SELECT * FROM "t" WHERE ("a" IS DISTINCT FROM "b")
DB[:t].where(~Sequel.is_distinct_from(:a, :b))
# => SELECT * FROM "t" WHERE NOT ("a" IS DISTINCT FROM "b")

duckdb_ops extension

The duckdb_ops extension adds DuckDB-specific expression builders to Sequel. It is opt-in. If you do not load it, Sequel generates the same SQL as before. Load it with either form. Both forms make the builders available globally.

Sequel.extension :duckdb_ops
# or
DB.extension :duckdb_ops

Sequel.try_cast(expr, type) builds a DuckDB TRY_CAST. It returns NULL when the cast fails, where CAST raises an error. The type works as in Sequel.cast: a Ruby class maps to the adapter's type name, and a Symbol or String passes through unchanged.

DB[:raw].select(Sequel.try_cast(:amount, :DOUBLE).as(:amount))
# => SELECT TRY_CAST("amount" AS DOUBLE) AS "amount" FROM "raw"
Sequel.try_cast(:amount, Float)
# => TRY_CAST("amount" AS double precision)

DuckDB table functions take named parameters. Sequel.duckdb_named(**params) builds them for the last argument of Sequel.function. Sequel.duckdb_read_parquet, Sequel.duckdb_read_csv and Sequel.duckdb_read_json take a path or an array of paths, plus named parameters. An Array value becomes a DuckDB list. A Hash value becomes a struct, in which a Symbol is a bare type name and a String is a string literal.

parts = Sequel.duckdb_read_parquet("/data/events/**/*.parquet",
                                   hive_partitioning: true, hive_types: { year: :BIGINT })
DB.from(parts.as(:events)).where(year: 2024)
# => SELECT * FROM read_parquet(['/data/events/**/*.parquet'], hive_partitioning = TRUE,
#      hive_types = {'year': BIGINT}) AS "events" WHERE ("year" = 2024)

Sequel.function(:read_json, "/data/a.json", Sequel.duckdb_named(columns: { a: "INTEGER" }))
# => read_json('/data/a.json', columns = {'a': 'INTEGER'})

Lambda expressions

Sequel.duckdb_lambda builds a DuckDB lambda expression for list functions such as list_transform, list_filter and list_reduce. Call those functions through Sequel.function. The builder has two forms. In the block form, every argument is a parameter name, and the block gets one identifier for each parameter. In the body form, the first argument is a parameter name or an Array of names, and the second argument is the body. The body names the parameters as Symbols or with Sequel[:x].

Sequel.duckdb_lambda(:acc, :x) { |acc, x| acc + x }
Sequel.duckdb_lambda([:acc, :x], Sequel[:acc] + :x)
# => lambda "acc", "x": ("acc" + "x")

Sequel.function(:list_reduce, :amounts,
                Sequel.duckdb_lambda(:acc, :x) { |acc, x| acc + x },
                Sequel.cast(0, :BIGINT))
# => list_reduce("amounts", lambda "acc", "x": ("acc" + "x"), CAST(0 AS BIGINT))

DuckDB has no let. To compute a value once and use it more than once, wrap it in a one-element list, transform it, and take element 1 with Sequel.subscript:

square_plus = Sequel.duckdb_lambda(:v) { |v| (v * v) + v }
Sequel.subscript(Sequel.function(:list_transform, Sequel.function(:list_value, :n), square_plus), 1)
# => (list_transform(list_value("n"), lambda "v": (("v" * "v") + "v")))[1]

The builder emits DuckDB's lambda keyword syntax, never the x -> body arrow, which DuckDB deprecated. It needs a DuckDB version that accepts the lambda keyword. Parameter names are quoted identifiers, the same as the references to them in the body.

Model Integration

Sequel::Model Usage

class User < Sequel::Model
  # Sequel automatically introspects the users table schema

  # Associations work normally
  one_to_many :orders

  # Validations
  def validate
    super
    errors.add(:email, 'must be present') if !email || email.empty?
    errors.add(:email, 'must be valid') unless email =~ /@/
  end

  # Custom methods
  def full_name
    "#{first_name} #{last_name}"
  end
end

class Order < Sequel::Model
  many_to_one :user

  def total_with_tax(tax_rate = 0.08)
    total * (1 + tax_rate)
  end
end

# Usage
user = User.create(name: 'John Doe', email: 'john@example.com')
order = user.add_order(total: 99.99, status: 'pending')

# Associations work seamlessly
user.orders.where(status: 'completed').sum(:total)

Error Handling

The adapter maps DuckDB errors to appropriate Sequel exception types:

begin
  # Various operations that might fail
  db[:users].insert(name: nil)  # NOT NULL violation
rescue Sequel::NotNullConstraintViolation => e
  # Handle null constraint violation
rescue Sequel::UniqueConstraintViolation => e
  # Handle unique constraint violation
rescue Sequel::ForeignKeyConstraintViolation => e
  # Handle foreign key violation
rescue Sequel::CheckConstraintViolation => e
  # Handle check constraint violation
rescue Sequel::DatabaseConnectionError => e
  # Handle connection issues
rescue Sequel::DatabaseError => e
  # Handle other database errors
end

Troubleshooting

Common Issues

Connection Problems

# Issue: Cannot connect to database file
# Solution: Check file path and permissions
begin
  db = Sequel.connect('duckdb:///path/to/database.duckdb')
rescue Sequel::DatabaseConnectionError => e
  puts "Connection failed: #{e.message}"
  # Check if directory exists and is writable
  # Ensure DuckDB gem is properly installed
end

Memory Issues

# Issue: Out of memory with large datasets
# Solution: Use streaming or limit result sets
db[:large_table].limit(1000).each do |row|
  # Process row by row instead of loading all at once
end

# Or use DuckDB's memory configuration
db = Sequel.connect(
  adapter: 'duckdb',
  database: ':memory:',
  config: { memory_limit: '2GB' }
)

Performance Issues

# Issue: Slow queries
# Solution: Add appropriate indexes
db.add_index :users, :email
db.add_index :orders, [:user_id, :created_at]

# Use EXPLAIN to analyze query plans
puts db[:users].where(email: 'john@example.com').explain

Debugging

# Enable SQL logging to see generated queries
require 'logger'
db.loggers << Logger.new($stdout)

# This will now log all SQL queries
db[:users].where(active: true).all
# Logs: SELECT * FROM users WHERE (active = true)

Version Compatibility

  • Ruby: 3.3 or higher (CI tests 3.3, 3.4 and 4.0)
  • Sequel: 5.0 or higher
  • DuckDB C library: 1.5.x and 2.x. Support for 1.5.x ends when DuckDB 1.5.x reaches end of life in March 2027.
  • ruby-duckdb (the duckdb gem): 1.5.x and 2.x. sequel-duckdb does not depend on it, so add gem "duckdb" to your own Gemfile. ruby-duckdb 2.x builds against library 1.4 or newer, so it works with library 1.5.x.

DuckDB 2.x aborts an open transaction after any failed statement, including a query on a missing table; 1.5.x does not. table_exists? and schema keep the transaction on both versions.

Getting Help

Performance Tips

Query Optimization

  1. Select only needed columns: DuckDB's columnar storage makes this very efficient

    # Good
    db[:users].select(:id, :name).where(active: true)
    
    # Less efficient
    db[:users].where(active: true)  # Selects all columns
  2. Use appropriate indexes: Especially for frequently queried columns

    db.add_index :users, :email
    db.add_index :orders, [:user_id, :status]
  3. Leverage DuckDB's analytical capabilities: Use window functions and aggregations

    # Efficient analytical query
    db[:sales]
      .select(
        :product_id,
        Sequel.function(:sum, :amount).as(:total),
        Sequel.function(:rank).over(order: Sequel.desc(:amount)).as(:rank)
      )
      .group(:product_id)

Memory Management

  1. Use streaming for large result sets:

    db[:large_table].paged_each(rows_per_fetch: 1000) do |row|
      # Process row by row
    end
  2. Configure DuckDB memory limits:

    db = Sequel.connect(
      adapter: 'duckdb',
      database: '/path/to/db.duckdb',
      config: {
        memory_limit: '4GB',
        threads: 8
      }
    )

Bulk Operations

  1. Use multi_insert for bulk data loading:

    # Efficient bulk insert
    data = 1000.times.map { |i| {name: "User #{i}", email: "user#{i}@example.com"} }
    db[:users].multi_insert(data)
  2. Use transactions for multiple operations:

    db.transaction do
      # Multiple related operations
      user_id = db[:users].insert(name: 'John', email: 'john@example.com')
      db[:profiles].insert(user_id: user_id, bio: 'Software developer')
      db[:preferences].insert(user_id: user_id, theme: 'dark')
    end

Documentation

Complete API Reference

For comprehensive API documentation including all methods, configuration options, and advanced features, see:

SQL Generation Patterns

The sequel-duckdb adapter generates SQL optimized for DuckDB while maintaining Sequel compatibility. Key patterns include:

  • LIKE clauses: Clean syntax without ESCAPE clauses
  • ILIKE support: Converted to UPPER() LIKE UPPER() for case-insensitive matching
  • Regular expressions: Using DuckDB's regexp_matches() function
  • Qualified columns: Standard dot notation (table.column)
  • Recursive CTEs: Automatic WITH RECURSIVE detection
  • Proper parentheses: Consistent expression grouping

Example SQL patterns:

# LIKE patterns
dataset.where(Sequel.like(:name, "%John%"))
# SQL: SELECT * FROM users WHERE (name LIKE '%John%')

# ILIKE patterns (case-insensitive)
dataset.where(Sequel.ilike(:name, "%john%"))
# SQL: SELECT * FROM users WHERE (UPPER(name) LIKE UPPER('%john%'))

# Regular expressions
dataset.where(name: /^John/)
# SQL: SELECT * FROM users WHERE (regexp_matches(name, '^John'))

For complete SQL pattern documentation, see docs/DUCKDB_SQL_PATTERNS.md.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/aguynamedryan/sequel-duckdb.

License

The gem is available as open source under the terms of the MIT License.