diff --git a/doc/_cli-options.md b/doc/_cli-options.md index eba25496..2364b354 100644 --- a/doc/_cli-options.md +++ b/doc/_cli-options.md @@ -21,7 +21,7 @@ Dialect and checks: -dialect mysql|postgresql|sqlite|tidb Set SQL dialect. Queries can only use its features - -no-check {all|{,}+} Disable dialect feature checks (possible features: collation|join_on_subquery|create_table_as_select|on_duplicate_key|on_conflict|straight_join|lock_in_share_mode|fulltext_index|unsigned_types|autoincrement|replace_into|row_locking|default_expr|ttl|alter_column|user_defined_type) + -no-check {all|{,}+} Disable dialect feature checks (possible features: collation|join_on_subquery|create_table_as_select|on_duplicate_key|on_conflict|straight_join|lock_in_share_mode|fulltext_index|unsigned_types|autoincrement|replace_into|row_locking|default_expr|ttl|alter_column|user_defined_type|returning) -allow-write-notnull-null Accept writing a nullable value into a NOT NULL column, instead of failing (MySQL, TiDB and SQLite only) Generated header: diff --git a/lib/dialect.ml b/lib/dialect.ml index 4a906343..d1445861 100644 --- a/lib/dialect.ml +++ b/lib/dialect.ml @@ -28,6 +28,7 @@ type feature = | Ttl [@as "ttl"] | AlterColumn [@as "alter_column"] | UserDefinedType [@as "user_defined_type"] + | Returning [@as "returning"] [@@deriving show { with_path = false }, enumerate, to_string, of_string] let show_feature x = @@ -106,6 +107,8 @@ let get_on_duplicate_key pos = only OnDuplicateKey [MySQL; TiDB] pos let get_on_conflict pos = only OnConflict [SQLite; PostgreSQL] pos +let get_returning pos = only Returning [PostgreSQL; SQLite] pos + let get_straight_join pos = only StraightJoin [MySQL; TiDB] pos let get_lock_in_share_mode pos = only LockInShareMode [MySQL] pos @@ -220,6 +223,10 @@ and analyze_column acc cols k = match cols with | { value = (All | AllOf _); _ } -> analyze_column acc rest k | { value = Expr ({ value = expr; _ }, _); _ } -> analyze_expr acc [expr] (fun acc -> analyze_column acc rest k) +and analyze_returning acc returning k = match returning with + | None -> k acc + | Some { value = cols; pos } -> analyze_column (get_returning pos :: acc) cols k + and analyze_source acc srcs k = match srcs with | [] -> k acc | src :: rest -> @@ -364,7 +371,7 @@ and analyze_alter_action acc actions k = match actions with and analyze_insert_action acc ias k = match ias with | [] -> k acc - | { action; on_conflict_clause; insert_action_kind; _ } :: rest -> + | { action; on_conflict_clause; insert_action_kind; returning; _ } :: rest -> let acc = match insert_action_kind with | Replace_into pos -> get_replace_into pos :: acc | Insert_into -> acc @@ -391,7 +398,8 @@ and analyze_insert_action acc ias k = match ias with analyze_action acc (fun acc -> let conflict_aes = List.map snd conflict_assignments in analyze_assignment_expr acc conflict_aes (fun acc -> - analyze_insert_action acc rest k)) + analyze_returning acc returning (fun acc -> + analyze_insert_action acc rest k))) let analyze_schema_index idx = match idx.value.Sql.idx_kind with | Regular_idx -> None @@ -414,8 +422,9 @@ let rec analyze stmt = | CreateIndex { ci_cols; _ } -> List.concat_map check_collated ci_cols | Insert insert_action -> analyze_insert_action acc [insert_action] List.rev - | Delete (_, where_opt) -> - analyze_expr acc (option_list where_opt) List.rev + | Delete (_, where_opt, returning) -> + analyze_expr acc (option_list where_opt) (fun acc -> + analyze_returning acc returning List.rev) | DeleteMulti (_, nested, where_opt) -> analyze_nested acc [nested] (fun acc -> analyze_expr acc (option_list where_opt) List.rev) @@ -424,11 +433,12 @@ let rec analyze stmt = analyze_expr acc exprs (fun acc -> let stmt_features = Option.map_default analyze [] stmt_opt in List.rev (List.rev_append stmt_features acc)) - | Update (_, assignments, where_opt, order, _) -> + | Update (_, assignments, where_opt, order, _, returning) -> let aes = List.map snd assignments in analyze_assignment_expr acc aes (fun acc -> let exprs = option_list where_opt @ List.map fst order in - analyze_expr acc exprs List.rev) + analyze_expr acc exprs (fun acc -> + analyze_returning acc returning List.rev)) | UpdateMulti (nesteds, assignments, where_opt, order, _) -> analyze_nested acc nesteds (fun acc -> let aes = List.map snd assignments in diff --git a/lib/sql.ml b/lib/sql.ml index 540af6ae..f8edcbdc 100644 --- a/lib/sql.ml +++ b/lib/sql.ml @@ -753,6 +753,8 @@ and column_kind = type columns = column list [@@deriving show] +type returning = columns located [@@deriving show] + let source_fun_kind_to_infer = function | Ret t -> Ret (Source_type.to_infer_type t) | Agg (Self | Count | Avg | With_order _) @@ -826,6 +828,7 @@ type insert_action = | `Param of (string list option * param_id) | `Select of (string list option * select_full) ]; on_conflict_clause : conflict_clause located option; + returning : returning option; } [@@deriving show {with_path=false}] type table_constraints = [ `Ignore | `Primary of string list | `Unique of string option * string list ] [@@deriving show {with_path=false}] @@ -971,10 +974,10 @@ type stmt = | Rename of (table_name * table_name) list | CreateIndex of create_index_def | Insert of insert_action - | Delete of table_name * expr option + | Delete of table_name * expr option * returning option | DeleteMulti of table_name list * nested * expr option | Set of (string * expr) list * stmt option - | Update of table_name * assignments * expr option * order * Source_type.t param list (* where, order, limit *) + | Update of table_name * assignments * expr option * order * Source_type.t param list * returning option (* where, order, limit, returning *) | 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 *) diff --git a/lib/sql_lexer.mll b/lib/sql_lexer.mll index 654ffe37..135f5816 100644 --- a/lib/sql_lexer.mll +++ b/lib/sql_lexer.mll @@ -146,6 +146,7 @@ let keywords = "rename",RENAME; "replace",REPLACE; "restrict",RESTRICT; + "returning",RETURNING; "returns", RETURNS; "row", ROW; "rows", ROWS; diff --git a/lib/sql_parser.mly b/lib/sql_parser.mly index e3ab6bc9..a2951446 100644 --- a/lib/sql_parser.mly +++ b/lib/sql_parser.mly @@ -35,7 +35,7 @@ LIMIT ORDER BY DESC ASC EQUAL DELETE FROM DEFAULT OFFSET SET STRAIGHT_JOIN JOIN LIKE_OP LIKE EXCL TILDE NOT BETWEEN AND XOR ESCAPE USING UNION EXCEPT INTERSECT AS TO CONCAT_OP LEFT RIGHT FULL INNER OUTER NATURAL CROSS REPLACE IN GROUP HAVING - UNIQUE PRIMARY KEY FOREIGN AUTOINCREMENT ON CONFLICT DO NOTHING TEMPORARY IF EXISTS + UNIQUE PRIMARY KEY FOREIGN AUTOINCREMENT ON CONFLICT DO NOTHING TEMPORARY IF EXISTS RETURNING PRECISION SIGNED UNSIGNED ZEROFILL VARYING CHARSET NATIONAL ASCII UNICODE COLLATE BINARY CHARACTER DATETIME_FUNC DATE TIME TIMESTAMP ALTER RENAME ADD COLUMN CASCADE RESTRICT DROP GLOBAL LOCAL REFERENCES CHECK CONSTRAINT IGNORED AFTER INDEX FULLTEXT SPATIAL FIRST @@ -141,34 +141,34 @@ statement: CREATE ioption(temporary) TABLE ioption(if_not_exists) name=table_nam 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)? r=located(returning)? { - Insert { insert_action_kind; target; action=`Values (names, values); on_conflict_clause=ss; } + Insert { insert_action_kind; target; action=`Values (names, values); on_conflict_clause=ss; returning=r; } } - | 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)? r=located(returning)? { - Insert { insert_action_kind; target; action=`Param (names, p); on_conflict_clause=ss; } + Insert { insert_action_kind; target; action=`Param (names, p); on_conflict_clause=ss; returning=r; } } - | 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)? r=located(returning)? { - Insert { insert_action_kind; target; action=`Select (names, select); on_conflict_clause=ss; } + Insert { insert_action_kind; target; action=`Select (names, select); on_conflict_clause=ss; returning=r; } } - | insert_action_kind=insert_cmd target=table_name SET set=commas(set_column)? ss=located(conflict_clause)? + | insert_action_kind=insert_cmd target=table_name SET set=commas(set_column)? ss=located(conflict_clause)? r=located(returning)? { - Insert { insert_action_kind; target; action=`Set set; on_conflict_clause=ss; } + Insert { insert_action_kind; target; action=`Set set; on_conflict_clause=ss; returning=r; } } - | update_cmd table=table_name SET ss=commas(set_column) w=where? o=loption(order) lim=loption(limit) + | update_cmd table=table_name SET ss=commas(set_column) w=where? o=loption(order) lim=loption(limit) r=located(returning)? { - Update (table,ss,w,o,lim) + Update (table,ss,w,o,lim,r) } /* http://dev.mysql.com/doc/refman/5.1/en/update.html multi-table syntax */ | update_cmd tables=commas(table_list) SET ss=commas(set_column) w=where? o=loption(order) lim=loption(limit) { UpdateMulti (tables,ss,w,o,lim) } - | DELETE FROM table=table_name w=where? + | DELETE FROM table=table_name w=where? r=located(returning)? { - Delete (table,w) + Delete (table,w,r) } /* https://dev.mysql.com/doc/refman/5.7/en/delete.html multi-table syntax */ | DELETE targets=commas(table_name) FROM tables=table_list w=where? @@ -349,6 +349,7 @@ from: FROM t=table_list { t } where: WHERE e=expr { e } group: GROUP BY l=expr_list { l } having: HAVING e=expr { e } +returning: RETURNING r=commas(column1) { r } column1: | c=located(column1_kind) { c } diff --git a/lib/stmt.ml b/lib/stmt.ml index 7f0ec1f2..120a9c8a 100644 --- a/lib/stmt.ml +++ b/lib/stmt.ml @@ -13,7 +13,7 @@ type cardinality = [`Zero_one | `One | `Nat] [@@deriving show] let cardinality_to_string = show_cardinality type kind = | Select of cardinality - | Insert of inferred * Sql.table_name + | Insert of inferred * Sql.table_name * cardinality | Create of Sql.table_name | CreateIndex of string | Update of Sql.table_name option (** name for single-table UPDATEs *) @@ -26,6 +26,12 @@ type kind = | Select of cardinality | Other [@@deriving show {with_path=false}] +let cardinality_of_kind = function +| Select c -> c +| Insert (_, _, c) -> c +| Create _ | CreateIndex _ | Update _ | Delete _ | Alter _ | Drop _ +| CreateRoutine _ | CreateType _ | DropType _ | Other -> `Nat + type category = DDL | DQL | DML | DCL | TCL | OTHER [@@deriving show {with_path=false}, enum] let all_categories = List.init (max_category - min_category) (fun i -> Option.get @@ category_of_enum @@ min_category + i) diff --git a/lib/syntax.ml b/lib/syntax.ml index cccd435f..b133805b 100644 --- a/lib/syntax.ml +++ b/lib/syntax.ml @@ -1590,11 +1590,33 @@ let with_constraints attrs constraints : Schema.t = ) attrs +let with_returning env (returning : Sql.returning option) ((schema, params, kind) as stmt) = + match returning with + | None -> stmt + | Some returning -> + let projection = make_dynamic_select ~env returning.value in + schema @ List.map drop_sources (infer_schema ~not_null_keys:[] env projection), + params @ get_params_of_columns env projection, + kind + +let single_row_insert_cardinality (on_conflict_clause : Sql.conflict_clause Sql.located option) = + match on_conflict_clause with + | Some { value = On_conflict { action = Do_nothing; _ }; _ } -> `Zero_one + | Some { value = (On_conflict { action = Do_update _; _ } | On_duplicate _); _ } + | None -> `One + let rec eval (stmt:Sql.stmt) = let open Stmt in let open Schema.Source in let open Attr in match stmt with + | Insert { action = (`Values (_, None) | `Set None); returning = Some { pos; _ }; _ } -> + (* currently not handled as generated VALUES tuple require shifting indices *) + failed ~at:pos "RETURNING is not supported when inserted columns are inferred" + | Insert { action = (`Values (_, None) | `Set None); on_conflict_clause = Some { pos; value }; _ } -> + (* currently not handled as generated VALUES tuple require shifting indices *) + let what = match value with On_conflict _ -> "ON CONFLICT" | On_duplicate _ -> "ON DUPLICATE KEY UPDATE" in + failed ~at:pos "%s is not supported when inserted columns are inferred" what | Create (name, Schema { schema; constraints; indexes }) -> let attrs = List.map Alter_action_attr.to_attr schema in let attrs = with_constraints attrs constraints in @@ -1684,14 +1706,14 @@ let rec eval (stmt:Sql.stmt) = Sql.Schema.project cols (Tables.get_schema ci_table) |> ignore; 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; _ } -> + | Insert { target=table; action=`Values (names, values); on_conflict_clause; returning; _ } -> let expect = values_or_all table names in let t = Tables.get_schema table in let schema = List.map (fun attr -> { sources=[table]; attr }) t in let env = { empty_env with tables = [Tables.get table]; schema; } in begin match values with - | None -> - [], [], Insert(Some (Values, expect), table) + | None -> + [], [], Insert (Some (Values, expect), table, `One) | Some values -> let vl = List.map List.length values in let cl = List.length expect in @@ -1746,9 +1768,14 @@ let rec eval (stmt:Sql.stmt) = let p1 = List.concat_map (fun (_c, p, _t) -> p) resolved in let conflict_assigns = resolve_on_conflict_clause ~env table.tn on_conflict_clause in let params2 = params_of_assigns { env with is_update = true; } conflict_assigns in - [], p1 @ params2, Insert (None, table) + let cardinality = + match values with + | [_] -> single_row_insert_cardinality on_conflict_clause + | _ -> `Nat + in + with_returning env returning ([], p1 @ params2, Insert (None, table, cardinality)) end - | Insert { target=table; action=`Param (names, param_id); on_conflict_clause; _ } -> + | Insert { target=table; action=`Param (names, param_id); on_conflict_clause; returning; _ } -> let schema = List.map (fun attr -> { Schema.Source.Attr.sources=[table]; attr }) (Tables.get_schema table) in let env = { empty_env with tables = [Tables.get table]; schema; } in let conflict_assigns = resolve_on_conflict_clause ~env table.tn on_conflict_clause in @@ -1756,8 +1783,8 @@ let rec eval (stmt:Sql.stmt) = List.iter (fun a -> Hashtbl.add env.insert_resolved_types a.attr.name a.attr.domain ) schema; let params2 = params_of_assigns { env with is_update = true } conflict_assigns in let params = [ TupleList (param_id, Insertion expect) ] in - [], params @ params2, Insert (None, table) - | Insert { target=table; action=`Select (names, select); on_conflict_clause; _ } -> + with_returning env returning ([], params @ params2, Insert (None, table, `Nat)) + | Insert { target=table; action=`Select (names, select); on_conflict_clause; returning; _ } -> let expect = values_or_all table names in let env = { empty_env with tables = [Tables.get table]; schema = List.map (fun attr -> { sources=[table]; attr }) (Tables.get_schema table); @@ -1775,8 +1802,8 @@ let rec eval (stmt:Sql.stmt) = let conflict_assigns = resolve_on_conflict_clause ~env table.tn on_conflict_clause in List.iter2 (fun a1 a2 -> Hashtbl.add env.insert_resolved_types a2.name a1.attr.domain ) schema expect; let params2 = params_of_assigns { env with is_update = true } conflict_assigns in - [], params @ params2, Insert (None,table) - | Insert { target=table; action=`Set ss; on_conflict_clause; _ } -> + with_returning env returning ([], params @ params2, Insert (None, table, `Nat)) + | Insert { target=table; action=`Set ss; on_conflict_clause; returning; _ } -> let env = { empty_env with tables = [Tables.get table]; schema = List.map (fun attr -> { sources=[table]; attr }) (Tables.get_schema table); } in @@ -1786,14 +1813,15 @@ let rec eval (stmt:Sql.stmt) = in let conflict_assigns = resolve_on_conflict_clause ~env table.tn on_conflict_clause in let params2 = params_of_assigns { env with is_update = true } conflict_assigns in - [], params @ params2, Insert (inferred,table) - | Delete (table, where) -> + with_returning env returning + ([], params @ params2, Insert (inferred, table, single_row_insert_cardinality on_conflict_clause)) + | Delete (table, where, returning) -> let t = Tables.get table in - let p = get_params_opt { empty_env with tables=[t]; - schema=List.map (fun attr -> { Schema.Source.Attr.sources=[t |> fst]; attr }) (t |> snd); - set_tyvar_strict = true - } where in - [], p, Delete [table] + let env = { empty_env with tables=[t]; + schema=List.map (fun attr -> { Schema.Source.Attr.sources=[t |> fst]; attr }) (t |> snd); + } in + let p = get_params_opt { env with set_tyvar_strict = true } where in + with_returning env returning ([], p, Delete [table]) | DeleteMulti (targets, tables, where) -> (* use dummy columns to verify targets match the provided tables *) let select = ({ columns = [dummy_loc All]; from = Some tables; where; group = []; having = None }, []) in @@ -1811,7 +1839,7 @@ let rec eval (stmt:Sql.stmt) = | None -> [], p, Other | Some stmt -> let (schema,p2,kind) = eval stmt in (schema, p @ p2, kind) end - | Update (table,ss,w,o,lim) -> + | Update (table,ss,w,o,lim,returning) -> let f, s = Tables.get table in let env = { empty_env with is_update = true } in let r = List.map (fun attr -> {Schema.Source.Attr.attr; sources=[f] }) s in @@ -1820,7 +1848,8 @@ let rec eval (stmt:Sql.stmt) = let env = { env with schema = update_schema_with_aliases [] r; is_update = true } in let p3 = params_of_order o [] { env with tables = [(f, s)] } in let lim = List.map (fun p -> make_param ~id:p.id ~typ:(Source_type.to_infer_type p.typ)) lim in - [], params @ p3 @ (List.map (fun p -> Single (p, Meta.empty())) lim), Update (Some table) + with_returning { empty_env with tables = [(f, s)]; schema = r } returning + ([], params @ p3 @ (List.map (fun p -> Single (p, Meta.empty())) lim), Update (Some table)) | UpdateMulti (tables,ss,w,o,lim) -> let env = { empty_env with is_update = true } in let sources = List.map (fun src -> resolve_source { env with scope = Subquery } ((`Nested src), None)) tables in @@ -1991,7 +2020,7 @@ let common_prefix = function (* fill inferred sql for VALUES or SET *) let complete_sql kind sql = match kind with - | Stmt.Insert (Some (kind,schema), _) -> + | Stmt.Insert (Some (kind,schema), _, _) -> let (pre,each,post) = match kind with | Values -> "(", (fun _ -> ""), ")" | Assign -> "", (fun name -> name ^" = "), "" diff --git a/src/gen.ml b/src/gen.ml index 34deeb1b..387c81d2 100644 --- a/src/gen.ml +++ b/src/gen.ml @@ -10,6 +10,9 @@ type subst_mode = | Named | Unnamed | Oracle | PostgreSQL type stmt = { schema : Sql.schema_column list; vars : Sql.var list; kind : kind; props : Props.t; } +(** whether the statement produces a rowset : a SELECT, or a DML statement with a RETURNING clause *) +let returns_rows stmt = stmt.schema <> [] + (** defines substitution function for parameter literals *) let params_mode = ref None @@ -58,7 +61,7 @@ let choose_name props kind index = | CreateIndex t -> sprintf "create_index_%s" (fix' t) | Update (Some t) -> sprintf "update_%s_%u" (fix t) index | Update None -> sprintf "update_%u" index - | Insert (_,t) -> sprintf "insert_%s_%u" (fix t) index + | Insert (_,t,_) -> sprintf "insert_%s_%u" (fix t) index | Delete t -> sprintf "delete_%s_%u" (String.concat "_" @@ List.map fix t) index | Alter t -> sprintf "alter_%s_%u" (String.concat "_" @@ List.map fix t) index | Drop t -> sprintf "drop_%s" (fix t) diff --git a/src/gen_caml.ml b/src/gen_caml.ml index f4fda375..74db1415 100644 --- a/src/gen_caml.ml +++ b/src/gen_caml.ml @@ -256,20 +256,21 @@ let output_select1_cb _ schema = emit_row_binder "get_row" (fun () -> List.mapi get_column attrs |> String.concat ", " |> indent_endline) -let select_func_of_kind = function -| Stmt.Select `Zero_one -> "select_one_maybe" -| Stmt.Select `One -> "select_one" -| _ -> "select" - -let is_single_row_select stmt = - match stmt.Gen.kind, stmt.Gen.schema with - | Stmt.Select (`One | `Zero_one), _ :: _ -> true +let select_func_of_kind kind = + match Stmt.cardinality_of_kind kind with + | `Zero_one -> "select_one_maybe" + | `One -> "select_one" + | `Nat -> "select" + +let is_single_row_result stmt = + match stmt.Gen.schema, Stmt.cardinality_of_kind stmt.Gen.kind with + | _ :: _, (`One | `Zero_one) -> true | _ -> false let has_row_callback stmt = - match stmt.Gen.schema, stmt.Gen.kind with + match stmt.Gen.schema, Stmt.cardinality_of_kind stmt.Gen.kind with | [], _ -> false - | _, Stmt.Select (`Zero_one | `One) -> false + | _, (`Zero_one | `One) -> false | _ -> true let module_kind_name = function @@ -281,7 +282,7 @@ let module_kind_name = function let supports_module_kind module_kind stmt = match module_kind with | `List | `Fold -> has_row_callback stmt - | `Single -> is_single_row_select stmt + | `Single -> is_single_row_result stmt | `Direct -> true let emit_module_gen ~footer name body = @@ -924,26 +925,27 @@ let emit_sql_with_subst subst stmt = output "in"; "__sqlgg_sql" -let empty_exec_result = {|IO.return { T.affected_rows = 0L; insert_id = None }|} - let generate_stmt ~module_kind index stmt = if not (supports_module_kind module_kind stmt) then () else let c = consumer module_kind in + let returns_rows = Gen.returns_rows stmt in + let empty_result = + if returns_rows then {|IO.return ()|} else {|IO.return { T.affected_rows = 0L; insert_id = None }|} + in if Props.get stmt.props "noop" <> None then begin let _ = gen_func_signature ~dynamic_infos:[] ~module_kind ~index stmt in output "ignore db;"; - output "%s" empty_exec_result; + output "%s" empty_result; complete_func c end else let subst = gen_func_signature ~dynamic_infos:[] ~module_kind ~index stmt in let sql = emit_sql_with_subst subst stmt in let (func, callback) = - match stmt.schema with - | [] -> "execute", "" - | _ -> + if not returns_rows then "execute", "" + else select_func_of_kind stmt.kind, - match module_kind, stmt.kind with - | (`Direct | `Fold | `List), Stmt.Select (`Zero_one | `One) -> output_select1_cb index stmt.schema + match module_kind, Stmt.cardinality_of_kind stmt.kind with + | (`Direct | `Fold | `List), (`Zero_one | `One) -> output_select1_cb index stmt.schema | _ -> output_schema_binder_labeled index stmt.schema in let params_binder_name = output_params_binder index stmt.vars in @@ -968,7 +970,7 @@ let generate_stmt ~module_kind index stmt = | None -> exec | Some { value = None; _ } -> failwith "empty label in tuple substitution" | Some { value = Some value; _ } -> - sprintf {|( match %s with [] -> %s | _ :: _ -> %s)|} value empty_exec_result exec + sprintf {|( match %s with [] -> %s | _ :: _ -> %s)|} value empty_result exec in output "%s%s" bind exec; complete_func c diff --git a/src/gen_xml.ml b/src/gen_xml.ml index 6a7b4fad..d5db39d0 100644 --- a/src/gen_xml.ml +++ b/src/gen_xml.ml @@ -103,6 +103,8 @@ let get_sql_string stmt = in String.concat "" @@ List.mapi map @@ get_sql stmt +let show_cardinality = function `Nat -> "n" | `Zero_one -> "0,1" | `One -> "1" + let rec params_only l = List.concat @@ List.map @@ -121,17 +123,18 @@ let generate_code (x,_) index stmt = in let output = Node ("out",[],schema_to_values (schema_to_attrs stmt.schema)) in let sql = get_sql_string stmt in + let dml_cardinality = + if Gen.returns_rows stmt then show_cardinality (Stmt.cardinality_of_kind stmt.kind) else "0" + in let attrs = match stmt.kind with - | Select `Nat -> ["kind", "select"; "cardinality", "n"] - | Select `Zero_one -> ["kind", "select"; "cardinality", "0,1"] - | Select `One -> ["kind", "select"; "cardinality", "1"] - | Insert (_, t) -> ["kind", "insert"; "target", Sql.show_table_name t; "cardinality", "0"] + | Select c -> ["kind", "select"; "cardinality", show_cardinality c] + | Insert (_, t, _) -> ["kind", "insert"; "target", Sql.show_table_name t; "cardinality", dml_cardinality] | Create t -> ["kind", "create"; "target", Sql.show_table_name t; "cardinality", "0"] | CreateIndex t -> ["kind", "create_index"; "target",t;"cardinality","0"] - | Update None -> ["kind", "update"; "cardinality", "0"] - | Update (Some t) -> ["kind", "update"; "target", Sql.show_table_name t; "cardinality", "0"] - | Delete t -> ["kind", "delete"; "target", String.concat "," @@ List.map Sql.show_table_name t; "cardinality", "0"] + | Update None -> ["kind", "update"; "cardinality", dml_cardinality] + | Update (Some t) -> ["kind", "update"; "target", Sql.show_table_name t; "cardinality", dml_cardinality] + | Delete t -> ["kind", "delete"; "target", String.concat "," @@ List.map Sql.show_table_name t; "cardinality", dml_cardinality] | Alter t -> ["kind", "alter"; "target", String.concat "," @@ List.map Sql.show_table_name t; "cardinality", "0"] | Drop t -> ["kind", "drop"; "target", Sql.show_table_name t; "cardinality", "0"] | CreateRoutine s -> ["kind", "create_routine"; "target", Sql.show_table_name s] diff --git a/src/main.ml b/src/main.ml index 6a92d681..6c66c0a2 100644 --- a/src/main.ml +++ b/src/main.ml @@ -99,7 +99,7 @@ let check_statement stmt sql = if not (Sql.Schema.is_unique schema) then Printf.eprintf "Warning: this SQL statement will produce rowset with duplicate column names:\n%s\n" sql; match stmt.kind with - | Insert (Some _, _) when !Gen.params_mode = None -> + | Insert (Some _, _, _) when !Gen.params_mode = None -> Error.log "Cannot use `-params none` with autogenerated parameters" | _ -> () @@ -109,7 +109,7 @@ let parse_one' (sql, props) = let (sql, schema, vars, kind, dialect_features) = Syntax.parse sql in check_dialect sql dialect_features; begin match kind, !Gen.params_mode with - | Insert (Some _, _), None -> Error.log "Cannot use `-params none` with autogenerated parameters" + | Insert (Some _, _, _), None -> Error.log "Cannot use `-params none` with autogenerated parameters" | _ -> () end; let props = Props.set props "sql" sql in diff --git a/src/test.ml b/src/test.ml index fe704806..c1a52887 100644 --- a/src/test.ml +++ b/src/test.ml @@ -68,12 +68,20 @@ let do_test ?kind sql schema params = | Some k -> assert_equal ~msg:"kind" ~printer:[%derive.show: Stmt.kind] k stmt.kind | None -> () -let tt sql ?kind schema params = +let in_dialect ?dialect f = + match dialect with + | None -> f + | Some d -> fun () -> + let old = !Dialect.selected in + Dialect.set_selected d; + Fun.protect ~finally:(fun () -> Dialect.set_selected old) f + +let tt sql ?kind ?dialect schema params = let test () = do_test sql ?kind schema params in - sql >:: test + sql >:: in_dialect ?dialect test (** Test helper for queries with Choice parameters - only checks schema *) -let tt_schema_only sql ?kind schema = +let tt_schema_only sql ?kind ?dialect schema = let test () = let stmt = parse sql in assert_equal ~msg:"schema" ~printer:Sql.Schema.to_string schema (schema_to_attrs stmt.schema); @@ -81,10 +89,11 @@ let tt_schema_only sql ?kind schema = | Some k -> assert_equal ~msg:"kind" ~printer:[%derive.show: Stmt.kind] k stmt.kind | None -> () in - sql >:: test + sql >:: in_dialect ?dialect test -let wrong sql = - sql >:: (fun () -> ("Expected error in : " ^ sql) @? (try ignore (Main.parse_one' (sql,[])); false with _ -> true)) +let wrong ?dialect sql = + sql >:: in_dialect ?dialect + (fun () -> ("Expected error in : " ^ sql) @? (try ignore (Main.parse_one' (sql,[])); false with _ -> true)) let attr ?(extra=[]) ?(meta = []) n d = make_attribute ~meta n (Some d) (Constraints.of_list extra) let attr' ?(extra=[]) ?(nullability=Type.Strict) ?(meta = []) name kind = @@ -1815,6 +1824,83 @@ let test_on_conflict_do_update = [ |}; ] +let tt_pg sql ?kind schema params = tt sql ?kind ~dialect:Dialect.PostgreSQL schema params +let wrong_pg sql = wrong ~dialect:Dialect.PostgreSQL sql + +let returning_table = make_table_name "table_returning" + +let test_returning = [ + tt {| + CREATE TABLE table_returning ( + id INT PRIMARY KEY, + name TEXT NOT NULL, + nick TEXT + ) + |} [] []; + tt_pg "INSERT INTO table_returning (id, name, nick) VALUES (@id, @name, @nick) RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `One)) + [attr' ~extra:[PrimaryKey] "id" Int] + [named "id" Int; named "name" Text; named_nullable "nick" Text]; + tt_pg "INSERT INTO table_returning (id, name, nick) VALUES (@id, @name, @nick) RETURNING *" + [attr' ~extra:[PrimaryKey] "id" Int; attr' ~extra:[NotNull] "name" Text; attr' ~nullability:Nullable "nick" Text] + [named "id" Int; named "name" Text; named_nullable "nick" Text]; + tt_pg "INSERT INTO table_returning (id, name, nick) VALUES (@id, @name, @nick) RETURNING id + 1 AS next_id" + [attr' "next_id" Int] + [named "id" Int; named "name" Text; named_nullable "nick" Text]; + tt_pg "INSERT INTO table_returning (id, name, nick) VALUES (@id, @name, @nick) RETURNING id, CONCAT(name, @suffix) AS tagged" + [attr' ~extra:[PrimaryKey] "id" Int; attr' "tagged" Text] + [named "id" Int; named "name" Text; named_nullable "nick" Text; named "suffix" Text]; + tt_pg "INSERT INTO table_returning SET id = @id, name = @name RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `One)) + [attr' ~extra:[PrimaryKey] "id" Int] + [named "id" Int; named "name" Text]; + tt_pg "INSERT INTO table_returning (id, name) VALUES (@id1, @name1), (@id2, @name2) RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `Nat)) + [attr' ~extra:[PrimaryKey] "id" Int] + [named "id1" Int; named "name1" Text; named "id2" Int; named "name2" Text]; + (* ON CONFLICT DO NOTHING may swallow the row : zero or one *) + tt_pg "INSERT INTO table_returning (id, name) VALUES (@id, @name) ON CONFLICT(id) DO NOTHING RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `Zero_one)) + [attr' ~extra:[PrimaryKey] "id" Int] + [named "id" Int; named "name" Text]; + tt_pg "INSERT INTO table_returning (id, name) VALUES (@id, @name) ON CONFLICT(id) DO UPDATE SET name = excluded.name RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `One)) + [attr' ~extra:[PrimaryKey] "id" Int] + [named "id" Int; named "name" Text]; + tt_pg "INSERT INTO table_returning (id, name) SELECT id, name FROM table_returning WHERE id > @min RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `Nat)) + [attr' ~extra:[PrimaryKey] "id" Int] + [named "min" Int]; + tt_schema_only "INSERT INTO table_returning (id, name) VALUES @values RETURNING id" + ~kind:(Stmt.Insert (None, returning_table, `Nat)) ~dialect:Dialect.PostgreSQL + [attr' ~extra:[PrimaryKey] "id" Int]; + tt_pg "UPDATE table_returning SET name = @name WHERE id = @id RETURNING id, nick" + ~kind:(Stmt.Update (Some returning_table)) + [attr' ~extra:[PrimaryKey] "id" Int; attr' ~nullability:Nullable "nick" Text] + [named "name" Text; named "id" Int]; + tt_pg "UPDATE table_returning SET name = @name WHERE id = @id RETURNING *" + [attr' ~extra:[PrimaryKey] "id" Int; attr' ~extra:[NotNull] "name" Text; attr' ~nullability:Nullable "nick" Text] + [named "name" Text; named "id" Int]; + tt_pg "UPDATE table_returning SET name = @name WHERE id = @id RETURNING id, CONCAT(name, @suffix) AS tagged" + [attr' ~extra:[PrimaryKey] "id" Int; attr' "tagged" Text] + [named "name" Text; named "id" Int; named "suffix" Text]; + tt_pg "DELETE FROM table_returning WHERE id = @id RETURNING id, name" + ~kind:(Stmt.Delete [returning_table]) + [attr' ~extra:[PrimaryKey] "id" Int; attr' ~extra:[NotNull] "name" Text] + [named "id" Int]; + tt_pg "DELETE FROM table_returning WHERE id = @id RETURNING *" + [attr' ~extra:[PrimaryKey] "id" Int; attr' ~extra:[NotNull] "name" Text; attr' ~nullability:Nullable "nick" Text] + [named "id" Int]; + tt_pg "DELETE FROM table_returning WHERE id = @id RETURNING id, CONCAT(name, @suffix) AS tagged" + [attr' ~extra:[PrimaryKey] "id" Int; attr' "tagged" Text] + [named "id" Int; named "suffix" Text]; + wrong_pg "DELETE FROM table_returning WHERE id = @id RETURNING no_such_column"; + wrong_pg "INSERT INTO table_returning VALUES RETURNING id"; + wrong_pg "INSERT INTO table_returning VALUES ON CONFLICT(id) DO NOTHING"; + wrong "INSERT INTO table_returning VALUES ON DUPLICATE KEY UPDATE name = @name"; +] + + let test_enum_with_in_and_between = [ tt {| CREATE TABLE table_20250807 ( @@ -2395,6 +2481,7 @@ let run () = "test_meta_insert_update" >:: test_meta_insert_update; "test_multi_functions" >::: test_multi_functions; "test_on_conflict_do_update" >::: test_on_conflict_do_update; + "test_returning" >::: test_returning; "test_enum_with_in_and_between" >::: test_enum_with_in_and_between; "test_datefns" >::: test_datefns; "test_json_and_fixed_then_pairs_fn_kind" >::: test_json_and_fixed_then_pairs_fn_kind; diff --git a/test/cram/dune b/test/cram/dune index 59e83682..485ed463 100644 --- a/test/cram/dune +++ b/test/cram/dune @@ -3,6 +3,7 @@ %{bin:sqlgg} (glob_files *.sql) (glob_files *.compare.ml) + (glob_files *.compare.mli) (source_tree test_build_json_functions) (source_tree test_build_enum_literals) (source_tree test_build_dynamic_subquery) diff --git a/test/cram/returning.compare.ml b/test/cram/returning.compare.ml new file mode 100644 index 00000000..4c7d9556 --- /dev/null +++ b/test/cram/returning.compare.ml @@ -0,0 +1,532 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + + let create_users db = + T.execute db ("CREATE TABLE users (\n\ + id INTEGER PRIMARY KEY,\n\ + name TEXT NOT NULL,\n\ + nick TEXT\n\ +)") T.no_params + + let insert_returning_id db ~name ~nick = + let get_row stmt = + (T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING id") set_params get_row + + let insert_returning_all db ~name ~nick = + let get_row stmt = + (T.get_column_Int stmt 0), (T.get_column_Text stmt 1), (T.get_column_Text_nullable stmt 2) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING *") set_params get_row + + let insert_returning_nullable db ~name ~nick = + let get_row stmt = + (T.get_column_Text_nullable stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING nick") set_params get_row + + let insert_returning_expr db ~name ~nick = + let get_row stmt = + (T.get_column_Int stmt 0), (T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING id, CONCAT(name, '!') AS greeting") set_params get_row + + let insert_returning_param db ~name ~nick ~suffix = + let get_row stmt = + (T.get_column_Int stmt 0), (T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.set_param_Text p suffix; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING id, CONCAT(name, ?) AS tagged") set_params get_row + + let insert_set_returning db ~name ~nick = + let get_row stmt = + (T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users SET name = ?, nick = ? RETURNING id") set_params get_row + + let insert_tuple_list_returning db ~values callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + ( match values with [] -> IO.return () | _ :: _ -> T.select db ("INSERT INTO users (name, nick) VALUES " ^ (let _sqlgg_b = Buffer.create 13 in List.iteri (fun _sqlgg_idx (name, nick) -> Buffer.add_string _sqlgg_b (if _sqlgg_idx = 0 then "(" else ", ("); Buffer.add_string _sqlgg_b (T.Types.Text.to_literal name); Buffer.add_string _sqlgg_b ", "; Buffer.add_string _sqlgg_b (match nick with None -> "NULL" | Some v -> T.Types.Text.to_literal v); Buffer.add_char _sqlgg_b ')') values; Buffer.contents _sqlgg_b) ^ " RETURNING id, nick") T.no_params invoke_callback) + + let insert_multi_values_returning db ~name1 ~nick1 ~name2 ~nick2 callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (4) in + T.set_param_Text p name1; + begin match nick1 with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.set_param_Text p name2; + begin match nick2 with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select db ("INSERT INTO users (name, nick) VALUES (?, ?), (?, ?) RETURNING id") set_params invoke_callback + + let insert_select_returning db ~min callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (1) in + T.set_param_Int p min; + T.finish_params p + in + T.select db ("INSERT INTO users (name, nick) SELECT name, nick FROM users WHERE id > ? RETURNING id") set_params invoke_callback + + let insert_on_conflict_returning db ~id ~name = + let get_row stmt = + (T.get_column_Int stmt 0), (T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p name; + T.finish_params p + in + T.select_one db ("INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name RETURNING id, nick") set_params get_row + + let insert_do_nothing_returning db ~id ~name = + let get_row stmt = + (T.get_column_Int stmt 0), (T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p name; + T.finish_params p + in + T.select_one_maybe db ("INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO NOTHING RETURNING id, nick") set_params get_row + + let update_returning db ~name ~id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + T.set_param_Int p id; + T.finish_params p + in + T.select db ("UPDATE users SET name = ? WHERE id = ? RETURNING id, nick") set_params invoke_callback + + let update_returning_param db ~name ~id ~suffix callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + T.set_param_Int p id; + T.set_param_Text p suffix; + T.finish_params p + in + T.select db ("UPDATE users SET name = ? WHERE id = ? RETURNING id, CONCAT(name, ?) AS tagged") set_params invoke_callback + + let delete_returning db ~id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~name:(T.get_column_Text stmt 1) + ~nick:(T.get_column_Text_nullable stmt 2) + in + let set_params stmt = + let p = T.start_params stmt (1) in + T.set_param_Int p id; + T.finish_params p + in + T.select db ("DELETE FROM users WHERE id = ? RETURNING *") set_params invoke_callback + + let delete_returning_param db ~id ~suffix callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p suffix; + T.finish_params p + in + T.select db ("DELETE FROM users WHERE id = ? RETURNING id, CONCAT(name, ?) AS tagged") set_params invoke_callback + + module Single = struct + let insert_returning_id db ~name ~nick callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING id") set_params invoke_callback + + let insert_returning_all db ~name ~nick callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~name:(T.get_column_Text stmt 1) + ~nick:(T.get_column_Text_nullable stmt 2) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING *") set_params invoke_callback + + let insert_returning_nullable db ~name ~nick callback = + let invoke_callback stmt = + callback + ~nick:(T.get_column_Text_nullable stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING nick") set_params invoke_callback + + let insert_returning_expr db ~name ~nick callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~greeting:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING id, CONCAT(name, '!') AS greeting") set_params invoke_callback + + let insert_returning_param db ~name ~nick ~suffix callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.set_param_Text p suffix; + T.finish_params p + in + T.select_one db ("INSERT INTO users (name, nick) VALUES (?, ?) RETURNING id, CONCAT(name, ?) AS tagged") set_params invoke_callback + + let insert_set_returning db ~name ~nick callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + begin match nick with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + T.select_one db ("INSERT INTO users SET name = ?, nick = ? RETURNING id") set_params invoke_callback + + let insert_on_conflict_returning db ~id ~name callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p name; + T.finish_params p + in + T.select_one db ("INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET name = excluded.name RETURNING id, nick") set_params invoke_callback + + let insert_do_nothing_returning db ~id ~name callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p name; + T.finish_params p + in + T.select_one_maybe db ("INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO NOTHING RETURNING id, nick") set_params invoke_callback + + end (* module Single *) + + module Fold = struct + let insert_tuple_list_returning db ~values callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let r_acc = ref acc in + IO.(>>=) (( match values with [] -> IO.return () | _ :: _ -> T.select db ("INSERT INTO users (name, nick) VALUES " ^ (let _sqlgg_b = Buffer.create 13 in List.iteri (fun _sqlgg_idx (name, nick) -> Buffer.add_string _sqlgg_b (if _sqlgg_idx = 0 then "(" else ", ("); Buffer.add_string _sqlgg_b (T.Types.Text.to_literal name); Buffer.add_string _sqlgg_b ", "; Buffer.add_string _sqlgg_b (match nick with None -> "NULL" | Some v -> T.Types.Text.to_literal v); Buffer.add_char _sqlgg_b ')') values; Buffer.contents _sqlgg_b) ^ " RETURNING id, nick") T.no_params (fun x -> r_acc := invoke_callback x !r_acc))) + (fun () -> IO.return !r_acc) + + let insert_multi_values_returning db ~name1 ~nick1 ~name2 ~nick2 callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (4) in + T.set_param_Text p name1; + begin match nick1 with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.set_param_Text p name2; + begin match nick2 with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("INSERT INTO users (name, nick) VALUES (?, ?), (?, ?) RETURNING id") set_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + let insert_select_returning db ~min callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (1) in + T.set_param_Int p min; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("INSERT INTO users (name, nick) SELECT name, nick FROM users WHERE id > ? RETURNING id") set_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + let update_returning db ~name ~id callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + T.set_param_Int p id; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("UPDATE users SET name = ? WHERE id = ? RETURNING id, nick") set_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + let update_returning_param db ~name ~id ~suffix callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + T.set_param_Int p id; + T.set_param_Text p suffix; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("UPDATE users SET name = ? WHERE id = ? RETURNING id, CONCAT(name, ?) AS tagged") set_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + let delete_returning db ~id callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~name:(T.get_column_Text stmt 1) + ~nick:(T.get_column_Text_nullable stmt 2) + in + let set_params stmt = + let p = T.start_params stmt (1) in + T.set_param_Int p id; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("DELETE FROM users WHERE id = ? RETURNING *") set_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + let delete_returning_param db ~id ~suffix callback acc = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p suffix; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db ("DELETE FROM users WHERE id = ? RETURNING id, CONCAT(name, ?) AS tagged") set_params (fun x -> r_acc := invoke_callback x !r_acc)) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let insert_tuple_list_returning db ~values callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let r_acc = ref [] in + IO.(>>=) (( match values with [] -> IO.return () | _ :: _ -> T.select db ("INSERT INTO users (name, nick) VALUES " ^ (let _sqlgg_b = Buffer.create 13 in List.iteri (fun _sqlgg_idx (name, nick) -> Buffer.add_string _sqlgg_b (if _sqlgg_idx = 0 then "(" else ", ("); Buffer.add_string _sqlgg_b (T.Types.Text.to_literal name); Buffer.add_string _sqlgg_b ", "; Buffer.add_string _sqlgg_b (match nick with None -> "NULL" | Some v -> T.Types.Text.to_literal v); Buffer.add_char _sqlgg_b ')') values; Buffer.contents _sqlgg_b) ^ " RETURNING id, nick") T.no_params (fun x -> r_acc := invoke_callback x :: !r_acc))) + (fun () -> IO.return (List.rev !r_acc)) + + let insert_multi_values_returning db ~name1 ~nick1 ~name2 ~nick2 callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (4) in + T.set_param_Text p name1; + begin match nick1 with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.set_param_Text p name2; + begin match nick2 with None -> T.set_param_null p | Some v -> T.set_param_Text p v end; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("INSERT INTO users (name, nick) VALUES (?, ?), (?, ?) RETURNING id") set_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + let insert_select_returning db ~min callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + in + let set_params stmt = + let p = T.start_params stmt (1) in + T.set_param_Int p min; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("INSERT INTO users (name, nick) SELECT name, nick FROM users WHERE id > ? RETURNING id") set_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + let update_returning db ~name ~id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~nick:(T.get_column_Text_nullable stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + T.set_param_Int p id; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("UPDATE users SET name = ? WHERE id = ? RETURNING id, nick") set_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + let update_returning_param db ~name ~id ~suffix callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + T.set_param_Int p id; + T.set_param_Text p suffix; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("UPDATE users SET name = ? WHERE id = ? RETURNING id, CONCAT(name, ?) AS tagged") set_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + let delete_returning db ~id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~name:(T.get_column_Text stmt 1) + ~nick:(T.get_column_Text_nullable stmt 2) + in + let set_params stmt = + let p = T.start_params stmt (1) in + T.set_param_Int p id; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("DELETE FROM users WHERE id = ? RETURNING *") set_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + let delete_returning_param db ~id ~suffix callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~tagged:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Int p id; + T.set_param_Text p suffix; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db ("DELETE FROM users WHERE id = ? RETURNING id, CONCAT(name, ?) AS tagged") set_params (fun x -> r_acc := invoke_callback x :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/returning.compare.mli b/test/cram/returning.compare.mli new file mode 100644 index 00000000..1654c560 --- /dev/null +++ b/test/cram/returning.compare.mli @@ -0,0 +1,214 @@ +module Sqlgg : + (T : Sqlgg_traits.M) -> + sig + module IO = Sqlgg_io.Blocking + val create_users : [> `WR ] T.connection -> T.execute_response + val insert_returning_id : + [> `RO ] T.connection -> + name:T.Types.Text.t -> nick:T.Types.Text.t option -> T.Types.Int.t + val insert_returning_all : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> + T.Types.Int.t * T.Types.Text.t * T.Types.Text.t option + val insert_returning_nullable : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> T.Types.Text.t option + val insert_returning_expr : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> T.Types.Int.t * T.Types.Text.t + val insert_returning_param : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> + suffix:T.Types.Text.t -> T.Types.Int.t * T.Types.Text.t + val insert_set_returning : + [> `RO ] T.connection -> + name:T.Types.Text.t -> nick:T.Types.Text.t option -> T.Types.Int.t + val insert_tuple_list_returning : + [> `RO ] T.connection -> + values:(T.Types.Text.t * T.Types.Text.t option) list -> + (id:T.Types.Int.t -> nick:T.Types.Text.t option -> unit) -> + unit IO.future + val insert_multi_values_returning : + [> `RO ] T.connection -> + name1:T.Types.Text.t -> + nick1:T.Types.Text.t option -> + name2:T.Types.Text.t -> + nick2:T.Types.Text.t option -> (id:T.Types.Int.t -> unit) -> unit + val insert_select_returning : + [> `RO ] T.connection -> + min:T.Types.Int.t -> (id:T.Types.Int.t -> unit) -> unit + val insert_on_conflict_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + name:T.Types.Text.t -> T.Types.Int.t * T.Types.Text.t option + val insert_do_nothing_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + name:T.Types.Text.t -> (T.Types.Int.t * T.Types.Text.t option) option + val update_returning : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + id:T.Types.Int.t -> + (id:T.Types.Int.t -> nick:T.Types.Text.t option -> unit) -> unit + val update_returning_param : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + id:T.Types.Int.t -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> tagged:T.Types.Text.t -> unit) -> unit + val delete_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + (id:T.Types.Int.t -> + name:T.Types.Text.t -> nick:T.Types.Text.t option -> unit) -> + unit + val delete_returning_param : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> tagged:T.Types.Text.t -> unit) -> unit + module Single : + sig + val insert_returning_id : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> (id:T.Types.Int.t -> 'a) -> 'a + val insert_returning_all : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> + (id:T.Types.Int.t -> + name:T.Types.Text.t -> nick:T.Types.Text.t option -> 'a) -> + 'a + val insert_returning_nullable : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> + (nick:T.Types.Text.t option -> 'a) -> 'a + val insert_returning_expr : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> + (id:T.Types.Int.t -> greeting:T.Types.Text.t -> 'a) -> 'a + val insert_returning_param : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> tagged:T.Types.Text.t -> 'a) -> 'a + val insert_set_returning : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> (id:T.Types.Int.t -> 'a) -> 'a + val insert_on_conflict_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + name:T.Types.Text.t -> + (id:T.Types.Int.t -> nick:T.Types.Text.t option -> 'a) -> 'a + val insert_do_nothing_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + name:T.Types.Text.t -> + (id:T.Types.Int.t -> nick:T.Types.Text.t option -> 'a) -> + 'a option + end + module Fold : + sig + val insert_tuple_list_returning : + [> `RO ] T.connection -> + values:(T.Types.Text.t * T.Types.Text.t option) list -> + (id:T.Types.Int.t -> + nick:T.Types.Text.t option -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + val insert_multi_values_returning : + [> `RO ] T.connection -> + name1:T.Types.Text.t -> + nick1:T.Types.Text.t option -> + name2:T.Types.Text.t -> + nick2:T.Types.Text.t option -> + (id:T.Types.Int.t -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + val insert_select_returning : + [> `RO ] T.connection -> + min:T.Types.Int.t -> + (id:T.Types.Int.t -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + val update_returning : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + id:T.Types.Int.t -> + (id:T.Types.Int.t -> + nick:T.Types.Text.t option -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + val update_returning_param : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + id:T.Types.Int.t -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> + tagged:T.Types.Text.t -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + val delete_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + (id:T.Types.Int.t -> + name:T.Types.Text.t -> + nick:T.Types.Text.t option -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + val delete_returning_param : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> + tagged:T.Types.Text.t -> 'a IO.future -> 'a IO.future) -> + 'a IO.future -> 'a IO.future + end + module List : + sig + val insert_tuple_list_returning : + [> `RO ] T.connection -> + values:(T.Types.Text.t * T.Types.Text.t option) list -> + (id:T.Types.Int.t -> nick:T.Types.Text.t option -> 'a) -> + 'a list IO.future IO.future + val insert_multi_values_returning : + [> `RO ] T.connection -> + name1:T.Types.Text.t -> + nick1:T.Types.Text.t option -> + name2:T.Types.Text.t -> + nick2:T.Types.Text.t option -> + (id:T.Types.Int.t -> 'a) -> 'a list IO.future IO.future + val insert_select_returning : + [> `RO ] T.connection -> + min:T.Types.Int.t -> + (id:T.Types.Int.t -> 'a) -> 'a list IO.future IO.future + val update_returning : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + id:T.Types.Int.t -> + (id:T.Types.Int.t -> nick:T.Types.Text.t option -> 'a) -> + 'a list IO.future IO.future + val update_returning_param : + [> `RO ] T.connection -> + name:T.Types.Text.t -> + id:T.Types.Int.t -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> tagged:T.Types.Text.t -> 'a) -> + 'a list IO.future IO.future + val delete_returning : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + (id:T.Types.Int.t -> + name:T.Types.Text.t -> nick:T.Types.Text.t option -> 'a) -> + 'a list IO.future IO.future + val delete_returning_param : + [> `RO ] T.connection -> + id:T.Types.Int.t -> + suffix:T.Types.Text.t -> + (id:T.Types.Int.t -> tagged:T.Types.Text.t -> 'a) -> + 'a list IO.future IO.future + end + end diff --git a/test/cram/returning.sql b/test/cram/returning.sql new file mode 100644 index 00000000..a455bfa6 --- /dev/null +++ b/test/cram/returning.sql @@ -0,0 +1,50 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + nick TEXT +); + +-- [sqlgg] name=insert_returning_id +INSERT INTO users (name, nick) VALUES (@name, @nick) RETURNING id; + +-- [sqlgg] name=insert_returning_all +INSERT INTO users (name, nick) VALUES (@name, @nick) RETURNING *; + +-- [sqlgg] name=insert_returning_nullable +INSERT INTO users (name, nick) VALUES (@name, @nick) RETURNING nick; + +-- [sqlgg] name=insert_returning_expr +INSERT INTO users (name, nick) VALUES (@name, @nick) RETURNING id, CONCAT(name, '!') AS greeting; + +-- [sqlgg] name=insert_returning_param +INSERT INTO users (name, nick) VALUES (@name, @nick) RETURNING id, CONCAT(name, @suffix) AS tagged; + +-- [sqlgg] name=insert_set_returning +INSERT INTO users SET name = @name, nick = @nick RETURNING id; + +-- [sqlgg] name=insert_tuple_list_returning +INSERT INTO users (name, nick) VALUES @values RETURNING id, nick; + +-- [sqlgg] name=insert_multi_values_returning +INSERT INTO users (name, nick) VALUES (@name1, @nick1), (@name2, @nick2) RETURNING id; + +-- [sqlgg] name=insert_select_returning +INSERT INTO users (name, nick) SELECT name, nick FROM users WHERE id > @min RETURNING id; + +-- [sqlgg] name=insert_on_conflict_returning +INSERT INTO users (id, name) VALUES (@id, @name) ON CONFLICT(id) DO UPDATE SET name = excluded.name RETURNING id, nick; + +-- [sqlgg] name=insert_do_nothing_returning +INSERT INTO users (id, name) VALUES (@id, @name) ON CONFLICT(id) DO NOTHING RETURNING id, nick; + +-- [sqlgg] name=update_returning +UPDATE users SET name = @name WHERE id = @id RETURNING id, nick; + +-- [sqlgg] name=update_returning_param +UPDATE users SET name = @name WHERE id = @id RETURNING id, CONCAT(name, @suffix) AS tagged; + +-- [sqlgg] name=delete_returning +DELETE FROM users WHERE id = @id RETURNING *; + +-- [sqlgg] name=delete_returning_param +DELETE FROM users WHERE id = @id RETURNING id, CONCAT(name, @suffix) AS tagged; diff --git a/test/cram/returning.t b/test/cram/returning.t new file mode 100644 index 00000000..56de9c8a --- /dev/null +++ b/test/cram/returning.t @@ -0,0 +1,119 @@ +RETURNING clause (PostgreSQL/SQLite): INSERT/UPDATE/DELETE gain a result row, so the +generated code uses T.select* with a row binder instead of T.execute. When the number +of returned rows is statically known (single tuple INSERT / INSERT ... SET) the +single-row variants are used instead of the many-rows callback. + + $ cat returning.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect postgresql - > output.ml + $ diff output.ml returning.compare.ml + +The generated module must typecheck; its inferred interface pins the cardinality of the +RETURNING rowset per statement shape (single tuple VALUES / SET return the row directly, +ON CONFLICT DO NOTHING returns an option, everything else takes a row callback) as well +as column nullability (option types): + + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -i output.ml > output.mli + $ diff output.mli returning.compare.mli + +SQLite supports RETURNING as well: + + $ sqlgg -gen caml -dialect=sqlite - <<'EOF' >/dev/null + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users (id, name) VALUES (1, 'John') RETURNING id; + > EOF + $ echo $? + 0 + +MySQL does not support RETURNING (should fail): + + $ sqlgg -gen caml -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users (id, name) VALUES (1, 'John') RETURNING id; + > EOF + Feature Returning is not supported for dialect MySQL (supported by: PostgreSQL, SQLite) at RETURNING id + Errors encountered, no code generated + [1] + + $ sqlgg -gen caml -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > UPDATE users SET name = 'John' WHERE id = 1 RETURNING id; + > EOF + Feature Returning is not supported for dialect MySQL (supported by: PostgreSQL, SQLite) at RETURNING id + Errors encountered, no code generated + [1] + + $ sqlgg -gen caml -dialect=tidb - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > DELETE FROM users WHERE id = 1 RETURNING id; + > EOF + Feature Returning is not supported for dialect TiDB (supported by: PostgreSQL, SQLite) at RETURNING id + Errors encountered, no code generated + [1] + +The check can be disabled like any other dialect feature check: + + $ sqlgg -gen caml -dialect=mysql -no-check=returning - <<'EOF' 2>&1 >/dev/null | grep -i "warning" + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users (id, name) VALUES (1, 'John') RETURNING id; + > EOF + Warning: Feature Returning is not supported for dialect MySQL, proceeding anyway at RETURNING id + +RETURNING is not supported when the inserted columns are inferred : the generated +VALUES tuple / SET assignments are appended at the end of the statement, which would +put them after the RETURNING clause: + + $ sqlgg -gen caml -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users VALUES RETURNING id; + > EOF + Failed : INSERT INTO users VALUES RETURNING id + At : RETURNING id + Fatal error: exception Failure("RETURNING is not supported when inserted columns are inferred") + [2] + + $ sqlgg -gen caml -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users SET RETURNING id, CONCAT(name, @suffix) AS tagged; + > EOF + Failed : INSERT INTO users SET RETURNING id, CONCAT(name, @suffix) AS tagged + At : RETURNING id, CONCAT(name, @suffix) AS tagged + Fatal error: exception Failure("RETURNING is not supported when inserted columns are inferred") + [2] + +For the same reason a conflict clause cannot follow inferred columns either: + + $ sqlgg -gen caml -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users VALUES ON CONFLICT(id) DO NOTHING; + > EOF + Failed : INSERT INTO users VALUES ON CONFLICT(id) DO NOTHING + At : ON CONFLICT(id) DO NOTHING + Fatal error: exception Failure("ON CONFLICT is not supported when inserted columns are inferred") + [2] + + $ sqlgg -gen caml -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users SET ON CONFLICT(id) DO UPDATE SET name = excluded.name; + > EOF + Failed : INSERT INTO users SET ON CONFLICT(id) DO UPDATE SET name = excluded.name + At : ON CONFLICT(id) DO UPDATE SET name = excluded.name + Fatal error: exception Failure("ON CONFLICT is not supported when inserted columns are inferred") + [2] + + $ sqlgg -gen caml -dialect=mysql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > INSERT INTO users VALUES ON DUPLICATE KEY UPDATE name = 'x'; + > EOF + Failed : INSERT INTO users VALUES ON DUPLICATE KEY UPDATE name = 'x' + At : ON DUPLICATE KEY UPDATE name = 'x' + Fatal error: exception Failure("ON DUPLICATE KEY UPDATE is not supported when inserted columns are inferred") + [2] + +Unknown columns in RETURNING are rejected: + + $ sqlgg -gen caml -dialect=postgresql - <<'EOF' 2>&1 + > CREATE TABLE users (id INT PRIMARY KEY, name TEXT); + > DELETE FROM users WHERE id = 1 RETURNING nope; + > EOF + Failed : DELETE FROM users WHERE id = 1 RETURNING nope + Fatal error: exception Sqlgg.Sql.Schema.Error(_, "missing attribute : nope") + [2]