a database layer insipred by caqti and ecto
at main 4.9 kB view raw
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)