From 1979ce8898f4473e4a4eff7fc010f469129dc68d Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sun, 16 Aug 2026 12:23:51 +0200 Subject: [PATCH] add support for RETURNING clause (PostgreSQL, SQLite) INSERT/UPDATE/DELETE accept a trailing RETURNING clause. When the number of returned rows is statically known the single-row variants are used: single-tuple INSERT ... VALUES and INSERT ... SET map to select_one, ON CONFLICT DO NOTHING makes it select_one_maybe. The feature is dialect-checked (PostgreSQL, SQLite; -no-check=returning downgrades to a warning). RETURNING and conflict clauses on INSERTs with inferred columns (INSERT INTO t VALUES / SET with nothing following) are rejected, since the generated tuple is appended textually at the end of the statement (previously an inferred INSERT with ON CONFLICT silently produced invalid SQL). This is a limitation of the SQL codegen which can be lifted in the future. Co-Authored-By: Claude Fable 5 --- doc/_cli-options.md | 2 +- lib/dialect.ml | 22 +- lib/sql.ml | 7 +- lib/sql_lexer.mll | 1 + lib/sql_parser.mly | 27 +- lib/stmt.ml | 8 +- lib/syntax.ml | 67 ++-- src/gen.ml | 5 +- src/gen_caml.ml | 42 +-- src/gen_xml.ml | 17 +- src/main.ml | 4 +- src/test.ml | 99 +++++- test/cram/dune | 1 + test/cram/returning.compare.ml | 532 ++++++++++++++++++++++++++++++++ test/cram/returning.compare.mli | 214 +++++++++++++ test/cram/returning.sql | 50 +++ test/cram/returning.t | 119 +++++++ 17 files changed, 1139 insertions(+), 78 deletions(-) create mode 100644 test/cram/returning.compare.ml create mode 100644 test/cram/returning.compare.mli create mode 100644 test/cram/returning.sql create mode 100644 test/cram/returning.t 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]