Kweerie
Kweerie is a Ruby gem that helps you manage SQL queries in standalone files with parameter binding for PostgreSQL. It's designed to be a lightweight alternative to database views when you need to keep SQL logic separate from your Ruby code.
Installation
Add this line to your application's Gemfile:
gem 'kweerie'
Then execute:
bundle install
Usage
Basic Usage
Create a new query class and its corresponding SQL file:
# app/queries/user_search.rb
class UserSearch < Kweerie::Base
bind :name, as: '$1'
bind :email, as: '$2'
end
-- app/queries/user_search.sql
SELECT
users.id,
users.name,
users.email
FROM
users
WHERE
name ILIKE $1
AND email ILIKE $2;
Execute your query:
results = UserSearch.with(
name: 'Eclipsoid%',
email: '%@example.com'
)
# => [{"id"=>109981, "name"=>"Eclipsoid Doe", "email"=>"eclipsoiddoe@example.com"}]
Object Mapping
While Kweerie::Base
returns plain hashes, you can use Kweerie::BaseObject
to get typed Ruby objects with proper attribute methods:
class UserSearch < Kweerie::BaseObject
bind :name, as: '$1'
bind :created_at, as: '$2'
end
# Returns array of objects instead of hashes
users = UserSearch.with(
name: 'Eclipsoid',
created_at: '2024-01-01'
)
user = users.first
user.name # => "Eclipsoid"
user.created_at # => 2024-01-01 00:00:00 +0000 (Time object)
Querying
Kweerie provides two main ways to execute queries based on whether they have parameters:
Parameterized Queries
When your query needs parameters, use the .with
method:
class UsersByDepartment < Kweerie::Base
bind :department, as: '$1'
bind :active, as: '$2'
end
# app/queries/users_by_department.sql
SELECT *
FROM users
WHERE department = $1
AND active = $2;
# Using the query
users = UsersByDepartment.with(
department: 'Engineering',
active: true
)
Parameter-free Queries
For queries that don't require any parameters, you can use the more semantically appropriate .all
method:
class AllUsers < Kweerie::Base
end
# app/queries/all_users.sql
SELECT *
FROM users
WHERE active = true
ORDER BY created_at DESC;
# Using the query
users = AllUsers.all
The .all
method provides a cleaner interface when you're not binding any parameters. It will raise an error if you try to use it on a query class that has parameter bindings:
# This will raise an ArgumentError
UsersByDepartment.all
# => ArgumentError: Cannot use .all on queries with bindings. Use .with instead.
Choosing Between .all and .with
- Use
.all
when your SQL query is completely static with no parameters - Use
.with
when you need to pass parameters to your query - Even for parameterized queries, you can use
.with
without arguments if all parameters are optional
# A query with no parameters
class RecentUsers < Kweerie::Base
end
RecentUsers.all # ✓ Clean and semantic
RecentUsers.with # ✓ Works but less semantic
# A query with parameters
class UsersByStatus < Kweerie::Base
bind :status, as: '$1'
end
UsersByStatus.all # ✗ Raises ArgumentError
UsersByStatus.with(status: 'active') # ✓ Correct usage
Both methods work with Kweerie::Base
and Kweerie::BaseObject
, returning arrays of hashes or objects respectively:
# Returns array of hashes
class AllUsers < Kweerie::Base
end
users = AllUsers.all
# => [{"id" => 1, "name" => "Eclipsoid"}, ...]
# Returns array of objects
class AllUsers < Kweerie::BaseObject
end
users = AllUsers.all
# => [#<AllUsers id=1 name="Eclipsoid">, ...]
Object Interface
BaseObject provide several useful methods:
# Hash-like access
user[:name] # => "Eclipsoid"
user.fetch(:email, 'N/A') # => Returns 'N/A' if email is nil
# Serialization
user.to_h # => Hash with string keys
user.to_json # => JSON string
# Comparison
user1 == user2 # Compare all attributes
users.sort_by(&:created_at) # Sortable
SQL File Location
By default, Kweerie looks for SQL files adjacent to their Ruby query classes. You can customize this behavior:
# Default behavior - looks for user_search.sql next to this file
class UserSearch < Kweerie::Base
end
# Specify absolute path from Rails root
class UserSearch < Kweerie::Base
sql_file_location root: 'db/queries/complex_user_search.sql'
end
# Specify path relative to the Ruby file
class UserSearch < Kweerie::Base
sql_file_location relative: '../sql/user_search.sql'
end
# Explicitly use default behavior
class UserSearch < Kweerie::Base
sql_file_location :default
end
Type Casting
cast_select
provides flexible, explicit type casting for your query result fields. Instead of relying on automatic type inference, you can specify exactly how each field should be cast. By default it will return as the string from the database.
Basic Usage
class UserQuery < Kweerie::BaseObjects
cast_select :age, as: ->(val) { val.to_i }
cast_select :active, as: Types::Boolean
cast_select :metadata, as: Types::PgJsonb
end
Casting Options
Built-in Type Classes
The gem includes a few built-in type classes for specific scenarios, you'll find a comprehensive amount of them in rails:
# Boolean casting (handles various boolean representations)
cast_select :active, as: Types::Boolean
# JSONB casting (handles both objects and arrays)
cast_select :metadata, as: Types::PgJsonb
# Postgres Array casting
cast_select :tags, as: Types::PgArray
Lambda/Proc Casting
For simple transformations, you can use a lambda or proc:
class ProductQuery < Kweerie::BaseObjects
cast_select :price, as: ->(val) { val.to_f }
cast_select :quantity, as: ->(val) { val.to_i }
cast_select :sku, as: ->(val) { val.upcase }
end
Method Reference Casting
You can reference an instance method for complex casting logic:
class OrderQuery < Kweerie::BaseObjects
cast_select :total, as: :calculate_total
def calculate_total(val)
return if val.nil?
Money.new(val)
end
end
Custom Type Classes
For reusable, complex casting logic, you can create custom type classes, or use the ones provided by rails:
class Types::Money
def cast(value)
return nil if value.nil?
(value.to_f * 100).to_i # Store as cents
end
end
class Types::PGIntArray < Types::PgArray
def cast(value)
super.map(&:to_i) # Convert array elements to integers
end
end
class InvoiceQuery < Kweerie::BaseObjects
cast_select :amount, as: Types::Money
cast_select :line_items, as: Types::PGIntArray
end
Performance Considerations
BaseObject creates a unique class for each query result set, with the following optimizations:
- Classes are cached and reused for subsequent queries
- Attribute readers are defined upfront
- Type casting happens once during initialization
- No method_missing or dynamic method definition per instance
- Efficient pattern matching support
For queries where you don't need the object interface, use Kweerie::Base
instead for slightly better performance.
Rails Generator
If you're using Rails, you can use the generator to create new query files:
# Using underscored name
rails generate kweerie user_search email name
# Using CamelCase name
rails generate kweerie UserSearch email name
This will create both the Ruby class and SQL file with the appropriate structure.
Configuration
By default, Kweerie uses ActiveRecord's connection if available. You can configure this and other options:
# config/initializers/kweerie.rb
Kweerie.configure do |config|
# Use a custom connection provider
config.connection_provider = -> { MyCustomConnectionPool.connection }
# Configure where to look for SQL files
config.sql_paths = -> { ['db/queries', 'app/sql'] }
end
Requirements
- Ruby 3 or higher
- PostgreSQL (this gem is PostgreSQL-specific and uses the
pg
gem) - Rails 7+ (optional, needed for the generator and default ActiveRecord integration)
Features
- ✅ Separate SQL files from Ruby code
- ✅ Strong parameter binding for SQL injection protection
- ✅ Rails generator for quick file creation
- ✅ Configurable connection handling
- ✅ Parameter validation
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests.
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
License
The gem is available as open source under the terms of the MIT License.
FAQ
Q: Why does Kweerie exist?
A: PostgreSQL DB views are powerful and (honestly) preferred for what kweerie offers, but they need migrations to change, which isn't always practical when you just need a query. Kweerie provides that flexibility. Plus, SQL is more readable in a single file than when nested in Ruby code. SQL is powerful and doesn’t always need to be hidden behind abstractions.
Q: Why PostgreSQL only?
A: Kweerie uses PostgreSQL-specific features for parameter binding and result handling. Supporting other databases would require different parameter binding syntax and result handling.
Q: Do I need Rails?
A: No, Kweerie works with any Ruby application. Rails is only required if you want to use the generator or the automatic ActiveRecord integration.
Q: Can I use this with views?
A: Yes! You can write any valid PostgreSQL query, including queries that use views.
Q: How do I handle optional parameters?
A: Currently, all bound parameters are required. For optional parameters, you'll need to handle the conditionals in your SQL using COALESCE or similar PostgreSQL functions.
Q: How do I convert types in parameters?
A: PostgreSQL has robust type handling built-in, so you can handle type conversion directly in your SQL. Here are some examples:
-- Convert string to integer
WHERE id = $1::integer
-- Convert string to date
WHERE created_at > $1::date
-- Convert string to array
WHERE tags && $1::text[]
-- Convert to timestamp with timezone
WHERE created_at > $1::timestamptz
-- Multiple conversions in one query
SELECT *
FROM users
WHERE
created_at > $1::timestamptz
AND age > $2::integer
AND tags && $3::text[]
AND metadata @> $4::jsonb
This approach leverages PostgreSQL's native type casting system, which is both more efficient and more reliable than converting types in Ruby.