a database layer insipred by caqti and ecto
1type table = { name : string; schema : string option }
2
3let table ?(schema = None) name = { name; schema }
4
5let table_name t =
6 match t.schema with Some s -> s ^ "." ^ t.name | None -> t.name
7
8type fk_action = Cascade | Restrict | SetNull | SetDefault | NoAction
9
10type foreign_key_ref = {
11 fk_table : string;
12 fk_column : string;
13 fk_on_delete : fk_action option;
14 fk_on_update : fk_action option;
15}
16
17type column_constraint =
18 | PrimaryKey
19 | NotNull
20 | Unique
21 | Default of string
22 | Check of string
23 | ForeignKey of foreign_key_ref
24
25type 'a column = {
26 col_name : string;
27 col_type : 'a Types.t;
28 col_constraints : column_constraint list;
29}
30
31type wrapped_column = Column : 'a column -> wrapped_column
32
33type table_def = {
34 tbl_table : table;
35 tbl_columns : wrapped_column list;
36 tbl_primary_key : string list option;
37 tbl_unique : string list list;
38 tbl_checks : (string option * string) list;
39}
40
41let column ?(primary_key = false) ?(not_null = false) ?(unique = false) ?default
42 ?check ?references name ty =
43 let constraints = [] in
44 let constraints =
45 if primary_key then PrimaryKey :: constraints else constraints
46 in
47 let constraints = if not_null then NotNull :: constraints else constraints in
48 let constraints = if unique then Unique :: constraints else constraints in
49 let constraints =
50 match default with
51 | Some d -> Default d :: constraints
52 | None -> constraints
53 in
54 let constraints =
55 match check with Some c -> Check c :: constraints | None -> constraints
56 in
57 let constraints =
58 match references with
59 | Some r -> ForeignKey r :: constraints
60 | None -> constraints
61 in
62 { col_name = name; col_type = ty; col_constraints = constraints }
63
64let references ?(on_delete = None) ?(on_update = None) ~table ~column () =
65 {
66 fk_table = table;
67 fk_column = column;
68 fk_on_delete = on_delete;
69 fk_on_update = on_update;
70 }
71
72let define ?(schema = None) name columns =
73 let tbl = { name; schema } in
74 {
75 tbl_table = tbl;
76 tbl_columns = List.map (fun c -> Column c) columns;
77 tbl_primary_key = None;
78 tbl_unique = [];
79 tbl_checks = [];
80 }
81
82let with_primary_key cols def = { def with tbl_primary_key = Some cols }
83let with_unique cols def = { def with tbl_unique = cols :: def.tbl_unique }
84
85let with_check ?name expr def =
86 { def with tbl_checks = (name, expr) :: def.tbl_checks }
87
88let id_column () = column "id" Types.int64 ~primary_key:true ~not_null:true
89
90let timestamps () =
91 [
92 column "inserted_at" Types.ptime ~not_null:true ~default:"NOW()";
93 column "updated_at" Types.ptime ~not_null:true ~default:"NOW()";
94 ]
95
96let has_constraint c col = List.exists (fun con -> con = c) col.col_constraints
97let is_primary_key col = has_constraint PrimaryKey col
98let is_not_null col = has_constraint NotNull col || is_primary_key col
99let is_unique col = has_constraint Unique col
100
101let get_default col =
102 List.find_map (function Default d -> Some d | _ -> None) col.col_constraints
103
104let get_foreign_key col =
105 List.find_map
106 (function ForeignKey fk -> Some fk | _ -> None)
107 col.col_constraints
108
109let fk_action_to_sql = function
110 | Cascade -> "CASCADE"
111 | Restrict -> "RESTRICT"
112 | SetNull -> "SET NULL"
113 | SetDefault -> "SET DEFAULT"
114 | NoAction -> "NO ACTION"
115
116let constraint_to_sql = function
117 | PrimaryKey -> "PRIMARY KEY"
118 | NotNull -> "NOT NULL"
119 | Unique -> "UNIQUE"
120 | Default expr -> "DEFAULT " ^ expr
121 | Check expr -> "CHECK (" ^ expr ^ ")"
122 | ForeignKey { fk_table; fk_column; fk_on_delete; fk_on_update } -> (
123 let base = Printf.sprintf "REFERENCES %s(%s)" fk_table fk_column in
124 let base =
125 match fk_on_delete with
126 | Some od -> base ^ " ON DELETE " ^ fk_action_to_sql od
127 | None -> base
128 in
129 match fk_on_update with
130 | Some ou -> base ^ " ON UPDATE " ^ fk_action_to_sql ou
131 | None -> base)
132
133let column_to_sql (Column col) =
134 let type_name = Types.sql_type_name col.col_type in
135 let constraints = List.map constraint_to_sql col.col_constraints in
136 String.concat " " (col.col_name :: type_name :: constraints)
137
138let table_def_to_sql def =
139 let col_defs = List.map column_to_sql def.tbl_columns in
140 let pk_constraint =
141 match def.tbl_primary_key with
142 | Some cols ->
143 [ Printf.sprintf "PRIMARY KEY (%s)" (String.concat ", " cols) ]
144 | None -> []
145 in
146 let unique_constraints =
147 List.map
148 (fun cols -> Printf.sprintf "UNIQUE (%s)" (String.concat ", " cols))
149 def.tbl_unique
150 in
151 let check_constraints =
152 List.map
153 (fun (name, expr) ->
154 match name with
155 | Some n -> Printf.sprintf "CONSTRAINT %s CHECK (%s)" n expr
156 | None -> Printf.sprintf "CHECK (%s)" expr)
157 def.tbl_checks
158 in
159 let all_parts =
160 col_defs @ pk_constraint @ unique_constraints @ check_constraints
161 in
162 Printf.sprintf "CREATE TABLE %s (\n %s\n)" (table_name def.tbl_table)
163 (String.concat ",\n " all_parts)