From 3ff577991e5ed94bc954c8fdbd083aa538e5d0e0 Mon Sep 17 00:00:00 2001 From: Gleb Patsiia Date: Mon, 11 May 2026 12:20:33 +0000 Subject: [PATCH] gen_migrate: varchar(N), blob(N) default, collates default is only as sql str --- lib/dialect.ml | 13 +- lib/parser_state.ml | 15 + lib/parser_utils.ml | 1 + lib/sql.ml | 73 +- lib/sql_lexer.mll | 21 +- lib/sql_parser.mly | 101 +- lib/syntax.ml | 44 +- lib/tables.ml | 137 +- src/gen_caml.ml | 13 +- src/gen_migrations.ml | 335 ++-- src/gen_xml.ml | 5 +- src/main.ml | 73 +- test/cram/gen_migrations.t | 2963 +++++++++++++++++++++++++++++++----- test/cram/test.t | 18 +- 14 files changed, 3175 insertions(+), 637 deletions(-) diff --git a/lib/dialect.ml b/lib/dialect.ml index 13ad3f11..540bc55e 100644 --- a/lib/dialect.ml +++ b/lib/dialect.ml @@ -224,7 +224,7 @@ open Sql let check_unsigned_type pos = function | Source_type.Infer Type.UInt64 -> [get_unsigned_types pos] - | Source_type.Int (_, Unsigned) -> [get_unsigned_types pos] + | Source_type.Int { sign = Unsigned; _ } -> [get_unsigned_types pos] | _ -> [] let check_collation_opt (collation : string located option) = @@ -376,10 +376,11 @@ and analyze_column_def_internal acc cds k = match cds with | None -> acc in let acc = extra - |> List.find_map (function - | { value = Alter_action_attr.Default { value = expr; pos }; _ } -> + |> List.find_map (fun c -> + match c.value with + | Alter_action_attr.Default { expr = { value = expr; _ }; _ } -> let col_kind = Option.map (fun k -> k.value.collated) kind in - Some (get_default_expr ~kind:col_kind ~expr pos) + Some (get_default_expr ~kind:col_kind ~expr c.pos) | _ -> None) |> Option.map_default (fun f -> f :: acc) acc in @@ -437,7 +438,7 @@ and analyze_insert_action acc ias k = match ias with analyze_assignment_expr acc conflict_aes (fun acc -> analyze_insert_action acc rest k)) -let analyze_schema_index idx = match idx.value with +let analyze_schema_index idx = match idx.value.Sql.idx_kind with | Regular_idx -> None | Fulltext -> Some (get_fulltext_index idx.pos) | Spatial -> None @@ -455,7 +456,7 @@ let rec analyze stmt = | Alter (_, actions) -> analyze_alter_action acc actions List.rev | Rename _ -> [] - | CreateIndex (_, _, cols) -> List.concat_map check_collated cols + | CreateIndex { ci_cols; _ } -> List.concat_map check_collated ci_cols | Insert insert_action -> analyze_insert_action acc [insert_action] List.rev | Delete (_, where_opt) -> diff --git a/lib/parser_state.ml b/lib/parser_state.ml index c44661d1..a435f479 100644 --- a/lib/parser_state.ml +++ b/lib/parser_state.ml @@ -11,3 +11,18 @@ module Stmt_metadata = struct let find_all k = Hashtbl.find_all stmt_metadata k let reset () = Hashtbl.reset stmt_metadata end + +let current_lexbuf : Lexing.lexbuf option ref = ref None + +let with_lexbuf lexbuf f = + let saved = !current_lexbuf in + current_lexbuf := Some lexbuf; + Fun.protect f ~finally:(fun () -> current_lexbuf := saved) + +let extract_source (start_, end_) = + Stdlib.Option.bind !current_lexbuf (fun lexbuf -> + let len = end_ - start_ in + if len > 0 && start_ >= 0 && end_ <= lexbuf.Lexing.lex_buffer_len then + Some (Bytes.sub_string lexbuf.lex_buffer start_ len) + else + None) diff --git a/lib/parser_utils.ml b/lib/parser_utils.ml index 63fe0eca..73ee31d2 100644 --- a/lib/parser_utils.ml +++ b/lib/parser_utils.ml @@ -13,6 +13,7 @@ exception Error of exn * (int * int * string * string) module Make(T : Parser_type) = struct let parse_buf_exn lexbuf = + Parser_state.with_lexbuf lexbuf @@ fun () -> try T.input T.rule lexbuf with exn -> diff --git a/lib/sql.ml b/lib/sql.ml index 66e38798..640dafa3 100644 --- a/lib/sql.ml +++ b/lib/sql.ml @@ -530,10 +530,23 @@ type float_precision = Single | Double [@@deriving show {with_path=false}, eq] module Source_type = struct + type text_flavor = + | PlainText of lob_size option (* TEXT/TINYTEXT/MEDIUMTEXT/LONGTEXT *) + | Char of int option + | Varchar of int option + | Varchar2 of int option + [@@deriving show, eq] + + type blob_flavor = + | PlainBlob of lob_size option (* BLOB/TINYBLOB/MEDIUMBLOB/LONGBLOB *) + | Varbinary of int option + [@@deriving show, eq] + type kind = Infer of Type.kind - | Int of int_size option * signedness + | Int of { size : int_size option; sign : signedness; display_width : int option } | Float of float_precision - | Blob of lob_size option | Text of lob_size option + | Blob of blob_flavor + | Text of text_flavor [@@deriving show, eq] type t = { t : kind; nullability : Type.nullability; } [@@deriving eq, show{with_path=false}, make] @@ -546,7 +559,7 @@ module Source_type = struct let to_infer_type { t; nullability; } = let t = match t with | Infer ty -> ty - | Int (Some Big, Unsigned) -> Type.UInt64 + | Int { size = Some Big; sign = Unsigned; _ } -> Type.UInt64 | Int _ -> Type.Int | Float _ -> Type.Float | Blob _ -> Type.Blob @@ -733,7 +746,7 @@ type insert_action = on_conflict_clause : conflict_clause located option; } [@@deriving show {with_path=false}] -type table_constraints = [ `Ignore | `Primary of string list | `Unique of string list ] [@@deriving show {with_path=false}] +type table_constraints = [ `Ignore | `Primary of string list | `Unique of string option * string list ] [@@deriving show {with_path=false}] type index_kind = | Regular_idx @@ -743,7 +756,10 @@ type index_kind = module Alter_action_attr = struct - type constraint_ = Syntax_constraint of Constraint.t | Default of expr located + type default = { expr : expr located; sql : string option } + [@@deriving show {with_path=false}] + + type constraint_ = Syntax_constraint of Constraint.t | Default of default [@@deriving show {with_path=false}] type t = { @@ -758,9 +774,16 @@ module Alter_action_attr = struct | Syntax_constraint c -> c | Default _ -> WithDefault + let default_sql (col : t) = + List.find_map (fun (c : constraint_ located) -> + match c.value with + | Default { sql; _ } -> sql + | Syntax_constraint _ -> None + ) col.extra + let kind_to_type_kind = function | Source_type.Infer k -> k - | Source_type.Int (Some Big, Unsigned) -> Type.UInt64 + | Source_type.Int { size = Some Big; sign = Unsigned; _ } -> Type.UInt64 | Source_type.Int _ -> Type.Int | Source_type.Float _ -> Type.Float | Source_type.Blob _ -> Type.Blob @@ -777,8 +800,10 @@ module Alter_action_attr = struct let from_attr (attr: attr): t = let extra = attr.extra |> Constraints.elements |> List.map (fun c -> let c = match c with - | Constraint.WithDefault -> Default (make_located ~pos:(0,0) ~value:(Value - (make_collated ~collated:(Type.depends Any) ()))) + | Constraint.WithDefault -> Default { + expr = make_located ~pos:(0,0) ~value:(Value (make_collated ~collated:(Type.depends Any) ())); + sql = None; + } | x -> Syntax_constraint x in make_located ~pos:(0,0) ~value:c @@ -788,10 +813,36 @@ module Alter_action_attr = struct { name = attr.name; kind; extra; meta } end +type index_op_kind = + | Plain_idx + | Unique_idx + | Fulltext_idx + | Spatial_idx + [@@deriving show {with_path=false}, eq] + +type table_inline_index = { + idx_kind : index_kind; + idx_name : string option; + idx_cols : string list; + idx_unique : bool; +} +[@@deriving show {with_path=false}] + +type add_index = { add_idx_name : string option; add_idx_kind : index_op_kind; add_idx_cols : string list } + [@@deriving show {with_path=false}] + +type create_index_def = { + ci_name : string; + ci_table : table_name; + ci_cols : string collated list; + ci_kind : index_op_kind; +} +[@@deriving show {with_path=false}] + type create_target_schema = { schema: Alter_action_attr.t list; constraints: table_constraints list; - indexes: index_kind located list; + indexes: table_inline_index located list; } [@@deriving show] @@ -814,7 +865,7 @@ type alter_action = [ | `RenameIndex of string * string | `Drop of string | `Change of string * Alter_action_attr.t * alter_pos - | `AddIndex of string option * string list + | `AddIndex of add_index | `DropIndex of string | `AddPrimaryKey of string list | `DropPrimaryKey @@ -830,7 +881,7 @@ type stmt = | Drop of table_name | Alter of table_name * alter_action list | Rename of (table_name * table_name) list - | CreateIndex of string * table_name * string collated list (* index name, table name, columns *) + | CreateIndex of create_index_def | Insert of insert_action | Delete of table_name * expr option | DeleteMulti of table_name list * nested * expr option diff --git a/lib/sql_lexer.mll b/lib/sql_lexer.mll index 9cc8f623..1e95d0a7 100644 --- a/lib/sql_lexer.mll +++ b/lib/sql_lexer.mll @@ -224,16 +224,19 @@ let keywords = all T_BOOLEAN ["bool";"boolean"]; all T_FLOAT ["float";"real";"float4";"float8";"int1";"int2";"int3";"int4";"int8"]; all T_DOUBLE ["double"]; - all (T_BLOB None) ["blob";"varbinary"]; - all (T_BLOB (Some Tiny)) ["tinyblob"]; - all (T_BLOB (Some Medium)) ["mediumblob"]; - all (T_BLOB (Some Long)) ["longblob"]; - all (T_TEXT None) ["text";"char";"varchar"]; - all (T_TEXT (Some Tiny)) ["tinytext"]; - all (T_TEXT (Some Medium)) ["mediumtext"]; - all (T_TEXT (Some Long)) ["longtext"]; + all T_BLOB ["blob"]; + all T_TINYBLOB ["tinyblob"]; + all T_MEDIUMBLOB ["mediumblob"]; + all T_LONGBLOB ["longblob"]; + all T_VARBINARY ["varbinary"]; + all T_TEXT ["text"]; + all T_TINYTEXT ["tinytext"]; + all T_MEDIUMTEXT ["mediumtext"]; + all T_LONGTEXT ["longtext"]; + all T_CHAR ["char"]; + all T_VARCHAR ["varchar"]; + all T_VARCHAR2 ["varchar2"]; (* oracle *) all T_JSON ["json"]; - all (T_TEXT None) ["varchar2"]; (* oracle *) all T_DATETIME ["datetime"]; all T_UUID ["uuid"]; (* http://www.postgresql.org/docs/9.4/static/datatype-uuid.html *) !k diff --git a/lib/sql_parser.mly b/lib/sql_parser.mly index 82aefb96..65d1ab00 100644 --- a/lib/sql_parser.mly +++ b/lib/sql_parser.mly @@ -19,6 +19,9 @@ List.filter_map param l, List.mem (`Limit,`Const 1) l let poly name ret parameters = Fun { fn_name = name; kind = (F (Typ ret, List.map (fun _ -> Var 0) parameters)); parameters; is_over_clause = false } + + let mk_inline_index ~kind ~pos (idx_name, idx_cols) = + `Index (make_located ~value:{ idx_kind = kind; idx_name; idx_cols; idx_unique = false } ~pos) %} %token INTEGER @@ -54,8 +57,8 @@ %token NUM_DIV_OP NUM_EQ_OP NUM_CMP_OP PLUS MINUS NOT_DISTINCT_OP NUM_BIT_SHIFT NUM_BIT_OR NUM_BIT_AND %token JSON_EXTRACT_OP JSON_UNQUOTE_EXTRACT_OP %token T_INTEGER -%token T_BLOB -%token T_TEXT +%token T_TEXT T_TINYTEXT T_MEDIUMTEXT T_LONGTEXT T_CHAR T_VARCHAR T_VARCHAR2 +%token T_BLOB T_TINYBLOB T_MEDIUMBLOB T_LONGBLOB T_VARBINARY %token T_FLOAT T_DOUBLE T_BOOLEAN T_DATETIME T_UUID T_DECIMAL T_JSON (* @@ -133,9 +136,10 @@ statement: CREATE ioption(temporary) TABLE ioption(if_not_exists) name=table_nam { Drop name } - | CREATE UNIQUE? INDEX if_not_exists? name=IDENT ON table=table_name cols=sequence(index_column) + | CREATE u=boption(UNIQUE) INDEX if_not_exists? name=IDENT ON table=table_name cols=sequence(index_column) { - CreateIndex (name, table, cols) + let ci_kind = if u then Sql.Unique_idx else Sql.Plain_idx in + CreateIndex { ci_name = name; ci_table = table; ci_cols = cols; ci_kind } } | select_stmt { Select $1 } | insert_action_kind=insert_cmd target=table_name names=sequence(IDENT)? VALUES values=commas(sequence(set_column_expr))? ss=located(conflict_clause)? @@ -355,7 +359,8 @@ maybe_as_with_detupled: AS? name=IDENT names=sequence(IDENT)? { name, names } maybe_parenth(X): x=X | LPAREN x=X RPAREN { x } alter_action: ADD COLUMN? col=maybe_parenth(column_def) pos=alter_pos { `Add (col,pos) } - | ADD index_type name=IDENT? cols=sequence(IDENT) { `AddIndex (name, cols) } + | ADD PRIMARY KEY cols=sequence(IDENT) { `AddPrimaryKey cols } + | ADD k=index_type name=IDENT? cols=sequence(IDENT) { `AddIndex { add_idx_name = name; add_idx_kind = k; add_idx_cols = cols } } | ADD CONSTRAINT name=IDENT? table_constraint_1 index_options { `AddConstraint name } | RENAME either(TO,AS)? new_name=table_name { `RenameTable new_name } | RENAME COLUMN old_name=IDENT TO new_name=IDENT { `RenameColumn (old_name, new_name) } @@ -378,7 +383,11 @@ ttl_option: TTL EQUAL col=IDENT PLUS INTERVAL n=INTEGER unit=INTERVAL_UNIT { `TtlSet (col, n, unit) } | TTL_ENABLE EQUAL v=TEXT { `TtlEnable v } index_or_key: INDEX | KEY { } -index_type: index_or_key | UNIQUE index_or_key? | either(FULLTEXT,SPATIAL) index_or_key? | PRIMARY KEY { } +index_type: + | index_or_key { Sql.Plain_idx } + | UNIQUE index_or_key? { Sql.Unique_idx } + | FULLTEXT index_or_key? { Sql.Fulltext_idx } + | SPATIAL index_or_key? { Sql.Spatial_idx } alter_pos: AFTER col=IDENT { `After col } | FIRST { `First } | { `Default } @@ -392,24 +401,27 @@ column_def: name=IDENT sql_kind=located_sql_type? extra=located(column_def_extra { Alter_action_attr.name = name; meta; kind = sql_kind; extra; } } +inline_idx_kind: + | index_or_key { Sql.Regular_idx } + | FULLTEXT index_or_key? { Sql.Fulltext } + | SPATIAL index_or_key? { Sql.Spatial } + column_def1: c=column_def { `Attr c } | pair(CONSTRAINT,IDENT?)? l=table_constraint_1 index_options { `Constraint l } - | index_or_key table_index { `Index (make_located ~value:Regular_idx ~pos:($startofs, $endofs)) } - | FULLTEXT index_or_key? table_index { `Index (make_located ~value:Fulltext ~pos:($startofs, $endofs)) } - | SPATIAL index_or_key? table_index { `Index (make_located ~value:Spatial ~pos:($startofs, $endofs)) } + | kind=inline_idx_kind t=table_index { mk_inline_index ~kind ~pos:($startofs, $endofs) t } -int_arg: delimited(LPAREN,INTEGER,RPAREN) {} +int_arg: LPAREN n=INTEGER RPAREN { n } key_part: n=IDENT int_arg? either(ASC,DESC)? { n } index_options: list(IDENT)? { } -table_index: IDENT? l=sequence(key_part) index_options { l } +table_index: name=IDENT? l=sequence(key_part) index_options { (name, l) } (* FIXME check columns *) table_constraint_1: | PRIMARY KEY l=sequence(key_part) { `Primary l } - | UNIQUE index_or_key? IDENT? l=sequence(key_part) { `Unique l } + | UNIQUE index_or_key? name=IDENT? l=sequence(key_part) { `Unique (name, l) } | FOREIGN KEY IDENT? sequence(IDENT) REFERENCES IDENT sequence(IDENT)? reference_action_clause* { `Ignore } @@ -427,7 +439,13 @@ column_def_extra: PRIMARY? KEY { Some (Alter_action_attr.Syntax_constraint Prima | NULL { Some (Alter_action_attr.Syntax_constraint Null) } | UNIQUE KEY? { Some (Alter_action_attr.Syntax_constraint Unique) } | AUTOINCREMENT { Some (Alter_action_attr.Syntax_constraint Autoincrement) } - | DEFAULT def=default_value { Some (Alter_action_attr.Default (make_located ~value:def ~pos:($startofs, $endofs))) } + | DEFAULT def=default_value { + let pos = ($startofs(def), $endofs(def)) in + Some (Alter_action_attr.Default { + expr = make_located ~value:def ~pos; + sql = Parser_state.extract_source pos; + }) + } | on_conflict { None } | CHECK LPAREN expr RPAREN { None } | COLLATE IDENT { None } @@ -633,8 +651,12 @@ interval_unit: INTERVAL_UNIT | YEAR_MONTH { Value (make_collated ~collated:(strict Datetime) ()) } int_type: - | s=T_INTEGER int_arg? u=UNSIGNED? { - Sql.Source_type.Int (s, Option.map_default (Fun.const Sql.Unsigned) Sql.Signed u) + | s=T_INTEGER n=int_arg? u=boption(UNSIGNED) { + Sql.Source_type.Int { + size = s; + sign = if u then Sql.Unsigned else Sql.Signed; + display_width = n; + } } (* expr_sql_type_flavor returns Type.kind for use in CAST *) @@ -651,13 +673,29 @@ expr_sql_type_flavor: | T_UUID { Blob } | T_JSON { Json } +%inline lob_size(PLAIN, TINY, MEDIUM, LONG): + | PLAIN { None } + | TINY { Some Sql.Tiny } + | MEDIUM { Some Sql.Medium } + | LONG { Some Sql.Long } + +%inline text_plain: s=lob_size(T_TEXT, T_TINYTEXT, T_MEDIUMTEXT, T_LONGTEXT) { Source_type.PlainText s } +%inline blob_plain: s=lob_size(T_BLOB, T_TINYBLOB, T_MEDIUMBLOB, T_LONGBLOB) { Source_type.PlainBlob s } + +%inline text_var: + | T_CHAR n=int_arg? { Source_type.Char n } + | T_VARCHAR n=int_arg? { Source_type.Varchar n } + | T_VARCHAR2 n=int_arg? { Source_type.Varchar2 n } + (* sql_type_flavor returns Source_type.kind *) sql_type_flavor: | t=int_type ZEROFILL? { t } | T_FLOAT { Source_type.Float Single } | T_DOUBLE PRECISION? { Source_type.Float Double } - | s=T_BLOB { Source_type.Blob s } - | NATIONAL? s=T_TEXT int_arg? VARYING? charset? { Source_type.Text s } + | f=blob_plain { Source_type.Blob f } + | T_VARBINARY n=int_arg? { Source_type.Blob (Varbinary n) } + | NATIONAL? f=text_plain charset? { Source_type.Text f } + | NATIONAL? f=text_var VARYING? charset? { Source_type.Text f } | t=expr_sql_type_flavor { Source_type.Infer t } | ENUM ctors=sequence(TEXT) charset? { Source_type.Infer (make_enum_kind ctors) } @@ -675,12 +713,11 @@ cast_as: %inline sequence_(X): LPAREN l=commas(X) { l } %inline sequence(X): l=sequence_(X) RPAREN { l } -charset: CHARSET c=IDENT { Named c } - | CHARSET BINARY { Binary } - | CHARACTER SET c=IDENT { Named c } - | CHARACTER SET BINARY { Binary } - | ASCII { Ascii } - | UNICODE { Unicode } +%inline charset_kw: CHARSET {} | CHARACTER SET {} +charset: charset_kw c=IDENT { Named c } + | charset_kw BINARY { Binary } + | charset_kw? ASCII { Ascii } + | charset_kw? UNICODE { Unicode } collate: COLLATE c=IDENT { make_located ~value:c ~pos:($startofs, $endofs) } sql_type: t=collated(sql_type_flavor) @@ -696,8 +733,8 @@ cast_sql_type: | t=expr_sql_type_flavor LPAREN INTEGER COMMA INTEGER RPAREN { t } | T_FLOAT { Float } | T_DOUBLE PRECISION? { Float } - | T_BLOB { Blob } - | T_TEXT int_arg? { Text } + | blob_plain | T_VARBINARY int_arg? { Blob } + | text_plain | text_var { Text } compound_op: | UNION { `Union } @@ -707,17 +744,21 @@ compound_op: (* manual_type returns Source_type.t for parameter type annotations *) manual_type: - | s=T_TEXT { { Source_type.t = Text s; nullability = Type.Strict } } + | f=text_plain { { Source_type.t = Text f; nullability = Type.Strict } } + | f=text_var { { Source_type.t = Text f; nullability = Type.Strict } } | T_JSON { Source_type.strict Json } - | s=T_BLOB { { Source_type.t = Blob s; nullability = Type.Strict } } + | f=blob_plain { { Source_type.t = Blob f; nullability = Type.Strict } } + | T_VARBINARY n=int_arg? { { Source_type.t = Blob (Varbinary n); nullability = Type.Strict } } | t=int_type { { Source_type.t; nullability = Type.Strict } } | T_FLOAT { { Source_type.t = Float Single; nullability = Type.Strict } } | T_DOUBLE { { Source_type.t = Float Double; nullability = Type.Strict } } | T_BOOLEAN { Source_type.strict Bool } | T_DATETIME { Source_type.strict Datetime } - | s=T_TEXT NULL { { Source_type.t = Text s; nullability = Type.Nullable } } - | T_JSON NULL { Source_type.nullable Json } - | s=T_BLOB NULL { { Source_type.t = Blob s; nullability = Type.Nullable } } + | f=text_plain NULL { { Source_type.t = Text f; nullability = Type.Nullable } } + | f=text_var NULL { { Source_type.t = Text f; nullability = Type.Nullable } } + | T_JSON NULL { Source_type.nullable Json } + | f=blob_plain NULL { { Source_type.t = Blob f; nullability = Type.Nullable } } + | T_VARBINARY n=int_arg? NULL { { Source_type.t = Blob (Varbinary n); nullability = Type.Nullable } } | t=int_type NULL { { Source_type.t; nullability = Type.Nullable } } | T_FLOAT NULL { { Source_type.t = Float Single; nullability = Type.Nullable } } | T_DOUBLE NULL { { Source_type.t = Float Double; nullability = Type.Nullable } } diff --git a/lib/syntax.ml b/lib/syntax.ml index aafea866..8dc19391 100644 --- a/lib/syntax.ml +++ b/lib/syntax.ml @@ -1386,7 +1386,7 @@ let with_constraints attrs constraints : Schema.t = List.iter (fun constr -> match constr with | `Primary [] -> fail "Schema Error: PRIMARY KEY must have at least one column" - | `Unique [] -> fail "Schema Error: UNIQUE constraint must have at least one column" + | `Unique (_, []) -> fail "Schema Error: UNIQUE constraint must have at least one column" | `Primary [ col_name ] -> begin match Hashtbl.find_opt constraints_table col_name with | None -> fail "Schema Error: no such column: %s" col_name @@ -1394,7 +1394,7 @@ let with_constraints attrs constraints : Schema.t = let new_constraints = Constraints.add PrimaryKey constraints in Hashtbl.replace constraints_table col_name new_constraints end - | `Unique [ col_name ] -> begin + | `Unique (_, [ col_name ]) -> begin match Hashtbl.find_opt constraints_table col_name with | None -> fail "Schema Error: no such column: %s" col_name | Some constraints -> @@ -1410,7 +1410,7 @@ let with_constraints attrs constraints : Schema.t = Hashtbl.replace constraints_table col new_constraints ) cols end - | `Unique cols -> begin + | `Unique (_, cols) -> begin List.iter (fun col -> match Hashtbl.find_opt constraints_table col with | None -> fail "Schema Error: no such column: %s" col @@ -1427,18 +1427,24 @@ let with_constraints attrs constraints : Schema.t = | None -> attr ) attrs + let rec eval (stmt:Sql.stmt) = let open Stmt in let open Schema.Source in let open Attr in match stmt with - | Create (name, Schema { schema; constraints; _ }) -> + | Create (name, Schema { schema; constraints; indexes }) -> let attrs = List.map Alter_action_attr.to_attr schema in let attrs = with_constraints attrs constraints in let columns = List.map2 (fun (col : Alter_action_attr.t) attr -> - { Tables.attr; source_kind = Option.map (fun k -> k.value.collated) col.kind } + { + Tables.attr; + source_kind = Option.map (fun k -> k.value) col.kind; + default_sql = Alter_action_attr.default_sql col; + } ) schema attrs in Tables.add_columns (name, columns); + Tables.add_inline_indexes name ~indexes ~constraints; ([],[],Create name) | Create (name, Select { value=select; _ }) -> let (schema,params,_) = eval_select_full empty_env select in @@ -1451,13 +1457,15 @@ let rec eval (stmt:Sql.stmt) = | Alter (name,actions) -> List.iter (function | `Add (col,pos) -> - let source_kind = Option.map (fun k -> k.value.collated) col.Alter_action_attr.kind in - Tables.alter_add name ~col:{ attr = Alter_action_attr.to_attr col; source_kind } ~pos + let source_kind = Option.map (fun k -> k.value) col.Alter_action_attr.kind in + let default_sql = Alter_action_attr.default_sql col in + Tables.alter_add name ~col:{ attr = Alter_action_attr.to_attr col; source_kind; default_sql } ~pos | `Drop col -> Tables.alter_drop name ~col | `Change (oldcol,col,pos) -> - let source_kind = Option.map (fun k -> k.value.collated) col.Alter_action_attr.kind in - Tables.alter_change name ~oldcol ~col:{ attr = Alter_action_attr.to_attr col; source_kind } ~pos + let source_kind = Option.map (fun k -> k.value) col.Alter_action_attr.kind in + let default_sql = Alter_action_attr.default_sql col in + Tables.alter_change name ~oldcol ~col:{ attr = Alter_action_attr.to_attr col; source_kind; default_sql } ~pos | `RenameColumn (oldcol,newcol) -> Tables.rename_column name ~old_name:oldcol ~new_name:newcol | `RenameTable new_name -> @@ -1466,7 +1474,14 @@ let rec eval (stmt:Sql.stmt) = Tables.drop_primary_key name | `AddPrimaryKey cols -> Tables.add_primary_key name ~cols - | `RenameIndex _ | `AddIndex _ | `DropIndex _ | `AddConstraint _ | `DropConstraint _ -> () (* indices are not tracked yet *) + | `AddIndex { add_idx_name = Some index_name; add_idx_kind = kind; add_idx_cols = cols } -> + Tables.index_add name ~index_name ~kind ~cols + | `AddIndex { add_idx_name = None; _ } -> () + | `DropIndex index_name -> + Tables.index_drop name ~index_name + | `RenameIndex (old_name, new_name) -> + Tables.index_rename name ~old_name ~new_name + | `AddConstraint _ | `DropConstraint _ -> () (* constraints are not tracked yet *) | `TtlOptions _ | `RemoveTtl _ -> () (* TTL is a TiDB-specific table property, not tracked in schema *) | `Default_or_convert_to (cs, collation) -> let old = Tables.get_charset name in @@ -1483,10 +1498,11 @@ let rec eval (stmt:Sql.stmt) = | Drop name -> Tables.drop name; ([],[],Drop name) - | CreateIndex (name,table,cols) -> - let cols = List.map (fun x -> x.collated) cols in - Sql.Schema.project cols (Tables.get_schema table) |> ignore; (* just check *) - [],[],CreateIndex name + | CreateIndex { ci_name; ci_table; ci_cols; ci_kind } -> + let cols = List.map (fun x -> x.collated) ci_cols in + Sql.Schema.project cols (Tables.get_schema ci_table) |> ignore; (* just check *) + Tables.index_add ci_table ~index_name:ci_name ~kind:ci_kind ~cols; + [],[],CreateIndex ci_name | Insert { target=table; action=`Values (names, values); on_conflict_clause; _ } -> let expect = values_or_all table names in let t = Tables.get_schema table in diff --git a/lib/tables.ml b/lib/tables.ml index 4c5f8c74..57148d3e 100644 --- a/lib/tables.ml +++ b/lib/tables.ml @@ -4,75 +4,124 @@ open Printf open ExtLib open Prelude -type column = { attr : Sql.attr; source_kind : Sql.Source_type.kind option } +type column = { + attr : Sql.attr; + source_kind : Sql.Source_type.kind Sql.collated option; + default_sql : string option; +} type table = Sql.table type table_charset = { charset : Sql.charset_name option; collation : string option } -type stored_table = Sql.table_name * column list +module SMap = Map.Make(String) + +type stored_index = { kind : Sql.index_op_kind; cols : string list } + +type stored_table = { + name : Sql.table_name; + columns : column list; + tbl_charset : table_charset option; + tbl_indexes : stored_index SMap.t; +} let store : stored_table list ref = ref [] -let charsets : (Sql.table_name, table_charset) Hashtbl.t = Hashtbl.create 16 (** FIXME table names case sensitivity? *) -(* NB compares with db name *) -let by_name (name:Sql.table_name) = fun (n,_) -> n = name +(* NB compares with db name. *) +let by_name (name:Sql.table_name) ((n, _) : Sql.table) = n = name let columns_to_schema cols = List.map (fun c -> c.attr) cols -let column_of_attr attr = { attr; source_kind = None } +let column_of_attr attr = { attr; source_kind = None; default_sql = None } -(** @raise Error when no such table *) -let get_from tables name = +(** @raise Failure when no such table is present *) +let get_from (tables : Sql.table list) name = try List.find (by_name name) tables with Not_found -> failwith (sprintf "no such table %s" (Sql.show_table_name name)) -let get_stored name = get_from !store name +let find_stored name = List.find_opt (fun t -> t.name = name) !store + +let get_stored name = match find_stored name with + | Some t -> t + | None -> failwith (sprintf "no such table %s" (Sql.show_table_name name)) let get name = - let (n, cols) = get_stored name in - (n, columns_to_schema cols) + let t = get_stored name in + (t.name, columns_to_schema t.columns) let get_schema name = snd (get name) -let get_columns name = snd (get_stored name) +let get_columns name = (get_stored name).columns let check name = ignore (get_stored name) let add_columns (name, cols) = - match List.find_all (by_name name) !store with - | [] -> store := (name, cols) :: !store - | _ -> failwith (sprintf "table %s already exists" (Sql.show_table_name name)) + match find_stored name with + | None -> store := { name; columns = cols; tbl_charset = None; tbl_indexes = SMap.empty } :: !store + | Some _ -> failwith (sprintf "table %s already exists" (Sql.show_table_name name)) let add (name, schema) = add_columns (name, List.map column_of_attr schema) -let get_charset name = - Hashtbl.find_opt charsets name - -let set_charset name cs = - Hashtbl.replace charsets name cs - -let drop name = check name; store := List.remove_if (by_name name) !store; Hashtbl.remove charsets name - -let rename oldname newname = - let (_,cols) = get_stored oldname in - add_columns (newname, cols); - Option.may (set_charset newname) (get_charset oldname); - drop oldname - let alter name f = check name; - let alter_stored ((n, cols) as tbl) = - if n = name then - name, f cols - else - tbl - in + let alter_stored t = if t.name = name then f t else t in store := List.map alter_stored !store +let alter_columns name f = alter name (fun t -> { t with columns = f t.columns }) + +let get_charset name = Option.map_default (fun t -> t.tbl_charset) None (find_stored name) + +let set_charset name cs = alter name (fun t -> { t with tbl_charset = Some cs }) + +let index_find name ~index_name = + Option.map_default (fun t -> SMap.find_opt index_name t.tbl_indexes) None (find_stored name) + +let column_find name ~column_name = + Option.map_default + (fun t -> List.find_opt (fun c -> c.attr.Sql.name = column_name) t.columns) + None (find_stored name) + +let index_add name ~index_name ~kind ~cols = + alter name (fun t -> + { t with tbl_indexes = SMap.add index_name { kind; cols } t.tbl_indexes }) + +let add_inline_indexes name ~indexes ~constraints = + List.iter (fun (idx : Sql.table_inline_index Sql.located) -> + match idx.value with + | { idx_kind = Sql.Regular_idx; idx_name = Some index_name; idx_cols = cols; idx_unique } -> + let kind = if idx_unique then Sql.Unique_idx else Sql.Plain_idx in + index_add name ~index_name ~kind ~cols + | _ -> () + ) indexes; + List.iter (function + | `Unique (Some index_name, (_ :: _ as cols)) -> + index_add name ~index_name ~kind:Sql.Unique_idx ~cols + | _ -> () + ) constraints + +let index_drop name ~index_name = + alter name (fun t -> { t with tbl_indexes = SMap.remove index_name t.tbl_indexes }) + +let index_rename name ~old_name ~new_name = + alter name (fun t -> + t.tbl_indexes + |> SMap.find_opt old_name + |> Option.map_default (fun idx -> + let m = SMap.remove old_name t.tbl_indexes in + { t with tbl_indexes = SMap.add new_name idx m }) t) + +let drop name = + check name; + store := List.filter (fun t -> t.name <> name) !store + +let rename oldname newname = + match find_stored newname with + | Some _ -> failwith (sprintf "table %s already exists" (Sql.show_table_name newname)) + | None -> alter oldname (fun t -> { t with name = newname }) + let schema_to_columns ~cols ~new_col new_schema = List.map (fun attr -> if attr.Sql.name = new_col.attr.Sql.name then new_col @@ -82,21 +131,21 @@ let schema_to_columns ~cols ~new_col new_schema = ) new_schema let alter_add name ~col ~pos = - alter name (fun cols -> + alter_columns name (fun cols -> let new_schema = Sql.Schema.add (columns_to_schema cols) col.attr pos in schema_to_columns ~cols ~new_col:col new_schema) let alter_drop name ~col:col_name = - alter name (fun cols -> + alter_columns name (fun cols -> List.filter (fun c -> c.attr.Sql.name <> col_name) cols) let alter_change name ~oldcol ~col ~pos = - alter name (fun cols -> + alter_columns name (fun cols -> let new_schema = Sql.Schema.change (columns_to_schema cols) oldcol col.attr pos in schema_to_columns ~cols ~new_col:col new_schema) let rename_column name ~old_name ~new_name = - alter name (fun cols -> + alter_columns name (fun cols -> List.map (fun c -> if c.attr.Sql.name = old_name then { c with attr = { c.attr with name = new_name } } else c @@ -110,7 +159,7 @@ let get_primary_key_columns = List.map (fun c -> c.attr.Sql.name) $ List.filter (fun c -> Sql.Constraints.exists is_pk_constraint c.attr.Sql.extra) let drop_primary_key name = - alter name (List.map (fun c -> + alter_columns name (List.map (fun c -> { c with attr = { c.attr with extra = Sql.Constraints.filter (Fun.negate is_pk_constraint) c.attr.Sql.extra } })) @@ -119,15 +168,15 @@ let add_primary_key name ~cols:col_names = | [_] -> Sql.Constraint.PrimaryKey | _ -> Sql.Constraint.make_composite_primary col_names in - alter name (List.map (fun c -> + alter_columns name (List.map (fun c -> if List.mem c.attr.Sql.name col_names then { c with attr = { c.attr with extra = Sql.Constraints.add pk c.attr.Sql.extra } } else c)) let print ch tables = let out = IO.output_channel ch in List.iter (Sql.print_table out) tables; IO.flush out -let print_all () = print stdout (List.map (fun (n, cols) -> (n, columns_to_schema cols)) !store) +let print_all () = print stdout (List.map (fun t -> (t.name, columns_to_schema t.columns)) !store) let print1 name = print stdout [get @@ Sql.make_table_name name] -let reset () = store := []; Hashtbl.clear charsets +let reset () = store := [] -let all () = List.map (fun (n, cols) -> (n, columns_to_schema cols)) !store +let all () = List.map (fun t -> (t.name, columns_to_schema t.columns)) !store diff --git a/src/gen_caml.ml b/src/gen_caml.ml index 131ed809..46424ba0 100644 --- a/src/gen_caml.ml +++ b/src/gen_caml.ml @@ -1249,14 +1249,17 @@ end module Header = Gen.Make(Generator_io) let generate_migrations name migrations = - let migration_names = List.map (fun (m : Gen_migrations.migration) -> m.name) migrations in + let named = List.mapi (fun index (m : Gen_migrations.migration) -> + Gen.choose_name m.props m.kind index, m + ) migrations in + let migration_names = List.map fst named in let make_stmt fn_name sql = { Gen.schema = []; vars = []; kind = Stmt.Other; props = Props.set (Props.set Props.empty "name" fn_name) "sql" sql } in - let stmts = List.concat_map (fun (m : Gen_migrations.migration) -> - [make_stmt ("apply_" ^ m.name) m.apply; - make_stmt ("revert_" ^ m.name) m.revert] - ) migrations in + let stmts = List.concat_map (fun (name, (m : Gen_migrations.migration)) -> + [make_stmt ("apply_" ^ name) m.apply; + make_stmt ("revert_" ^ name) m.revert] + ) named in Option.may (Header.generate_header ()) !Sqlgg_config.gen_header; generate ~gen_io:true ~migration_names:(Some migration_names) name stmts diff --git a/src/gen_migrations.ml b/src/gen_migrations.ml index 52e2c1f7..a15a134f 100644 --- a/src/gen_migrations.ml +++ b/src/gen_migrations.ml @@ -1,15 +1,23 @@ open Printf -open ExtLib open Sqlgg +exception Migration_error of string +let fail fmt = ksprintf (fun s -> raise (Migration_error s)) fmt + +let loc value = Sql.make_located ~pos:(0,0) ~value + let quote_id name = - sprintf "`%s`" name + sprintf "`%s`" (String.concat "``" (String.split_on_char '`' name)) let quote_table_name (name : Sql.table_name) = Option.map_default (fun db -> sprintf "`%s`.`%s`" db name.tn) (quote_id name.tn) name.db -let type_kind_to_sql = function - | Sql.Type.Int -> "INT" +let with_len base = Option.map_default (sprintf "%s(%d)" base) base + +let escape_sql_string s = String.concat "''" (String.split_on_char '\'' s) + +let type_kind_to_sql (k : Sql.Type.kind) = match k with + | Int -> "INT" | UInt64 -> "BIGINT UNSIGNED" | Text -> "TEXT" | Blob -> "BLOB" @@ -17,31 +25,51 @@ let type_kind_to_sql = function | Bool -> "BOOLEAN" | Datetime -> "DATETIME" | Decimal { precision = Some p; scale = Some s } -> sprintf "DECIMAL(%d,%d)" p s - | Decimal { precision = Some p; scale = None } -> sprintf "DECIMAL(%d)" p - | Decimal _ -> "DECIMAL" + | Decimal { precision; scale = None } -> with_len "DECIMAL" precision + | Decimal { precision = None; scale = Some _ } -> + fail "DECIMAL with scale but without precision is not representable" | Json -> "JSON" | Union { ctors; _ } -> + let ctors = Sql.Type.Enum_kind.Ctors.elements ctors in sprintf "ENUM(%s)" - (String.concat ", " (List.map (sprintf "'%s'") (Sql.Type.Enum_kind.Ctors.elements ctors))) - | Any | StringLiteral _ | Json_path | One_or_all -> "TEXT" + (String.concat ", " (List.map (fun c -> sprintf "'%s'" (escape_sql_string c)) ctors)) + | (Any | StringLiteral _ | Json_path | One_or_all) as k -> + fail "cannot serialize inferred type %s for migration SQL (non-storable)" + (Sql.Type.show_kind k) | FloatingLiteral _ -> "FLOAT" -let source_type_kind_to_sql = function - | Sql.Source_type.Infer k -> type_kind_to_sql k - | Int (None, Sql.Signed) -> "INT" - | Int (Some Tiny, Signed) -> "TINYINT" | Int (Some Small, Signed) -> "SMALLINT" - | Int (Some Medium, Signed) -> "MEDIUMINT" | Int (Some Big, Signed) -> "BIGINT" - | Int (None, Unsigned) -> "INT UNSIGNED" - | Int (Some Tiny, Unsigned) -> "TINYINT UNSIGNED" | Int (Some Small, Unsigned) -> "SMALLINT UNSIGNED" - | Int (Some Medium, Unsigned) -> "MEDIUMINT UNSIGNED" | Int (Some Big, Unsigned) -> "BIGINT UNSIGNED" - | Float Sql.Single -> "FLOAT" +let int_size_name : Sql.int_size option -> string = function + | None -> "INT" + | Some Tiny -> "TINYINT" + | Some Small -> "SMALLINT" + | Some Medium -> "MEDIUMINT" + | Some Big -> "BIGINT" + +let lob_size_name ~plain ~tiny ~medium ~long : Sql.lob_size option -> string = function + | None -> plain + | Some Tiny -> tiny + | Some Medium -> medium + | Some Long -> long + +let blob_plain_name = + lob_size_name ~plain:"BLOB" ~tiny:"TINYBLOB" ~medium:"MEDIUMBLOB" ~long:"LONGBLOB" + +let text_plain_name = + lob_size_name ~plain:"TEXT" ~tiny:"TINYTEXT" ~medium:"MEDIUMTEXT" ~long:"LONGTEXT" + +let source_type_kind_to_sql (k : Sql.Source_type.kind) = match k with + | Infer k -> type_kind_to_sql k + | Int { size; sign; display_width } -> + let suffix = match sign with Signed -> "" | Unsigned -> " UNSIGNED" in + with_len (int_size_name size) display_width ^ suffix + | Float Single -> "FLOAT" | Float Double -> "DOUBLE" - | Blob None -> "BLOB" - | Blob (Some Tiny) -> "TINYBLOB" | Blob (Some Medium) -> "MEDIUMBLOB" - | Blob (Some Long) -> "LONGBLOB" - | Text None -> "TEXT" - | Text (Some Tiny) -> "TINYTEXT" | Text (Some Medium) -> "MEDIUMTEXT" - | Text (Some Long) -> "LONGTEXT" + | Blob (PlainBlob s) -> blob_plain_name s + | Blob (Varbinary len) -> with_len "VARBINARY" len + | Text (PlainText s) -> text_plain_name s + | Text (Char len) -> with_len "CHAR" len + | Text (Varchar len) -> with_len "VARCHAR" len + | Text (Varchar2 len) -> with_len "VARCHAR2" len let constraint_to_sql = function | Sql.Constraint.PrimaryKey -> Some "PRIMARY KEY" @@ -49,157 +77,196 @@ let constraint_to_sql = function | Null -> Some "NULL" | Unique -> Some "UNIQUE" | Autoincrement -> Some "AUTO_INCREMENT" - | WithDefault -> None - | OnConflict _ -> None - | Composite _ -> None + | WithDefault | OnConflict _ | Composite _ -> None -let alter_action_attr_to_sql (col : Sql.Alter_action_attr.t) = +let alter_action_attr_to_sql ~default_sql_lookup (col : Sql.Alter_action_attr.t) = let name = quote_id col.name in - let kind = Option.map_default (fun (k : _ Sql.collated Sql.located) -> " " ^ source_type_kind_to_sql k.value.collated) "" col.kind in - let extras = col.extra |> List.filter_map (fun (c : Sql.Alter_action_attr.constraint_ Sql.located) -> - match c.value with - | Syntax_constraint cst -> constraint_to_sql cst - | Default _ -> None - ) in - let extras = match extras with [] -> "" | l -> " " ^ String.concat " " l in - sprintf "%s%s%s" name kind extras - -let attr_to_column_sql (attr : Sql.attr) = - let col = Sql.Alter_action_attr.from_attr attr in - alter_action_attr_to_sql col + let kind_sql = + Option.map_default (fun (k : Sql.Source_type.kind Sql.collated Sql.located) -> + let type_sql = " " ^ source_type_kind_to_sql k.value.collated in + Option.map_default (fun c -> type_sql ^ " COLLATE " ^ c.Sql.value) type_sql k.value.collation) + "" col.kind + in + let extras = + col.extra |> List.filter_map (fun (c : Sql.Alter_action_attr.constraint_ Sql.located) -> + match c.value with + | Syntax_constraint cst -> constraint_to_sql cst + | Default _ -> Option.map (sprintf "DEFAULT %s") (default_sql_lookup col.name)) + in + let extras_sql = match extras with [] -> "" | l -> " " ^ String.concat " " l in + sprintf "%s%s%s" name kind_sql extras_sql let alter_pos_to_sql = function | `Default -> "" | `First -> " FIRST" | `After col -> sprintf " AFTER %s" (quote_id col) -let enrich_with_source_kind source_kind (col : Sql.Alter_action_attr.t) = - Option.map_default (fun sk -> - { col with kind = Some (Sql.make_located ~pos:(0,0) - ~value:(Sql.make_collated ~collated:sk ())) }) col source_kind - -let find_column columns col_name = - List.find (fun (c : Tables.column) -> c.attr.Sql.name = col_name) columns - -let inverse_action table_name (columns : Tables.column list) (action : Sql.alter_action) : Sql.alter_action = +let inverse_action ~table_name ~current_name ~pk_columns ~pk_dropped + (action : Sql.alter_action) = + let restore_column ~src col_name = + match Tables.column_find table_name ~column_name:col_name with + | Some (e : Tables.column) -> + let col = Sql.Alter_action_attr.from_attr e.attr in + Option.map_default (fun sk -> { col with kind = Some (loc sk) }) col e.source_kind + | None -> fail "%s %s: column not tracked in schema state" src (quote_id col_name) + in match action with | `Add (col, _pos) -> `Drop col.Sql.Alter_action_attr.name + | `Drop col_name when List.mem col_name pk_columns + && pk_columns <> [col_name] + && not pk_dropped -> + fail "DROP COLUMN %s: column participates in composite PRIMARY KEY (%s); \ + write a composite ALTER explicitly: \ + DROP COLUMN %s, DROP PRIMARY KEY, ADD PRIMARY KEY ()" + (quote_id col_name) + (String.concat ", " (List.map quote_id pk_columns)) + (quote_id col_name) | `Drop col_name -> - let entry = find_column columns col_name in - let col = Sql.Alter_action_attr.from_attr entry.attr |> enrich_with_source_kind entry.source_kind in - `Add (col, `Default) - | `Change (old_name, _new_col, _pos) -> - let entry = find_column columns old_name in - let old_col = Sql.Alter_action_attr.from_attr entry.attr |> enrich_with_source_kind entry.source_kind in - `Change (_new_col.Sql.Alter_action_attr.name, old_col, `Default) - | `RenameTable _new_name -> `RenameTable table_name + `Add (restore_column ~src:"DROP COLUMN" col_name, `Default) + | `Change (old_name, new_col, `Default) -> + let col = restore_column ~src:"CHANGE COLUMN" old_name in + `Change (new_col.Sql.Alter_action_attr.name, col, `Default) + | `Change (old_name, _, (`First | `After _)) -> + fail "CHANGE COLUMN %s with FIRST/AFTER: original column position is not tracked, \ + cannot restore on revert" (quote_id old_name) + | `RenameTable _ -> `RenameTable current_name | `RenameColumn (old_name, new_name) -> `RenameColumn (new_name, old_name) | `RenameIndex (old_name, new_name) -> `RenameIndex (new_name, old_name) - | `AddIndex (Some name, _cols) -> `DropIndex name - | `AddIndex (None, _) -> `None - | `DropIndex _name -> `None + | `AddIndex { add_idx_name = Some name; _ } -> `DropIndex name + | `AddIndex { add_idx_name = None; _ } -> + fail "ADD INDEX without a name has no identifier to DROP later" + | `DropIndex name -> + (match Tables.index_find table_name ~index_name:name with + | Some (idx : Tables.stored_index) -> + `AddIndex { Sql.add_idx_name = Some name; + add_idx_kind = idx.kind; + add_idx_cols = idx.cols } + | None -> fail "DROP INDEX %s: index not tracked in schema state" (quote_id name)) | `DropPrimaryKey -> - let pk_cols = Tables.get_primary_key_columns columns in - `AddPrimaryKey pk_cols - | `AddPrimaryKey _cols -> `DropPrimaryKey + (match pk_columns with + | [] -> fail "DROP PRIMARY KEY: no primary key in schema state, cannot restore on revert" + | cols -> `AddPrimaryKey cols) + | `AddPrimaryKey _ -> `DropPrimaryKey | `AddConstraint (Some name) -> `DropConstraint name - | `AddConstraint None -> `None - | `DropConstraint _name -> `None + | `AddConstraint None -> fail "ADD CONSTRAINT without a name cannot be reverted" + | `DropConstraint name -> + fail "DROP CONSTRAINT %s: definition not tracked in schema state" (quote_id name) | `Default_or_convert_to _ -> - let cs, collation = match Tables.get_charset table_name with - | Some old -> old.charset, Option.map (fun v -> Sql.make_located ~pos:(0,0) ~value:v) old.collation - | None -> None, None - in - `Default_or_convert_to (cs, collation) - | `TtlOptions (_, _) -> `RemoveTtl (0, 0) - | `RemoveTtl _ -> `None - | `None -> `None + (match Tables.get_charset table_name with + | Some { charset = Some _ as cs; collation } -> + `Default_or_convert_to (cs, Option.map loc collation) + | _ -> + fail "CONVERT TO CHARACTER SET: previous CHARACTER SET is not tracked \ + (no explicit charset in CREATE TABLE), revert cannot restore column encodings") + | `TtlOptions _ -> `RemoveTtl (0, 0) + | `RemoveTtl _ -> + fail "REMOVE TTL: previous TTL parameters are not tracked in schema, \ + cannot restore on revert" + | `None -> fail "no-op ALTER clause cannot be auto-reverted" -let action_to_sql_fragment (action : Sql.alter_action) = - match action with +let action_to_sql_fragment ~default_sql_lookup (action : Sql.alter_action) = match action with + | `None -> None | `Add (col, pos) -> - sprintf "ADD COLUMN %s%s" (alter_action_attr_to_sql col) (alter_pos_to_sql pos) + Some (sprintf "ADD COLUMN %s%s" + (alter_action_attr_to_sql ~default_sql_lookup col) + (alter_pos_to_sql pos)) | `Drop col_name -> - sprintf "DROP COLUMN %s" (quote_id col_name) + Some (sprintf "DROP COLUMN %s" (quote_id col_name)) | `Change (old_name, new_col, pos) -> - sprintf "CHANGE COLUMN %s %s%s" (quote_id old_name) - (alter_action_attr_to_sql new_col) (alter_pos_to_sql pos) + Some (sprintf "CHANGE COLUMN %s %s%s" + (quote_id old_name) + (alter_action_attr_to_sql ~default_sql_lookup new_col) + (alter_pos_to_sql pos)) | `RenameTable new_name -> - sprintf "RENAME TO %s" (quote_table_name new_name) + Some (sprintf "RENAME TO %s" (quote_table_name new_name)) | `RenameColumn (old_name, new_name) -> - sprintf "RENAME COLUMN %s TO %s" (quote_id old_name) (quote_id new_name) + Some (sprintf "RENAME COLUMN %s TO %s" (quote_id old_name) (quote_id new_name)) | `RenameIndex (old_name, new_name) -> - sprintf "RENAME INDEX %s TO %s" (quote_id old_name) (quote_id new_name) - | `AddIndex (name, cols) -> - let name_s = Option.map_default (fun n -> " " ^ quote_id n) "" name in - sprintf "ADD INDEX%s (%s)" name_s (String.concat ", " (List.map quote_id cols)) + Some (sprintf "RENAME INDEX %s TO %s" (quote_id old_name) (quote_id new_name)) + | `AddIndex { add_idx_name; add_idx_kind; add_idx_cols } -> + let kw = match add_idx_kind with + | Sql.Plain_idx -> "INDEX" + | Sql.Unique_idx -> "UNIQUE INDEX" + | Sql.Fulltext_idx -> "FULLTEXT INDEX" + | Sql.Spatial_idx -> "SPATIAL INDEX" + in + let name_sql = Option.map_default (fun n -> " " ^ quote_id n) "" add_idx_name in + Some (sprintf "ADD %s%s (%s)" kw name_sql + (String.concat ", " (List.map quote_id add_idx_cols))) | `DropIndex name -> - sprintf "DROP INDEX %s" (quote_id name) + Some (sprintf "DROP INDEX %s" (quote_id name)) | `AddPrimaryKey cols -> - sprintf "ADD PRIMARY KEY (%s)" (String.concat ", " (List.map quote_id cols)) - | `DropPrimaryKey -> - "DROP PRIMARY KEY" - | `AddConstraint name -> - sprintf "ADD CONSTRAINT%s" (Option.map_default (fun n -> " " ^ quote_id n) "" name) + Some (sprintf "ADD PRIMARY KEY (%s)" (String.concat ", " (List.map quote_id cols))) + | `DropPrimaryKey -> Some "DROP PRIMARY KEY" + | `AddConstraint _ -> + fail "ADD CONSTRAINT body is not preserved in AST, cannot be serialized" | `DropConstraint name -> - sprintf "DROP CONSTRAINT %s" (quote_id name) + Some (sprintf "DROP CONSTRAINT %s" (quote_id name)) | `Default_or_convert_to (cs, collation) -> - let charset_to_sql = function + let charset_name = function | Sql.Named s -> s - | Binary -> "binary" - | Ascii -> "ascii" + | Binary -> "binary" + | Ascii -> "ascii" | Unicode -> "unicode" in - let parts = List.filter_map Fun.id [ - Option.map (fun c -> sprintf "CONVERT TO CHARACTER SET %s" (charset_to_sql c)) cs; - Option.map (fun c -> sprintf "COLLATE %s" c.Sql.value) collation; - ] in - begin match parts with - | [] -> "(* unsupported: unknown previous charset *)" - | _ -> String.concat " " parts - end + let collate c = sprintf "COLLATE %s" c.Sql.value in + let convert cs = sprintf "CONVERT TO CHARACTER SET %s" (charset_name cs) in + (match cs, collation with + | None, None -> None + | None, Some c -> Some (collate c) + | Some cs, None -> Some (convert cs) + | Some cs, Some c -> Some (convert cs ^ " " ^ collate c)) | `TtlOptions (opts, _) -> - let opt_to_sql = function - | `TtlSet (col, n, unit) -> - sprintf "TTL = %s + INTERVAL %d %s" (quote_id col) n (String.uppercase_ascii unit) - | `TtlEnable v -> sprintf "TTL_ENABLE = '%s'" v - in - String.concat " " (List.map opt_to_sql opts) - | `RemoveTtl _ -> "REMOVE TTL" - | `None -> "(* unsupported: index/constraint operation *)" + Some (opts + |> List.map (function + | `TtlSet (col, n, unit) -> + sprintf "TTL = %s + INTERVAL %d %s" (quote_id col) n (String.uppercase_ascii unit) + | `TtlEnable v -> sprintf "TTL_ENABLE = '%s'" v) + |> String.concat " ") + | `RemoveTtl _ -> Some "REMOVE TTL" -let alter_to_sql table_name actions = - let fragments = List.map action_to_sql_fragment actions in - sprintf "ALTER TABLE %s %s" (quote_table_name table_name) (String.concat ", " fragments) +let alter_to_sql ~default_sql_lookup table_name actions = + match List.filter_map (action_to_sql_fragment ~default_sql_lookup) actions with + | [] -> None + | fs -> Some (sprintf "ALTER TABLE %s %s" + (quote_table_name table_name) (String.concat ", " fs)) type migration = { - name : string; + props : Props.t; + kind : Stmt.kind; apply : string; revert : string; } let inverse table_name (columns : Tables.column list) (actions : Sql.alter_action list) = - let non_invertible = List.filter (function - | `None -> true - | `AddIndex (None, _) | `DropIndex _ -> true - | `AddConstraint None | `DropConstraint _ -> true - | `RemoveTtl _ -> true - | _ -> false) actions in - if non_invertible <> [] then - None - else - let inverse_actions = List.rev_map (inverse_action table_name columns) actions in - let effective_name = List.fold_left (fun name action -> - match action with `RenameTable new_name -> new_name | _ -> name - ) table_name actions in - Some (alter_to_sql effective_name inverse_actions) + let pk_columns = Tables.get_primary_key_columns columns in + let pk_dropped = List.mem `DropPrimaryKey actions in + try + let inverse_actions, effective_name = + List.fold_left (fun (revs, current_name) action -> + let inv = inverse_action ~table_name ~current_name ~pk_columns ~pk_dropped action in + let next_name = match action with `RenameTable n -> n | _ -> current_name in + (inv :: revs, next_name)) + ([], table_name) actions + in + let default_sql_lookup column_name = + Tables.column_find table_name ~column_name + |> Option.map_default (fun (c : Tables.column) -> c.default_sql) None + in + match alter_to_sql ~default_sql_lookup effective_name inverse_actions with + | Some sql -> Ok sql + | None -> Error "ALTER has no actions to revert" + with Migration_error msg -> + Error (sprintf "table %s:\n %s" (quote_table_name table_name) msg) let drop_index_sql index_name table_name = sprintf "DROP INDEX %s ON %s" (quote_id index_name) (quote_table_name table_name) -let rename_inverse_sql pairs = - let inverse_pairs = List.map (fun (old_name, new_name) -> - sprintf "%s TO %s" (quote_table_name new_name) (quote_table_name old_name) - ) pairs in - sprintf "RENAME TABLE %s" (String.concat ", " inverse_pairs) - +let rename_inverse_sql = function + | [] -> None + | pairs -> + Some (sprintf "RENAME TABLE %s" + (pairs + |> List.map (fun (old_name, new_name) -> + sprintf "%s TO %s" (quote_table_name new_name) (quote_table_name old_name)) + |> String.concat ", ")) diff --git a/src/gen_xml.ml b/src/gen_xml.ml index 80421877..54cd47a5 100644 --- a/src/gen_xml.ml +++ b/src/gen_xml.ml @@ -169,8 +169,9 @@ let generate out _ stmts = finish_output out let generate_migrations _name migrations = - let nodes = List.map (fun (m : Gen_migrations.migration) -> - Node ("migration", ["name", m.name; "apply", m.apply; "revert", m.revert], []) + let nodes = List.mapi (fun index (m : Gen_migrations.migration) -> + let name = Gen.choose_name m.props m.kind index in + Node ("migration", ["name", name; "apply", m.apply; "revert", m.revert], []) ) migrations in let root = Node ("migrations", [], nodes) in print_endline xml_declaration; diff --git a/src/main.ml b/src/main.ml index 8b07bbf5..7815e088 100644 --- a/src/main.ml +++ b/src/main.ml @@ -101,7 +101,11 @@ let parse_one' (sql, props) = let props = Props.set props "sql" sql in { Gen.schema; vars; kind; props } -type migration_info = Auto_revert of string | Needs_explicit_revert | Create_table | Non_migration_stmt of string +type migration_info = + | Auto_revert of string + | Needs_explicit_revert of string option + | Create_table + | Non_migration_stmt of string let parse_one_migration' (sql, props) = if Sqlgg_config.debug1 () then Printf.eprintf "------\n%s\n%!" sql; @@ -110,15 +114,18 @@ let parse_one_migration' (sql, props) = let migration = match parsed.Parser.statement with | Sql.Alter (table_name, actions) -> let columns = Tables.get_columns table_name in - Gen_migrations.inverse table_name columns actions - |> Option.map_default (fun revert -> Auto_revert revert) Needs_explicit_revert - | Sql.CreateIndex (index_name, table_name, _cols) -> - Auto_revert (Gen_migrations.drop_index_sql index_name table_name) + (match Gen_migrations.inverse table_name columns actions with + | Ok revert -> Auto_revert revert + | Error reason -> Needs_explicit_revert (Some reason)) + | Sql.CreateIndex { ci_name; ci_table; _ } -> + Auto_revert (Gen_migrations.drop_index_sql ci_name ci_table) | Sql.Rename pairs -> - Auto_revert (Gen_migrations.rename_inverse_sql pairs) - | Sql.Drop _ -> Needs_explicit_revert + (match Gen_migrations.rename_inverse_sql pairs with + | Some sql -> Auto_revert sql + | None -> Needs_explicit_revert (Some "RENAME TABLE without targets is not invertible")) + | Sql.Drop _ -> Needs_explicit_revert None | Sql.Create _ -> Create_table - | Sql.Insert _ | Sql.Delete _ | Sql.DeleteMulti _ | Sql.Update _ | Sql.UpdateMulti _ -> Needs_explicit_revert + | Sql.Insert _ | Sql.Delete _ | Sql.DeleteMulti _ | Sql.Update _ | Sql.UpdateMulti _ -> Needs_explicit_revert None | stmt -> Non_migration_stmt (Sql.show_stmt stmt) in let (sql, schema, vars, kind, dialect_features) = Syntax.eval_parsed sql parsed in @@ -254,47 +261,55 @@ let get_statements ch = check_statement stmt buffer; stmt) ) |> List.of_enum -let read_explicit_revert tokens name = - let r = extract_statement' tokens |> Option.map (fun (buffer, _) -> String.trim buffer) in - if Option.is_none r then Error.log "migrations mode: down=explicit but no following statement for %s" name; - r - let get_migrations ch = let lexbuf = Lexing.from_channel ch in let tokens = lex_tokens lexbuf in - let rec aux acc index = + let take_next_stmt () = + extract_statement' tokens |> Option.map (fun (buffer, _) -> String.trim buffer) + in + let rec aux acc = match extract_statement' tokens with | None -> List.rev acc | Some (buffer, props) -> let revert_explicit = Props.get props "down" = Some "explicit" in - let next_index = index + 1 in match parse_one_migration (buffer, props) with - | None -> aux acc next_index + | None -> aux acc | Some (stmt, migration) -> - let name = Gen.choose_name stmt.props stmt.kind index in let apply = String.trim buffer in - let add revert = aux ({ Gen_migrations.name; apply; revert } :: acc) next_index in + let add revert = + aux ({ Gen_migrations.props = stmt.props; kind = stmt.kind; apply; revert } :: acc) + in + let no_following_stmt () = + Error.log "migrations mode: down=explicit but no following statement for:\n%s" apply + in match migration with - | Auto_revert revert -> + | Auto_revert auto -> let revert = - if revert_explicit then Option.default revert (read_explicit_revert tokens name) - else Option.default revert (Props.get props "down") + if revert_explicit then + match take_next_stmt () with + | Some r -> r + | None -> no_following_stmt (); auto + else Option.default auto (Props.get props "down") in add revert - | Needs_explicit_revert -> + | Needs_explicit_revert reason -> if not revert_explicit then begin - Error.log "migrations mode: %s contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit" name; - aux acc next_index - end else begin match read_explicit_revert tokens name with + let detail = Option.default "this statement is not auto-invertible" reason in + Error.log + "cannot auto-generate revert: %s\n %s\n \ + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually." + detail apply; + exit 1 + end else begin match take_next_stmt () with | Some revert -> add revert - | None -> aux acc next_index + | None -> no_following_stmt (); aux acc end - | Create_table -> aux acc next_index + | Create_table -> aux acc | Non_migration_stmt _ -> Error.log "migrations mode: unsupported statement type: %s" apply; - aux acc next_index + aux acc in - aux [] 0 + aux [] let with_channel filename f = match try Some (open_in filename) with _ -> None with diff --git a/test/cram/gen_migrations.t b/test/cram/gen_migrations.t index d2d29133..739e247b 100644 --- a/test/cram/gen_migrations.t +++ b/test/cram/gen_migrations.t @@ -7,14 +7,14 @@ Basic migrations - ADD COLUMN inverse is DROP: module IO = T.IO - let apply_alter_users_1 db = + let apply_alter_users_0 db = T.execute db ("ALTER TABLE users ADD COLUMN age INT NOT NULL") T.no_params - let revert_alter_users_1 db = + let revert_alter_users_0 db = T.execute db ("ALTER TABLE `users` DROP COLUMN `age`") T.no_params let migrations = [ - ("alter_users_1", [(apply_alter_users_1, revert_alter_users_1)]); + ("alter_users_0", [(apply_alter_users_0, revert_alter_users_0)]); ] end (* module Mig *) @@ -28,14 +28,14 @@ DROP COLUMN inverse is ADD with reconstructed column definition: module IO = T.IO - let apply_alter_users_1 db = + let apply_alter_users_0 db = T.execute db ("ALTER TABLE users DROP COLUMN age") T.no_params - let revert_alter_users_1 db = + let revert_alter_users_0 db = T.execute db ("ALTER TABLE `users` ADD COLUMN `age` INT NOT NULL") T.no_params let migrations = [ - ("alter_users_1", [(apply_alter_users_1, revert_alter_users_1)]); + ("alter_users_0", [(apply_alter_users_0, revert_alter_users_0)]); ] end (* module Mig *) @@ -49,14 +49,14 @@ CHANGE COLUMN inverse restores old column definition: module IO = T.IO - let apply_alter_users_1 db = + let apply_alter_users_0 db = T.execute db ("ALTER TABLE users CHANGE COLUMN name full_name TEXT NOT NULL") T.no_params - let revert_alter_users_1 db = + let revert_alter_users_0 db = T.execute db ("ALTER TABLE `users` CHANGE COLUMN `full_name` `name` TEXT") T.no_params let migrations = [ - ("alter_users_1", [(apply_alter_users_1, revert_alter_users_1)]); + ("alter_users_0", [(apply_alter_users_0, revert_alter_users_0)]); ] end (* module Mig *) @@ -70,14 +70,14 @@ RENAME COLUMN inverse swaps old and new names: module IO = T.IO - let apply_alter_users_1 db = + let apply_alter_users_0 db = T.execute db ("ALTER TABLE users RENAME COLUMN email TO mail") T.no_params - let revert_alter_users_1 db = + let revert_alter_users_0 db = T.execute db ("ALTER TABLE `users` RENAME COLUMN `mail` TO `email`") T.no_params let migrations = [ - ("alter_users_1", [(apply_alter_users_1, revert_alter_users_1)]); + ("alter_users_0", [(apply_alter_users_0, revert_alter_users_0)]); ] end (* module Mig *) @@ -91,35 +91,14 @@ RENAME TABLE inverse uses new name and renames back to old: module IO = T.IO - let apply_alter_users_1 db = + let apply_alter_users_0 db = T.execute db ("ALTER TABLE users RENAME TO accounts") T.no_params - let revert_alter_users_1 db = + let revert_alter_users_0 db = T.execute db ("ALTER TABLE `accounts` RENAME TO `users`") T.no_params let migrations = [ - ("alter_users_1", [(apply_alter_users_1, revert_alter_users_1)]); - ] - - end (* module Mig *) - -Multi-action ALTER inverts all actions in reverse order: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (a INT NOT NULL, b TEXT, c INT NOT NULL); - > ALTER TABLE t DROP COLUMN b, ADD COLUMN d TEXT NOT NULL, RENAME COLUMN a TO aa; - > EOF - module Mig (T : Sqlgg_traits.M_io) = struct - - module IO = T.IO - - let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t DROP COLUMN b, ADD COLUMN d TEXT NOT NULL, RENAME COLUMN a TO aa") T.no_params - - let revert_alter_t_1 db = - T.execute db ("ALTER TABLE `t` RENAME COLUMN `aa` TO `a`, DROP COLUMN `d`, ADD COLUMN `b` TEXT") T.no_params - - let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_users_0", [(apply_alter_users_0, revert_alter_users_0)]); ] end (* module Mig *) @@ -134,21 +113,21 @@ Multiple ALTER statements produce a list of migrations: module IO = T.IO - let apply_alter_items_1 db = + let apply_alter_items_0 db = T.execute db ("ALTER TABLE items ADD COLUMN stock INT NOT NULL") T.no_params - let revert_alter_items_1 db = + let revert_alter_items_0 db = T.execute db ("ALTER TABLE `items` DROP COLUMN `stock`") T.no_params - let apply_alter_items_2 db = + let apply_alter_items_1 db = T.execute db ("ALTER TABLE items DROP COLUMN price") T.no_params - let revert_alter_items_2 db = + let revert_alter_items_1 db = T.execute db ("ALTER TABLE `items` ADD COLUMN `price` INT NOT NULL") T.no_params let migrations = [ + ("alter_items_0", [(apply_alter_items_0, revert_alter_items_0)]); ("alter_items_1", [(apply_alter_items_1, revert_alter_items_1)]); - ("alter_items_2", [(apply_alter_items_2, revert_alter_items_2)]); ] end (* module Mig *) @@ -193,44 +172,55 @@ DDL and migrations in separate files (like real usage): end (* module Mig *) -ENUM column - DROP inverse reconstructs ENUM type: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE orders (id INT NOT NULL, status ENUM('pending','shipped','delivered') NOT NULL); - > ALTER TABLE orders DROP COLUMN status; +DEFAULT from DDL in a separate file is preserved in inverse migrations: + $ cat > ddl_default.sql <<'EOF' + > CREATE TABLE items (id INT NOT NULL, status INT NOT NULL DEFAULT 5, label TEXT DEFAULT 'unknown'); > EOF + $ cat > migrations_default.sql <<'EOF' + > ALTER TABLE items DROP COLUMN status; + > ALTER TABLE items DROP COLUMN label; + > EOF + $ sqlgg -no-header -gen none ddl_default.sql -gen caml -migrations -name mig -dialect mysql migrations_default.sql module Mig (T : Sqlgg_traits.M_io) = struct module IO = T.IO - let apply_alter_orders_1 db = - T.execute db ("ALTER TABLE orders DROP COLUMN status") T.no_params + let apply_alter_items_0 db = + T.execute db ("ALTER TABLE items DROP COLUMN status") T.no_params - let revert_alter_orders_1 db = - T.execute db ("ALTER TABLE `orders` ADD COLUMN `status` ENUM('delivered', 'pending', 'shipped') NOT NULL") T.no_params + let revert_alter_items_0 db = + T.execute db ("ALTER TABLE `items` ADD COLUMN `status` INT NOT NULL DEFAULT 5") T.no_params + + let apply_alter_items_1 db = + T.execute db ("ALTER TABLE items DROP COLUMN label") T.no_params + + let revert_alter_items_1 db = + T.execute db ("ALTER TABLE `items` ADD COLUMN `label` TEXT DEFAULT 'unknown'") T.no_params let migrations = [ - ("alter_orders_1", [(apply_alter_orders_1, revert_alter_orders_1)]); + ("alter_items_0", [(apply_alter_items_0, revert_alter_items_0)]); + ("alter_items_1", [(apply_alter_items_1, revert_alter_items_1)]); ] end (* module Mig *) -ENUM column - ADD inverse is DROP (trivial): +ENUM column - DROP inverse reconstructs ENUM type: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE orders (id INT NOT NULL); - > ALTER TABLE orders ADD COLUMN priority ENUM('low','medium','high') NOT NULL; + > CREATE TABLE orders (id INT NOT NULL, status ENUM('pending','shipped','delivered') NOT NULL); + > ALTER TABLE orders DROP COLUMN status; > EOF module Mig (T : Sqlgg_traits.M_io) = struct module IO = T.IO - let apply_alter_orders_1 db = - T.execute db ("ALTER TABLE orders ADD COLUMN priority ENUM('low','medium','high') NOT NULL") T.no_params + let apply_alter_orders_0 db = + T.execute db ("ALTER TABLE orders DROP COLUMN status") T.no_params - let revert_alter_orders_1 db = - T.execute db ("ALTER TABLE `orders` DROP COLUMN `priority`") T.no_params + let revert_alter_orders_0 db = + T.execute db ("ALTER TABLE `orders` ADD COLUMN `status` ENUM('delivered', 'pending', 'shipped') NOT NULL") T.no_params let migrations = [ - ("alter_orders_1", [(apply_alter_orders_1, revert_alter_orders_1)]); + ("alter_orders_0", [(apply_alter_orders_0, revert_alter_orders_0)]); ] end (* module Mig *) @@ -244,48 +234,18 @@ ENUM column - CHANGE preserves old ENUM type in inverse: module IO = T.IO - let apply_alter_orders_1 db = + let apply_alter_orders_0 db = T.execute db ("ALTER TABLE orders CHANGE COLUMN status order_status ENUM('pending','shipped','delivered','returned') NOT NULL") T.no_params - let revert_alter_orders_1 db = + let revert_alter_orders_0 db = T.execute db ("ALTER TABLE `orders` CHANGE COLUMN `order_status` `status` ENUM('pending', 'shipped') NOT NULL") T.no_params let migrations = [ - ("alter_orders_1", [(apply_alter_orders_1, revert_alter_orders_1)]); - ] - - end (* module Mig *) - -ALTER TABLE ADD INDEX (named) generates DROP INDEX as inverse: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL, name TEXT); - > ALTER TABLE t ADD INDEX idx_name (name); - > EOF - module Mig (T : Sqlgg_traits.M_io) = struct - - module IO = T.IO - - let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t ADD INDEX idx_name (name)") T.no_params - - let revert_alter_t_1 db = - T.execute db ("ALTER TABLE `t` DROP INDEX `idx_name`") T.no_params - - let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_orders_0", [(apply_alter_orders_0, revert_alter_orders_0)]); ] end (* module Mig *) -ALTER TABLE DROP INDEX is non-invertible (columns unknown), requires down=explicit: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 - > CREATE TABLE t (id INT NOT NULL, name TEXT); - > ALTER TABLE t DROP INDEX idx_name; - > EOF - migrations mode: alter_t_1 contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated - [1] - ALTER TABLE DROP INDEX with down=explicit works: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - > CREATE TABLE t (id INT NOT NULL, name TEXT); @@ -297,14 +257,14 @@ ALTER TABLE DROP INDEX with down=explicit works: module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t DROP INDEX idx_name") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE t ADD INDEX idx_name (name)") T.no_params let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ] end (* module Mig *) @@ -318,112 +278,153 @@ Compound ALTER with ADD INDEX and column ops - all invertible: module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t ADD COLUMN age INT, ADD INDEX idx_name (name)") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE `t` DROP INDEX `idx_name`, DROP COLUMN `age`") T.no_params let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ] end (* module Mig *) -ALTER TABLE ADD CONSTRAINT (named) generates DROP CONSTRAINT as inverse: +ALTER TABLE DROP FOREIGN KEY with down=explicit works: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL, email TEXT); - > ALTER TABLE t ADD CONSTRAINT chk_email CHECK (email IS NOT NULL); + > CREATE TABLE t (id INT NOT NULL); + > -- [sqlgg] down=explicit + > ALTER TABLE t DROP FOREIGN KEY fk_foo; + > ALTER TABLE t ADD CONSTRAINT fk_foo FOREIGN KEY (id) REFERENCES other(id); > EOF module Mig (T : Sqlgg_traits.M_io) = struct module IO = T.IO - let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t ADD CONSTRAINT chk_email CHECK (email IS NOT NULL)") T.no_params + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP FOREIGN KEY fk_foo") T.no_params - let revert_alter_t_1 db = - T.execute db ("ALTER TABLE `t` DROP CONSTRAINT `chk_email`") T.no_params + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD CONSTRAINT fk_foo FOREIGN KEY (id) REFERENCES other(id)") T.no_params let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ] end (* module Mig *) -ALTER TABLE DROP FOREIGN KEY is non-invertible, requires down=explicit: +Compound ALTER with DROP INDEX (non-invertible) errors without down=explicit: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 - > CREATE TABLE t (id INT NOT NULL); - > ALTER TABLE t DROP FOREIGN KEY fk_foo; + > CREATE TABLE t (id INT NOT NULL, data TEXT); + > ALTER TABLE t DROP COLUMN data, DROP INDEX idx_id; > EOF - migrations mode: alter_t_1 contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated + cannot auto-generate revert: table `t`: + DROP INDEX `idx_id`: index not tracked in schema state + ALTER TABLE t DROP COLUMN data, DROP INDEX idx_id + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. [1] -ALTER TABLE DROP CHECK is non-invertible, requires down=explicit: +DROP COLUMN of a non-existent column is non-invertible (no Not_found is raised): $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 - > CREATE TABLE t (id INT NOT NULL); - > ALTER TABLE t DROP CHECK chk_foo; + > CREATE TABLE t (id INT NOT NULL, data TEXT); + > ALTER TABLE t DROP COLUMN ghost, DROP INDEX idx_id; > EOF - migrations mode: alter_t_1 contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated + cannot auto-generate revert: table `t`: + DROP COLUMN `ghost`: column not tracked in schema state + ALTER TABLE t DROP COLUMN ghost, DROP INDEX idx_id + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. [1] -ALTER TABLE DROP FOREIGN KEY with down=explicit works: +DROP INDEX is auto-invertible when the index was created via ALTER ADD INDEX: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL); - > -- [sqlgg] down=explicit - > ALTER TABLE t DROP FOREIGN KEY fk_foo; - > ALTER TABLE t ADD CONSTRAINT fk_foo FOREIGN KEY (id) REFERENCES other(id); + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t ADD INDEX idx_x (x); + > ALTER TABLE t DROP INDEX idx_x; > EOF module Mig (T : Sqlgg_traits.M_io) = struct module IO = T.IO + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD INDEX idx_x (x)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP INDEX `idx_x`") T.no_params + let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t DROP FOREIGN KEY fk_foo") T.no_params + T.execute db ("ALTER TABLE t DROP INDEX idx_x") T.no_params let revert_alter_t_1 db = - T.execute db ("ALTER TABLE t ADD CONSTRAINT fk_foo FOREIGN KEY (id) REFERENCES other(id)") T.no_params + T.execute db ("ALTER TABLE `t` ADD INDEX `idx_x` (`x`)") T.no_params let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); ] end (* module Mig *) -Compound ALTER with DROP INDEX (non-invertible) errors without down=explicit: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 - > CREATE TABLE t (id INT NOT NULL, data TEXT); - > ALTER TABLE t DROP COLUMN data, DROP INDEX idx_id; +DROP INDEX is auto-invertible when the index was declared inline in CREATE TABLE: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, email TEXT, INDEX idx_email (email)); + > ALTER TABLE t DROP INDEX idx_email; > EOF - migrations mode: alter_t_1 contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated - [1] + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP INDEX idx_email") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD INDEX `idx_email` (`email`)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) -Compound ALTER with DROP INDEX (non-invertible) works with down=explicit: +DROP INDEX is auto-invertible when the index was created via CREATE INDEX: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL, data TEXT); - > -- [sqlgg] down=explicit - > ALTER TABLE t DROP COLUMN data, DROP INDEX idx_id; - > ALTER TABLE t ADD INDEX idx_id (data), ADD COLUMN data TEXT; + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > CREATE INDEX idx_x ON t (x); + > ALTER TABLE t DROP INDEX idx_x; > EOF module Mig (T : Sqlgg_traits.M_io) = struct module IO = T.IO + let apply_create_index_idx_x db = + T.execute db ("CREATE INDEX idx_x ON t (x)") T.no_params + + let revert_create_index_idx_x db = + T.execute db ("DROP INDEX `idx_x` ON `t`") T.no_params + let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t DROP COLUMN data, DROP INDEX idx_id") T.no_params + T.execute db ("ALTER TABLE t DROP INDEX idx_x") T.no_params let revert_alter_t_1 db = - T.execute db ("ALTER TABLE t ADD INDEX idx_id (data), ADD COLUMN data TEXT") T.no_params + T.execute db ("ALTER TABLE `t` ADD INDEX `idx_x` (`x`)") T.no_params let migrations = [ + ("create_index_idx_x", [(apply_create_index_idx_x, revert_create_index_idx_x)]); ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); ] end (* module Mig *) +DROP INDEX of an index that was never declared remains non-invertible: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t DROP INDEX never_added_idx; + > EOF + cannot auto-generate revert: table `t`: + DROP INDEX `never_added_idx`: index not tracked in schema state + ALTER TABLE t DROP INDEX never_added_idx + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + Manual down via down=explicit, next statement is the down migration: $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - > CREATE TABLE t (id INT NOT NULL, data TEXT); @@ -457,35 +458,14 @@ MODIFY COLUMN (parsed as CHANGE) inverse restores old definition: module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t MODIFY COLUMN val INT NOT NULL") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE `t` CHANGE COLUMN `val` `val` TEXT") T.no_params let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); - ] - - end (* module Mig *) - -RENAME INDEX inverse swaps old and new names: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL); - > ALTER TABLE t RENAME INDEX old_idx TO new_idx; - > EOF - module Mig (T : Sqlgg_traits.M_io) = struct - - module IO = T.IO - - let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t RENAME INDEX old_idx TO new_idx") T.no_params - - let revert_alter_t_1 db = - T.execute db ("ALTER TABLE `t` RENAME INDEX `new_idx` TO `old_idx`") T.no_params - - let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ] end (* module Mig *) @@ -500,50 +480,21 @@ ADD COLUMN with position (AFTER/FIRST): module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t ADD COLUMN age INT AFTER id") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE `t` DROP COLUMN `age`") T.no_params - let apply_alter_t_2 db = - T.execute db ("ALTER TABLE t ADD COLUMN flag INT FIRST") T.no_params - - let revert_alter_t_2 db = - T.execute db ("ALTER TABLE `t` DROP COLUMN `flag`") T.no_params - - let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); - ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); - ] - - end (* module Mig *) - -Sequential ALTERs - schema evolves, second DROP sees updated schema: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL); - > ALTER TABLE t ADD COLUMN x TEXT NOT NULL; - > ALTER TABLE t DROP COLUMN x; - > EOF - module Mig (T : Sqlgg_traits.M_io) = struct - - module IO = T.IO - let apply_alter_t_1 db = - T.execute db ("ALTER TABLE t ADD COLUMN x TEXT NOT NULL") T.no_params + T.execute db ("ALTER TABLE t ADD COLUMN flag INT FIRST") T.no_params let revert_alter_t_1 db = - T.execute db ("ALTER TABLE `t` DROP COLUMN `x`") T.no_params - - let apply_alter_t_2 db = - T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params - - let revert_alter_t_2 db = - T.execute db ("ALTER TABLE `t` ADD COLUMN `x` TEXT NOT NULL") T.no_params + T.execute db ("ALTER TABLE `t` DROP COLUMN `flag`") T.no_params let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); - ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); ] end (* module Mig *) @@ -561,28 +512,28 @@ Mixed down=explicit and auto-computed in same file: module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t ADD COLUMN b INT NOT NULL") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE `t` DROP COLUMN `b`") T.no_params - let apply_alter_t_2 db = + let apply_alter_t_1 db = T.execute db ("ALTER TABLE t ADD INDEX idx_a (a)") T.no_params - let revert_alter_t_2 db = + let revert_alter_t_1 db = T.execute db ("ALTER TABLE t DROP INDEX idx_a") T.no_params - let apply_alter_t_3 db = + let apply_alter_t_2 db = T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params - let revert_alter_t_3 db = + let revert_alter_t_2 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `a` TEXT") T.no_params let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); - ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); ] end (* module Mig *) @@ -617,14 +568,14 @@ Standalone RENAME TABLE generates inverse rename: module IO = T.IO - let apply_alter_old_t_1 db = + let apply_alter_old_t_0 db = T.execute db ("RENAME TABLE old_t TO new_t") T.no_params - let revert_alter_old_t_1 db = + let revert_alter_old_t_0 db = T.execute db ("RENAME TABLE `new_t` TO `old_t`") T.no_params let migrations = [ - ("alter_old_t_1", [(apply_alter_old_t_1, revert_alter_old_t_1)]); + ("alter_old_t_0", [(apply_alter_old_t_0, revert_alter_old_t_0)]); ] end (* module Mig *) @@ -634,8 +585,9 @@ DROP TABLE requires down=explicit: > CREATE TABLE t (id INT NOT NULL); > DROP TABLE t; > EOF - migrations mode: drop_t contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated + cannot auto-generate revert: this statement is not auto-invertible + DROP TABLE t + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. [1] Full migration scenario - DDL file then migrations file: @@ -742,145 +694,146 @@ Numeric types roundtrip - DROP inverse reconstructs SQL types from schema: module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `a` INT NOT NULL") T.no_params - let apply_alter_t_2 db = + let apply_alter_t_1 db = T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params - let revert_alter_t_2 db = + let revert_alter_t_1 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `b` INT UNSIGNED NOT NULL") T.no_params - let apply_alter_t_3 db = + let apply_alter_t_2 db = T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params - let revert_alter_t_3 db = + let revert_alter_t_2 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `c` BIGINT NOT NULL") T.no_params - let apply_alter_t_4 db = + let apply_alter_t_3 db = T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params - let revert_alter_t_4 db = + let revert_alter_t_3 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `d` BIGINT UNSIGNED NOT NULL") T.no_params - let apply_alter_t_5 db = + let apply_alter_t_4 db = T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params - let revert_alter_t_5 db = + let revert_alter_t_4 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `e` TINYINT NOT NULL") T.no_params - let apply_alter_t_6 db = + let apply_alter_t_5 db = T.execute db ("ALTER TABLE t DROP COLUMN f") T.no_params - let revert_alter_t_6 db = + let revert_alter_t_5 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `f` SMALLINT NOT NULL") T.no_params - let apply_alter_t_7 db = + let apply_alter_t_6 db = T.execute db ("ALTER TABLE t DROP COLUMN g") T.no_params - let revert_alter_t_7 db = + let revert_alter_t_6 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `g` MEDIUMINT NOT NULL") T.no_params - let apply_alter_t_8 db = + let apply_alter_t_7 db = T.execute db ("ALTER TABLE t DROP COLUMN h") T.no_params - let revert_alter_t_8 db = + let revert_alter_t_7 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `h` FLOAT NOT NULL") T.no_params - let apply_alter_t_9 db = + let apply_alter_t_8 db = T.execute db ("ALTER TABLE t DROP COLUMN i") T.no_params - let revert_alter_t_9 db = + let revert_alter_t_8 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `i` DOUBLE NOT NULL") T.no_params - let apply_alter_t_10 db = + let apply_alter_t_9 db = T.execute db ("ALTER TABLE t DROP COLUMN j") T.no_params - let revert_alter_t_10 db = + let revert_alter_t_9 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `j` DECIMAL(10,2) NOT NULL") T.no_params - let apply_alter_t_11 db = + let apply_alter_t_10 db = T.execute db ("ALTER TABLE t DROP COLUMN k") T.no_params - let revert_alter_t_11 db = + let revert_alter_t_10 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `k` DECIMAL(5) NOT NULL") T.no_params - let apply_alter_t_12 db = + let apply_alter_t_11 db = T.execute db ("ALTER TABLE t DROP COLUMN l") T.no_params - let revert_alter_t_12 db = + let revert_alter_t_11 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `l` DECIMAL NOT NULL") T.no_params - let apply_alter_t_13 db = + let apply_alter_t_12 db = T.execute db ("ALTER TABLE t DROP COLUMN m") T.no_params - let revert_alter_t_13 db = + let revert_alter_t_12 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `m` BOOLEAN NOT NULL") T.no_params - let apply_alter_t_14 db = + let apply_alter_t_13 db = T.execute db ("ALTER TABLE t DROP COLUMN n") T.no_params - let revert_alter_t_14 db = + let revert_alter_t_13 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `n` TEXT") T.no_params - let apply_alter_t_15 db = + let apply_alter_t_14 db = T.execute db ("ALTER TABLE t DROP COLUMN o") T.no_params - let revert_alter_t_15 db = + let revert_alter_t_14 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `o` BLOB") T.no_params - let apply_alter_t_16 db = + let apply_alter_t_15 db = T.execute db ("ALTER TABLE t DROP COLUMN p") T.no_params - let revert_alter_t_16 db = + let revert_alter_t_15 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `p` DATETIME NOT NULL") T.no_params - let apply_alter_t_17 db = + let apply_alter_t_16 db = T.execute db ("ALTER TABLE t DROP COLUMN q") T.no_params - let revert_alter_t_17 db = + let revert_alter_t_16 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `q` JSON NOT NULL") T.no_params - let apply_alter_t_18 db = + let apply_alter_t_17 db = T.execute db ("ALTER TABLE t DROP COLUMN r") T.no_params - let revert_alter_t_18 db = + let revert_alter_t_17 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `r` TINYBLOB") T.no_params - let apply_alter_t_19 db = + let apply_alter_t_18 db = T.execute db ("ALTER TABLE t DROP COLUMN s") T.no_params - let revert_alter_t_19 db = + let revert_alter_t_18 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `s` MEDIUMBLOB") T.no_params - let apply_alter_t_20 db = + let apply_alter_t_19 db = T.execute db ("ALTER TABLE t DROP COLUMN t2") T.no_params - let revert_alter_t_20 db = + let revert_alter_t_19 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `t2` LONGBLOB") T.no_params - let apply_alter_t_21 db = + let apply_alter_t_20 db = T.execute db ("ALTER TABLE t DROP COLUMN u") T.no_params - let revert_alter_t_21 db = + let revert_alter_t_20 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `u` TINYTEXT") T.no_params - let apply_alter_t_22 db = + let apply_alter_t_21 db = T.execute db ("ALTER TABLE t DROP COLUMN v") T.no_params - let revert_alter_t_22 db = + let revert_alter_t_21 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `v` MEDIUMTEXT") T.no_params - let apply_alter_t_23 db = + let apply_alter_t_22 db = T.execute db ("ALTER TABLE t DROP COLUMN w") T.no_params - let revert_alter_t_23 db = + let revert_alter_t_22 db = T.execute db ("ALTER TABLE `t` ADD COLUMN `w` LONGTEXT") T.no_params let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); @@ -903,22 +856,10 @@ Numeric types roundtrip - DROP inverse reconstructs SQL types from schema: ("alter_t_20", [(apply_alter_t_20, revert_alter_t_20)]); ("alter_t_21", [(apply_alter_t_21, revert_alter_t_21)]); ("alter_t_22", [(apply_alter_t_22, revert_alter_t_22)]); - ("alter_t_23", [(apply_alter_t_23, revert_alter_t_23)]); ] end (* module Mig *) -XML output - basic ADD COLUMN: - $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - - > CREATE TABLE users (id INT NOT NULL, name TEXT); - > ALTER TABLE users ADD COLUMN age INT NOT NULL; - > EOF - - - - - - XML output - multiple migrations: $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - > CREATE TABLE t (a INT NOT NULL, b TEXT, c INT NOT NULL); @@ -928,8 +869,8 @@ XML output - multiple migrations: - - + + XML output - DDL and migrations in separate files: @@ -957,14 +898,14 @@ DROP PRIMARY KEY inverse is ADD PRIMARY KEY (single column): module IO = T.IO - let apply_alter_users_1 db = + let apply_alter_users_0 db = T.execute db ("ALTER TABLE users DROP PRIMARY KEY") T.no_params - let revert_alter_users_1 db = + let revert_alter_users_0 db = T.execute db ("ALTER TABLE `users` ADD PRIMARY KEY (`id`)") T.no_params let migrations = [ - ("alter_users_1", [(apply_alter_users_1, revert_alter_users_1)]); + ("alter_users_0", [(apply_alter_users_0, revert_alter_users_0)]); ] end (* module Mig *) @@ -978,54 +919,45 @@ DROP PRIMARY KEY inverse is ADD PRIMARY KEY (composite): module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t DROP PRIMARY KEY") T.no_params - let revert_alter_t_1 db = + let revert_alter_t_0 db = T.execute db ("ALTER TABLE `t` ADD PRIMARY KEY (`a`, `b`)") T.no_params let migrations = [ - ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ] end (* module Mig *) -XML output - special characters in SQL are escaped: - $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL); - > ALTER TABLE t ADD COLUMN label TEXT; - > EOF - - - - - - -CONVERT TO CHARACTER SET - inverse restores previous charset: +CONVERT TO CHARACTER SET — inverse restores previous charset (first ALTER needs down=explicit since no prior charset is known): $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - > CREATE TABLE t (id INT NOT NULL, name TEXT); + > -- [sqlgg] down=explicit > ALTER TABLE t CONVERT TO CHARACTER SET utf8; + > ALTER TABLE t CONVERT TO CHARACTER SET latin1; > ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; > EOF module Mig (T : Sqlgg_traits.M_io) = struct module IO = T.IO - let apply_alter_t_1 db = + let apply_alter_t_0 db = T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET utf8") T.no_params - let revert_alter_t_1 db = - T.execute db ("ALTER TABLE `t` (* unsupported: unknown previous charset *)") T.no_params + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET latin1") T.no_params - let apply_alter_t_2 db = + let apply_alter_t_1 db = T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") T.no_params - let revert_alter_t_2 db = + let revert_alter_t_1 db = T.execute db ("ALTER TABLE `t` CONVERT TO CHARACTER SET utf8") T.no_params let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); - ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); ] end (* module Mig *) @@ -1040,61 +972,6 @@ Non-migration statements (SELECT) produce an error: Errors encountered, no code generated [1] -DML statements (INSERT/UPDATE/DELETE) require down=explicit: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 - > CREATE TABLE t (id INT NOT NULL, x TEXT); - > INSERT INTO t (id, x) VALUES (1, 'hello'); - > EOF - migrations mode: insert_t_1 contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated - [1] - -DML statements work with down=explicit: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL, x TEXT); - > -- [sqlgg] down=explicit - > INSERT INTO t (id, x) VALUES (1, 'hello'); - > DELETE FROM t WHERE id = 1; - > EOF - module Mig (T : Sqlgg_traits.M_io) = struct - - module IO = T.IO - - let apply_insert_t_1 db = - T.execute db ("INSERT INTO t (id, x) VALUES (1, 'hello')") T.no_params - - let revert_insert_t_1 db = - T.execute db ("DELETE FROM t WHERE id = 1") T.no_params - - let migrations = [ - ("insert_t_1", [(apply_insert_t_1, revert_insert_t_1)]); - ] - - end (* module Mig *) - -UPDATE with down=explicit: - $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - - > CREATE TABLE t (id INT NOT NULL, x TEXT); - > -- [sqlgg] down=explicit - > UPDATE t SET x = 'new' WHERE id = 1; - > UPDATE t SET x = 'old' WHERE id = 1; - > EOF - module Mig (T : Sqlgg_traits.M_io) = struct - - module IO = T.IO - - let apply_update_t_1 db = - T.execute db ("UPDATE t SET x = 'new' WHERE id = 1") T.no_params - - let revert_update_t_1 db = - T.execute db ("UPDATE t SET x = 'old' WHERE id = 1") T.no_params - - let migrations = [ - ("update_t_1", [(apply_update_t_1, revert_update_t_1)]); - ] - - end (* module Mig *) - Realistic migration scenario - DDL schema, then mixed ALTER/DML/index migrations: $ cat > schema.sql <<'EOF' > CREATE TABLE reports ( @@ -1155,3 +1032,2399 @@ Realistic migration scenario - DDL schema, then mixed ALTER/DML/index migrations ] end (* module Mig *) + +DROP COLUMN preserves DEFAULT (literal, string, NULL, function, parenthesised expr, ENUM): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t ( + > id INT NOT NULL, + > a INT NOT NULL DEFAULT 5, + > b TEXT NOT NULL DEFAULT 'active', + > c INT DEFAULT NULL, + > d DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + > e INT NOT NULL DEFAULT (1 + 2), + > f ENUM('low','high') NOT NULL DEFAULT 'low' + > ); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > ALTER TABLE t DROP COLUMN e; + > ALTER TABLE t DROP COLUMN f; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` INT NOT NULL DEFAULT 5") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` TEXT NOT NULL DEFAULT 'active'") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` INT DEFAULT NULL") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP") T.no_params + + let apply_alter_t_4 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_4 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` INT NOT NULL DEFAULT (1 + 2)") T.no_params + + let apply_alter_t_5 db = + T.execute db ("ALTER TABLE t DROP COLUMN f") T.no_params + + let revert_alter_t_5 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `f` ENUM('high', 'low') NOT NULL DEFAULT 'low'") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ("alter_t_4", [(apply_alter_t_4, revert_alter_t_4)]); + ("alter_t_5", [(apply_alter_t_5, revert_alter_t_5)]); + ] + + end (* module Mig *) + +CHANGE COLUMN — revert restores the original DEFAULT: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, status INT NOT NULL DEFAULT 5); + > ALTER TABLE t CHANGE COLUMN status state INT NOT NULL DEFAULT 10; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN status state INT NOT NULL DEFAULT 10") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `state` `status` INT NOT NULL DEFAULT 5") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD COLUMN with DEFAULT — schema remembers DEFAULT for a later DROP's revert: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t ADD COLUMN x INT NOT NULL DEFAULT 100; + > ALTER TABLE t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN x INT NOT NULL DEFAULT 100") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `x`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` INT NOT NULL DEFAULT 100") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) +numeric types — signed + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a TINYINT NOT NULL, b SMALLINT NOT NULL, c MEDIUMINT NOT NULL, + > d INT NOT NULL, e BIGINT NOT NULL); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > ALTER TABLE t DROP COLUMN e; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` TINYINT NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` SMALLINT NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` MEDIUMINT NOT NULL") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` INT NOT NULL") T.no_params + + let apply_alter_t_4 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_4 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` BIGINT NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ("alter_t_4", [(apply_alter_t_4, revert_alter_t_4)]); + ] + + end (* module Mig *) + +numeric types — unsigned + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a TINYINT UNSIGNED NOT NULL, b SMALLINT UNSIGNED NOT NULL, + > c MEDIUMINT UNSIGNED NOT NULL, d INT UNSIGNED NOT NULL, + > e BIGINT UNSIGNED NOT NULL); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > ALTER TABLE t DROP COLUMN e; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` TINYINT UNSIGNED NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` SMALLINT UNSIGNED NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` MEDIUMINT UNSIGNED NOT NULL") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` INT UNSIGNED NOT NULL") T.no_params + + let apply_alter_t_4 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_4 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` BIGINT UNSIGNED NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ("alter_t_4", [(apply_alter_t_4, revert_alter_t_4)]); + ] + + end (* module Mig *) + +float / double + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x FLOAT NOT NULL, y DOUBLE NOT NULL, + > z DOUBLE PRECISION NOT NULL); + > ALTER TABLE t DROP COLUMN x; + > ALTER TABLE t DROP COLUMN y; + > ALTER TABLE t DROP COLUMN z; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` FLOAT NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN y") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `y` DOUBLE NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN z") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `z` DOUBLE NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +text size fidelity + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a TEXT, b TINYTEXT, c MEDIUMTEXT, d LONGTEXT); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` TEXT") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` TINYTEXT") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` MEDIUMTEXT") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` LONGTEXT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ] + + end (* module Mig *) + +blob size fidelity + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a BLOB, b TINYBLOB, c MEDIUMBLOB, d LONGBLOB); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` BLOB") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` TINYBLOB") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` MEDIUMBLOB") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` LONGBLOB") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ] + + end (* module Mig *) + +VARCHAR/CHAR/VARBINARY length fidelity (precision regression net) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a VARCHAR(1), b VARCHAR(255), c VARCHAR(65535), + > d CHAR(1), e CHAR(64), f VARBINARY(16), g VARBINARY(4096)); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > ALTER TABLE t DROP COLUMN e; + > ALTER TABLE t DROP COLUMN f; + > ALTER TABLE t DROP COLUMN g; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` VARCHAR(1)") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` VARCHAR(255)") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` VARCHAR(65535)") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` CHAR(1)") T.no_params + + let apply_alter_t_4 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_4 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` CHAR(64)") T.no_params + + let apply_alter_t_5 db = + T.execute db ("ALTER TABLE t DROP COLUMN f") T.no_params + + let revert_alter_t_5 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `f` VARBINARY(16)") T.no_params + + let apply_alter_t_6 db = + T.execute db ("ALTER TABLE t DROP COLUMN g") T.no_params + + let revert_alter_t_6 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `g` VARBINARY(4096)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ("alter_t_4", [(apply_alter_t_4, revert_alter_t_4)]); + ("alter_t_5", [(apply_alter_t_5, revert_alter_t_5)]); + ("alter_t_6", [(apply_alter_t_6, revert_alter_t_6)]); + ] + + end (* module Mig *) + +VARCHAR/CHAR/VARBINARY through CHANGE COLUMN inverse: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a VARCHAR(8) NOT NULL, b CHAR(2) NOT NULL, c VARBINARY(64) NOT NULL); + > ALTER TABLE t CHANGE COLUMN a a VARCHAR(255) NOT NULL; + > ALTER TABLE t CHANGE COLUMN b b CHAR(8) NOT NULL; + > ALTER TABLE t CHANGE COLUMN c c VARBINARY(1024) NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN a a VARCHAR(255) NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `a` `a` VARCHAR(8) NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN b b CHAR(8) NOT NULL") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `b` `b` CHAR(2) NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN c c VARBINARY(1024) NOT NULL") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `c` `c` VARBINARY(64) NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +oracle VARCHAR2 length fidelity: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, a VARCHAR2(4000) NOT NULL); + > ALTER TABLE t DROP COLUMN a; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` VARCHAR2(4000) NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +INT(N) display-width fidelity — preserved in revert (incl. UNSIGNED variants): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a INT(11) NOT NULL, b BIGINT(20) NOT NULL, + > c SMALLINT(5) UNSIGNED NOT NULL, d TINYINT(1) NOT NULL); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` INT(11) NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` BIGINT(20) NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` SMALLINT(5) UNSIGNED NOT NULL") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` TINYINT(1) NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ] + + end (* module Mig *) + +DECIMAL precision/scale fidelity + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a DECIMAL NOT NULL, b DECIMAL(5) NOT NULL, + > c DECIMAL(18,6) NOT NULL, d DECIMAL(38,12) NOT NULL, + > e NUMERIC NOT NULL, f NUMERIC(10,2) NOT NULL); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > ALTER TABLE t DROP COLUMN d; + > ALTER TABLE t DROP COLUMN e; + > ALTER TABLE t DROP COLUMN f; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` DECIMAL NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` DECIMAL(5) NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` DECIMAL(18,6) NOT NULL") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` DECIMAL(38,12) NOT NULL") T.no_params + + let apply_alter_t_4 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_4 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` DECIMAL NOT NULL") T.no_params + + let apply_alter_t_5 db = + T.execute db ("ALTER TABLE t DROP COLUMN f") T.no_params + + let revert_alter_t_5 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `f` DECIMAL(10,2) NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ("alter_t_4", [(apply_alter_t_4, revert_alter_t_4)]); + ("alter_t_5", [(apply_alter_t_5, revert_alter_t_5)]); + ] + + end (* module Mig *) + +DATETIME / JSON / BOOLEAN + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > d DATETIME NOT NULL, j JSON NOT NULL, b BOOLEAN NOT NULL); + > ALTER TABLE t DROP COLUMN d; + > ALTER TABLE t DROP COLUMN j; + > ALTER TABLE t DROP COLUMN b; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN d") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `d` DATETIME NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN j") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `j` JSON NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` BOOLEAN NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +single ctor: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, e ENUM('only') NOT NULL); + > ALTER TABLE t DROP COLUMN e; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` ENUM('only') NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +alphabetical reordering: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > e ENUM('zebra','apple','mango','banana') NOT NULL); + > ALTER TABLE t DROP COLUMN e; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` ENUM('apple', 'banana', 'mango', 'zebra') NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +numeric-looking ctors: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, e ENUM('1','2','10','11') NOT NULL); + > ALTER TABLE t DROP COLUMN e; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` ENUM('1', '10', '11', '2') NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +underscore ctors: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, e ENUM('a_b','a_a','b_a') NOT NULL); + > ALTER TABLE t DROP COLUMN e; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN e") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `e` ENUM('a_a', 'a_b', 'b_a') NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD COLUMN with inline constraints (apply preserved verbatim, revert is DROP) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t ADD COLUMN a INT; + > ALTER TABLE t ADD COLUMN b INT NOT NULL; + > ALTER TABLE t ADD COLUMN c INT NULL; + > ALTER TABLE t ADD COLUMN d INT UNIQUE; + > ALTER TABLE t ADD COLUMN e INT DEFAULT 1; + > ALTER TABLE t ADD COLUMN f INT NOT NULL DEFAULT 7; + > ALTER TABLE t ADD COLUMN g INT NOT NULL AUTO_INCREMENT PRIMARY KEY; + > ALTER TABLE t ADD COLUMN h INT PRIMARY KEY; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN a INT") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `a`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t ADD COLUMN b INT NOT NULL") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `b`") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t ADD COLUMN c INT NULL") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `c`") T.no_params + + let apply_alter_t_3 db = + T.execute db ("ALTER TABLE t ADD COLUMN d INT UNIQUE") T.no_params + + let revert_alter_t_3 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `d`") T.no_params + + let apply_alter_t_4 db = + T.execute db ("ALTER TABLE t ADD COLUMN e INT DEFAULT 1") T.no_params + + let revert_alter_t_4 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `e`") T.no_params + + let apply_alter_t_5 db = + T.execute db ("ALTER TABLE t ADD COLUMN f INT NOT NULL DEFAULT 7") T.no_params + + let revert_alter_t_5 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `f`") T.no_params + + let apply_alter_t_6 db = + T.execute db ("ALTER TABLE t ADD COLUMN g INT NOT NULL AUTO_INCREMENT PRIMARY KEY") T.no_params + + let revert_alter_t_6 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `g`") T.no_params + + let apply_alter_t_7 db = + T.execute db ("ALTER TABLE t ADD COLUMN h INT PRIMARY KEY") T.no_params + + let revert_alter_t_7 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `h`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ("alter_t_3", [(apply_alter_t_3, revert_alter_t_3)]); + ("alter_t_4", [(apply_alter_t_4, revert_alter_t_4)]); + ("alter_t_5", [(apply_alter_t_5, revert_alter_t_5)]); + ("alter_t_6", [(apply_alter_t_6, revert_alter_t_6)]); + ("alter_t_7", [(apply_alter_t_7, revert_alter_t_7)]); + ] + + end (* module Mig *) + +DEFAULT — preserved in revert: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a INT NOT NULL DEFAULT 7, + > b VARCHAR(8) NOT NULL DEFAULT 'x', + > c BOOLEAN NOT NULL DEFAULT TRUE); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` INT NOT NULL DEFAULT 7") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` VARCHAR(8) NOT NULL DEFAULT 'x'") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` BOOLEAN NOT NULL DEFAULT TRUE") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +UNIQUE / NOT NULL / NULL — preserved: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, + > a INT UNIQUE, + > b INT NOT NULL, + > c INT NULL); + > ALTER TABLE t DROP COLUMN a; + > ALTER TABLE t DROP COLUMN b; + > ALTER TABLE t DROP COLUMN c; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN a") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `a` INT UNIQUE") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN b") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `b` INT NOT NULL") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t DROP COLUMN c") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `c` INT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +PRIMARY KEY + AUTO_INCREMENT inline — preserved: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, x TEXT); + > ALTER TABLE t DROP COLUMN id; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN id") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `id` INT PRIMARY KEY NOT NULL AUTO_INCREMENT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +DROP COLUMN from composite PRIMARY KEY — not auto-invertible, suggests composite ALTER: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b)); + > ALTER TABLE t DROP COLUMN b; + > EOF + cannot auto-generate revert: table `t`: + DROP COLUMN `b`: column participates in composite PRIMARY KEY (`a`, `b`); write a composite ALTER explicitly: DROP COLUMN `b`, DROP PRIMARY KEY, ADD PRIMARY KEY () + ALTER TABLE t DROP COLUMN b + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +Composite ALTER for PK rebuild is auto-invertible action-by-action: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b)); + > ALTER TABLE t DROP COLUMN b, DROP PRIMARY KEY, ADD PRIMARY KEY (a); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN b, DROP PRIMARY KEY, ADD PRIMARY KEY (a)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP PRIMARY KEY, ADD PRIMARY KEY (`a`, `b`), ADD COLUMN `b` INT NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +type widen, then revert restores narrow type: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, n SMALLINT NOT NULL); + > ALTER TABLE t CHANGE COLUMN n n BIGINT NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN n n BIGINT NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `n` `n` SMALLINT NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +nullability flip: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, n INT NOT NULL); + > ALTER TABLE t CHANGE COLUMN n n INT; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN n n INT") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `n` `n` INT NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, n INT); + > ALTER TABLE t CHANGE COLUMN n n INT NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN n n INT NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `n` `n` INT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +rename via CHANGE: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t CHANGE COLUMN x y INT NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN x y INT NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `y` `x` TEXT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +MODIFY (no rename): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, val TEXT); + > ALTER TABLE t MODIFY COLUMN val INT NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t MODIFY COLUMN val INT NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `val` `val` TEXT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +CHANGE — DEFAULT from the original column is preserved in revert: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, n INT NOT NULL DEFAULT 7); + > ALTER TABLE t CHANGE COLUMN n n BIGINT NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN n n BIGINT NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `n` `n` INT NOT NULL DEFAULT 7") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +CHANGE keeps source_kind precision: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, n MEDIUMINT UNSIGNED NOT NULL); + > ALTER TABLE t MODIFY COLUMN n BIGINT UNSIGNED NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t MODIFY COLUMN n BIGINT UNSIGNED NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `n` `n` MEDIUMINT UNSIGNED NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD COLUMN positional (FIRST / AFTER) — apply keeps position, revert is plain DROP (DROP has no positional syntax) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, name TEXT); + > ALTER TABLE t ADD COLUMN a INT FIRST; + > ALTER TABLE t ADD COLUMN b INT AFTER id; + > ALTER TABLE t ADD COLUMN c INT; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN a INT FIRST") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `a`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t ADD COLUMN b INT AFTER id") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `b`") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t ADD COLUMN c INT") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `c`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +CHANGE COLUMN positional — position discarded in revert + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, n INT NOT NULL, m INT); + > ALTER TABLE t CHANGE COLUMN m m INT NOT NULL FIRST; + > ALTER TABLE t CHANGE COLUMN n n INT AFTER id; + > EOF + cannot auto-generate revert: table `t`: + CHANGE COLUMN `m` with FIRST/AFTER: original column position is not tracked, cannot restore on revert + ALTER TABLE t CHANGE COLUMN m m INT NOT NULL FIRST + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +DROP PRIMARY KEY (single): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL PRIMARY KEY, x TEXT); + > ALTER TABLE t DROP PRIMARY KEY; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP PRIMARY KEY") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD PRIMARY KEY (`id`)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +DROP PRIMARY KEY (composite): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b INT NOT NULL, c TEXT, PRIMARY KEY (a, b)); + > ALTER TABLE t DROP PRIMARY KEY; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP PRIMARY KEY") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD PRIMARY KEY (`a`, `b`)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD PRIMARY KEY (single + composite): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b INT NOT NULL); + > ALTER TABLE t ADD PRIMARY KEY (a); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD PRIMARY KEY (a)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP PRIMARY KEY") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b INT NOT NULL); + > ALTER TABLE t ADD PRIMARY KEY (a, b); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD PRIMARY KEY (a, b)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP PRIMARY KEY") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +DROP PRIMARY KEY when schema has no PK — non invertible: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b INT NOT NULL); + > ALTER TABLE t DROP PRIMARY KEY; + > EOF + cannot auto-generate revert: table `t`: + DROP PRIMARY KEY: no primary key in schema state, cannot restore on revert + ALTER TABLE t DROP PRIMARY KEY + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +Multi-action ALTER (compound ordering preserved, then reverse) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (a INT NOT NULL, b TEXT, c INT NOT NULL); + > ALTER TABLE t DROP COLUMN b, ADD COLUMN d TEXT NOT NULL, + > RENAME COLUMN a TO aa, ADD INDEX idx_d (d); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN b, ADD COLUMN d TEXT NOT NULL,\n\ + RENAME COLUMN a TO aa, ADD INDEX idx_d (d)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP INDEX `idx_d`, RENAME COLUMN `aa` TO `a`, DROP COLUMN `d`, ADD COLUMN `b` TEXT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ALTER chain — RENAME TABLE inside compound ALTER (effective_name fold) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t RENAME TO t1; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t RENAME TO t1") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t1` RENAME TO `t`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t ADD COLUMN y INT, RENAME TO t2; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN y INT, RENAME TO t2") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t2` RENAME TO `t`, DROP COLUMN `y`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +single rename: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE old_t (id INT NOT NULL); + > RENAME TABLE old_t TO new_t; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_old_t_0 db = + T.execute db ("RENAME TABLE old_t TO new_t") T.no_params + + let revert_alter_old_t_0 db = + T.execute db ("RENAME TABLE `new_t` TO `old_t`") T.no_params + + let migrations = [ + ("alter_old_t_0", [(apply_alter_old_t_0, revert_alter_old_t_0)]); + ] + + end (* module Mig *) + +multi-pair rename: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE a (id INT NOT NULL); + > CREATE TABLE b (id INT NOT NULL); + > RENAME TABLE a TO a2, b TO b2; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_a_b_0 db = + T.execute db ("RENAME TABLE a TO a2, b TO b2") T.no_params + + let revert_alter_a_b_0 db = + T.execute db ("RENAME TABLE `a2` TO `a`, `b2` TO `b`") T.no_params + + let migrations = [ + ("alter_a_b_0", [(apply_alter_a_b_0, revert_alter_a_b_0)]); + ] + + end (* module Mig *) + +ALTER TABLE db.t ...: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE app.t (id INT NOT NULL, x TEXT); + > ALTER TABLE app.t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_app_t_0 db = + T.execute db ("ALTER TABLE app.t DROP COLUMN x") T.no_params + + let revert_alter_app_t_0 db = + T.execute db ("ALTER TABLE `app`.`t` ADD COLUMN `x` TEXT") T.no_params + + let migrations = [ + ("alter_app_t_0", [(apply_alter_app_t_0, revert_alter_app_t_0)]); + ] + + end (* module Mig *) + +RENAME TABLE between two qualified names: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE a.t (id INT NOT NULL); + > RENAME TABLE a.t TO b.t; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_a_t_0 db = + T.execute db ("RENAME TABLE a.t TO b.t") T.no_params + + let revert_alter_a_t_0 db = + T.execute db ("RENAME TABLE `b`.`t` TO `a`.`t`") T.no_params + + let migrations = [ + ("alter_a_t_0", [(apply_alter_a_t_0, revert_alter_a_t_0)]); + ] + + end (* module Mig *) + +ADD INDEX (named, single column): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, name TEXT); + > ALTER TABLE t ADD INDEX idx_name (name); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD INDEX idx_name (name)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP INDEX `idx_name`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD INDEX (named, multi-column): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, a TEXT, b TEXT); + > ALTER TABLE t ADD INDEX idx_ab (a, b); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD INDEX idx_ab (a, b)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP INDEX `idx_ab`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +RENAME INDEX: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t RENAME INDEX old_idx TO new_idx; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t RENAME INDEX old_idx TO new_idx") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` RENAME INDEX `new_idx` TO `old_idx`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ALTER ... ADD INDEX (anonymous) — non invertible + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL, name TEXT); + > ALTER TABLE t ADD INDEX (name); + > EOF + cannot auto-generate revert: table `t`: + ADD INDEX without a name has no identifier to DROP later + ALTER TABLE t ADD INDEX (name) + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +ADD CONSTRAINT (named): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, email TEXT); + > ALTER TABLE t ADD CONSTRAINT chk_email CHECK (email IS NOT NULL); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD CONSTRAINT chk_email CHECK (email IS NOT NULL)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP CONSTRAINT `chk_email`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD CONSTRAINT (anonymous) — non invertible: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL, email TEXT); + > ALTER TABLE t ADD CONSTRAINT CHECK (email IS NOT NULL); + > EOF + cannot auto-generate revert: table `t`: + ADD CONSTRAINT without a name cannot be reverted + ALTER TABLE t ADD CONSTRAINT CHECK (email IS NOT NULL) + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +DROP CONSTRAINT — non invertible: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t DROP CONSTRAINT chk_foo; + > EOF + ==> ALTER TABLE t DROP CONSTRAINT chk_foo + Position 1:29 Tokens: CONSTRAINT chk_foo + Error: Sqlgg.Sql_parser.MenhirBasics.Error + Errors encountered, no code generated + [1] + +DROP FOREIGN KEY: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t DROP FOREIGN KEY fk_foo; + > EOF + cannot auto-generate revert: table `t`: + DROP CONSTRAINT `fk_foo`: definition not tracked in schema state + ALTER TABLE t DROP FOREIGN KEY fk_foo + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +DROP CHECK: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t DROP CHECK chk_foo; + > EOF + cannot auto-generate revert: table `t`: + DROP CONSTRAINT `chk_foo`: definition not tracked in schema state + ALTER TABLE t DROP CHECK chk_foo + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +CREATE INDEX (single): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, name TEXT); + > CREATE INDEX idx_name ON t (name); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_create_index_idx_name db = + T.execute db ("CREATE INDEX idx_name ON t (name)") T.no_params + + let revert_create_index_idx_name db = + T.execute db ("DROP INDEX `idx_name` ON `t`") T.no_params + + let migrations = [ + ("create_index_idx_name", [(apply_create_index_idx_name, revert_create_index_idx_name)]); + ] + + end (* module Mig *) + +CREATE INDEX (multi-column): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, a TEXT, b TEXT); + > CREATE INDEX idx_ab ON t (a, b); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_create_index_idx_ab db = + T.execute db ("CREATE INDEX idx_ab ON t (a, b)") T.no_params + + let revert_create_index_idx_ab db = + T.execute db ("DROP INDEX `idx_ab` ON `t`") T.no_params + + let migrations = [ + ("create_index_idx_ab", [(apply_create_index_idx_ab, revert_create_index_idx_ab)]); + ] + + end (* module Mig *) + +CREATE UNIQUE INDEX: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, email TEXT); + > CREATE UNIQUE INDEX uniq_email ON t (email); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_create_index_uniq_email db = + T.execute db ("CREATE UNIQUE INDEX uniq_email ON t (email)") T.no_params + + let revert_create_index_uniq_email db = + T.execute db ("DROP INDEX `uniq_email` ON `t`") T.no_params + + let migrations = [ + ("create_index_uniq_email", [(apply_create_index_uniq_email, revert_create_index_uniq_email)]); + ] + + end (* module Mig *) + +DROP INDEX of a UNIQUE index (created via CREATE UNIQUE INDEX) reverts to ADD UNIQUE INDEX: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, email TEXT); + > CREATE UNIQUE INDEX uniq_email ON t (email); + > ALTER TABLE t DROP INDEX uniq_email; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_create_index_uniq_email db = + T.execute db ("CREATE UNIQUE INDEX uniq_email ON t (email)") T.no_params + + let revert_create_index_uniq_email db = + T.execute db ("DROP INDEX `uniq_email` ON `t`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP INDEX uniq_email") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD UNIQUE INDEX `uniq_email` (`email`)") T.no_params + + let migrations = [ + ("create_index_uniq_email", [(apply_create_index_uniq_email, revert_create_index_uniq_email)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) + +DROP INDEX of a UNIQUE index added via ALTER TABLE ADD UNIQUE INDEX reverts to ADD UNIQUE INDEX: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, email TEXT); + > ALTER TABLE t ADD UNIQUE INDEX uniq_email (email); + > ALTER TABLE t DROP INDEX uniq_email; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD UNIQUE INDEX uniq_email (email)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP INDEX `uniq_email`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP INDEX uniq_email") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD UNIQUE INDEX `uniq_email` (`email`)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) + +DROP INDEX of a UNIQUE index declared inline via CREATE TABLE ... UNIQUE KEY name (col): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, email TEXT, UNIQUE KEY uniq_email (email)); + > ALTER TABLE t DROP INDEX uniq_email; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP INDEX uniq_email") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD UNIQUE INDEX `uniq_email` (`email`)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD UNIQUE INDEX with multi-column then DROP — UNIQUE preserved: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, a TEXT, b TEXT); + > ALTER TABLE t ADD UNIQUE INDEX uniq_ab (a, b); + > ALTER TABLE t DROP INDEX uniq_ab; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD UNIQUE INDEX uniq_ab (a, b)") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP INDEX `uniq_ab`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP INDEX uniq_ab") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD UNIQUE INDEX `uniq_ab` (`a`, `b`)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) + +chain of charset changes — inverse uses prior known charset (first ALTER needs down=explicit): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, name TEXT); + > -- [sqlgg] down=explicit + > ALTER TABLE t CONVERT TO CHARACTER SET utf8; + > ALTER TABLE t CONVERT TO CHARACTER SET latin1; + > ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + > ALTER TABLE t CONVERT TO CHARACTER SET ascii; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET utf8") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET latin1") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` CONVERT TO CHARACTER SET utf8") T.no_params + + let apply_alter_t_2 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET ascii") T.no_params + + let revert_alter_t_2 db = + T.execute db ("ALTER TABLE `t` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ("alter_t_2", [(apply_alter_t_2, revert_alter_t_2)]); + ] + + end (* module Mig *) + +CONVERT TO CHARACTER SET without a known previous charset is non-invertible, requires down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4; + > EOF + cannot auto-generate revert: table `t`: + CONVERT TO CHARACTER SET: previous CHARACTER SET is not tracked (no explicit charset in CREATE TABLE), revert cannot restore column encodings + ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +CONVERT TO CHARACTER SET when CREATE TABLE has only COLLATE (no charset) — non-invertible: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL, name TEXT) COLLATE utf8mb4_unicode_ci; + > ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4; + > EOF + cannot auto-generate revert: table `t`: + CONVERT TO CHARACTER SET: previous CHARACTER SET is not tracked (no explicit charset in CREATE TABLE), revert cannot restore column encodings + ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +CONVERT TO CHARACTER SET works with down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > -- [sqlgg] down=explicit + > ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4; + > ALTER TABLE t CONVERT TO CHARACTER SET latin1; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE t CONVERT TO CHARACTER SET latin1") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +TTL = col + INTERVAL N : + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, ts DATETIME NOT NULL); + > ALTER TABLE t TTL = ts + INTERVAL 7 DAY; + > EOF + Feature Ttl is not supported for dialect MySQL (supported by: TiDB) at TTL = ts + INTERVAL 7 DAY + Errors encountered, no code generated + [1] + +TTL_ENABLE = 'true': + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t TTL_ENABLE = 'true'; + > EOF + Feature Ttl is not supported for dialect MySQL (supported by: TiDB) at TTL_ENABLE = 'true' + Errors encountered, no code generated + [1] + +REMOVE TTL — non invertible: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t REMOVE TTL; + > EOF + Feature Ttl is not supported for dialect MySQL (supported by: TiDB) at REMOVE TTL + cannot auto-generate revert: table `t`: + REMOVE TTL: previous TTL parameters are not tracked in schema, cannot restore on revert + ALTER TABLE t REMOVE TTL + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +INSERT without down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > INSERT INTO t (id, x) VALUES (1, 'hello'); + > EOF + cannot auto-generate revert: this statement is not auto-invertible + INSERT INTO t (id, x) VALUES (1, 'hello') + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. + [1] + +INSERT + DELETE pair with down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- [sqlgg] down=explicit + > INSERT INTO t (id, x) VALUES (1, 'hello'); + > DELETE FROM t WHERE id = 1; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_insert_t_0 db = + T.execute db ("INSERT INTO t (id, x) VALUES (1, 'hello')") T.no_params + + let revert_insert_t_0 db = + T.execute db ("DELETE FROM t WHERE id = 1") T.no_params + + let migrations = [ + ("insert_t_0", [(apply_insert_t_0, revert_insert_t_0)]); + ] + + end (* module Mig *) + +UPDATE pair with down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- [sqlgg] down=explicit + > UPDATE t SET x = 'new' WHERE id = 1; + > UPDATE t SET x = 'old' WHERE id = 1; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_update_t_0 db = + T.execute db ("UPDATE t SET x = 'new' WHERE id = 1") T.no_params + + let revert_update_t_0 db = + T.execute db ("UPDATE t SET x = 'old' WHERE id = 1") T.no_params + + let migrations = [ + ("update_t_0", [(apply_update_t_0, revert_update_t_0)]); + ] + + end (* module Mig *) + +down=explicit consumed by next statement + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- [sqlgg] down=explicit + > ALTER TABLE t DROP COLUMN x; + > ALTER TABLE t ADD COLUMN x VARCHAR(99); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN x VARCHAR(99)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +down=explicit but no following statement — error: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- [sqlgg] down=explicit + > ALTER TABLE t DROP COLUMN x; + > EOF + migrations mode: down=explicit but no following statement for: + ALTER TABLE t DROP COLUMN x + Errors encountered, no code generated + [1] + +down= string property (non-explicit) + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- [sqlgg] down=ALTER TABLE t ADD COLUMN __marker INT + > ALTER TABLE t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN __marker INT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +@name with auto revert: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- @drop_x + > ALTER TABLE t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_drop_x db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_drop_x db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` TEXT") T.no_params + + let migrations = [ + ("drop_x", [(apply_drop_x, revert_drop_x)]); + ] + + end (* module Mig *) + +@name with down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > -- @add_idx + > -- [sqlgg] down=explicit + > ALTER TABLE t ADD INDEX idx_x (x); + > ALTER TABLE t DROP INDEX idx_x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_add_idx db = + T.execute db ("ALTER TABLE t ADD INDEX idx_x (x)") T.no_params + + let revert_add_idx db = + T.execute db ("ALTER TABLE t DROP INDEX idx_x") T.no_params + + let migrations = [ + ("add_idx", [(apply_add_idx, revert_add_idx)]); + ] + + end (* module Mig *) + +dialect=postgresql (drop column): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect postgresql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` TEXT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +dialect=sqlite (add column): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect sqlite - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t ADD COLUMN y TEXT; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN y TEXT") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `y`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +dialect=tidb (rename table): + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect tidb - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t RENAME TO t2; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t RENAME TO t2") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t2` RENAME TO `t`") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +Multi-file: DDL in one file, migrations in another + $ cat > _ddl.sql <<'EOF' + > CREATE TABLE products (id INT NOT NULL, name TEXT, weight FLOAT); + > EOF + $ cat > _mig1.sql <<'EOF' + > ALTER TABLE products ADD COLUMN color TEXT; + > EOF + $ cat > _mig2.sql <<'EOF' + > ALTER TABLE products DROP COLUMN weight; + > EOF + $ sqlgg -no-header -dialect mysql -gen none _ddl.sql -gen caml -migrations -name mig _mig1.sql _mig2.sql + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_products_0 db = + T.execute db ("ALTER TABLE products ADD COLUMN color TEXT") T.no_params + + let revert_alter_products_0 db = + T.execute db ("ALTER TABLE `products` DROP COLUMN `color`") T.no_params + + let apply_alter_products_1 db = + T.execute db ("ALTER TABLE products DROP COLUMN weight") T.no_params + + let revert_alter_products_1 db = + T.execute db ("ALTER TABLE `products` ADD COLUMN `weight` FLOAT") T.no_params + + let migrations = [ + ("alter_products_0", [(apply_alter_products_0, revert_alter_products_0)]); + ("alter_products_1", [(apply_alter_products_1, revert_alter_products_1)]); + ] + + end (* module Mig *) + +schema state IS updated, so the later migrations file sees the post-ALTER schema. + $ cat > _schema.sql <<'EOF' + > CREATE TABLE t (id INT NOT NULL, x VARCHAR(8) NOT NULL); + > ALTER TABLE t CHANGE COLUMN x x VARCHAR(64) NOT NULL; + > EOF + $ cat > _m.sql <<'EOF' + > ALTER TABLE t DROP COLUMN x; + > EOF + $ sqlgg -no-header -dialect mysql -gen none _schema.sql -gen caml -migrations -name mig _m.sql + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` VARCHAR(64) NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +ADD then DROP — DROP's revert should match ADD's column definition: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t ADD COLUMN x VARCHAR(64) NOT NULL DEFAULT 'd'; + > ALTER TABLE t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t ADD COLUMN x VARCHAR(64) NOT NULL DEFAULT 'd'") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` DROP COLUMN `x`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` VARCHAR(64) NOT NULL DEFAULT 'd'") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) + +RENAME COLUMN then DROP — DROP's revert uses new name: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, old_name VARCHAR(8)); + > ALTER TABLE t RENAME COLUMN old_name TO new_name; + > ALTER TABLE t DROP COLUMN new_name; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t RENAME COLUMN old_name TO new_name") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` RENAME COLUMN `new_name` TO `old_name`") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN new_name") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `new_name` VARCHAR(8)") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) + +CHANGE COLUMN then DROP — DROP's revert uses post-CHANGE definition: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x INT NOT NULL); + > ALTER TABLE t CHANGE COLUMN x x BIGINT UNSIGNED NOT NULL; + > ALTER TABLE t DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN x x BIGINT UNSIGNED NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `x` `x` INT NOT NULL") T.no_params + + let apply_alter_t_1 db = + T.execute db ("ALTER TABLE t DROP COLUMN x") T.no_params + + let revert_alter_t_1 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `x` BIGINT UNSIGNED NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t_1", [(apply_alter_t_1, revert_alter_t_1)]); + ] + + end (* module Mig *) + +RENAME TABLE then ALTER on new name: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, x TEXT); + > ALTER TABLE t RENAME TO t2; + > ALTER TABLE t2 DROP COLUMN x; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t RENAME TO t2") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t2` RENAME TO `t`") T.no_params + + let apply_alter_t2_1 db = + T.execute db ("ALTER TABLE t2 DROP COLUMN x") T.no_params + + let revert_alter_t2_1 db = + T.execute db ("ALTER TABLE `t2` ADD COLUMN `x` TEXT") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ("alter_t2_1", [(apply_alter_t2_1, revert_alter_t2_1)]); + ] + + end (* module Mig *) + +XML basic ADD COLUMN: + $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > ALTER TABLE t ADD COLUMN x VARCHAR(99) NOT NULL; + > EOF + + + + + + +XML DROP COLUMN with ENUM (special chars escape probe): + $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, status ENUM('pending','done') NOT NULL); + > ALTER TABLE t DROP COLUMN status; + > EOF + + + + + + +XML CREATE INDEX: + $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, name TEXT); + > CREATE INDEX idx_name ON t (name); + > EOF + + + + + + +XML RENAME TABLE: + $ cat <<'EOF' | sqlgg -no-header -gen xml -migrations -name mig -dialect mysql - + > CREATE TABLE old_t (id INT NOT NULL); + > RENAME TABLE old_t TO new_t; + > EOF + + + + + + +Empty input: + $ printf '' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let migrations = [ + ] + + end (* module Mig *) + +Only CREATE TABLE — no migration emitted: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let migrations = [ + ] + + end (* module Mig *) + +DROP TABLE with down=explicit: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL); + > -- [sqlgg] down=explicit + > DROP TABLE t; + > CREATE TABLE t (id INT NOT NULL); + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_drop_t db = + T.execute db ("DROP TABLE t") T.no_params + + let revert_drop_t db = + T.execute db ("CREATE TABLE t (id INT NOT NULL)") T.no_params + + let migrations = [ + ("drop_t", [(apply_drop_t, revert_drop_t)]); + ] + + end (* module Mig *) + +SET — also Non_migration_stmt: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - 2>&1 + > CREATE TABLE t (id INT NOT NULL); + > SET @x = 1; + > EOF + ==> SET @x = 1 + Position 1:6 Tokens: @x = 1 + Error: Sqlgg.Sql_parser.MenhirBasics.Error + Errors encountered, no code generated + [1] + +backtick-quoted names everywhere: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE `select` (`order` INT NOT NULL, `from` TEXT); + > ALTER TABLE `select` DROP COLUMN `from`; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_select_0 db = + T.execute db ("ALTER TABLE `select` DROP COLUMN `from`") T.no_params + + let revert_alter_select_0 db = + T.execute db ("ALTER TABLE `select` ADD COLUMN `from` TEXT") T.no_params + + let migrations = [ + ("alter_select_0", [(apply_alter_select_0, revert_alter_select_0)]); + ] + + end (* module Mig *) + +column with reserved keyword name in CHANGE: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, `key` VARCHAR(8) NOT NULL); + > ALTER TABLE t CHANGE COLUMN `key` `key` VARCHAR(64) NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN `key` `key` VARCHAR(64) NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `key` `key` VARCHAR(8) NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +DROP COLUMN on VARCHAR(20) COLLATE utf8mb4_bin — COLLATE preserved in revert: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, login VARCHAR(20) COLLATE utf8mb4_bin NOT NULL); + > ALTER TABLE t DROP COLUMN login; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t DROP COLUMN login") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` ADD COLUMN `login` VARCHAR(20) COLLATE utf8mb4_bin NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +CHANGE COLUMN replacing collation — old COLLATE preserved in revert: + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, login VARCHAR(20) COLLATE utf8mb4_bin NOT NULL); + > ALTER TABLE t CHANGE COLUMN login login VARCHAR(20) COLLATE utf8mb4_unicode_ci NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_alter_t_0 db = + T.execute db ("ALTER TABLE t CHANGE COLUMN login login VARCHAR(20) COLLATE utf8mb4_unicode_ci NOT NULL") T.no_params + + let revert_alter_t_0 db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `login` `login` VARCHAR(20) COLLATE utf8mb4_bin NOT NULL") T.no_params + + let migrations = [ + ("alter_t_0", [(apply_alter_t_0, revert_alter_t_0)]); + ] + + end (* module Mig *) + +either name handling or VARCHAR(n) revert reconstruction is caught at once. + $ cat <<'EOF' | sqlgg -no-header -gen caml -migrations -name mig -dialect mysql - + > CREATE TABLE t (id INT NOT NULL, login VARCHAR(64) NOT NULL); + > -- @shrink_login + > ALTER TABLE t CHANGE COLUMN login login VARCHAR(16) NOT NULL; + > EOF + module Mig (T : Sqlgg_traits.M_io) = struct + + module IO = T.IO + + let apply_shrink_login db = + T.execute db ("ALTER TABLE t CHANGE COLUMN login login VARCHAR(16) NOT NULL") T.no_params + + let revert_shrink_login db = + T.execute db ("ALTER TABLE `t` CHANGE COLUMN `login` `login` VARCHAR(64) NOT NULL") T.no_params + + let migrations = [ + ("shrink_login", [(apply_shrink_login, revert_shrink_login)]); + ] + + end (* module Mig *) diff --git a/test/cram/test.t b/test/cram/test.t index 15e22b08..9e7f3380 100644 --- a/test/cram/test.t +++ b/test/cram/test.t @@ -3536,14 +3536,14 @@ Test TiDB TTL in migration: module IO = T.IO - let apply_alter_smm_synced_comments_1 db = + let apply_alter_smm_synced_comments_0 db = T.execute db ("ALTER TABLE smm_synced_comments TTL = `created_at` + INTERVAL 6 MONTH TTL_ENABLE = 'ON'") T.no_params - let revert_alter_smm_synced_comments_1 db = + let revert_alter_smm_synced_comments_0 db = T.execute db ("ALTER TABLE `smm_synced_comments` REMOVE TTL") T.no_params let migrations = [ - ("alter_smm_synced_comments_1", [(apply_alter_smm_synced_comments_1, revert_alter_smm_synced_comments_1)]); + ("alter_smm_synced_comments_0", [(apply_alter_smm_synced_comments_0, revert_alter_smm_synced_comments_0)]); ] end (* module Mig *) @@ -3562,8 +3562,10 @@ ALTER TABLE REMOVE TTL is supported on TiDB: > CREATE TABLE foo (id INT NOT NULL, created_at TIMESTAMP NOT NULL); > ALTER TABLE foo REMOVE TTL; > EOF - migrations mode: alter_foo_1 contains non-invertible actions (index/constraint ops), use -- [sqlgg] down=explicit - Errors encountered, no code generated + cannot auto-generate revert: table `foo`: + REMOVE TTL: previous TTL parameters are not tracked in schema, cannot restore on revert + ALTER TABLE foo REMOVE TTL + Add `-- [sqlgg] down=explicit` before the statement and write the revert SQL manually. [1] Standalone TTL_ENABLE toggle: @@ -3575,14 +3577,14 @@ Standalone TTL_ENABLE toggle: module IO = T.IO - let apply_alter_foo_1 db = + let apply_alter_foo_0 db = T.execute db ("ALTER TABLE foo TTL_ENABLE = 'OFF'") T.no_params - let revert_alter_foo_1 db = + let revert_alter_foo_0 db = T.execute db ("ALTER TABLE `foo` REMOVE TTL") T.no_params let migrations = [ - ("alter_foo_1", [(apply_alter_foo_1, revert_alter_foo_1)]); + ("alter_foo_0", [(apply_alter_foo_0, revert_alter_foo_0)]); ] end (* module Mig *)