Last active
December 10, 2024 17:55
-
-
Save yawaramin/552c25a23549f15556d54954e39f946d to your computer and use it in GitHub Desktop.
Type-safe SQL query using phantom types to model query parts as state transitions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* | |
An incomplete implementation of a type-safe SQL query in F#. | |
The idea is that a query is built up clause by clause, by representing | |
each additional clause being added on to the query as a state transition | |
between different types of queries. We capture these different types as | |
phantom types to make sure that only valid transitions (query clause | |
additions) as defined by us can be carried out. | |
The final result is a 'total query' that can be converted to a string, | |
executed, etc. | |
*) | |
type select = interface end | |
type groupable = interface end | |
type orderable = interface end | |
type join = interface end | |
type total = interface end | |
type from = interface | |
inherit groupable | |
inherit orderable | |
inherit total | |
end | |
type cond = interface | |
inherit groupable | |
inherit orderable | |
inherit total | |
end | |
type group_by = interface inherit orderable inherit total end | |
type order_by = interface inherit total end | |
type 'a t = private T of string | |
let private commalist strings = String.concat ", " strings | |
let select columns : select t = | |
columns |> commalist |> sprintf "select %s" |> T | |
let from table ((T t) : select t) : from t = | |
table |> sprintf "%s\nfrom %s" t |> T | |
let select_all_from table : from t = select ["*"] |> from table | |
let inner_join table ((T t) : from t) : join t = | |
table |> sprintf "%s\ninner join %s" t |> T | |
let left_outer_join table ((T t) : from t) : join t = | |
table |> sprintf "%s\nleft outer join %s" t |> T | |
let right_outer_join table ((T t) : from t) : join t = | |
table |> sprintf "%s\nright outer join %s" t |> T | |
let full_outer_join table ((T t) : from t) : join t = | |
table |> sprintf "%s\nfull outer join %s" t |> T | |
let on cond ((T t) : join t) : cond t = | |
cond |> sprintf "%s\non %s" t |> T | |
let where cond ((T t) : #total t) : cond t = | |
cond |> sprintf "%s\nwhere %s" t |> T | |
let group_by columns ((T t) : #groupable t) : group_by t = | |
columns |> commalist |> sprintf "%s\ngroup by %s" t |> T | |
let order_by columns ((T t) : #orderable t) : order_by t = | |
columns |> commalist |> sprintf "%s\norder by %s" t |> T | |
let to_string ((T t) : #total t) : string = t | |
(* | |
Examples: | |
> Sql.select_all_from "employees" |> Sql.to_string;; | |
val it : string = "select * | |
from employees" | |
> select ["p.name"; "d.name"] | |
- |> from "people as p" | |
- |> inner_join "departments as d" | |
- |> on "p.department_id = d.id" | |
- |> where "d.id = 100" | |
- |> order_by ["p.id"] | |
- |> to_string;; | |
val it : string = | |
"select p.name, d.name | |
from people as p | |
inner join departments as d | |
on p.department_id = d.id | |
where d.id = 100 | |
order by p.id" | |
> select ["id"; "name"] |> to_string;; | |
select ["id"; "name"] |> to_string;; | |
-------------------------^^^^^^^^^ | |
/Users/yamin1/Downloads/stdin(63,26): error FS0001: The type 'select' is not compatible with the type 'total' | |
*) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment