From d08e10f54aa4dfa807709723c87b2b8ace70e53a Mon Sep 17 00:00:00 2001 From: Niols Date: Fri, 5 Jun 2026 17:20:56 +0200 Subject: [PATCH 1/2] Add support for PostgreSQL-style `CREATE TYPE` Co-authored-by: Cursor --- lib/dialect.ml | 9 ++- lib/sql.ml | 6 ++ lib/sql_parser.mly | 8 +++ lib/stmt.ml | 6 +- lib/syntax.ml | 6 ++ lib/types.ml | 23 +++++++ src/gen.ml | 2 + src/gen_migrations.ml | 3 + src/gen_xml.ml | 2 + test/cram/create_type.t | 146 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 lib/types.ml create mode 100644 test/cram/create_type.t diff --git a/lib/dialect.ml b/lib/dialect.ml index 448c5d83..db51d946 100644 --- a/lib/dialect.ml +++ b/lib/dialect.ml @@ -22,6 +22,7 @@ type feature = | DefaultExpr | Ttl | AlterColumn + | UserDefinedType [@@deriving show { with_path = false }] let show_feature x = @@ -45,6 +46,7 @@ let feature_to_string = function | DefaultExpr -> "default_expr" | Ttl -> "ttl" | AlterColumn -> "alter_column" + | UserDefinedType -> "user_defined_type" let feature_of_string s = match String.lowercase_ascii s with @@ -63,6 +65,7 @@ let feature_of_string s = | "default_expr" -> DefaultExpr | "ttl" -> Ttl | "alter_column" -> AlterColumn + | "user_defined_type" -> UserDefinedType | _ -> failwith (Printf.sprintf "Unknown feature: %s" s) type support_state = { @@ -159,6 +162,8 @@ let get_alter_column (change : Sql.Alter_column_pg.t) pos = | Set_type _ | Set_not_null | Drop_not_null -> only AlterColumn [PostgreSQL] pos | Set_default | Drop_default -> only AlterColumn [MySQL; PostgreSQL; TiDB] pos +let get_user_defined_type pos = only UserDefinedType [PostgreSQL] pos + let get_default_expr ~kind ~expr pos = let open Sql in let tidb_only_functions = @@ -506,5 +511,5 @@ let rec analyze stmt = analyze_expr acc (option_list default_expr_opt) (fun acc -> process_params acc rest) in process_params acc params - - \ No newline at end of file + | CreateType _ -> [get_user_defined_type (0, 0)] + | DropType _ -> [get_user_defined_type (0, 0)] diff --git a/lib/sql.ml b/lib/sql.ml index 78502721..dfd2c5b0 100644 --- a/lib/sql.ml +++ b/lib/sql.ml @@ -877,6 +877,10 @@ type alter_action = [ | `RemoveTtl of pos | `AlterColumnPG of string * Alter_column_pg.t located ] [@@deriving show {with_path=false}] +type create_type_target = + | TypeEnum of string list + [@@deriving show {with_path=false}] + type stmt = | Create of table_name * create_target | Drop of table_name @@ -891,6 +895,8 @@ type stmt = | UpdateMulti of nested list * assignments * expr option * order * Source_type.t param list (* where, order, limit *) | Select of select_full | CreateRoutine of table_name * Source_type.kind collated located option * (string * Source_type.kind collated located * expr option) list (* table_name represents possibly namespaced function name *) + | CreateType of string * create_type_target + | DropType of string [@@deriving show {with_path=false}] (* diff --git a/lib/sql_parser.mly b/lib/sql_parser.mly index 1b164ec9..0b3dbdee 100644 --- a/lib/sql_parser.mly +++ b/lib/sql_parser.mly @@ -195,6 +195,13 @@ statement: CREATE ioption(temporary) TABLE ioption(if_not_exists) name=table_nam Function.add (List.length params) (Ret (Source_type.depends Any)) name.tn; (* FIXME void *) CreateRoutine (name, None, params) } + (* TYPE is not a reserved word (columns may be named `type`), so match it as IDENT *) + | CREATE kw=IDENT name=IDENT AS ENUM LPAREN ctors=commas(TEXT) RPAREN + { if String.lowercase_ascii kw <> "type" then failwith ("expected TYPE after CREATE, got " ^ kw); + CreateType (name, TypeEnum ctors) } + | DROP kw=IDENT if_exists? name=IDENT + { if String.lowercase_ascii kw <> "type" then failwith ("expected TYPE after DROP, got " ^ kw); + DropType name } parameter_default_: DEFAULT | EQUAL { } parameter_default: parameter_default_ e=expr { e } @@ -708,6 +715,7 @@ sql_type_flavor: | 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) } + | name=IDENT { Source_type.Infer (Types.get name) } binary: BINARY | BINARY VARYING { } text: CHARACTER { } diff --git a/lib/stmt.ml b/lib/stmt.ml index f5af4773..7f0ec1f2 100644 --- a/lib/stmt.ml +++ b/lib/stmt.ml @@ -21,6 +21,8 @@ type kind = | Select of cardinality | Alter of Sql.table_name list | Drop of Sql.table_name | CreateRoutine of Sql.table_name (** namespaced function name *) + | CreateType of string + | DropType of string | Other [@@deriving show {with_path=false}] @@ -37,5 +39,7 @@ let category_of_stmt_kind = function | CreateIndex _ | CreateRoutine _ | Alter _ -| Drop _ -> DDL +| Drop _ +| CreateType _ +| DropType _ -> DDL | Other -> OTHER diff --git a/lib/syntax.ml b/lib/syntax.ml index 75830f20..6ce71daf 100644 --- a/lib/syntax.ml +++ b/lib/syntax.ml @@ -1671,6 +1671,12 @@ let rec eval (stmt:Sql.stmt) = List.map drop_sources schema, a, b | CreateRoutine (name,_,_) -> [], [], CreateRoutine name + | CreateType (name, TypeEnum ctors) -> + Types.add name (Sql.Type.make_enum_kind ctors); + ([], [], CreateType name) + | DropType name -> + Types.drop name; + ([], [], DropType name) (* FIXME unify each choice separately *) let unify_params l = diff --git a/lib/types.ml b/lib/types.ml new file mode 100644 index 00000000..b8eefa44 --- /dev/null +++ b/lib/types.ml @@ -0,0 +1,23 @@ +(** Global list of types *) + +open Printf + +type registry = (string, Sql.Type.kind) Hashtbl.t + +let registry : registry = Hashtbl.create 8 + +let add name kind = + match Hashtbl.mem registry name with + | true -> failwith (sprintf "duplicate type declaration for %S" name) + | false -> Hashtbl.add registry name kind + +let drop name = + match Hashtbl.mem registry name with + | true -> Hashtbl.remove registry name + | false -> failwith (sprintf "no such type %S" name) + +let get_opt = Hashtbl.find_opt registry + +let get = Hashtbl.find registry + +let reset () = Hashtbl.reset registry diff --git a/src/gen.ml b/src/gen.ml index 8c1d92f2..0e9d6c1b 100644 --- a/src/gen.ml +++ b/src/gen.ml @@ -65,6 +65,8 @@ let choose_name props kind index = | Select _ -> sprintf "select_%u" index | CreateRoutine s -> sprintf "create_routine_%s" (fix s) | Other -> sprintf "statement_%u" index + | CreateType n -> sprintf "create_type_%s" (fix' n) + | DropType n -> sprintf "drop_type_%s" (fix' n) in make_name props name diff --git a/src/gen_migrations.ml b/src/gen_migrations.ml index 4bfe390f..5b29e5ac 100644 --- a/src/gen_migrations.ml +++ b/src/gen_migrations.ml @@ -270,3 +270,6 @@ type migration = { apply : string list; revert : string list; } + +let drop_type_sql type_name = + sprintf "DROP TYPE %s" (quote_id type_name) diff --git a/src/gen_xml.ml b/src/gen_xml.ml index 764000c5..6f564717 100644 --- a/src/gen_xml.ml +++ b/src/gen_xml.ml @@ -144,6 +144,8 @@ let generate_code (x,_) index stmt = | Drop t -> ["kind", "drop"; "target", Sql.show_table_name t; "cardinality", "0"] | CreateRoutine s -> ["kind", "create_routine"; "target", Sql.show_table_name s] | Other -> ["kind", "other"] + | CreateType n -> ["kind", "create_type"; "target", n; "cardinality", "0"] + | DropType n -> ["kind", "drop_type"; "target", n; "cardinality", "0"] in let nodes = [ input; output] in x := Node ("stmt", ("name",name)::("sql",sql)::("category",show_category @@ category_of_stmt_kind stmt.kind)::attrs, nodes) :: !x diff --git a/test/cram/create_type.t b/test/cram/create_type.t new file mode 100644 index 00000000..fcfb40e7 --- /dev/null +++ b/test/cram/create_type.t @@ -0,0 +1,146 @@ +CREATE TYPE + $ sqlgg -gen caml -no-header -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > CREATE TABLE "shirt" ("id" INTEGER NOT NULL, "color" "color" NOT NULL); + > SELECT "id" FROM "shirt" WHERE "color" = 'red'; + > DROP TYPE "color"; + > EOF + module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + + let create_type_color db = + T.execute db ("CREATE TYPE \"color\" AS ENUM ('red', 'green', 'blue')") T.no_params + + let create_shirt db = + T.execute db ("CREATE TABLE \"shirt\" (\"id\" INTEGER NOT NULL, \"color\" \"color\" NOT NULL)") T.no_params + + let select_2 db callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + T.select db ("SELECT \"id\" FROM \"shirt\" WHERE \"color\" = 'red'") T.no_params invoke_callback + + let drop_type_color db = + T.execute db ("DROP TYPE \"color\"") T.no_params + + module Fold = struct + let select_2 db callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("SELECT \"id\" FROM \"shirt\" WHERE \"color\" = 'red'") T.no_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select_2 db callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("SELECT \"id\" FROM \"shirt\" WHERE \"color\" = 'red'") T.no_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + end (* module Sqlgg *) + +Duplicate CREATE TYPE + $ sqlgg -gen caml -no-header -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > CREATE TYPE "color" AS ENUM ('black', 'white'); + > EOF + Failed : CREATE TYPE "color" AS ENUM ('black', 'white') + Fatal error: exception Failure("duplicate type declaration for \"color\"") + [2] + +Duplicate DROP TYPE + $ sqlgg -gen caml -no-header -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > DROP TYPE "color"; + > DROP TYPE "color"; + > EOF + Failed : DROP TYPE "color" + Fatal error: exception Failure("no such type \"color\"") + [2] + +CREATE DROP CREATE + $ sqlgg -gen caml -no-header -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > DROP TYPE "color"; + > CREATE TYPE "color" AS ENUM ('black', 'white'); + > CREATE TABLE "shirt" ("id" INTEGER NOT NULL, "color" "color" NOT NULL); + > SELECT "id" FROM "shirt" WHERE "color" = 'black'; + > EOF + module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + + let create_type_color db = + T.execute db ("CREATE TYPE \"color\" AS ENUM ('red', 'green', 'blue')") T.no_params + + let drop_type_color db = + T.execute db ("DROP TYPE \"color\"") T.no_params + + let create_type_color db = + T.execute db ("CREATE TYPE \"color\" AS ENUM ('black', 'white')") T.no_params + + let create_shirt db = + T.execute db ("CREATE TABLE \"shirt\" (\"id\" INTEGER NOT NULL, \"color\" \"color\" NOT NULL)") T.no_params + + let select_4 db callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + T.select db ("SELECT \"id\" FROM \"shirt\" WHERE \"color\" = 'black'") T.no_params invoke_callback + + module Fold = struct + let select_4 db callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("SELECT \"id\" FROM \"shirt\" WHERE \"color\" = 'black'") T.no_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select_4 db callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("SELECT \"id\" FROM \"shirt\" WHERE \"color\" = 'black'") T.no_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + end (* module Sqlgg *) + + +CREATE TYPE misuse + $ sqlgg -gen caml -no-header -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > CREATE TABLE "shirt" ("id" INTEGER NOT NULL, "color" "color" NOT NULL); + > SELECT "id" FROM "shirt" WHERE "color" = 'black'; + > EOF + Failed : SELECT "id" FROM "shirt" WHERE "color" = 'black' + Fatal error: exception Failure("types Union (blue| green| red) and StringLiteral (black) for 'a do not match in 'a -> 'a -> Bool applied to (Union (blue| green| red), StringLiteral (black))") + [2] + +Only in PostgreSQL dialect + $ sqlgg -gen caml -no-header -dialect=mysql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > CREATE TABLE "shirt" ("id" INTEGER NOT NULL, "color" "color" NOT NULL); + > EOF + Feature UserDefinedType is not supported for dialect MySQL (supported by: PostgreSQL) at + Errors encountered, no code generated + [1] From d9582ce100b183a655921e538febf8a35547d66b Mon Sep 17 00:00:00 2001 From: Gleb Patsiia Date: Fri, 3 Jul 2026 10:19:29 +0000 Subject: [PATCH 2/2] Make TYPE an unreserved keyword --- lib/sql.ml | 2 +- lib/sql_lexer.mll | 1 + lib/sql_parser.mly | 96 +++++++++---------- lib/syntax.ml | 6 +- lib/{types.ml => user_types.ml} | 11 ++- src/cli.ml | 1 + test/cram/create_type.t | 57 +++++++++++ test/cram/type_unreserved.t | 164 ++++++++++++++++++++++++++++++++ 8 files changed, 281 insertions(+), 57 deletions(-) rename lib/{types.ml => user_types.ml} (60%) create mode 100644 test/cram/type_unreserved.t diff --git a/lib/sql.ml b/lib/sql.ml index dfd2c5b0..2e66c500 100644 --- a/lib/sql.ml +++ b/lib/sql.ml @@ -896,7 +896,7 @@ type stmt = | Select of select_full | CreateRoutine of table_name * Source_type.kind collated located option * (string * Source_type.kind collated located * expr option) list (* table_name represents possibly namespaced function name *) | CreateType of string * create_type_target - | DropType of string + | DropType of string * bool [@@deriving show {with_path=false}] (* diff --git a/lib/sql_lexer.mll b/lib/sql_lexer.mll index 5dbabd9b..a570ec6a 100644 --- a/lib/sql_lexer.mll +++ b/lib/sql_lexer.mll @@ -197,6 +197,7 @@ let keywords = "ttl", TTL; "ttl_enable", TTL_ENABLE; "remove", REMOVE; + "type", TYPE "type"; ] in (* more *) k := !k @ List.map (fun s -> s, INTERVAL_UNIT s) [ "microsecond"; "second"; "minute"; "hour"; "day"; "week"; "month"; "quarter"; "year" ]; let all token l = k := !k @ List.map (fun x -> x,token) l in diff --git a/lib/sql_parser.mly b/lib/sql_parser.mly index 0b3dbdee..2f2b5839 100644 --- a/lib/sql_parser.mly +++ b/lib/sql_parser.mly @@ -23,6 +23,7 @@ %token INTEGER %token IDENT TEXT BLOB +%token TYPE %token FLOAT %token PARAM %token SHARED_QUERY_REF @@ -101,15 +102,15 @@ param: if_not_exists: IF NOT EXISTS { } if_exists: IF EXISTS {} temporary: either(GLOBAL,LOCAL)? TEMPORARY { } -assign: name=IDENT EQUAL e=expr { name, e } +assign: name=ident EQUAL e=expr { name, e } -cte_item: | cte_name=IDENT names=maybe_parenth(sequence(IDENT))? AS LPAREN stmt=select_stmt_plain RPAREN +cte_item: | cte_name=ident names=maybe_parenth(sequence(ident))? AS LPAREN stmt=select_stmt_plain RPAREN { let cols = Option.map (List.map (fun name -> make_attribute' name (depends Any) )) names in { cte_name; cols; stmt = CteInline stmt } } - | cte_name=IDENT names=maybe_parenth(sequence(IDENT))? AS shared_query_ref_id=SHARED_QUERY_REF + | cte_name=ident names=maybe_parenth(sequence(ident))? AS shared_query_ref_id=SHARED_QUERY_REF { let cols = Option.map (List.map (fun name -> make_attribute' name (depends Any) )) names in { cte_name; cols; stmt = CteSharedQuery shared_query_ref_id; } @@ -133,21 +134,21 @@ statement: CREATE ioption(temporary) TABLE ioption(if_not_exists) name=table_nam { Drop name } - | CREATE u=boption(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) { 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)? + | insert_action_kind=insert_cmd target=table_name names=sequence(ident)? VALUES values=commas(sequence(set_column_expr))? ss=located(conflict_clause)? { Insert { insert_action_kind; target; action=`Values (names, values); on_conflict_clause=ss; } } - | insert_action_kind=insert_cmd target=table_name names=sequence(IDENT)? VALUES p=param ss=located(conflict_clause)? + | insert_action_kind=insert_cmd target=table_name names=sequence(ident)? VALUES p=param ss=located(conflict_clause)? { Insert { insert_action_kind; target; action=`Param (names, p); on_conflict_clause=ss; } } - | insert_action_kind=insert_cmd target=table_name names=sequence(IDENT)? select=maybe_parenth(select_stmt) ss=located(conflict_clause)? + | insert_action_kind=insert_cmd target=table_name names=sequence(ident)? select=maybe_parenth(select_stmt) ss=located(conflict_clause)? { Insert { insert_action_kind; target; action=`Select (names, select); on_conflict_clause=ss; } } @@ -195,17 +196,14 @@ statement: CREATE ioption(temporary) TABLE ioption(if_not_exists) name=table_nam Function.add (List.length params) (Ret (Source_type.depends Any)) name.tn; (* FIXME void *) CreateRoutine (name, None, params) } - (* TYPE is not a reserved word (columns may be named `type`), so match it as IDENT *) - | CREATE kw=IDENT name=IDENT AS ENUM LPAREN ctors=commas(TEXT) RPAREN - { if String.lowercase_ascii kw <> "type" then failwith ("expected TYPE after CREATE, got " ^ kw); - CreateType (name, TypeEnum ctors) } - | DROP kw=IDENT if_exists? name=IDENT - { if String.lowercase_ascii kw <> "type" then failwith ("expected TYPE after DROP, got " ^ kw); - DropType name } + | CREATE TYPE name=ident AS ENUM LPAREN ctors=commas(TEXT) RPAREN + { CreateType (name, TypeEnum ctors) } + | DROP TYPE ie=boption(if_exists) name=ident + { DropType (name, ie) } parameter_default_: DEFAULT | EQUAL { } parameter_default: parameter_default_ e=expr { e } -func_parameter: n=IDENT AS? t=located_sql_type e=parameter_default? { (n,t,e) } +func_parameter: n=ident AS? t=located_sql_type e=parameter_default? { (n,t,e) } parameter_mode: IN | OUT | INOUT { } proc_parameter: parameter_mode? p=func_parameter { p } @@ -217,10 +215,14 @@ compound_stmt: BEGIN statement+ END { } (* mysql *) routine_extra: LANGUAGE IDENT { } | COMMENT TEXT { } -%inline table_name: name=IDENT { Sql.make_table_name name } - | db=IDENT DOT name=IDENT { Sql.make_table_name ~db name } +(* cf. ColId / unreserved_keyword in PostgreSQL's gram.y (TYPE_P is unreserved there too): + https://github.com/postgres/postgres/blob/REL_18_0/src/backend/parser/gram.y#L17632 *) +ident: x=IDENT | x=TYPE { x } + +%inline table_name: name=ident { Sql.make_table_name name } + | db=ident DOT name=ident { Sql.make_table_name ~db name } index_prefix: LPAREN n=INTEGER RPAREN { n } -index_column: name=IDENT index_prefix? c=collate? order_type? { make_collated ?collation:c ~collated:name ()} +index_column: name=ident index_prefix? c=collate? order_type? { make_collated ?collation:c ~collated:name ()} table_definition: t=sequence_(column_def1) ignore_after(RPAREN) { @@ -280,7 +282,7 @@ join_source: COMMA src=source c=join_cond { src, make_located ~value:Schema.Join | j=cond(located(inner_join)) { j } | j=straight_cond(located(straight_join)) { j } join_cond: ON e=expr { On e } - | USING l=sequence(IDENT) { Using l } + | USING l=sequence(ident) { Using l } | (* *) { Default } source1: table_name { `Table $1 } @@ -324,7 +326,7 @@ select_row_locking: for_update_or_share: FOR either(UPDATE, SHARE) update_or_share_of? NOWAIT? with_lock? { } -update_or_share_of: OF commas(IDENT) { } +update_or_share_of: OF commas(ident) { } with_lock: WITH LOCK { } @@ -355,37 +357,35 @@ column1_kind: | ASTERISK { Sql.All } | c=pair(located(expr), maybe_as) { let (e, m) = c in Sql.Expr (e, m) } -maybe_as: AS? name=IDENT { Some name } +maybe_as: AS? name=ident { Some name } | { None } -maybe_as_with_detupled: AS? name=IDENT names=sequence(IDENT)? { name, names } +maybe_as_with_detupled: AS? name=ident names=sequence(ident)? { name, names } maybe_parenth(X): x=X | LPAREN x=X RPAREN { x } alter_column_pg_spec: - | kw=IDENT t=located_sql_type - { if String.lowercase_ascii kw <> "type" then failwith ("expected TYPE after ALTER COLUMN, got " ^ kw); - Alter_column_pg.Set_type t } + | TYPE t=located_sql_type { Alter_column_pg.Set_type t } | SET NOT NULL { Alter_column_pg.Set_not_null } | DROP NOT NULL { Alter_column_pg.Drop_not_null } | SET DEFAULT default_value { Alter_column_pg.Set_default } | DROP DEFAULT { Alter_column_pg.Drop_default } alter_action: ADD COLUMN? col=maybe_parenth(column_def) pos=alter_pos { `Add (col,pos) } - | 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 } + | 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) } - | RENAME index_or_key old_name=IDENT TO new_name=IDENT { `RenameIndex (old_name, new_name) } - | DROP INDEX name=IDENT { `DropIndex name } + | RENAME COLUMN old_name=ident TO new_name=ident { `RenameColumn (old_name, new_name) } + | RENAME index_or_key old_name=ident TO new_name=ident { `RenameIndex (old_name, new_name) } + | DROP INDEX name=ident { `DropIndex name } | DROP PRIMARY KEY { `DropPrimaryKey } - | DROP COLUMN? col=IDENT drop_behavior? { `Drop col } (* FIXME behavior? *) - | DROP FOREIGN KEY name=IDENT { `DropConstraint name } - | DROP CHECK name=IDENT { `DropConstraint name } - | CHANGE COLUMN? old_name=IDENT column=column_def pos=alter_pos { `Change (old_name,column,pos) } + | DROP COLUMN? col=ident drop_behavior? { `Drop col } (* FIXME behavior? *) + | DROP FOREIGN KEY name=ident { `DropConstraint name } + | DROP CHECK name=ident { `DropConstraint name } + | CHANGE COLUMN? old_name=ident column=column_def pos=alter_pos { `Change (old_name,column,pos) } | MODIFY COLUMN? column=column_def pos=alter_pos { `Change (column.Alter_action_attr.name,column,pos) } - | ALTER COLUMN? col=IDENT spec=located(alter_column_pg_spec) { `AlterColumnPG (col, spec) } + | ALTER COLUMN? col=ident spec=located(alter_column_pg_spec) { `AlterColumnPG (col, spec) } | opts=ttl_option+ { `TtlOptions (opts, ($startofs, $endofs)) } | REMOVE TTL { `RemoveTtl ($startofs, $endofs) } | either(DEFAULT,pair(CONVERT,TO))? cs=charset c=collate? { `Default_or_convert_to (cs, c) } @@ -396,7 +396,7 @@ alter_action_or_ignored: a=alter_action { Some a } | ALGORITHM EQUAL algorithm { None } | LOCK EQUAL lock { None } -ttl_option: TTL EQUAL col=IDENT PLUS INTERVAL n=INTEGER unit=INTERVAL_UNIT +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 { } @@ -405,12 +405,12 @@ index_type: | 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 } +alter_pos: AFTER col=ident { `After col } | FIRST { `First } | { `Default } drop_behavior: CASCADE | RESTRICT { } -column_def: name=IDENT sql_kind=located_sql_type? extra=located(column_def_extra)* +column_def: name=ident sql_kind=located_sql_type? extra=located(column_def_extra)* { let rule_start_pos_cnum = $startpos.Lexing.pos_cnum in let meta = List.concat @@ Parser_state.Stmt_metadata.find_all rule_start_pos_cnum in @@ -424,22 +424,22 @@ inline_idx_kind: | SPATIAL index_or_key? { Sql.Spatial } column_def1: c=column_def { `Attr c } - | pair(CONSTRAINT,IDENT?)? l=table_constraint_1 index_options { `Constraint l } + | pair(CONSTRAINT,ident?)? l=table_constraint_1 index_options { `Constraint l } | kind=inline_idx_kind t=table_index { let (idx_name, idx_cols) = t in `Index (make_located ~value:{ idx_kind = kind; idx_name; idx_cols; idx_unique = false } ~pos:($startofs, $endofs)) } int_arg: LPAREN n=INTEGER RPAREN { n } -key_part: n=IDENT int_arg? either(ASC,DESC)? { n } +key_part: n=ident int_arg? either(ASC,DESC)? { n } index_options: list(IDENT)? { } -table_index: name=IDENT? l=sequence(key_part) index_options { (name, 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? name=IDENT? l=sequence(key_part) { `Unique (name, l) } - | FOREIGN KEY IDENT? sequence(IDENT) REFERENCES IDENT sequence(IDENT)? + | 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 } | CHECK LPAREN expr RPAREN { `Ignore } @@ -484,8 +484,8 @@ anyall: ANY | ALL | SOME { } mnot(X): NOT x = X | x = X { x } -attr_name: cname=IDENT { { cname; tname=None} } - | table=table_name DOT cname=IDENT { {cname; tname=Some table} } (* FIXME database identifier *) +attr_name: cname=ident { { cname; tname=None} } + | table=table_name DOT cname=ident { {cname; tname=Some table} } (* FIXME database identifier *) distinct_from: DISTINCT FROM { } @@ -515,7 +515,7 @@ expr: | INTERVAL e=expr interval_unit { Fun { fn_name = "interval"; kind = (fixed Datetime [Int]); parameters = [e]; is_over_clause = false } } | LPAREN e=expr RPAREN { e } | a=attr_name c=collate? { Column (make_collated ?collation:c ~collated:a ()) } - | VALUES LPAREN n=IDENT RPAREN { Of_values n } + | VALUES LPAREN n=ident RPAREN { Of_values n } | v=literal_value | v=datetime_value { v } | INTERVAL_UNIT { Value (make_collated ~collated:(strict Datetime) ()) } | e1=expr mnot(IN) l=sequence(expr) { poly "in" (depends Bool) (e1::l) } @@ -715,7 +715,7 @@ sql_type_flavor: | 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) } - | name=IDENT { Source_type.Infer (Types.get name) } + | name=ident { Source_type.Infer (User_types.get name) } binary: BINARY | BINARY VARYING { } text: CHARACTER { } diff --git a/lib/syntax.ml b/lib/syntax.ml index 6ce71daf..8fcd3835 100644 --- a/lib/syntax.ml +++ b/lib/syntax.ml @@ -1672,10 +1672,10 @@ let rec eval (stmt:Sql.stmt) = | CreateRoutine (name,_,_) -> [], [], CreateRoutine name | CreateType (name, TypeEnum ctors) -> - Types.add name (Sql.Type.make_enum_kind ctors); + User_types.add name (Sql.Type.make_enum_kind ctors); ([], [], CreateType name) - | DropType name -> - Types.drop name; + | DropType (name, if_exists) -> + User_types.drop ~if_exists name; ([], [], DropType name) (* FIXME unify each choice separately *) diff --git a/lib/types.ml b/lib/user_types.ml similarity index 60% rename from lib/types.ml rename to lib/user_types.ml index b8eefa44..592f4742 100644 --- a/lib/types.ml +++ b/lib/user_types.ml @@ -1,4 +1,4 @@ -(** Global list of types *) +(** Global registry of user-defined types (CREATE TYPE) *) open Printf @@ -11,10 +11,11 @@ let add name kind = | true -> failwith (sprintf "duplicate type declaration for %S" name) | false -> Hashtbl.add registry name kind -let drop name = - match Hashtbl.mem registry name with - | true -> Hashtbl.remove registry name - | false -> failwith (sprintf "no such type %S" name) +let drop ~if_exists name = + match Hashtbl.mem registry name, if_exists with + | true, _ -> Hashtbl.remove registry name + | false, true -> () + | false, false -> failwith (sprintf "no such type %S" name) let get_opt = Hashtbl.find_opt registry diff --git a/src/cli.ml b/src/cli.ml index 34ec972e..76608fbc 100644 --- a/src/cli.ml +++ b/src/cli.ml @@ -218,6 +218,7 @@ let to_file_sources files = List.map (fun f -> From_file f) files let replay_sources sources = Tables.reset (); + User_types.reset (); List.iter (function | From_file f -> Main.with_channel f (Option.map_default Main.replay_schema ()) | From_sql sql -> Main.replay_sql sql) sources diff --git a/test/cram/create_type.t b/test/cram/create_type.t index fcfb40e7..1c01d49a 100644 --- a/test/cram/create_type.t +++ b/test/cram/create_type.t @@ -59,6 +59,13 @@ Duplicate CREATE TYPE Fatal error: exception Failure("duplicate type declaration for \"color\"") [2] +DROP TYPE IF EXISTS is idempotent + $ sqlgg -gen none -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); + > DROP TYPE IF EXISTS "color"; + > DROP TYPE IF EXISTS "color"; + > EOF + Duplicate DROP TYPE $ sqlgg -gen caml -no-header -dialect=postgresql - <<'EOF' 2>&1 > CREATE TYPE "color" AS ENUM ('red', 'green', 'blue'); @@ -144,3 +151,53 @@ Only in PostgreSQL dialect Feature UserDefinedType is not supported for dialect MySQL (supported by: PostgreSQL) at Errors encountered, no code generated [1] + +DROP TYPE is gated per-dialect too + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TYPE color AS ENUM ('red'); + > DROP TYPE color; + > EOF + Feature UserDefinedType is not supported for dialect MySQL (supported by: PostgreSQL) at + Feature UserDefinedType is not supported for dialect MySQL (supported by: PostgreSQL) at + Errors encountered, no code generated + [1] + +ALTER COLUMN ... TYPE is gated per-dialect (AlterColumn feature) + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE t (a INT NOT NULL); + > ALTER TABLE t ALTER COLUMN a TYPE BIGINT; + > EOF + Feature AlterColumn is not supported for dialect MySQL (supported by: PostgreSQL) at TYPE BIGINT + Errors encountered, no code generated + [1] + +TYPE is unreserved: still usable as a column name, even next to the TYPE keyword + $ sqlgg -gen none -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TABLE kw (type INTEGER NOT NULL, val TEXT); + > SELECT type FROM kw WHERE type = 1; + > ALTER TABLE kw ALTER COLUMN type TYPE SMALLINT; + > EOF + +Unquoted CREATE TYPE / DROP TYPE + $ sqlgg -gen none -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE color AS ENUM ('red', 'green', 'blue'); + > CREATE TABLE shirt (id INTEGER NOT NULL, c color NOT NULL); + > SELECT id FROM shirt WHERE c = 'red'; + > DROP TYPE color; + > EOF + +Diff mode: the type registry is reset per schema replay, so the same +CREATE TYPE in both schemas must not clash as a duplicate + $ cat > diff_initial.sql <<'EOF' + > CREATE TYPE color AS ENUM ('red'); + > CREATE TABLE t (id INTEGER NOT NULL, c color NOT NULL); + > EOF + $ cat > diff_target.sql <<'EOF' + > CREATE TYPE color AS ENUM ('red'); + > CREATE TABLE t (id INTEGER NOT NULL, c color NOT NULL, extra INTEGER); + > EOF + $ sqlgg -no-header -dialect postgresql -diff -now 20260101000000 -gen sql -base diff_initial.sql -target diff_target.sql + -- [sqlgg] generated + -- [sqlgg] id=20260101000000_alter_t_add_col_extra + ALTER TABLE `t` ADD COLUMN `extra` INT; + ALTER TABLE `t` DROP COLUMN `extra`; diff --git a/test/cram/type_unreserved.t b/test/cram/type_unreserved.t new file mode 100644 index 00000000..3fcdbfd2 --- /dev/null +++ b/test/cram/type_unreserved.t @@ -0,0 +1,164 @@ +TYPE is an unreserved keyword: it must still work as a plain identifier in +every name position of the grammar (`ident`), in all dialects. Empty output +means everything parsed and type-checked. + +Column named "type" through the whole DML lifecycle (MySQL, the common case in +the wild): + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE kw (type INT NOT NULL, val TEXT); + > SELECT type, COUNT(*) FROM kw WHERE type > 0 GROUP BY type HAVING type < 10 ORDER BY type DESC LIMIT 1; + > INSERT INTO kw (type, val) VALUES (1, 'a'); + > INSERT INTO kw SET type = 2; + > INSERT INTO kw (type, val) VALUES (3, 'b') ON DUPLICATE KEY UPDATE type = VALUES(type); + > UPDATE kw SET type = type + 1 WHERE type = 3; + > DELETE FROM kw WHERE type = 4; + > EOF + +Table named "type", including qualified column access type.type: + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE type (id INT NOT NULL, type INT NOT NULL); + > SELECT id FROM type; + > SELECT type.id, type.type FROM type; + > INSERT INTO type (id, type) VALUES (1, 2); + > UPDATE type SET type = 2 WHERE type.id = 1; + > DELETE FROM type WHERE id = 3; + > DROP TABLE type; + > EOF + +Aliases and CTEs named "type": + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE t1 (a INT NOT NULL); + > SELECT a AS type FROM t1; + > SELECT type.a FROM t1 AS type; + > WITH type AS (SELECT a AS type FROM t1) SELECT type FROM type; + > EOF + +Keys, indexes and index names: + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE ix (type INT NOT NULL, val TEXT, PRIMARY KEY (type)); + > CREATE TABLE ix2 (type INT NOT NULL, UNIQUE (type)); + > CREATE INDEX type ON ix (type); + > EOF + +JOIN ... USING (type): + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE j1 (type INT NOT NULL, x INT NOT NULL); + > CREATE TABLE j2 (type INT NOT NULL, y INT NOT NULL); + > SELECT x, y FROM j1 JOIN j2 USING (type); + > EOF + +Every ALTER TABLE action that takes a name: + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE al (a INT NOT NULL); + > ALTER TABLE al ADD COLUMN type INT; + > ALTER TABLE al ADD INDEX idx_type (type); + > ALTER TABLE al DROP INDEX idx_type; + > ALTER TABLE al ADD INDEX type (type); + > ALTER TABLE al DROP INDEX type; + > ALTER TABLE al RENAME COLUMN type TO type2; + > ALTER TABLE al RENAME COLUMN type2 TO type; + > ALTER TABLE al CHANGE COLUMN type type BIGINT; + > ALTER TABLE al MODIFY COLUMN type INT; + > ALTER TABLE al ADD COLUMN b INT AFTER type; + > ALTER TABLE al DROP COLUMN type; + > CREATE TABLE type_tbl (a INT NOT NULL); + > ALTER TABLE type_tbl RENAME TO type; + > EOF + +ALTER TABLE on a table named "type": + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE type (a INT NOT NULL); + > ALTER TABLE type ADD COLUMN b INT; + > DROP TABLE type; + > EOF + +Function parameter named "type": + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > CREATE FUNCTION f(type INT) RETURNS INT AS 'select 1'; + > EOF + +Session variable assignment and function-call position (regression: `assign` +used a bare IDENT and rejected the TYPE token): + + $ sqlgg -gen none -dialect=mysql - <<'EOF' 2>&1 + > SET type = 1; + > SELECT type(1); + > EOF + W: unknown function "type" of 1 arguments, treating as untyped + +PostgreSQL: the hot zone where the TYPE keyword and the identifier meet. +A type named "type", a column named "type" of type "type", and +ALTER COLUMN type TYPE type in one place: + + $ sqlgg -gen none -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TYPE type AS ENUM ('a', 'b'); + > CREATE TABLE pg (type type NOT NULL); + > SELECT type FROM pg WHERE type = 'a'; + > ALTER TABLE pg ALTER COLUMN type TYPE type; + > ALTER TABLE pg ALTER COLUMN type SET NOT NULL; + > ALTER TABLE pg ALTER COLUMN type DROP NOT NULL; + > ALTER TABLE pg ALTER COLUMN type SET DEFAULT 'a'; + > ALTER TABLE pg ALTER COLUMN type DROP DEFAULT; + > DROP TYPE type; + > EOF + +SQLite (default keyword handling elsewhere must be untouched): + + $ sqlgg -gen none -dialect=sqlite - <<'EOF' 2>&1 + > CREATE TABLE kw (type INTEGER NOT NULL, val TEXT); + > SELECT type FROM kw WHERE type = 1; + > EOF + +Code generation still works for a column named "type" (OCaml output must not +produce a bare `type` keyword clash): + + $ sqlgg -gen caml -no-header -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE kw (type INT NOT NULL); + > SELECT type FROM kw; + > EOF + module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + + let create_kw db = + T.execute db ("CREATE TABLE kw (type INT NOT NULL)") T.no_params + + let select_1 db callback = + let invoke_callback stmt = + callback + ~type_:(T.get_column_Int stmt 0) + in + T.select db ("SELECT type FROM kw") T.no_params invoke_callback + + module Fold = struct + let select_1 db callback acc = + let invoke_callback stmt = + callback + ~type_:(T.get_column_Int stmt 0) + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("SELECT type FROM kw") T.no_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select_1 db callback = + let invoke_callback stmt = + callback + ~type_:(T.get_column_Int stmt 0) + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("SELECT type FROM kw") T.no_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + end (* module Sqlgg *)