package petrol

  1. Overview
  2. Docs

Source file schema.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
type table_name = Types.table_name

type conflict_clause = [`ROLLBACK | `ABORT | `FAIL | `IGNORE | `REPLACE]
type foreign_conflict_clause = [`SET_NULL | `SET_DEFAULT | `CASCADE | `RESTRICT | `NO_ACTION ]

type 'a sql_constraint =
  | PrimaryKey of {
    name: string option;
    ordering: [`ASC | `DESC] option;
    local_columns: string list option;
    on_conflict: conflict_clause option;
    auto_increment: bool
  }
  | NotNull of {
      name: string option;
      on_conflict: conflict_clause option
    }
  | Unique of {
      name: string option;
      local_columns: string list option;
      on_conflict: conflict_clause option;
    }
  | ForeignKey of {
      local_columns: string list option;
      name: string option;
      table: Types.table_name;
      columns: string list;
      on_update: foreign_conflict_clause option;
      on_delete: foreign_conflict_clause option;
    }

type 'a field = string * 'a Type.t * [`Column] sql_constraint list
type 'a constraint_ = 'a sql_constraint

let pp_conflict_clause fmt = function
  | `ROLLBACK ->
    Format.fprintf fmt "ON CONFLICT ROLLBACK"
  | `ABORT ->
    Format.fprintf fmt "ON CONFLICT ABORT"
  | `FAIL ->
    Format.fprintf fmt "ON CONFLICT FAIL"
  | `IGNORE ->
    Format.fprintf fmt "ON CONFLICT IGNORE"
  | `REPLACE ->
    Format.fprintf fmt "ON CONFLICT REPLACE"


let pp_foreign_conflict_clause fmt = function
  | `SET_NULL -> Format.fprintf fmt "SET NULL"
  | `SET_DEFAULT -> Format.fprintf fmt "SET DEFAULT"
  | `CASCADE -> Format.fprintf fmt "CASCADE"
  | `RESTRICT -> Format.fprintf fmt "RESTRICT"
  | `NO_ACTION -> Format.fprintf fmt "NO ACTION"


let pp_opt f fmt = function
    None -> ()
  | Some v -> Format.fprintf fmt " %a" f v
let pp_parens f fmt = fun v ->
  Format.fprintf fmt "(%a)" f v

let pp_ordering fmt = function
  | `ASC -> Format.fprintf fmt "ASC"
  | `DESC -> Format.fprintf fmt "DESC"

let pp_column_list fmt ls =
  Format.pp_print_list
    ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
    Format.pp_print_string fmt ls

let pp_constraint_name fmt = function
  | None -> ()
  | Some name ->
    Format.fprintf fmt "CONSTRAINT %s " name

let pp_sql_constraint fmt = function
  | PrimaryKey {
    name; ordering; local_columns;
    on_conflict; auto_increment;
  } ->
    Format.fprintf fmt
      "%aPRIMARY KEY%a%a%a%a"
      pp_constraint_name name
      (pp_opt pp_ordering) ordering
      (pp_opt (pp_parens pp_column_list)) local_columns
      (pp_opt pp_conflict_clause) on_conflict
      (fun fmt vl ->
         if vl then
           Format.fprintf fmt " AUTOINCREMENT")
      auto_increment
  | NotNull { name; on_conflict } ->
    Format.fprintf fmt
      "%aNOT NULL%a"
      pp_constraint_name name
      (pp_opt pp_conflict_clause) on_conflict
  | Unique { name; local_columns; on_conflict } ->
    Format.fprintf fmt
      "%aUNIQUE%a%a"
      pp_constraint_name name
      (pp_opt (pp_parens pp_column_list)) local_columns
      (pp_opt pp_conflict_clause) on_conflict
  | ForeignKey {
    local_columns;
    name;
    table;
    columns;
    on_update;
    on_delete
  } ->
    Format.fprintf fmt
      "%a%a%sREFERENCES %s %a%a%a"
      pp_constraint_name name
      (pp_opt (fun fmt vl ->
         Format.fprintf fmt
           "FOREIGN KEY %a"
           (pp_parens pp_column_list)
           vl)) local_columns
      (if Option.is_some name || Option.is_some local_columns then " " else "")
      (snd table)
      (pp_parens pp_column_list) columns
      (pp_opt (fun fmt vl ->
         Format.fprintf fmt "ON UPDATE %a"
           pp_foreign_conflict_clause vl)) on_update
      (pp_opt (fun fmt vl ->
         Format.fprintf fmt "ON DELETE %a"
           pp_foreign_conflict_clause vl)) on_delete

let ensure_table_constraint : 'a sql_constraint -> unit =
  function
  | NotNull _ ->
    invalid_arg "NOT NULL constraints are not table constraints"
  | PrimaryKey {
    name=_; ordering; local_columns; on_conflict=_;
    auto_increment } ->
    if auto_increment then
      invalid_arg "PRIMARY KEY constraints when given as a table \
                   constraint can not auto-increment";
    if Option.is_some ordering then
      invalid_arg "PRIMARY KEY constraints when given as a table \
                   constraint can not specify ordering";
    if Option.is_none local_columns then
      invalid_arg "PRIMARY KEY constraints when given as a table \
                   constraint must specify columns explicitly";
  | Unique { name=_; local_columns; on_conflict=_ } ->
    if Option.is_none local_columns then
      invalid_arg "UNIQUE constraints when given as a table \
                   constraint must specify columns explicitly";
  | ForeignKey { local_columns; name=_; table=_; columns=_;
                 on_update=_; on_delete=_ } ->
    if Option.is_none local_columns then
      invalid_arg "UNIQUE constraints when given as a table \
                   constraint must specify the local columns \
                   explicitly"

let ensure_column_constraint : 'a sql_constraint -> unit =
  function
  | NotNull _ -> ()
  | PrimaryKey {
    name=_; ordering=_; local_columns; on_conflict=_;
    auto_increment=_ } ->
    if Option.is_some local_columns then
      invalid_arg "PRIMARY KEY column constraints can not list \
                   columns explicitly";
  | Unique { name=_; local_columns; on_conflict=_ } ->
    if Option.is_some local_columns then
      invalid_arg "UNIQUE column constraints can not list \
                   columns explicitly";
  | ForeignKey { local_columns; name=_; table=_; columns=_;
                 on_update=_; on_delete=_ } ->
    if Option.is_some local_columns then
      invalid_arg "FOREIGN KEY column constraints can not list \
                   columns explicitly"

let field ?(constraints : _ list =[]) name ~ty : 'a field =
  List.iter ensure_column_constraint constraints;
  (name, ty, constraints)

let field_name = function (name, _, _) -> name

let primary_key
      ?name ?ordering ?on_conflict
      ?(auto_increment=false) () : [`Column] sql_constraint =
  PrimaryKey {
    name;
    ordering;
    local_columns=None;
    on_conflict; auto_increment
  }

let table_primary_key
      ?name ?on_conflict
      columns : [`Table] sql_constraint =
  PrimaryKey {
    name;
    ordering=None;
    local_columns=Some columns;
    on_conflict;
    auto_increment=false
  }

let not_null ?name ?on_conflict () : [`Column] sql_constraint =
  NotNull { name; on_conflict }

let unique ?name ?on_conflict () : [`Column] sql_constraint =
  Unique {
    name;
    local_columns=None;
    on_conflict;
  }

let table_unique ?name ?on_conflict columns : [`Table] sql_constraint =
  Unique {
    name;
    local_columns=Some columns;
    on_conflict;
  }

let rec expr_list_to_column_names : 'a . Types.table_name -> 'a Expr.expr_list -> string list =
  fun (type a) table_name (ls: a Types.expr_list) : string list ->
  match ls with
  | [] -> []
  | Types.FIELD (table_name', name, _) :: t ->
    if not (table_name = table_name') then
      invalid_arg "foreign key constraint uses fields from a \
                   different table than the one specified";
    name :: expr_list_to_column_names table_name t
  | _ :: _ ->
    invalid_arg "foreign key constraint must operate on fields \
                 directly not derived expressions" 

let foreign_key ?name ?on_update ?on_delete ~table ~columns () : [`Column] sql_constraint =
  ForeignKey {
    local_columns=None;
    name;
    table;
    columns=expr_list_to_column_names table columns;
    on_update;
    on_delete;
  }

let table_foreign_key ?name ?on_update ?on_delete ~table ~columns local_columns : [`Table] sql_constraint =
  ForeignKey {
    local_columns=Some local_columns;
    name;
    table;
    columns=expr_list_to_column_names table columns;
    on_update;
    on_delete;
  }


let ty : 'a field -> 'a Type.t = function (_, ty, _) -> ty

type 'a table =
  | [] : unit table
  | (::) : ('a field * 'b table) -> ('a * 'b) table

let to_sql ~name table (constraints: 'a sql_constraint list) = 
  let rec loop  : 'a . string option -> 'a table -> string option = fun acc (type a) (table: a table) ->
    let update_acc_with acc res =
      let acc = match acc with None -> "" | Some v -> v ^ ", "  in
      let acc = acc ^ res in
      Some acc in
    match table with
    | [] -> acc
    | ((f, ty, constraints) :: rest) ->
      let constraints_text =
        match constraints with
          [] -> ""
        | _ :: _ ->
          Format.asprintf " %a"
            (Format.pp_print_list ~pp_sep:Format.pp_print_space pp_sql_constraint)
            constraints in
      let acc = update_acc_with acc @@ f ^ " " ^ Type.show ty ^ constraints_text in
      loop acc rest in
  let acc = (loop None table) in
  let acc = match acc,constraints with
    | None, [] -> ""
    | Some acc, [] -> acc
    | None, constraints ->
      Format.asprintf "%a"
        (Format.pp_print_list
           ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
           pp_sql_constraint) constraints
    | Some acc, constraints ->
      Format.asprintf "%s, %a" acc
        (Format.pp_print_list
           ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
           pp_sql_constraint) constraints in
  Format.sprintf "CREATE TABLE IF NOT EXISTS %s (%s)"
    name acc