Current section
Files
Jump to
Current section
Files
src/ormlette/query/query.gleam
//// The main Query module that provides a high-level API for building SQL queries
//// It is built on top of the Cake library to provide a more ergonomic interface, but can easily be exported
//// to standard cake-select queries.
import cake
import cake/dialect/postgres_dialect
import cake/join
import cake/select
import cake/where
import gleam/dynamic
import gleam/list
import gleam/option.{None, Some}
import gleam/string
import ormlette/schema/create
// Helper function to find a column by name in a table, returning Option instead of Result
fn find_column(
table: create.Table,
column_name: String,
) -> option.Option(create.Column) {
case list.find(table.columns, fn(col) { col.name == column_name }) {
Ok(v) -> Some(v)
Error(_) -> None
}
}
// Helper function to check if a column exists in the schema
fn column_exists(table: create.Table, column_name: String) -> Bool {
list.any(table.columns, fn(column) { column.name == column_name })
}
// Convert dynamic value to a compatible `WhereValue`
fn to_where_value(value: dynamic.Dynamic) -> where.WhereValue {
case dynamic.int(value) {
Ok(value) -> where.int(value)
Error(_) ->
case dynamic.string(value) {
Ok(value) -> where.string(value)
Error(_) ->
case dynamic.float(value) {
Ok(value) -> where.float(value)
Error(_) ->
case dynamic.bool(value) {
Ok(value) ->
case value {
True -> where.true()
False -> where.false()
}
Error(_) -> where.null()
}
}
}
}
}
// Finds foreign key relationship between source and target tables, if any
fn find_relationship(
source_table: create.Table,
target_table: create.Table,
) -> option.Option(#(create.Column, create.Column)) {
case
list.find_map(source_table.columns, fn(column) {
case column.references {
Some(ref) if ref.table.name == target_table.name ->
case find_column(target_table, ref.column) {
Some(target_col) -> Ok(#(column, target_col))
None -> Error(#())
}
_ -> Error(#())
}
})
{
Ok(v) -> option.Some(v)
Error(_) -> option.None
}
}
pub type Query {
Query(table: create.Table, select: select.Select)
}
// Initialize a new query for a given table
pub fn from_table(table: create.Table) -> Query {
Query(table, select.from_table(select.new(), table.name))
}
// Generic join function that applies a specific join type based on the relationship
pub fn join(
query: Query,
target_table: create.Table,
join_type: fn(join.JoinTarget, where.Where, String) -> join.Join,
) -> Query {
let foreign_key = find_relationship(query.table, target_table)
case foreign_key {
Some(#(source_col, target_col)) -> {
let join_clause =
join_type(
join.table(target_table.name),
where.eq(
where.col(query.table.name <> "." <> source_col.name),
where.col(target_table.name <> "." <> target_col.name),
),
target_table.name,
)
let joined_query = select.join(query.select, join_clause)
Query(query.table, joined_query)
}
None -> query
// If no relationship, return original query
}
}
// Adds an INNER JOIN automatically based on a foreign key
pub fn inner_join(query: Query, target_table: create.Table) -> Query {
join(query, target_table, join.inner)
}
// Adds a LEFT JOIN automatically based on a foreign key
pub fn left_join(query: Query, target_table: create.Table) -> Query {
join(query, target_table, join.left)
}
// Add a SELECT clause to the query for specific columns
pub fn select(query: Query, columns: List(String)) -> Query {
let select_query =
list.fold(columns, query.select, fn(column, select_query) {
select.select(
column,
select.col(select_query)
|> select.alias(string.replace(select_query, ".", "_")),
)
})
Query(query.table, select_query)
}
// Add an EQUALS clause to the query for filtering
pub fn equals(query: Query, column: String, value: a) -> Query {
let where_clause =
where.eq(where.col(column), to_where_value(dynamic.from(value)))
Query(query.table, select.where(query.select, where_clause))
}
// Add a GREATER_THAN clause to the query for filtering
pub fn greater_than(query: Query, column: String, value: a) -> Query {
let where_clause =
where.gt(where.col(column), to_where_value(dynamic.from(value)))
Query(query.table, select.where(query.select, where_clause))
}
// Add a LESS_THAN clause to the query for filtering
pub fn less_than(query: Query, column: String, value: a) -> Query {
let where_clause =
where.lt(where.col(column), to_where_value(dynamic.from(value)))
Query(query.table, select.where(query.select, where_clause))
}
// Add an ORDER BY clause to the query for sorting
pub fn order_by(
query: Query,
column: String,
direction: select.Direction,
) -> Query {
let ordered_query = select.order_by(query.select, column, direction)
Query(query.table, ordered_query)
}
// Export the SQL representation of the query
pub fn sql(query: Query) {
#(
cake.get_sql(
postgres_dialect.read_query_to_prepared_statement(select.to_query(
query.select,
)),
),
cake.get_params(
postgres_dialect.read_query_to_prepared_statement(select.to_query(
query.select,
)),
),
)
}
pub fn export(query: Query) {
query.select
}