Source file dialect.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
type t = PostgreSQL | MariaDB | SQLite

module type DIALECT = sig
  val name : string
  val default_port : int option
  val admin_database : string option
  val database_exists_sql : string
  val create_database_sql : string -> string
  val drop_database_sql : string -> string
  val timestamp_to_string : string -> string
  val supports_database_lifecycle : bool
end

module PostgreSQL_dialect : DIALECT = struct
  let name = "PostgreSQL"
  let default_port = Some 5432
  let admin_database = Some "postgres"
  let supports_database_lifecycle = true

  let database_exists_sql =
    "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)"

  let create_database_sql db_name = Printf.sprintf "CREATE DATABASE %s" db_name

  let drop_database_sql db_name =
    Printf.sprintf "DROP DATABASE IF EXISTS %s" db_name

  let timestamp_to_string col = Printf.sprintf "%s::text" col
end

module MariaDB_dialect : DIALECT = struct
  let name = "MariaDB"
  let default_port = Some 3306
  let admin_database = Some "mysql"
  let supports_database_lifecycle = true

  let database_exists_sql =
    "SELECT EXISTS(SELECT 1 FROM information_schema.SCHEMATA WHERE SCHEMA_NAME \
     = ?)"

  let create_database_sql db_name =
    (* Use backticks for identifier quoting, IF NOT EXISTS for idempotency *)
    Printf.sprintf "CREATE DATABASE IF NOT EXISTS `%s`" db_name

  let drop_database_sql db_name =
    Printf.sprintf "DROP DATABASE IF EXISTS `%s`" db_name

  let timestamp_to_string col = Printf.sprintf "CAST(%s AS CHAR)" col
end

module SQLite_dialect : DIALECT = struct
  let name = "SQLite"
  let default_port = None
  let admin_database = None
  let supports_database_lifecycle = false

  (* These are unused for SQLite but required by signature *)
  let database_exists_sql = ""
  let create_database_sql _ = ""
  let drop_database_sql _ = ""
  let timestamp_to_string col = col
end

(** Normalize database URL for Caqti compatibility

    SQLite: caqti-driver-sqlite3 expects sqlite3:path (single colon), not
    sqlite3://path We accept both formats for user convenience but normalize to
    what Caqti expects. *)
let normalize_url (url : string) : string =
  let prefix = "sqlite3://" in
  if String.starts_with ~prefix url then
    let n = String.length prefix in
    "sqlite3:" ^ String.sub url n (String.length url - n)
  else url

let detect_from_url (url : string) : (t, string) result =
  if String.starts_with ~prefix:"postgresql://" url then Ok PostgreSQL
  else if String.starts_with ~prefix:"postgres://" url then Ok PostgreSQL
  else if String.starts_with ~prefix:"mariadb://" url then Ok MariaDB
  else if String.starts_with ~prefix:"mysql://" url then Ok MariaDB
  else if String.starts_with ~prefix:"sqlite3://" url then Ok SQLite
  else if String.starts_with ~prefix:"sqlite3:" url then Ok SQLite
  else
    let scheme =
      match String.index_opt url ':' with
      (* Grab "scheme://" for the message, but clamp the length: a URL like
         "ht:" has no room for the "//" and would otherwise overflow String.sub. *)
      | Some idx -> String.sub url 0 (min (idx + 3) (String.length url))
      | None -> url
    in
    Error
      (Printf.sprintf
         "Unsupported database URL scheme: '%s'\n\n\
          Supported databases:\n\
          - PostgreSQL: postgresql:// or postgres://\n\
          - MariaDB/MySQL: mariadb:// or mysql://\n\
          - SQLite: sqlite3:// or sqlite3:\n\n\
          Examples:\n\
          - postgresql://user@localhost:5432/mydb\n\
          - mariadb://root@localhost:3306/mydb\n\
          - sqlite3://./dev.db (normalized to sqlite3:./dev.db)\n\
          - sqlite3::memory: (in-memory database)\n\n"
         scheme)

let get_dialect (db_type : t) : (module DIALECT) =
  match db_type with
  | PostgreSQL -> (module PostgreSQL_dialect)
  | MariaDB -> (module MariaDB_dialect)
  | SQLite -> (module SQLite_dialect)

let to_string = function
  | PostgreSQL -> "PostgreSQL"
  | MariaDB -> "MariaDB"
  | SQLite -> "SQLite"