diff --git a/lib/dialect.ml b/lib/dialect.ml index 13ad3f11..b4b70a45 100644 --- a/lib/dialect.ml +++ b/lib/dialect.ml @@ -164,25 +164,18 @@ let get_default_expr ~kind ~expr pos = let rec analyze = function | Value _ -> (true, false, true) | Column _ -> (true, true, false) - | Case { case; branches; else_ } -> - let parts = option_list case @ option_list else_ in - let parts = parts @ List.concat_map (fun { Sql.when_; then_ } -> [when_; then_]) branches in - List.fold_left - (fun (v_acc, c_acc, o_acc) e -> - let v, c, o = analyze e in - (v_acc && v, c_acc || c, o_acc && o)) - (true, false, true) - parts - | Fun { parameters; _ } -> - List.fold_left - (fun (v_acc, c_acc, o_acc) e -> - let v, c, o = analyze e in - (v_acc && v, c_acc || c, o_acc && o)) - (true, false, true) - parameters - | ( Param _ | Inparam _ | Choices _ | InChoice _ - | SelectExpr _ | InTupleList _ | OptionActions _ | Of_values _ ) -> + | Case _ as e -> fold_parts (sub_exprs e) + | Fun { parameters; _ } -> fold_parts parameters + | Param _ | Inparam _ | Choices _ | InChoice _ + | SelectExpr _ | InTupleList _ | OptionActions _ | Of_values _ -> (false, false, false) + and fold_parts parts = + List.fold_left + (fun (v_acc, c_acc, o_acc) e -> + let v, c, o = analyze e in + (v_acc && v, c_acc || c, o_acc && o)) + (true, false, true) + parts in let valid, has_column, only_value = analyze expr in if not valid then only DefaultExpr [] pos @@ -205,11 +198,7 @@ let get_default_expr ~kind ~expr pos = | _ -> all in let dialects = - base_dialects - |> List.to_seq - |> Seq.filter (fun d -> not (has_column && d = PostgreSQL)) - |> Seq.filter (fun d -> only_value || d <> SQLite) - |> List.of_seq + List.filter (fun d -> not (has_column && d = PostgreSQL) && (only_value || d <> SQLite)) base_dialects in only DefaultExpr dialects pos @@ -239,26 +228,12 @@ let rec analyze_expr acc exprs k = match exprs with | [] -> k acc | expr :: rest -> match expr with - | Value _ | Param _ | Inparam _ | Column _ | Of_values _ -> - analyze_expr acc rest k - | Choices (_, choices) -> - let new_exprs = List.filter_map (fun (_, expr_opt) -> expr_opt) choices in - analyze_expr acc (new_exprs @ rest) k - | InChoice (_, _, e) -> - analyze_expr acc (e :: rest) k | Fun { parameters; _ } -> analyze_expr acc (parameters @ rest) k | SelectExpr (select_full, _) -> analyze_select_full acc [select_full] (fun acc -> analyze_expr acc rest k) - | InTupleList { value = { exprs; _ }; _ } -> - analyze_expr acc (exprs @ rest) k - | OptionActions { choice; _ } -> - analyze_expr acc (choice :: rest) k - | Case { case; branches; else_ } -> - let case_exprs = option_list case in - let branches_exprs = List.concat_map (fun { when_; then_ } -> [when_; then_]) branches in - let else_exprs = option_list else_ in - analyze_expr acc (case_exprs @ branches_exprs @ else_exprs @ rest) k + | e -> + analyze_expr acc (sub_exprs e @ rest) k and analyze_column acc cols k = match cols with | [] -> k acc diff --git a/lib/sql.ml b/lib/sql.ml index 66e38798..a7794814 100644 --- a/lib/sql.ml +++ b/lib/sql.ml @@ -4,7 +4,7 @@ open Printf open ExtLib open Prelude -type pos = (int * int) [@@deriving show] +type pos = int * int [@@deriving show] type 'a located = { value : 'a; pos : pos } [@@deriving show, make] type 'a collated = { collated: 'a; collation: string located option } [@@deriving show, make] @@ -328,6 +328,17 @@ end type attr = {name : string; domain : Type.t; extra : Constraints.t; meta: Meta.t; } [@@deriving show {with_path=false}] +let unique_keys schema = + let keys_of a = + Constraints.fold (fun c acc -> + match c with + | Constraint.PrimaryKey | Unique -> Constraint.StringSet.singleton a.name :: acc + | Composite (CompositePrimary s | CompositeUnique s) -> s :: acc + | NotNull | Null | Autoincrement | OnConflict _ | WithDefault -> acc) + a.extra [] + in + List.concat_map keys_of schema |> List.sort_uniq Constraint.StringSet.compare + let make_attribute name kind extra ~meta = if Constraints.mem Null extra && Constraints.mem NotNull extra then fail "Column %s can be either NULL or NOT NULL, but not both" name; let domain = Type.{ t = Option.default Int kind; nullability = if List.exists (fun cstrt -> Constraints.mem cstrt extra) [NotNull; PrimaryKey] @@ -350,6 +361,8 @@ struct type 'a t = { attr: attr; sources: 'a list } [@@deriving show] let by_name name sattr = sattr.attr.name = name + + let map_attr f sattr = { sattr with attr = f sattr.attr } end type 'a t = 'a Attr.t list @@ -463,7 +476,7 @@ struct List.combine t1 t2 |> List.mapi begin fun i (a1,a2) -> match Type.supertype a1.attr.domain a2.attr.domain with - | Some t -> { a1 with attr = { a1.attr with domain=t } } + | Some t -> Attr.map_attr (fun attr -> { attr with domain = t }) a1 | None -> raise (Error (List.map (fun i -> i.attr) t1, sprintf "Attributes do not match : %s of type %s and %s of type %s" (show_name i a1.attr) (Type.show a1.attr.domain) (show_name i a2.attr) (Type.show a2.attr.domain))) @@ -506,6 +519,9 @@ let make_table_name ?db tn = { db; tn } type schema = Schema.t [@@deriving show] type table = table_name * schema [@@deriving show] +type join_source = { table : table_name; alias : table_name option } [@@deriving show] +let join_source_name { table; alias } = Option.default table alias + let print_table out (name,schema) = IO.write_line out (show_table_name name); schema |> List.iter begin fun {name;domain;extra;_} -> @@ -569,6 +585,7 @@ and var = | ChoiceIn of { param: param_id; kind : in_or_not_in; vars: var list } | Choice of param_id * ctor list | DynamicSelect of param_id * ctor list +| DynamicSelectJoin of { pid : param_id; pos : pos; source : join_source } | TupleList of param_id * tuple_list_kind (* It differs from Choice that in this case we should generate sql "TRUE", it doesn't seem reusable *) | OptionActionChoice of param_id * var list * (pos * pos) * option_actions_kind @@ -580,6 +597,38 @@ and tuple_list_kind = [@@deriving show] and vars = var list [@@deriving show] +let ctor_vars = function + | Simple (_, vars) -> Option.default [] vars + | Verbatim _ -> [] + +let sub_vars = function + | Single _ | SingleIn _ | TupleList _ | DynamicSelectJoin _ -> [] + | ChoiceIn { vars; _ } -> vars + | OptionActionChoice (_, vars, _, _) -> vars + | SharedVarsGroup (vars, _) -> vars + | Choice (_, ctors) | DynamicSelect (_, ctors) -> List.concat_map ctor_vars ctors + +let map_sub_vars f = + let map_ctor = function + | Simple (n, vars) -> Simple (n, Option.map f vars) + | Verbatim _ as c -> c + in + function + | Single _ | SingleIn _ | TupleList _ | DynamicSelectJoin _ as v -> v + | ChoiceIn t -> ChoiceIn { t with vars = f t.vars } + | OptionActionChoice (p, vars, pos, kind) -> OptionActionChoice (p, f vars, pos, kind) + | SharedVarsGroup (vars, id) -> SharedVarsGroup (f vars, id) + | Choice (p, ctors) -> Choice (p, List.map map_ctor ctors) + | DynamicSelect (p, ctors) -> DynamicSelect (p, List.map map_ctor ctors) + +let var_pos = function + | Single (p, _) | SingleIn (p, _) -> fst p.id.pos + | Choice (id, _) | DynamicSelect (id, _) | TupleList (id, _) + | OptionActionChoice (id, _, _, _) -> fst id.pos + | ChoiceIn { param; _ } -> fst param.pos + | SharedVarsGroup (_, id) -> fst id.pos + | DynamicSelectJoin { pos = (j1, _); _ } -> j1 + type alter_pos = [ `After of string | `Default | `First ] [@@deriving show {with_path=false}] type direction = [ `Fixed | `Param of param_id ] [@@deriving show] @@ -701,6 +750,40 @@ let source_fun_kind_to_infer = function let expr_to_string = show_expr +let sub_exprs = function + | Value _ | Param _ | Inparam _ | Column _ | Of_values _ | SelectExpr _ -> [] + | Choices (_, l) -> List.filter_map snd l + | InChoice (_, _, e) -> [e] + | OptionActions { choice; _ } -> [choice] + | Fun { kind = Agg (With_order { order; _ }); parameters; _ } -> parameters @ List.map fst order + | Fun { parameters; _ } -> parameters + | InTupleList { value = { exprs; _ }; _ } -> exprs + | Case { case; branches; else_ } -> + option_list case + @ List.concat_map (fun (b : case_branch) -> [b.when_; b.then_]) branches + @ option_list else_ + +let map_sub_exprs f = function + | Value _ | Param _ | Inparam _ | Column _ | Of_values _ | SelectExpr _ as e -> e + | Choices (n, l) -> Choices (n, List.map (fun (n, e) -> n, Option.map f e) l) + | InChoice (n, k, e) -> InChoice (n, k, f e) + | OptionActions ({ choice; _ } as o) -> OptionActions { o with choice = f choice } + | Fun ({ kind = Agg (With_order ({ order; _ } as wo)); parameters; _ } as fn) -> + Fun { fn with + kind = Agg (With_order { wo with order = List.map (fun (e, dir) -> f e, dir) order }); + parameters = List.map f parameters } + | Fun ({ parameters; _ } as fn) -> Fun { fn with parameters = List.map f parameters } + | InTupleList ({ value = { exprs; _ } as tl; _ } as loc) -> + InTupleList { loc with value = { tl with exprs = List.map f exprs } } + | Case { case; branches; else_ } -> + Case { + case = Option.map f case; + branches = List.map (fun (b : case_branch) -> { when_ = f b.when_; then_ = f b.then_ }) branches; + else_ = Option.map f else_; + } + +let rec expr_exists p e = p e || List.exists (expr_exists p) (sub_exprs e) + let make_partition_by = List.iter (function | Value _ -> fail "ORDER BY or PARTITION BY uses legacy position indication which is not supported, use expression." | _ -> ()) @@ -784,7 +867,7 @@ module Alter_action_attr = struct make_located ~pos:(0,0) ~value:c ) in let kind = Some (make_located ~pos:(0,0) ~value:(make_collated ~collated:(Source_type.Infer attr.domain.Type.t) ())) in - let meta = Meta.StringMap.to_seq attr.meta |> List.of_seq in + let meta = Meta.StringMap.bindings attr.meta in { name = attr.name; kind; extra; meta } end @@ -852,19 +935,28 @@ let () = print (project ["b";"d"] test) let () = print (rename test "a" "new_a") *) +type 'attr dynamic_field = { + field_id : param_id; + field_attr : 'attr; + join_deps : int list; +} +[@@deriving show] + type schema_column_with_sources = | AttrWithSources of table_name Schema.Source.Attr.t - | DynamicWithSources of param_id * (param_id * table_name Schema.Source.Attr.t) list + | DynamicWithSources of param_id * table_name Schema.Source.Attr.t dynamic_field list [@@deriving show] type schema_column = | Attr of attr - | Dynamic of param_id * (param_id * attr) list + | Dynamic of param_id * attr dynamic_field list [@@deriving show] let drop_sources : schema_column_with_sources -> schema_column = function | AttrWithSources { attr; _ } -> Attr attr - | DynamicWithSources (p, l) -> Dynamic (p, List.map (fun (p, { Schema.Source.Attr.attr; _ }) -> p, attr) l) + | DynamicWithSources (p, l) -> + Dynamic (p, List.map (fun { field_id; field_attr = { Schema.Source.Attr.attr; _ }; join_deps } -> + { field_id; field_attr = attr; join_deps }) l) let monomorphic ret args = F (Typ ret, List.map (fun t -> Type.Typ t) args) let fixed ret args = monomorphic (Type.depends ret) (List.map Type.depends args) diff --git a/lib/syntax.ml b/lib/syntax.ml index aafea866..d15408ab 100644 --- a/lib/syntax.ml +++ b/lib/syntax.ml @@ -13,6 +13,11 @@ module Config = struct let dynamic_select = ref false end +type query_scope = + | Top_level + | Subquery + | From_passthrough + type env = { tables : Tables.table list; schema : table_name Schema.Source.t; @@ -29,7 +34,7 @@ type env = { (* Check if the current query is an UPDATE statement *) is_update: bool; insert_resolved_types: (string, Type.t) Hashtbl.t; (* for INSERT .. VALUES *) - is_subquery: bool; (* whether we are inside a subquery *) + scope: query_scope; } (* Merge global tables with ctes during resolving sources in SELECT .. FROM sources, JOIN *) @@ -71,7 +76,7 @@ let empty_env = { query_has_grouping = false; is_order_by = false; is_update = false; insert_resolved_types = Hashtbl.create 16; - is_subquery = false; + scope = Top_level; } let flat_map f l = List.flatten (List.map f l) @@ -110,44 +115,18 @@ let list_same l = | x::xs -> if List.for_all (fun y -> x = y) xs then Some x else None let rec is_grouping = function -| Value _ -| Param _ -| Column _ -| SelectExpr _ -| Inparam _ -| InTupleList _ -| Of_values _ -| OptionActions _ -> false | Choices (p,l) -> begin match list_same @@ List.map (fun (_,expr) -> Option.map_default is_grouping false expr) l with | None -> failed ~at:p.pos "inconsistent grouping in choice branches" | Some v -> v end -| Case { case; branches; else_ } -> - List.exists is_grouping - (option_list case @ - List.flatten (List.map (fun { Sql.when_; then_ } -> [when_; then_]) branches) @ - option_list else_) -| InChoice (_, _, e) -> is_grouping e | Fun { kind ; parameters; _ } -> (* grouping function of zero or single parameter or function on grouping result *) (Sql.is_grouping kind && List.length parameters <= 1) || List.exists is_grouping parameters +| e -> List.exists is_grouping (sub_exprs e) -let rec is_windowing = function -| Value _ -| Param _ -| Column _ -| SelectExpr _ -| Inparam _ -| InTupleList _ -| Of_values _ -| OptionActions _ -> false -| Choices (_, l) -> List.exists (fun (_, e) -> Option.map_default is_windowing false e ) l -| InChoice (_, _, e) -> is_windowing e -| Case { case; branches; else_ } -> - List.exists is_windowing - (option_list case @ List.flatten (List.map (fun { Sql.when_; then_ } -> [when_; then_]) branches) @ option_list else_) -| Fun { is_over_clause; _ } -> is_over_clause +let is_windowing = + expr_exists (function Sql.Fun { is_over_clause; _ } -> is_over_clause | _ -> false) let exists_grouping columns = List.exists (function @@ -200,29 +179,19 @@ let resolve_column ~env {cname;tname} = | None -> find (Option.map_default (schema_of ~env) env.schema tname) cname | Some result -> result +let resolve_column_opt ~env col = + match resolve_column ~env col with + | attr -> Some attr + | exception (Schema.Error _ | Failure _) -> None + let rec merge_meta_into_params ~shallow meta expr = - match expr, shallow with - | Param (p, m), _ -> Param (p, Meta.merge_right meta m) - | Inparam (p, m), _ -> Inparam (p, Meta.merge_right meta m) - | OptionActions ({ choice; _ } as o), _-> + match expr with + | Param (p, m) -> Param (p, Meta.merge_right meta m) + | Inparam (p, m) -> Inparam (p, Meta.merge_right meta m) + | OptionActions ({ choice; _ } as o) -> OptionActions { o with choice = merge_meta_into_params ~shallow meta choice } - | Fun ({ parameters; _ } as fn), false -> - Fun { fn with parameters = List.map (merge_meta_into_params ~shallow meta) parameters } - | Case { case; branches; else_ }, false -> - let case = Option.map (merge_meta_into_params ~shallow meta) case in - let branches = List.map (fun { Sql.when_; then_ } -> - { Sql.when_ = merge_meta_into_params ~shallow meta when_; - then_ = merge_meta_into_params ~shallow meta then_ } - ) branches in - let else_ = Option.map (merge_meta_into_params ~shallow meta) else_ in - Case { case; branches; else_ } - | InChoice (n, k, e), false -> InChoice (n, k, merge_meta_into_params ~shallow meta e) - | InTupleList ({ value = { exprs; _ } as tl; _ } as tl_loc), false -> - InTupleList { tl_loc with value = { tl with exprs = List.map (merge_meta_into_params ~shallow meta) exprs } } - | Choices (n, l), false -> - Choices (n, List.map (fun (n, e) -> n, Option.map (merge_meta_into_params ~shallow meta) e) l) - | (Value _ | Column _ | SelectExpr _ | Of_values _) as e, false -> e - | expr , true -> expr + | e when shallow -> e + | e -> map_sub_exprs (merge_meta_into_params ~shallow meta) e let set_param_meta ~env col e = let m' = (resolve_column ~env col).attr.meta in @@ -279,22 +248,13 @@ let update_schema_with_aliases all_schema final_schema = applied @ final_schema let rec bool_choice_id = function - | Column _ - | SelectExpr _ - | OptionActions _ - | Of_values _ - | Value _ -> None | Inparam (p, _) | Param (p, _) -> Some p.id - | Fun { parameters; _ } -> List.find_map bool_choice_id parameters | Choices (p, _) | InTupleList { value = { param_id = p; _ }; _ } | InChoice(p, _, _) -> Some p - | Case { case; branches; else_ } -> - List.find_map bool_choice_id - (option_list case @ - List.flatten (List.map (fun { Sql.when_; then_ } -> [when_; then_]) branches) @ - option_list else_) + | OptionActions _ -> None + | e -> List.find_map bool_choice_id (sub_exprs e) let extract_meta_from_col ~env expr = let rec aux = function @@ -307,31 +267,18 @@ let extract_meta_from_col ~env expr = (* col_name IN @param *) | Fun ({ parameters = ([(Inparam _) as b; Column a;]); _ } as fn) -> Fun { fn with parameters = [set_param_meta ~env a.collated b; Column a;] } - (* (col_name, ..., any_expr, col_name2) IN @param *) - | InTupleList ({value = { exprs;_ } as in_tuple_list; _ } as in_tuple_list_loc) -> - InTupleList { in_tuple_list_loc with value = { in_tuple_list with exprs = List.map aux exprs } } - | Fun ({ parameters; _ } as fn) -> - Fun { fn with parameters = List.map aux parameters } - | Case { case; branches; else_ } -> - let case = Option.map aux case in - let branches = List.map (fun { Sql.when_; then_ } -> - { Sql.when_ = aux when_; then_ = aux then_ } - ) branches in - let else_ = Option.map aux else_ in - Case { case; branches; else_ } - | OptionActions ({ choice; _ } as o) -> - OptionActions { o with choice = aux choice; } - | InChoice (n, k, e) -> - InChoice (n, k, aux e) - | Choices (n,l) -> - Choices (n, List.map (fun (n,e) -> n, Option.map aux e) l) - | (Value _ | Param _ | Inparam _ - | SelectExpr (_, _) | Column _ | Of_values _) as e -> e + | e -> map_sub_exprs aux e in aux expr -let make_dynamic_select ~env ~is_subquery columns = - if not !Config.dynamic_select || is_subquery then +let dynamic_allowed env = + !Config.dynamic_select && + match env.scope with + | Top_level | From_passthrough -> true + | Subquery -> false + +let make_dynamic_select ~env columns = + if not (dynamic_allowed env) then columns else let module S = Set.Make(String) in @@ -385,6 +332,250 @@ let make_dynamic_select ~env ~is_subquery columns = [{ value = Expr ({ value = Choices ({ value = Some dynamic_col_param_name; pos = outer_pos }, choices); pos = outer_pos }, None); pos = outer_pos }] + +type resolved_source = { + rsrc_schema : table_name Schema.Source.t; + rsrc_params : vars; + rsrc_tables : Tables.table list; + rsrc_dynamic : schema_column_with_sources list; + rsrc_physical_table : Sql.join_source option; +} + +module From = struct + type join = { + src : resolved_source; + kind : Schema.Join.typ; + cond : join_condition; + pos : pos; + } + + type t = { + base : resolved_source; + joins : join list; + } + + let dynamic_columns from = + let sources { base; joins } = base :: List.map (fun j -> j.src) joins in + List.concat_map (fun src -> src.rsrc_dynamic) (Option.map_default sources [] from) +end + +module Table_refs : sig + type t + val of_expr : env:env -> Sql.expr -> t + val of_exprs : env:env -> Sql.expr list -> t + val may_refer : Sql.join_source -> t -> bool +end = struct + module Names = Set.Make(String) + + type t = Names.t option + + let anything = None + + let empty = Some Names.empty + + let union a b = + match a, b with + | Some x, Some y -> Some (Names.union x y) + | None, _ | _, None -> anything + + let of_attr attr = + Names.of_list (List.map (fun (s : table_name) -> s.tn) attr.Schema.Source.Attr.sources) + + let rec of_expr ~env = function + | Sql.Column c -> Option.map of_attr (resolve_column_opt ~env c.collated) + | SelectExpr _ -> anything + | e -> of_exprs ~env (sub_exprs e) + + and of_exprs ~env l = List.fold_left (fun acc e -> union acc (of_expr ~env e)) empty l + + let may_refer source = + Option.map_default (Names.mem (Sql.join_source_name source).tn) true +end + +let matches_at_most_one_row ~env table expr = + let module SS = Constraint.StringSet in + let table_name = Sql.join_source_name table in + let belongs (a : table_name Schema.Source.Attr.t) = + List.exists (fun (s : table_name) -> s.tn = table_name.tn) a.sources + in + let table_attrs = + List.filter_map + (fun a -> if belongs a then Some a.Schema.Source.Attr.attr else None) + env.schema + in + let keys = unique_keys table_attrs in + let independent_of_table e = + not (Table_refs.may_refer table (Table_refs.of_expr ~env e)) + in + let as_column = function + | Sql.Column c -> resolve_column_opt ~env c.collated + | _ -> None + in + let bound1 a b = + match as_column a with + | Some attr when belongs attr && independent_of_table b -> + Some attr.Schema.Source.Attr.attr.name + | _ -> None + in + let bound_part a b = match bound1 a b with Some _ as r -> r | None -> bound1 b a in + let rec bound_parts = function + | Sql.Fun { kind = Logical And; parameters; _ } -> + List.fold_left (fun acc e -> SS.union acc (bound_parts e)) SS.empty parameters + | Fun { kind = Comparison Comp_equal; parameters = [a; b]; _ } -> + b |> bound_part a |> Option.map_default SS.singleton SS.empty + | Choices (_, branches) -> + let of_branch (_, e) = Option.map_default bound_parts SS.empty e in + (match branches with + | [] -> SS.empty + | hd :: tl -> List.fold_left (fun acc b -> SS.inter acc (of_branch b)) (of_branch hd) tl) + | Fun _ | Value _ | Param _ | Inparam _ | Column _ | Of_values _ | SelectExpr _ + | InChoice _ | OptionActions _ | InTupleList _ | Case _ -> SS.empty + in + let bound = bound_parts expr in + List.exists (fun k -> SS.subset k bound) keys + +module Table_elimination = struct + + module Id_set = Set.Make(Int) + module Id_map = Map.Make(Int) + module Table_map = Map.Make(String) + + type candidate = { + table : Sql.join_source; + join : From.join; + } + + let join_id c = fst c.join.pos + + let eliminate ~env ~from ~columns ~where ~group ~having ~order final_schema from_params = + let unchanged = final_schema, from_params in + let joins = Option.map_default (fun f -> f.From.joins) [] from in + let eliminable ({ From.src; kind; cond; pos = _ } as join) = + let has_params = expr_exists (function + | Sql.Param _ | Inparam _ | InTupleList _ | Choices _ | InChoice _ + | OptionActions _ | SelectExpr _ -> true + | Value _ | Column _ | Of_values _ | Fun _ | Case _ -> false) + in + match kind, cond, src.rsrc_physical_table with + | Schema.Join.Left, Schema.Join.On e, Some table + when not (has_params e) && matches_at_most_one_row ~env table e -> + Some { table; join } + | _ -> None + in + let is_implicit j = match j.From.cond with + | Schema.Join.Natural | Using _ -> true + | On _ | Default -> false + in + let rec after_last_implicit l = + match List.dropwhile (not $ is_implicit) l with + | [] -> l + | _ :: rest -> after_last_implicit rest + in + let candidates = + joins + |> after_last_implicit + |> List.filter_map eliminable + |> List.fold_left (fun m c -> Id_map.add (join_id c) c m) Id_map.empty + in + if Id_map.is_empty candidates then unchanged else + let outside_select_list = option_list where @ group @ option_list having @ List.map fst order in + let query_exprs = + List.filter_map (fun c -> match c.Sql.value with + | All | AllOf _ -> None + | Expr ({ value = e; _ }, _) -> Some e) + columns + @ outside_select_list + in + if List.exists is_windowing query_exprs then unchanged else + let keys_where p m = Id_map.fold (fun k v acc -> if p k v then Id_set.add k acc else acc) m Id_set.empty in + let used_elsewhere = + let static_select_list = + List.concat_map (fun c -> match c.Sql.value with + | All | AllOf _ -> [] + | Expr ({ value = Choices (_, choices); _ }, _) -> + List.filter_map (function + | (_, Some (Sql.Column _)) | (_, None) -> None + | (_, Some e) -> Some e) choices + | Expr ({ value = Column _; _ }, _) -> [] + | Expr ({ value = e; _ }, _) -> [e]) + columns + in + Table_refs.of_exprs ~env (outside_select_list @ static_select_list) + in + let condition_refs = + List.fold_left (fun m { From.cond; pos; _ } -> + match cond with + | Schema.Join.On e -> + let refs = Table_refs.of_expr ~env e in + let j = fst pos in + let referenced = + candidates + |> keys_where (fun _ c -> Table_refs.may_refer c.table refs) + |> Id_set.remove j + in + Id_map.add j referenced m + | Default | Natural | Using _ -> m) + Id_map.empty joins + in + let condition_refs_of j = condition_refs |> Id_map.find_opt j |> Option.default Id_set.empty in + let saturate refs set = + let rec go s = + let expanded = Id_set.fold (fun j -> Id_set.union (refs j)) s s in + if Id_set.equal expanded s then s else go expanded + in + go set + in + let redundant_ids = + let unreferenced = + keys_where (fun _ c -> not (Table_refs.may_refer c.table used_elsewhere)) candidates + in + let retained = Id_set.diff (keys_where (fun _ _ -> true) condition_refs) unreferenced in + Id_set.diff unreferenced (saturate condition_refs_of retained) + in + let direct i = Id_set.inter (condition_refs_of i) redundant_ids in + let by_table = + Id_set.fold (fun j m -> + let tn = (Sql.join_source_name (Id_map.find j candidates).table).tn in + Table_map.update tn (fun old -> Some (Id_set.add j (Option.default Id_set.empty old))) m) + redundant_ids Table_map.empty + |> Table_map.map (saturate direct) + in + let join_of_column a = + List.find_map (fun s -> Table_map.find_opt s.tn by_table) a.Schema.Source.Attr.sources + in + let annotate_column needed field = + match join_of_column field.Sql.field_attr with + | None -> needed, field + | Some pre -> + Id_set.union needed pre, + { field with Sql.join_deps = Id_set.elements pre } + in + let pid = + List.find_map (function + | DynamicWithSources (p, _) -> Some p + | AttrWithSources _ -> None) final_schema + in + let needed, final_schema = + List.fold_left_map (fun needed -> function + | DynamicWithSources (p, cols) -> + let needed, cols = List.fold_left_map annotate_column needed cols in + needed, DynamicWithSources (p, cols) + | AttrWithSources _ as x -> needed, x) + Id_set.empty final_schema + in + let holes = match pid with + | None -> [] + | Some pid -> + needed + |> Id_set.elements + |> List.map (fun j -> + let c = Id_map.find j candidates in + Sql.DynamicSelectJoin { pid; pos = c.join.pos; source = c.table }) + in + let by_position a b = Int.compare (Sql.var_pos a) (Sql.var_pos b) in + final_schema, List.merge by_position from_params (List.sort ~cmp:by_position holes) +end + (** resolve each name reference (Column, Inserted, etc) into ResValue or ResFun of corresponding type *) let rec resolve_columns env expr = if !Config.debug then @@ -486,7 +677,7 @@ let rec resolve_columns env expr = end (* nested select *) | SelectExpr (select, usage) -> - let (schema, p, _) = eval_select_full { env with is_subquery = true } select in + let (schema, p, _) = eval_select_full { env with scope = Subquery } select in let schema = List.map (function | AttrWithSources a -> a | DynamicWithSources _ -> fail "nested select cannot have dynamic attributes" @@ -803,7 +994,7 @@ and infer_schema ~not_null_keys env columns = | Fun { kind = Null_handling (Coalesce _ | If_null); parameters = e :: _; _ } -> propagate_meta ~env e (* Or for subselect which always requests only one column, TODO: consider CTE in subselect, perhaps a rare occurrence *) | SelectExpr ({ select_complete = { select = ({columns = [{ value = Expr ({ value; _ }, _); _ }]; from; _}, _); _ }; _ }, _) -> - let (env,_) = eval_nested env from in + let (env,_,_) = eval_nested { env with scope = Subquery } from in propagate_meta ~env value | Case _ | Value _ @@ -825,24 +1016,34 @@ and infer_schema ~not_null_keys env columns = | { value = All; _ } -> List.map (fun x -> AttrWithSources (refine_column x)) env.schema | { value = AllOf t; _ } -> List.map (fun x -> AttrWithSources (refine_column x)) (schema_of ~env t) | { value = Expr ({ value = expr; _ }, alias); _ } -> + let apply_alias col = + Option.map_default + (fun n -> Schema.Source.Attr.map_attr (fun attr -> { attr with name = n }) col) + col alias + in let make_col e = let _, t = resolve_types env e in + let sources = match e with + | Column c -> (resolve_column ~env c.collated).sources + | _ -> [] + in let col = { Schema.Source.Attr.attr = unnamed_attribute ~meta:(propagate_meta ~env e) (get_or_failwith t); - sources = [] + sources } in let col = refine_column col in - Option.map_default (fun n -> {col with attr = { col.attr with name = n }}) col alias + apply_alias col in let col = match expr with | Column col -> let col = resolve_column ~env col.collated in let col = refine_column col in - AttrWithSources (Option.map_default (fun n -> {col with attr = { col.attr with name = n }}) col alias) - | Choices (p, choices) when not env.is_subquery && !Config.dynamic_select -> + AttrWithSources (apply_alias col) + | Choices (p, choices) when dynamic_allowed env -> let dynamic = choices |> List.filter_map (fun (choice_p, e_opt) -> - Option.map (fun choice_e -> choice_p, make_col choice_e) e_opt + Option.map (fun choice_e -> + { Sql.field_id = choice_p; field_attr = make_col choice_e; join_deps = [] }) e_opt ) in DynamicWithSources (p, dynamic) | e -> AttrWithSources (make_col e) @@ -862,15 +1063,11 @@ let _ = and get_params_of_columns env = let get = function | { value = (All | AllOf _); _ } -> [] - | { value = Expr ({ value = Choices (p, choices); _ }, _); _ } when not env.is_subquery && !Config.dynamic_select -> + | { value = Expr ({ value = Choices (p, choices); _ }, _); _ } when dynamic_allowed env -> [DynamicSelect (p, List.map (fun ((n : param_id), e) -> match e with | Some (Column { collated = { cname; tname }; _ }) when n.pos = dummy_pos -> - let sql = - match tname with - | Some t -> Printf.sprintf "%s.%s" (show_table_name t) cname - | None -> cname - in + let sql = tname |> Option.map_default (fun t -> Printf.sprintf "%s.%s" (show_table_name t) cname) cname in Verbatim (Option.default cname n.value, sql) | _ -> Simple (n, Option.map (fun e -> e |> resolve_types env |> fst |> get_params_of_res_expr env) e) @@ -885,20 +1082,20 @@ and get_params_opt env = function and get_params_l env l = flat_map (get_params env) l -and do_join (env,params) ((schema1,params1,_tables),join_type,join_cond) = - let schema = Schema.Join.join join_type join_cond env.schema schema1 in +and do_join (env,params) { From.src; kind; cond; _ } = + let schema = Schema.Join.join kind cond env.schema src.rsrc_schema in let env = { env with schema } in - let p = match join_cond with + let p = match cond with | Default | Natural | Using _ -> [] | On e -> get_params { env with set_tyvar_strict = true } e (* TODO should use final schema (same as tables)? *) in - env, params @ params1 @ p + env, params @ src.rsrc_params @ p -and join env ((schema,p0,ts0),joins) = +and join env { From.base; joins } = assert (env.schema = []); - let all_tables = List.flatten (ts0 :: List.map (fun ((_,_,ts),_,_) -> ts) joins) in - let env = { env with tables = env.tables @ all_tables; schema; } in - List.fold_left do_join (env, p0) joins + let all_tables = base.rsrc_tables @ List.concat_map (fun j -> j.From.src.rsrc_tables) joins in + let env = { env with tables = env.tables @ all_tables; schema = base.rsrc_schema } in + List.fold_left do_join (env, base.rsrc_params) joins and params_of_assigns env ss = @@ -970,8 +1167,17 @@ and eval_nested env nested = let env = { env with schema = [] } in (* FIXME resolved table schema depends on join (nullability with left), this is resolving too early *) match nested with - | Some (t,l) -> join env (resolve_source env t, List.map (fun loc -> let (x,jt,jc) = loc.value in resolve_source env x, jt.value, jc) l) - | None -> env, [] + | Some (t,l) -> + let resolve = resolve_source env in + let from = { + From.base = resolve t; + joins = List.map (fun loc -> + let (x,jt,jc) = loc.value in + { From.src = resolve x; kind = jt.value; cond = jc; pos = loc.pos }) l; + } in + let env, params = join env from in + env, params, Some from + | None -> env, [], None (** Extract (sources, name) pairs for columns with IS NOT NULL in WHERE/HAVING @@ -1056,21 +1262,33 @@ and extract_not_null_column_keys env = function expr |> analyze |> extract -and eval_select env { columns; from; where; group; having; } = - let (env,p2) = eval_nested env from in +and eval_select ~order env { columns; from; where; group; having; } = + let is_passthrough = columns <> [] && List.for_all (fun c -> match c.value with All | AllOf _ -> true | Expr _ -> false) columns in + let child_scope = + match env.scope with + | (Top_level | From_passthrough) when is_passthrough -> From_passthrough + | Top_level | From_passthrough | Subquery -> Subquery + in + let from_env, p2, resolved_from = eval_nested { env with scope = child_scope } from in + let env = { from_env with scope = env.scope } in let env = { env with query_has_grouping = List.length group > 0 } in (* Extract IS NOT NULL predicates from WHERE and HAVING *) let not_null_keys_where = extract_not_null_column_keys env where in let not_null_keys_having = extract_not_null_column_keys env having in let not_null_keys = not_null_keys_where @ not_null_keys_having in - let columns = make_dynamic_select ~env ~is_subquery:env.is_subquery columns in - let final_schema = infer_schema ~not_null_keys env columns in + let projection = make_dynamic_select ~env columns in + let final_schema = infer_schema ~not_null_keys env projection in + let final_schema = + match child_scope with + | From_passthrough -> final_schema @ From.dynamic_columns resolved_from + | Top_level | Subquery -> final_schema + in let final_schema' = List.concat_map (function | AttrWithSources attr -> [attr] - | DynamicWithSources (_, l) -> List.map snd l + | DynamicWithSources (_, l) -> List.map (fun f -> f.Sql.field_attr) l ) final_schema in (* use schema without aliases here *) - let p1 = get_params_of_columns env columns in + let p1 = get_params_of_columns env projection in let env, p3 = if Dialect.Semantic.is_where_aliases_dialect () then let env = { env with schema = make_unique (Schema.Join.cross env.schema final_schema') } in env, get_params_opt { env with set_tyvar_strict = true; } where @@ -1082,76 +1300,25 @@ and eval_select env { columns; from; where; group; having; } = in (* ORDER BY, HAVING, GROUP BY allow have column without explicit referring to source if it's specified in SELECT *) let env = { env with schema = update_schema_with_aliases env.schema final_schema' } in - let satisfies_some_relevant_constraint table where env = - let get_all_eql_checks expr = - let rec aux acc expr_list = - match expr_list with - | [] -> acc - | expr :: expr_list -> - match expr with - | Fun { kind = Comparison Comp_equal; parameters = [v1; v2]; _ } -> - let columns_in_eql_check = - match v1, v2 with - | Column _, Column _ -> [] (* Columns may refer to each other in foreign - key constraints, so don't add any of them for now. - TODO: consider foreign key constraints *) - | Column c, _ | _, Column c -> [c.collated] - | _ -> [] - in - aux (columns_in_eql_check @ acc) expr_list - | Fun { kind = Logical And; parameters = [x; y]; _ } -> aux acc (x :: y :: expr_list) - (* as OR, XOR can easily propagate to tautology, we avoid checking them *) - | Fun { kind = Logical Or; _ } -> aux acc expr_list - | Fun { kind = Logical Xor; _ } -> aux acc expr_list - | _ -> aux acc expr_list - in - aux [] [expr] - in - let open Schema in - (* identify if the set of columns used in WHERE clause contains/represents constraints *) - let columns_in_where = get_all_eql_checks where in - let attributes_in_where = List.map (resolve_column ~env) columns_in_where in - let attributes_in_where = List.filter (fun attr -> List.mem table attr.Schema.Source.Attr.sources) attributes_in_where in - let column_constraints = - List.map (fun (attr : table_name Source.Attr.t) -> attr.attr.extra) attributes_in_where - in - let satisfies_single_value_constraint () = - List.exists (fun column_constraint -> - Constraints.mem PrimaryKey column_constraint || Constraints.mem Unique column_constraint - ) column_constraints - in - let satisfies_composite_constraint () = - let open Constraint.StringSet in - let ids_set = of_list (List.map (fun col -> col.cname) columns_in_where) in - List.exists - (fun col -> - List.exists - (function - | Constraint.Composite (CompositePrimary s) - | Constraint.Composite (CompositeUnique s) -> subset s ids_set - | _ -> false) - (Constraints.elements col) - ) - column_constraints - in - satisfies_single_value_constraint () || satisfies_composite_constraint () - in let cardinality = match from, where with | None, None -> `One | None, Some _ -> `Zero_one - | Some _, _ when group = [] && exists_grouping columns && not (exists_windowing columns) -> + | Some _, _ when group = [] && exists_grouping projection && not (exists_windowing projection) -> `One (* TODO: analyse join types to determine if cardinality optimization can be done *) - | Some ((`Table t, _), []), Some w when satisfies_some_relevant_constraint t w env -> + | Some ((`Table t, _), []), Some w when matches_at_most_one_row ~env { Sql.table = t; alias = None } w -> `Zero_one | Some _, _ -> `Nat in let p4 = get_params_l env group in let p5 = get_params_opt env having in + let final_schema, p2 = + Table_elimination.eliminate ~env ~from:resolved_from ~columns ~where ~group ~having ~order final_schema p2 + in (final_schema, p1 @ p2 @ p3 @ p4 @ p5, env, cardinality) (** @return final schema, params and tables that can be referenced by outside scope *) @@ -1167,17 +1334,17 @@ and resolve_source env (x, alias) = end in match x with | `Select select -> - let (s,p,_) = eval_select_full { env with is_subquery = true } select in + let (s,p,_) = eval_select_full env select in let tbl_alias = Option.map (fun { table_name; _ } -> table_name) alias in - let s = List.map (function - | AttrWithSources a -> a - | DynamicWithSources _ -> failwith "nested select cannot have dynamic attributes" + let add_src i = { i with Schema.Source.Attr.sources = option_list tbl_alias @ i.Schema.Source.Attr.sources } in + let s, dyn = List.partition_map (function + | AttrWithSources a -> Left (add_src a) + | DynamicWithSources (dp, cols) -> Right (DynamicWithSources (dp, List.map (fun f -> { f with Sql.field_attr = add_src f.Sql.field_attr }) cols)) ) s in - let s = List.map (fun i -> { i with Schema.Source.Attr.sources = List.concat [option_list tbl_alias; i.Schema.Source.Attr.sources] }) s in let s, tables = resolve_schema_with_alias s in - s, p, tables + { rsrc_schema = s; rsrc_params = p; rsrc_tables = tables; rsrc_dynamic = dyn; rsrc_physical_table = None } | `Nested from -> - let (env,p) = eval_nested env (Some from) in + let (env,p,resolved_from) = eval_nested env (Some from) in let s = infer_schema ~not_null_keys:[] env [dummy_loc All] in if alias <> None then failwith "No alias allowed on nested tables"; let s = List.map (function @@ -1185,13 +1352,15 @@ and resolve_source env (x, alias) = (* TODO: next step optimize it *) | DynamicWithSources _ -> failwith "Nested source cannot have dynamic columns" ) s in - s, p, env.tables + { rsrc_schema = s; rsrc_params = p; rsrc_tables = env.tables; rsrc_dynamic = From.dynamic_columns resolved_from; rsrc_physical_table = None } | `Table s -> let (name,s) = Tables_with_derived.get ~env s in + let is_cte = List.exists (fun (n, _) -> n = name) env.ctes in let alias = Option.map (fun { table_name; _ } -> table_name) alias in let sources = (name :: option_list alias) in let s3 = List.map (fun attr -> { Schema.Source.Attr.attr; sources }) s in - s3, [], List.map (fun name -> name, s) sources + { rsrc_schema = s3; rsrc_params = []; rsrc_tables = List.map (fun name -> name, s) sources; rsrc_dynamic = []; + rsrc_physical_table = if is_cte then None else Some { Sql.table = name; alias } } | `ValueRows { row_constructor_list; row_order; row_limit; } -> (* The columns of the table output from VALUES have the implicitly @@ -1221,13 +1390,13 @@ and resolve_source env (x, alias) = [ TupleList (id, ValueRows { types = List.map Source_type.to_infer_type types; values_start_pos }) ], Stmt.Select `Nat in let s, tables = resolve_schema_with_alias s in - s, p, tables + { rsrc_schema = s; rsrc_params = p; rsrc_tables = tables; rsrc_dynamic = []; rsrc_physical_table = None } and eval_select_full env { select_complete; cte } = let ctes, p1 = Option.map_default eval_cte ([], []) cte in let env = { env with ctes = ctes @ env.ctes } in - let (s1, p2, env, cardinality) = eval_select env (fst @@ select_complete.select) in + let (s1, p2, env, cardinality) = eval_select ~order:select_complete.order env (fst @@ select_complete.select) in eval_compound ~env:{ env with tables = env.tables; } (p1 @ p2, s1, cardinality, select_complete) and eval_cte { cte_items; is_recursive } = @@ -1249,7 +1418,7 @@ and eval_cte { cte_items; is_recursive } = end in let stmt = { stmt_ with select = select, other } in - let s1, p1, env, cardinality = eval_select env (fst stmt.select) in + let s1, p1, env, cardinality = eval_select ~order:[] env (fst stmt.select) in let s1' = List.map (function | AttrWithSources attr -> attr (* TODO: next step is to support it for CTEs *) @@ -1264,7 +1433,7 @@ and eval_cte { cte_items; is_recursive } = else ( match cte.stmt with | CteInline stmt -> - let s1, p1, env, cardinality = eval_select env (fst stmt.select) in + let s1, p1, env, cardinality = eval_select ~order:[] env (fst stmt.select) in eval_compound ~env:{ env with tables = env.tables } (p1, s1, cardinality, stmt) | CteSharedQuery shared_query_name -> let (_, stmt) = Shared_queries.get shared_query_name.value in @@ -1285,7 +1454,7 @@ and eval_compound ~env result = let (p1, s1, cardinality, stmt) = result in let { select=(_select, other); order; limit; _; } = stmt in let other = List.map snd other in - let (s2l, p2l) = List.split (List.map (fun (s,p,_,_) -> s,p) @@ List.map (eval_select env) other) in + let (s2l, p2l) = List.split (List.map (fun (s,p,_,_) -> s,p) @@ List.map (eval_select ~order:[] env) other) in let cardinality = if other = [] then cardinality else `Nat in (* ignoring tables in compound statements - they cannot be used in ORDER BY *) let final_schema = @@ -1304,7 +1473,7 @@ and eval_compound ~env result = let p3 = let schema = List.concat_map (function | AttrWithSources attr -> [attr] - | DynamicWithSources (_, a) -> List.map snd a + | DynamicWithSources (_, a) -> List.map (fun f -> f.Sql.field_attr) a ) final_schema in params_of_order order schema env in let (p4,limit1) = match limit with Some (p,x) -> List.map (fun p -> @@ -1316,9 +1485,9 @@ and eval_compound ~env result = final_schema, ( p1 @ (List.flatten p2l) @ p3 @ p4 : var list), Stmt.Select cardinality let update_tables ~env sources ss w = - let schema = Schema.cross_all @@ List.map (fun (s,_,_) -> s) sources in - let p0 = List.flatten @@ List.map (fun (_,p,_) -> p) sources in - let tables = List.flatten @@ List.map (fun (_,_,ts) -> ts) sources in (* TODO assert equal duplicates if not unique *) + let schema = Schema.cross_all @@ List.map (fun src -> src.rsrc_schema) sources in + let p0 = List.flatten @@ List.map (fun src -> src.rsrc_params) sources in + let tables = List.flatten @@ List.map (fun src -> src.rsrc_tables) sources in (* TODO assert equal duplicates if not unique *) let env = { env with tables; schema; } in let p1 = params_of_assigns env ss in let p2 = get_params_opt { env with set_tyvar_strict = true } w in @@ -1618,17 +1787,18 @@ let rec eval (stmt:Sql.stmt) = 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 - let params = update_tables ~env [r,[],[(f, s)]] ss w in + let params = update_tables ~env [{ rsrc_schema = r; rsrc_params = []; rsrc_tables = [(f, s)]; rsrc_dynamic = []; + rsrc_physical_table = Some { Sql.table = f; alias = None } }] ss w in 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) | UpdateMulti (tables,ss,w,o,lim) -> let env = { empty_env with is_update = true } in - let sources = List.map (fun src -> resolve_source env ((`Nested src), None)) tables in - let tables = List.map (fun (_,_,table_list) -> table_list) sources |> List.flatten in + let sources = List.map (fun src -> resolve_source { env with scope = Subquery } ((`Nested src), None)) tables in + let tables = List.map (fun src -> src.rsrc_tables) sources |> List.flatten in let params = update_tables ~env sources ss w in - let p3 = params_of_order o [] { env with schema = Schema.cross_all @@ List.map (fun (s,_,_) -> s) sources; tables } in + let p3 = params_of_order o [] { env with schema = Schema.cross_all @@ List.map (fun src -> src.rsrc_schema) sources; tables } 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 None | Select select -> @@ -1661,14 +1831,14 @@ let unify_params l = Hashtbl.replace h name x | None -> fail "incompatible types for parameter %S : %s and %s" name (Type.show t) (Type.show t') in - let rec traverse = function - | Single ({ id; typ; }, _) - | SingleIn ({ id; typ; _ }, _) -> remember id.value typ - | SharedVarsGroup (vars, _) - | ChoiceIn { vars; _ } -> List.iter traverse vars - | OptionActionChoice (_, l, _, _) -> List.iter traverse l - | Choice (p,l) | DynamicSelect (p, l) -> check_choice_name ~sharing_disabled:true p; List.iter (function Simple (_,l) -> Option.may (List.iter traverse) l | Verbatim _ -> ()) l - | TupleList _ -> () + let rec traverse var = + match var with + | Single ({ id; typ; }, _) + | SingleIn ({ id; typ; _ }, _) -> remember id.value typ + | Choice (p, _) | DynamicSelect (p, _) -> + check_choice_name ~sharing_disabled:true p; + List.iter traverse (sub_vars var) + | _ -> List.iter traverse (sub_vars var) in let rec map = function | Single ({ id; typ; }, m) -> @@ -1677,12 +1847,7 @@ let unify_params l = | SingleIn ({ id; typ; }, m) -> let typ = match id.value with None -> typ | Some name -> try Hashtbl.find h name with _ -> assert false in SingleIn (make_param ~id ~typ:(Type.undepend typ Strict), m) (* if no other clues - input parameters are strict *) - | ChoiceIn t -> ChoiceIn { t with vars = List.map map t.vars } - | SharedVarsGroup (vars, pos) -> SharedVarsGroup (List.map map vars, pos) - | OptionActionChoice (p, l, pos, kind) -> OptionActionChoice (p, (List.map map l), pos, kind) - | Choice (p, l) -> Choice (p, List.map (function Simple (n,l) -> Simple (n, Option.map (List.map map) l) | Verbatim _ as v -> v) l) - | DynamicSelect (p, l) -> DynamicSelect (p, List.map (function Simple (n,l) -> Simple (n, Option.map (List.map map) l) | Verbatim _ as v -> v) l) - | TupleList _ as x -> x + | v -> map_sub_vars (List.map map) v in List.iter traverse l; List.map map l diff --git a/src/gen.ml b/src/gen.ml index 8c1d92f2..4a4c9f1a 100644 --- a/src/gen.ml +++ b/src/gen.ml @@ -74,6 +74,9 @@ type sql = | SubstIn of Sql.Type.t Sql.param * Sql.Meta.t | DynamicIn of Sql.param_id * [`In | `NotIn] * sql list | SubstTuple of Sql.param_id * Sql.tuple_list_kind + | Cond of cond * sql list + +and cond = Dep_selected of Sql.param_id * int and sql_dynamic_ctor = { ctor: Sql.param_id; @@ -123,6 +126,13 @@ let substitute_vars s vars subst_param = assert (i1 > i); let acc = Dynamic (name, dyn) :: Static (String.slice ~first:i ~last:i1 s) :: acc in loop s acc i2 parami tl + | DynamicSelectJoin { pid = name; pos = (j1, j2); _ } :: tl -> + assert (j2 > j1); + assert (j1 > i); + let join_text = " " ^ String.trim (String.slice ~first:j1 ~last:j2 s) in + let rec lead k = if k > i && String.contains " \t\n\r" s.[k - 1] then lead (k - 1) else k in + let acc = Cond (Dep_selected (name, j1), [Static join_text]) :: Static (String.slice ~first:i ~last:(lead j1) s) :: acc in + loop s acc j2 parami tl | TupleList (id, Where_in { value = (types, in_not_in); pos = (j1, j2) }) :: tl -> let (i1,i2) = id.pos in assert (i2 > i1); @@ -219,12 +229,12 @@ let substitute_vars s vars subst_param = squash [] acc and squash acc = function | [] -> List.rev acc + | Static "" :: tl -> squash acc tl | Static s1 :: Static s2 :: tl -> squash acc (Static (s1 ^ s2) :: tl) | x::xs -> squash (x::acc) xs in loop_and_squash s vars - let subst_named index p = "@" ^ (show_param_name p index) let subst_oracle index p = ":" ^ (show_param_name p index) let subst_postgresql index _ = "$" ^ string_of_int (index + 1) @@ -292,7 +302,8 @@ let rec find_param_ids l = | ChoiceIn { param; vars; _ } -> find_param_ids vars @ [param] | SharedVarsGroup (vars, _) -> find_param_ids vars | TupleList (id, _) -> [ id ] - | DynamicSelect (id, _) -> [ id ]) + | DynamicSelect (id, _) -> [ id ] + | DynamicSelectJoin _ -> []) l let names_of_vars l = @@ -305,13 +316,10 @@ let rec params_only l = List.map (function | Sql.Single (p, _) -> [p] - | SingleIn _ -> [] - | SharedVarsGroup (vars, _) - | ChoiceIn { vars; _ } -> params_only vars | OptionActionChoice _ | Choice _ -> fail "dynamic choices not supported for this host language" - | TupleList _ -> [] - | DynamicSelect _ -> fail "dynamic selects not supported for this host language (params_only)") + | DynamicSelect _ -> fail "dynamic selects not supported for this host language (params_only)" + | v -> params_only (Sql.sub_vars v)) l let rec inparams_only l = diff --git a/src/gen_caml.ml b/src/gen_caml.ml index 131ed809..2ba9dd4f 100644 --- a/src/gen_caml.ml +++ b/src/gen_caml.ml @@ -135,7 +135,7 @@ type dynamic_info = { module_name: string; param_name: string; ctors: Sql.ctor list; - schema_fields: (Sql.param_id * Sql.attr) list; + schema_fields: Sql.attr Sql.dynamic_field list; } module L = struct @@ -282,9 +282,9 @@ let gen_func_signature ~dynamic_infos ~module_kind ~index stmt = let name = choose_name stmt.props stmt.kind index |> String.uncapitalize_ascii in let subst = Props.get_all stmt.props "subst" in let dynamic_map = List.map (fun di -> (di.param_name, di.module_name)) dynamic_infos in - let format_input v = match List.assoc_opt v dynamic_map with - | Some module_name -> sprintf "(%s : _ %s.t)" v module_name - | None -> sprintf "~%s" v + let format_input v = + List.assoc_opt v dynamic_map + |> Option.map_default (fun module_name -> sprintf "(%s : _ %s.t)" v module_name) (sprintf "~%s" v) in let inputs = (subst @ names_of_vars stmt.vars) |> List.map format_input |> inline_values in let has_callback = has_row_callback stmt || (module_kind = `Single && dynamic_infos = []) in @@ -332,7 +332,8 @@ let match_variant_pattern i name args ~is_poly = | Choice ({ value = None; _ }, _) | TupleList ({ value = None; _ }, _) | OptionActionChoice ({ value = None; _ }, _, _, _) - | ChoiceIn { param = { value = None; _ }; _ } -> + | ChoiceIn { param = { value = None; _ }; _ } + | DynamicSelect ({ value = None; _ }, _) -> ((seen_wildcards, seen_names, all_wc), Some "_") | TupleList ({ value = Some s; _ }, _) | Choice ({ value = Some s; _ }, _) @@ -342,8 +343,8 @@ let match_variant_pattern i name args ~is_poly = if List.mem s seen_names then ((seen_wildcards, seen_names, false), None) else ((seen_wildcards, s :: seen_names, false), Some s) - | DynamicSelect ({ value = None; _ }, _) -> - ((seen_wildcards, seen_names, all_wc), Some "_") + | DynamicSelectJoin _ -> + ((seen_wildcards, seen_names, all_wc), None) ) ([], [], true) arg_list in let patterns = List.filter_map identity patterns in @@ -385,17 +386,9 @@ let rec has_set_params vars = List.exists (fun var -> match var with | Single _ -> true - | SharedVarsGroup (vars, _) -> has_set_params vars - | TupleList (_, Where_in _) -> false | SingleIn _ | TupleList _ -> false - | ChoiceIn { vars; _ } -> has_set_params vars - | OptionActionChoice (_, vars, _, _) -> has_set_params vars - | Choice (_, ctors) -> List.exists (function - | Simple (_, Some args) -> has_set_params args - | Simple (_, None) -> false - | Verbatim _ -> false - ) ctors | DynamicSelect _ -> false + | _ -> has_set_params (Sql.sub_vars var) ) vars let set_var index var = @@ -442,8 +435,7 @@ let set_var index var = | Some name when Hashtbl.mem seen name -> false | Some name -> Hashtbl.add seen name (); true | None -> true) - | ChoiceIn _ | OptionActionChoice _ - | SharedVarsGroup _ | Choice _ | DynamicSelect _ -> true + | _ -> true in if use_var then let pattern = match aux index var with @@ -517,7 +509,7 @@ let set_var index var = execute_generators all_generators; output "end;" ) - | DynamicSelect _ -> None + | DynamicSelect _ | DynamicSelectJoin _ -> None in Option.may (fun g -> g ()) (aux index var) @@ -531,7 +523,7 @@ let rec eval_count_params vars = | SharedVarsGroup (vars, _) -> `SharedVarsGroup vars | OptionActionChoice (param_id, vars, _, _) -> `OptionActionChoice (param_id, vars) | Choice (name, c) -> `Choice (name, c) - | DynamicSelect _ -> `Static false + | DynamicSelect _ | DynamicSelectJoin _ -> `Static false in let rec group_vars (static, choices, bool_choices, choices_in) = function | [] -> (List.rev static, List.rev choices, List.rev bool_choices, List.rev choices_in) @@ -602,22 +594,11 @@ let rec exclude_in_vars l = List.filter_map (function | SingleIn _ -> None - | Single _ as v -> Some v - | SharedVarsGroup (vars, p) -> Some (SharedVarsGroup (exclude_in_vars vars, p)) - | OptionActionChoice (p, v, pos, kind) -> Some (OptionActionChoice (p, exclude_in_vars v, pos, kind)) | TupleList (_, Where_in _) as v -> Some v | TupleList _ -> None - | ChoiceIn t -> Some (ChoiceIn { t with vars = exclude_in_vars t.vars }) - | Choice (param_id, ctors) -> - Some (Choice (param_id, List.map exclude_in_vars_in_constructors ctors)) - | DynamicSelect (param_id, ctors) -> - Some (DynamicSelect (param_id, List.map exclude_in_vars_in_constructors ctors))) + | v -> Some (Sql.map_sub_vars exclude_in_vars v)) l -and exclude_in_vars_in_constructors = function - | Verbatim _ as ctor -> ctor - | Simple (param_id, vars) -> Simple (param_id, Option.map exclude_in_vars vars) - let output_params_binder index vars = match exclude_in_vars vars with | [] -> "T.no_params" @@ -676,7 +657,35 @@ let make_schema_of_tuple_types label = name=(sprintf "%s_%Ln" label idx); domain; extra = Constraints.empty; meta; }) -let make_sql l = +let join_ctors_of_vars vars = + let module SM = Map.Make(String) in + let joins = List.filter_map (function + | Sql.DynamicSelectJoin { pos; source; _ } -> Some (fst pos, source) + | Sql.Single _ | SingleIn _ | ChoiceIn _ | Choice _ | DynamicSelect _ + | TupleList _ | OptionActionChoice _ | SharedVarsGroup _ -> None) vars + in + let occurrences = + List.fold_left (fun acc (_, s) -> + SM.add s.Sql.table.tn (1 + Option.default 0 (SM.find_opt s.Sql.table.tn acc)) acc) + SM.empty joins + in + let base (_, source) = + let tn = source.Sql.table.tn in + let name = (Sql.join_source_name source).tn in + if SM.find tn occurrences > 1 && name <> tn then tn ^ "_" ^ name else tn + in + let ctors = + joins |> List.map base |> Name.idents ~prefix:"join" |> List.map String.capitalize_ascii + in + List.map2 (fun (join_id, _) ctor -> join_id, ctor) joins ctors + +let join_ctor join_ctors join_id = + try List.assoc join_id join_ctors with Not_found -> fail "unknown dynamic join %d" join_id + +let cond_test ~ctor_of ~deps_of = function + | Gen.Dep_selected (pid, dep_id) -> sprintf "List.mem %s %s" (ctor_of dep_id) (deps_of pid) + +let make_sql ~join_ctors l = let b = Buffer.create 100 in let rec loop app = function | [] -> () @@ -705,6 +714,15 @@ let make_sql l = (match_variant_pattern i ctor.value args ~is_poly:is_poly); loop false sql); bprintf b ")"; loop true tl + | Cond (cond, body) :: tl -> + if app then bprintf b " ^ "; + bprintf b "(if %s then " + (cond_test cond + ~ctor_of:(join_ctor join_ctors) + ~deps_of:(fun pid -> make_param_name 0 pid ^ ".deps")); + loop false body; + bprintf b {| else "")|}; + loop true tl | SubstTuple (id, Insertion schema) :: tl -> if app then bprintf b " ^ "; let label = resolve_tuple_label id in @@ -746,20 +764,27 @@ type callback_build_state = { idx_expr: string option; } -let emit_dynamic_select_body ~module_kind ~dynamic_infos stmt = +let emit_dynamic_select_body ~outside ~module_kind ~dynamic_infos stmt = let sql_pieces = get_sql stmt in + let join_ctors = join_ctors_of_vars stmt.Gen.vars in + + let col_ref di = + if join_ctors = [] then di.param_name else di.param_name ^ ".projection" + in + let deps_ref di = di.param_name ^ ".deps" in + let ctor_qual di ctor = if outside then sprintf "%s.%s" di.module_name ctor else ctor in let other_vars = List.filter (function Sql.DynamicSelect _ -> false | _ -> true) stmt.vars in let static_count = eval_count_params other_vars in let dynamic_counts = dynamic_infos |> List.map (fun di -> - sprintf "%s.count" di.param_name + sprintf "%s.count" (col_ref di) ) |> String.concat " + " in output "let set_params stmt ="; inc_indent (); output "let p = T.start_params stmt (%s + %s) in" static_count dynamic_counts; List.iter (fun di -> - output "%s.set p;" di.param_name + output "%s.set p;" (col_ref di) ) dynamic_infos; List.iteri set_var other_vars; output "T.finish_params p"; @@ -782,11 +807,27 @@ let emit_dynamic_select_body ~module_kind ~dynamic_infos stmt = let di = find_di_by_pid pid in let dyn_expr = if pending_comma then - sprintf {|(match %s.column with "" -> "" | c -> ", " ^ c)|} di.param_name + sprintf {|(match %s.column with "" -> "" | c -> ", " ^ c)|} (col_ref di) else - sprintf "%s.column" di.param_name + sprintf "%s.column" (col_ref di) in build_parts (dyn_expr :: acc) false rest + | Gen.Cond (cond, body) :: rest -> + let Gen.Dep_selected (pid, _) = cond in + let di = find_di_by_pid pid in + let body_expr = + match build_parts [] false body with + | [] -> {|""|} + | parts -> String.concat " ^ " parts + in + let expr = + sprintf {|(if %s then %s else "")|} + (cond_test cond + ~ctor_of:(fun dep_id -> ctor_qual di (join_ctor join_ctors dep_id)) + ~deps_of:(fun _ -> deps_ref di)) + body_expr + in + build_parts (expr :: acc) pending_comma rest | _ :: rest -> build_parts acc pending_comma rest in let sql_parts = build_parts [] false sql_pieces in @@ -823,7 +864,7 @@ let emit_dynamic_select_body ~module_kind ~dynamic_infos stmt = let read_var = sprintf "__sqlgg_r_%s" di.param_name in let next_var = sprintf "__sqlgg_idx_after_%s" di.param_name in let start = col_idx_at ~base:st.idx_expr ~offset:st.static_idx in - let binding = sprintf "let (%s, %s) = %s.read row %s in " read_var next_var di.param_name start in + let binding = sprintf "let (%s, %s) = %s.read row %s in " read_var next_var (col_ref di) start in { bindings = binding :: st.bindings; reads = read_var :: st.reads; static_idx = 0; @@ -868,12 +909,12 @@ let emit_dynamic_module_select ~module_kind ~dynamic_infos stmt = let params = append_func_params ~has_callback:(has_row_callback stmt) ~module_kind inputs in output "let select db %s =" params; inc_indent (); - emit_dynamic_select_body ~module_kind ~dynamic_infos stmt + emit_dynamic_select_body ~outside:false ~module_kind ~dynamic_infos stmt let generate_stmt ~module_kind index stmt = if not (supports_module_kind module_kind stmt) then () else let subst = gen_func_signature ~dynamic_infos:[] ~module_kind ~index stmt in - let sql = make_sql @@ get_sql stmt in + let sql = make_sql ~join_ctors:(join_ctors_of_vars stmt.Gen.vars) @@ get_sql stmt in let sql = match subst with | [] -> sql | vars -> @@ -915,7 +956,7 @@ let generate_stmt ~module_kind index stmt = (function | SubstTuple (id, Insertion _) -> Some id | SubstTuple (_, ( Where_in _| ValueRows _ )) - | Static _ | Dynamic _ | DynamicIn _ | SubstIn _ -> None) + | Static _ | Dynamic _ | DynamicIn _ | SubstIn _ | Cond _ -> None) (get_sql stmt) with | None -> exec @@ -965,7 +1006,7 @@ let generate_enum_modules stmts = in List.concat_map (function | Sql.Attr attr -> attr_enum attr - | Dynamic (_, fields) -> List.concat_map (fun (_, attr) -> attr_enum attr) fields + | Dynamic (_, fields) -> List.concat_map (fun f -> attr_enum f.Sql.field_attr) fields ) schema_cols in @@ -975,20 +1016,12 @@ let generate_enum_modules stmts = List.concat_map (function | Single ({ typ; _ }, meta) | SingleIn ({ typ; _ }, meta) -> enum_opt_with_meta typ meta - | SharedVarsGroup (vars, _) - | OptionActionChoice (_, vars, _, _) - | ChoiceIn { vars; _ } -> vars_to_enums vars - | Choice (_, ctor_list) - | DynamicSelect (_, ctor_list) -> - List.concat_map ( function - | Simple (_, vars) -> Option.map vars_to_enums vars |> option_list |> List.concat - | Verbatim _ -> [] - ) ctor_list | TupleList (_, ValueRows { types; _ }) -> List.concat_map enum_opt types | TupleList (_, Where_in { value = (types, _); pos = _ }) -> List.concat_map (fun (typ, meta) -> enum_opt_with_meta typ meta) types | TupleList (_, Insertion schema) -> schemas_to_enums schema + | v -> vars_to_enums (Sql.sub_vars v) ) vars in Hashtbl.reset enums_hash_tbl; @@ -1040,33 +1073,41 @@ let generate_dynamic_select_modules stmts = List.iteri (fun index stmt -> let all_dis = get_all_dynamic_select_infos index stmt in let single_di = List.length all_dis = 1 in + let sql_pieces = get_sql stmt in + let join_ctors = join_ctors_of_vars stmt.Gen.vars in + let source_ctors = join_ctors |> List.map snd |> List.sort_uniq compare in + let deps_of_field (field : _ Sql.dynamic_field) = + match field.Sql.join_deps with + | [] -> "[]" + | ids -> sprintf "[%s]" (ids |> List.map (join_ctor join_ctors) |> String.concat "; ") + in all_dis |> List.iter (fun di -> let module_name = di.module_name in - let sql_pieces = get_sql stmt in - let field_sqls = List.find_map (function | Gen.Dynamic (pid, ctors) when pid = di.param_id -> Some (List.map (fun c -> c.Gen.ctor, c.Gen.sql) ctors) | _ -> None ) sql_pieces |> Option.default [] in - let fields = List.map2 (fun ctor (_field_param_id, attr) -> + let fields = List.map2 (fun ctor field -> match ctor with | Sql.Simple (ctor_param_id, args) -> let args_list = Option.default [] args in let all_param_names = names_of_vars args_list in let simple_params = args_list |> List.filter_map (function Sql.Single (p, m) -> Some (p, m) | _ -> None) in let param_name = match ctor_param_id.Sql.value with Some s -> String.lowercase_ascii s | None -> "v" in - (field_name_of_param_id ctor_param_id, param_name, all_param_names, simple_params, args_list, attr, ctor) + (field_name_of_param_id ctor_param_id, param_name, all_param_names, simple_params, args_list, field, ctor) | Sql.Verbatim (name, _) -> - (String.capitalize_ascii name, String.lowercase_ascii name, [], [], [], attr, ctor) + (String.capitalize_ascii name, String.lowercase_ascii name, [], [], [], field, ctor) ) di.ctors di.schema_fields in output "module %s = struct" module_name; inc_indent (); let ind = make_indent () in - String.split_on_char '\n' {|type 'a t = { + let template = + match source_ctors with + | [] -> {|type 'a t = { set: T.params -> unit; read: T.row -> int -> 'a * int; column: string; @@ -1096,33 +1137,72 @@ let map f a = apply (pure f) a let (let+) t f = map f t let (and+) a b = apply (map (fun a b -> (a, b)) a) b|} + | ctors -> sprintf {|type source = %s + +type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; +} + +type 'a t = { + projection: 'a projection; + deps: source list; +} + +let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; +} + +let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; +} + +let map f a = apply (pure f) a + +let (let+) t f = map f t +let (and+) a b = apply (map (fun a b -> (a, b)) a) b + +let lift deps projection = { projection; deps }|} (String.concat " | " ctors) + in + String.split_on_char '\n' template |> List.iter (fun line -> if line = "" then print_newline () else Printf.printf "%s%s\n" ind line); - List.iter2 (fun (field_name, _param_name, all_param_names, _simple_params, args_list, attr, ctor) (_, sql) -> - let field_name_lower = - let name = String.lowercase_ascii field_name in - if List.mem name Name.reserved then name ^ "_" else name - in - let read_body = sprintf "(fun row idx -> (%s, idx + 1))" (format_get_column ~row:"row" ~idx:"idx" attr) in - - let column_body = match ctor with - | Sql.Verbatim (_, v) -> quote v - | _ -> make_sql sql + let field_name_lower_of field_name = + let name = String.lowercase_ascii field_name in + if List.mem name Name.reserved then name ^ "_" else name + in + let emit_col_body (field_name, _param_name, _all_param_names, _simple_params, args_list, field, ctor) (_, sql) = + let field_name_lower = field_name_lower_of field_name in + let read_body = sprintf "(fun row idx -> (%s, idx + 1))" (format_get_column ~row:"row" ~idx:"idx" field.Sql.field_attr) in + let column_body = match ctor with + | Sql.Verbatim (_, v) -> quote v + | Sql.Simple _ -> make_sql ~join_ctors sql in - let count_expr = eval_count_params args_list in - let has_params = args_list <> [] in let set_helper_name = sprintf "_set_%s" field_name_lower in - - (match all_param_names with - | [] -> output "let %s =" field_name_lower - | _ -> output "let %s %s =" field_name_lower (String.concat " " all_param_names)); - inc_indent (); - - let set_ref = + let set_ref = if has_params && has_set_params args_list then begin output "let %s p =" set_helper_name; inc_indent (); @@ -1134,17 +1214,26 @@ let (and+) a b = apply (map (fun a b -> (a, b)) a) b|} end else "(fun _p -> ())" in - - output "{"; + (match source_ctors with + | [] -> output "{" + | _ :: _ -> output "lift %s {" (deps_of_field field)); inc_indent (); output "set = %s;" set_ref; output "read = %s;" read_body; output "column = %s;" column_body; output "count = %s;" count_expr; dec_indent (); - output "}"; + output "}" + in + let entries = List.combine fields field_sqls in + List.iter (fun ((field_name, _, all_param_names, _, _, _, _) as field, sql) -> + (match all_param_names with + | [] -> output "let %s =" (field_name_lower_of field_name) + | _ -> output "let %s %s =" (field_name_lower_of field_name) (String.concat " " all_param_names)); + inc_indent (); + emit_col_body field sql; dec_indent () - ) fields field_sqls; + ) entries; if single_di then begin empty_line (); @@ -1172,11 +1261,11 @@ let generate_stmt_wrapper ~module_kind index stmt = let dynamic_infos = get_all_dynamic_select_infos index stmt in match dynamic_infos with | [] -> generate_stmt ~module_kind index stmt - | [_] -> () (* single dynamic select — generated inside *_col module *) + | [_] -> () | _ :: _ :: _ -> if supports_module_kind module_kind stmt then begin let _subst = gen_func_signature ~dynamic_infos ~module_kind ~index stmt in - emit_dynamic_select_body ~module_kind ~dynamic_infos stmt + emit_dynamic_select_body ~outside:true ~module_kind ~dynamic_infos stmt end let generate ~gen_io ~migration_names name stmts = diff --git a/src/gen_xml.ml b/src/gen_xml.ml index 80421877..80fc558a 100644 --- a/src/gen_xml.ml +++ b/src/gen_xml.ml @@ -64,8 +64,7 @@ let value ?(inparam=false) v = Node ("value", attrs, []) let tuplelist_value_of_param = function - | Sql.Single _ | SingleIn _ | Choice _ | ChoiceIn _ | OptionActionChoice _ | SharedVarsGroup _ | DynamicSelect _ -> None - | TupleList ({ value = None; _ }, _) -> failwith "empty label in tuple subst" + | Sql.TupleList ({ value = None; _ }, _) -> failwith "empty label in tuple subst" | TupleList ({ value = Some name; _ }, kind) -> let schema = match kind with | Insertion schema -> schema @@ -79,6 +78,7 @@ let tuplelist_value_of_param = function let typ = "list(" ^ String.concat ", " (List.map (fun { Sql.domain; _ } -> Sql.Type.type_name domain) schema) ^ ")" in let attrs = ["name", name; "type", typ] in Some (Node ("value", attrs, [])) + | _ -> None (* open Gen_caml.L *) open Gen_caml.T @@ -99,6 +99,7 @@ let get_sql_string stmt = | SubstTuple (id, _) -> "@@@" ^ make_param_name i id | DynamicIn (_p, _, sqls) -> String.concat "" @@ List.map (map 0 ) sqls | Dynamic _ -> "{TODO dynamic choice}" + | Cond _ -> "{TODO dynamic join}" in String.concat "" @@ List.mapi map @@ get_sql stmt @@ -107,16 +108,7 @@ let rec params_only l = List.map (function | Sql.Single (p, _) -> [p] - | OptionActionChoice (_, v, _, _) -> params_only v - | SingleIn _ -> [] - | SharedVarsGroup (vars, _) - | ChoiceIn { vars; _ } -> params_only vars - | Choice (_,choices) - | DynamicSelect (_, choices) -> - choices - |> List.map (function Sql.Verbatim _ | Simple (_,None) -> [] | Simple (_name,Some vars) -> params_only vars) (* TODO prefix names *) - |> List.concat - | TupleList _ -> []) + | v -> params_only (Sql.sub_vars v)) (* TODO prefix names *) l let generate_code (x,_) index stmt = diff --git a/src/main.ml b/src/main.ml index 8b07bbf5..af153910 100644 --- a/src/main.ml +++ b/src/main.ml @@ -81,7 +81,7 @@ let check_dialect sql dialect_features = let check_statement stmt sql = let schema = List.concat_map (function | Sql.Attr attr -> [attr] - | Dynamic (_, l) -> List.map snd l) stmt.Gen.schema in + | Dynamic (_, l) -> List.map (fun f -> f.Sql.field_attr) l) stmt.Gen.schema in 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 diff --git a/src/test.ml b/src/test.ml index 7aec2578..6c7dec4d 100644 --- a/src/test.ml +++ b/src/test.ml @@ -53,10 +53,8 @@ let assert_params_with_meta stmt meta = | Single (p, m) -> (p, m) | SingleIn (p, m) -> (p, m) | ChoiceIn { vars = [ SingleIn (p, m) ]; _ } -> (p, m) - | ChoiceIn _ - | SharedVarsGroup _ | OptionActionChoice _ - | Choice _ | TupleList _ -> assert false | DynamicSelect _ -> failwith "dynamic selects not supported for this host language" + | _ -> assert false ) stmt.Gen.vars) @@ -64,7 +62,7 @@ let do_test ?kind sql schema params = let stmt = parse sql in assert_equal ~msg:"schema" ~printer:Sql.Schema.to_string schema (schema_to_attrs stmt.schema); assert_equal ~msg:"params" ~cmp:cmp_params ~printer:Sql.show_params params - (List.map (function Single (p, _) -> p | SharedVarsGroup _ | OptionActionChoice _ | SingleIn _ | Choice _ | ChoiceIn _ | TupleList _ -> assert false | DynamicSelect _ -> failwith "dynamic selects not supported for this host language") stmt.vars); + (List.map (function Single (p, _) -> p | DynamicSelect _ -> failwith "dynamic selects not supported for this host language" | _ -> assert false) stmt.vars); match kind with | Some k -> assert_equal ~msg:"kind" ~printer:[%derive.show: Stmt.kind] k stmt.kind @@ -2263,6 +2261,59 @@ let test_fn_group_by_arg = [ ]; ] +let test_join_hole_whitespace = + let join_var sql sub = + let j1 = String.find sql sub in + Sql.DynamicSelectJoin { + pid = { value = Some "col"; pos = (0,0) }; + pos = (j1, j1 + String.length sub); + source = { table = make_table_name "b"; alias = None }; + } + in + let rec show_piece = function + | Gen.Static s -> sprintf "Static %S" s + | Gen.Cond (_, body) -> sprintf "Cond [%s]" (String.concat "; " (List.map show_piece body)) + | _ -> "Other" + in + let check name sql joins expected = + name >:: (fun () -> + assert_equal + ~cmp:(fun a b -> List.map show_piece a = List.map show_piece b) + ~printer:(fun l -> String.concat "; " (List.map show_piece l)) + expected + (Gen.substitute_vars sql (List.map (join_var sql) joins) None)) + in + let join text = Gen.Cond (Gen.Dep_selected ({ value = Some "col"; pos = (0,0) }, 0), [Gen.Static text]) in + [ + check "no holes" + "SELECT x\nFROM a\nWHERE y = 1" [] + [Gen.Static "SELECT x\nFROM a\nWHERE y = 1"]; + check "newline before hole absorbed" + "SELECT x FROM a\nLEFT JOIN b ON b.a = a.id\nWHERE y = 1" + ["LEFT JOIN b ON b.a = a.id"] + [Gen.Static "SELECT x FROM a"; join " LEFT JOIN b ON b.a = a.id"; Gen.Static "\nWHERE y = 1"]; + check "space before hole absorbed" + "FROM a LEFT JOIN b ON b.a = a.id WHERE y = 1" + ["LEFT JOIN b ON b.a = a.id"] + [Gen.Static "FROM a"; join " LEFT JOIN b ON b.a = a.id"; Gen.Static " WHERE y = 1"]; + check "hole at end of query" + "FROM a\nLEFT JOIN b ON b.a = a.id" + ["LEFT JOIN b ON b.a = a.id"] + [Gen.Static "FROM a"; join " LEFT JOIN b ON b.a = a.id"]; + check "adjacent holes leave no gap" + "FROM a\nLEFT JOIN b ON b.a = a.id\nLEFT JOIN c ON c.a = a.id\nWHERE y = 1" + ["LEFT JOIN b ON b.a = a.id"; "LEFT JOIN c ON c.a = a.id"] + [Gen.Static "FROM a"; join " LEFT JOIN b ON b.a = a.id"; join " LEFT JOIN c ON c.a = a.id"; Gen.Static "\nWHERE y = 1"]; + check "static join between holes" + "FROM a\nLEFT JOIN b ON b.a = a.id\nJOIN o USING (x)\nLEFT JOIN c ON c.a = a.id" + ["LEFT JOIN b ON b.a = a.id"; "LEFT JOIN c ON c.a = a.id"] + [Gen.Static "FROM a"; join " LEFT JOIN b ON b.a = a.id"; Gen.Static "\nJOIN o USING (x)"; join " LEFT JOIN c ON c.a = a.id"]; + check "string literal whitespace preserved" + "FROM a\nLEFT JOIN b ON b.a = a.id\nWHERE note = ' two spaces '" + ["LEFT JOIN b ON b.a = a.id"] + [Gen.Static "FROM a"; join " LEFT JOIN b ON b.a = a.id"; Gen.Static "\nWHERE note = ' two spaces '"]; + ] + let run () = Gen.params_mode := Some Named; let tests = @@ -2306,6 +2357,7 @@ let run () = "test_cardinality_optimization_validity" >::: test_cardinality_optimization_validity; "test_nullability_rules" >::: test_nullability_rules; "test_fn_group_by_arg" >::: test_fn_group_by_arg; + "test_join_hole_whitespace" >::: test_join_hole_whitespace; ] in let test_suite = "main" >::: tests in diff --git a/test/cram/dune b/test/cram/dune index 1d3147f7..4e14d35f 100644 --- a/test/cram/dune +++ b/test/cram/dune @@ -6,6 +6,7 @@ (source_tree test_build_json_functions) (source_tree test_build_enum_literals) (source_tree test_build_dynamic_select) + (source_tree test_build_dynamic_subquery) (source_tree test_build_paren_poc) (glob_files print_ocaml_impl.ml) (glob_files test_cached_prepared_stmts.ml) diff --git a/test/cram/dynamic_subquery.t b/test/cram/dynamic_subquery.t new file mode 100644 index 00000000..7bc5dff5 --- /dev/null +++ b/test/cram/dynamic_subquery.t @@ -0,0 +1,261 @@ +Dynamic select penetrates a pass-through (SELECT *) subquery source: the dynamic +column placeholder is pushed down INTO the subquery. A non-pass-through outer query +keeps the dynamic placeholder at the outer level (subquery projection stays fixed). + + $ cat > dyn_subq.sql <<'EOF' + > CREATE TABLE products (id INT PRIMARY KEY, name TEXT, price INT); + > -- [sqlgg] dynamic_select=true + > -- @products_dyn + > SELECT * FROM (SELECT id, name, price FROM products WHERE price > @min) AS sub; + > -- [sqlgg] dynamic_select=true + > -- @cols_over_subq + > SELECT sub.id, sub.name FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = @id; + > EOF + +Full generated code: + + $ cat dyn_subq.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - | tee output.ml + module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Products_dyn_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("id"); + count = 0; + } + let name = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("name"); + count = 0; + } + let price = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int_nullable row idx, idx + 1)); + column = ("price"); + count = 0; + } + + let select db (col : _ t) ~min callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p min; + T.finish_params p + in + T.select db + ("SELECT * FROM (SELECT " ^ col.column ^ " FROM products WHERE price > ?) AS sub") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~min callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p min; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT * FROM (SELECT " ^ col.column ^ " FROM products WHERE price > ?) AS sub") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~min callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p min; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT * FROM (SELECT " ^ col.column ^ " FROM products WHERE price > ?) AS sub") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Cols_over_subq_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("sub.id"); + count = 0; + } + let name = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("sub.name"); + count = 0; + } + + let select db (col : _ t) ~id callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p id; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~id callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p id; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~id callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p id; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_products db = + T.execute db ("CREATE TABLE products (id INT PRIMARY KEY, name TEXT, price INT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) + end (* module Sqlgg *) + +Compile the generated module together with the printing mock traits implementation: + + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c output.ml + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + +A tiny driver that picks different fields at runtime and lets the mock print the SQL +that is actually sent (with the chosen columns spliced into the subquery): + + $ cat > run_subq.ml <<'EOF' + > module S = Output.Sqlgg(Print_impl) + > open S.Products_dyn_col + > + > let run label col = + > Printf.printf "=== %s ===\n%!" label; + > Print_impl.clear_mock_responses (); + > Print_impl.setup_select_response []; + > ignore (List.select () col ~min:10L (fun x -> x)) + > + > let () = + > run "pick id" id; + > run "pick name" name; + > run "pick id + name + price" (let+ i = id and+ n = name and+ p = price in (i, n, p)) + > EOF + $ ocamlfind ocamlc -package sqlgg.traits -I . -c run_subq.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run_subq.exe output.ml print_impl.ml run_subq.ml + +Run it and observe the final SQL per field selection: + + $ ./run_subq.exe 2>&1 | grep -E '^===|^\[SQL\]' + === pick id === + [SQL] SELECT * FROM (SELECT id FROM products WHERE price > 10) AS sub + === pick name === + [SQL] SELECT * FROM (SELECT name FROM products WHERE price > 10) AS sub + === pick id + name + price === + [SQL] SELECT * FROM (SELECT id, name, price FROM products WHERE price > 10) AS sub diff --git a/test/cram/dynamic_subquery_print.t b/test/cram/dynamic_subquery_print.t new file mode 100644 index 00000000..1f36ff7f --- /dev/null +++ b/test/cram/dynamic_subquery_print.t @@ -0,0 +1,37 @@ +Dynamic select over subquery sources, exercised through the printing mock so the +final SQL is shown for each runtime column selection. + + $ cp test_build_dynamic_subquery/dyn_subq.sql . + $ cp test_build_dynamic_subquery/test_run.ml . + $ cat dyn_subq.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > output.ml + $ ocamlfind ocamlc -package sqlgg.traits,yojson -I . -c print_ocaml_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c output.ml + $ ocamlfind ocamlc -package sqlgg.traits,yojson -I . -c test_run.ml + $ ocamlfind ocamlc -package sqlgg.traits,yojson -I . -linkpkg -o test_run.exe print_ocaml_impl.cmo output.cmo test_run.ml + $ ./test_run.exe 2>&1 | grep -E '^---|^\[[0-9]|^\[SQL\]' + --- Group 1: SELECT * over subquery (pushdown) --- + [1.1] pick id + [SQL] SELECT * FROM (SELECT id FROM products WHERE price > 10) AS sub + [1.2] pick name + [SQL] SELECT * FROM (SELECT name FROM products WHERE price > 10) AS sub + [1.3] pick price + [SQL] SELECT * FROM (SELECT price FROM products WHERE price > 10) AS sub + [1.4] pick id + name + price + [SQL] SELECT * FROM (SELECT id, name, price FROM products WHERE price > 10) AS sub + --- Group 2: SELECT sub.* over subquery (pushdown) --- + [2.1] pick name + [SQL] SELECT sub.* FROM (SELECT name FROM products) AS sub + [2.2] pick id + price + [SQL] SELECT sub.* FROM (SELECT id, price FROM products) AS sub + --- Group 3: SELECT * over LEFT JOIN subquery --- + [3.1] pick uid + [SQL] SELECT * FROM (SELECT u.id FROM users u LEFT JOIN orders o ON o.user_id = u.id) AS sub + [3.2] pick ototal (nullable) + [SQL] SELECT * FROM (SELECT o.total FROM users u LEFT JOIN orders o ON o.user_id = u.id) AS sub + [3.3] pick uid + uname + ototal + [SQL] SELECT * FROM (SELECT u.id, u.name, o.total FROM users u LEFT JOIN orders o ON o.user_id = u.id) AS sub + --- Group 4: non-pass-through (dynamic stays outside) --- + [4.1] pick id + [SQL] SELECT sub.id FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = 1 + [4.2] pick id + name + [SQL] SELECT sub.id, sub.name FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = 1 diff --git a/test/cram/test_build_dynamic_join/basic.t/basic.compare.ml b/test/cram/test_build_dynamic_join/basic.t/basic.compare.ml new file mode 100644 index 00000000..6fd30a9f --- /dev/null +++ b/test/cram/test_build_dynamic_join/basic.t/basic.compare.ml @@ -0,0 +1,323 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Ok_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let name = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("u.name"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Nonuniq_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let total = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int_nullable row idx, idx + 1)); + column = ("o.total"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Ref_in_where_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~b callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Text p b; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE p.bio = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~b callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Text p b; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE p.bio = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~b callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Text p b; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE p.bio = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + let create_orders db = + T.execute db ("CREATE TABLE orders (id INT PRIMARY KEY, user_id INT, total INT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/basic.t/basic.sql b/test/cram/test_build_dynamic_join/basic.t/basic.sql new file mode 100644 index 00000000..b29ef128 --- /dev/null +++ b/test/cram/test_build_dynamic_join/basic.t/basic.sql @@ -0,0 +1,12 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +CREATE TABLE orders (id INT PRIMARY KEY, user_id INT, total INT); +-- [sqlgg] dynamic_select=true +-- @ok +SELECT u.id, u.name, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @nonuniq +SELECT u.id, o.total FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @ref_in_where +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE p.bio = @b; diff --git a/test/cram/test_build_dynamic_join/basic.t/run.ml b/test/cram/test_build_dynamic_join/basic.t/run.ml new file mode 100644 index 00000000..e46cb1bb --- /dev/null +++ b/test/cram/test_build_dynamic_join/basic.t/run.ml @@ -0,0 +1,20 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Basic = Basic.Sqlgg(Print_impl) + +let () = + let open Basic.Ok_col in + run "basic/ok: pick id -> join dropped" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "basic/ok: pick bio -> join present" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))) + +let () = + let open Basic.Nonuniq_col in + run "basic/nonuniq: pick id -> join kept (non-unique key)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Basic.Ref_in_where_col in + run "basic/ref_in_where: pick id -> join kept (WHERE reference)" (fun () -> ignore (List.select () id ~b:"x" (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/basic.t/run.t b/test/cram/test_build_dynamic_join/basic.t/run.t new file mode 100644 index 00000000..6dc6415c --- /dev/null +++ b/test/cram/test_build_dynamic_join/basic.t/run.t @@ -0,0 +1,35 @@ +Basic: droppable PK join / non-unique key / table referenced in WHERE. + +Generated code matches the golden file: + + $ cat basic.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > basic.ml + $ diff basic.ml basic.compare.ml + +Runtime (print_impl mock): the join disappears only when safely droppable, +and is always present when its column is picked: + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c basic.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe basic.cmo print_impl.cmo run.ml + $ ./run.exe + === basic/ok: pick id -> join dropped === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === basic/ok: pick bio -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === basic/nonuniq: pick id -> join kept (non-unique key) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.id FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === basic/ref_in_where: pick id -> join kept (WHERE reference) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE p.bio = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE p.bio = 'x' + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/chain_bad.t/chain_bad.compare.ml b/test/cram/test_build_dynamic_join/chain_bad.t/chain_bad.compare.ml new file mode 100644 index 00000000..8bc5eed2 --- /dev/null +++ b/test/cram/test_build_dynamic_join/chain_bad.t/chain_bad.compare.ml @@ -0,0 +1,120 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Chain_bad_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let url = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.profile_id = p.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.profile_id = p.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.profile_id = p.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (id INT PRIMARY KEY, user_id INT, bio TEXT)") T.no_params + + let create_avatars db = + T.execute db ("CREATE TABLE avatars (id INT PRIMARY KEY, profile_id INT, url TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/chain_bad.t/chain_bad.sql b/test/cram/test_build_dynamic_join/chain_bad.t/chain_bad.sql new file mode 100644 index 00000000..1eff10e9 --- /dev/null +++ b/test/cram/test_build_dynamic_join/chain_bad.t/chain_bad.sql @@ -0,0 +1,6 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE profiles (id INT PRIMARY KEY, user_id INT, bio TEXT); +CREATE TABLE avatars (id INT PRIMARY KEY, profile_id INT, url TEXT); +-- [sqlgg] dynamic_select=true +-- @chain_bad +SELECT u.id, p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.profile_id = p.id WHERE u.id = @uid; diff --git a/test/cram/test_build_dynamic_join/chain_bad.t/run.ml b/test/cram/test_build_dynamic_join/chain_bad.t/run.ml new file mode 100644 index 00000000..b7c58b07 --- /dev/null +++ b/test/cram/test_build_dynamic_join/chain_bad.t/run.ml @@ -0,0 +1,11 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Chain_bad = Chain_bad.Sqlgg(Print_impl) + +let () = + let open Chain_bad.Chain_bad_col in + run "chain_bad: pick id -> both joins kept (child pins parent)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/chain_bad.t/run.t b/test/cram/test_build_dynamic_join/chain_bad.t/run.t new file mode 100644 index 00000000..8560500f --- /dev/null +++ b/test/cram/test_build_dynamic_join/chain_bad.t/run.t @@ -0,0 +1,19 @@ +If the child join is NOT droppable, its ON reference pins the parent too. + +Generated code matches the golden file: + + $ cat chain_bad.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > chain_bad.ml + $ diff chain_bad.ml chain_bad.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c chain_bad.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe chain_bad.cmo print_impl.cmo run.ml + $ ./run.exe + === chain_bad: pick id -> both joins kept (child pins parent) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.profile_id = p.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.profile_id = p.id WHERE u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/chains.t/chains.compare.ml b/test/cram/test_build_dynamic_join/chains.t/chains.compare.ml new file mode 100644 index 00000000..a1802b43 --- /dev/null +++ b/test/cram/test_build_dynamic_join/chains.t/chains.compare.ml @@ -0,0 +1,485 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Chain_col = struct + type source = Avatars | Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let url = + lift [Profiles; Avatars] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Chain3_col = struct + type source = Avatars | Badges | Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let url = + lift [Profiles; Avatars] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + let label = + lift [Profiles; Avatars; Badges] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("b.label"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ (if List.mem Badges col.deps then " LEFT JOIN badges b ON b.id = a.badge_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ (if List.mem Badges col.deps then " LEFT JOIN badges b ON b.id = a.badge_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ (if List.mem Badges col.deps then " LEFT JOIN badges b ON b.id = a.badge_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Diamond_col = struct + type source = Avatars | Badges | Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let url = + lift [Profiles; Avatars] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + let label = + lift [Profiles; Badges] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("b.label"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ (if List.mem Badges col.deps then " LEFT JOIN badges b ON b.id = p.user_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ (if List.mem Badges col.deps then " LEFT JOIN badges b ON b.id = p.user_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = p.avatar_id" else "") ^ (if List.mem Badges col.deps then " LEFT JOIN badges b ON b.id = p.user_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Chain_pinned_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let url = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + let label = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("b.label"); + count = 0; + } + + let select db (col : _ t) ~label callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Text p label; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE b.label = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~label callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Text p label; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE b.label = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~label callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Text p label; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE b.label = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT, avatar_id INT)") T.no_params + + let create_avatars db = + T.execute db ("CREATE TABLE avatars (id INT PRIMARY KEY, url TEXT, badge_id INT)") T.no_params + + let create_badges db = + T.execute db ("CREATE TABLE badges (id INT PRIMARY KEY, label TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/chains.t/chains.sql b/test/cram/test_build_dynamic_join/chains.t/chains.sql new file mode 100644 index 00000000..39850654 --- /dev/null +++ b/test/cram/test_build_dynamic_join/chains.t/chains.sql @@ -0,0 +1,16 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT, avatar_id INT); +CREATE TABLE avatars (id INT PRIMARY KEY, url TEXT, badge_id INT); +CREATE TABLE badges (id INT PRIMARY KEY, label TEXT); +-- [sqlgg] dynamic_select=true +-- @chain +SELECT u.id, p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @chain3 +SELECT u.id, p.bio, a.url, b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @diamond +SELECT u.id, a.url, b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = p.user_id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @chain_pinned +SELECT u.id, p.bio, a.url, b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE b.label = @label; diff --git a/test/cram/test_build_dynamic_join/chains.t/run.ml b/test/cram/test_build_dynamic_join/chains.t/run.ml new file mode 100644 index 00000000..a8bf4f5e --- /dev/null +++ b/test/cram/test_build_dynamic_join/chains.t/run.ml @@ -0,0 +1,36 @@ +module S = Chains.Sqlgg(Print_impl) + +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +let () = + let open S.Chain_col in + run "chain: pick id" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "chain: pick bio" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))); + run "chain: pick url (pulls profiles transitively)" (fun () -> ignore (List.select () url ~uid:1L (fun x -> x))); + run "chain: pick all" (fun () -> + ignore (List.select () (let+ i = id and+ b = bio and+ u = url in (i, b, u)) ~uid:1L (fun x -> x))) + +let () = + let open S.Chain3_col in + run "chain3: pick label (pulls the whole ancestor chain)" (fun () -> + ignore (List.select () label ~uid:1L (fun x -> x))); + run "chain3: pick url (badges not pulled)" (fun () -> + ignore (List.select () url ~uid:1L (fun x -> x))) + +let () = + let open S.Diamond_col in + run "diamond: pick url (one branch)" (fun () -> + ignore (List.select () url ~uid:1L (fun x -> x))); + run "diamond: pick label (other branch)" (fun () -> + ignore (List.select () label ~uid:1L (fun x -> x))); + run "diamond: pick both (parent emitted once)" (fun () -> + ignore (List.select () (let+ u = url and+ l = label in (u, l)) ~uid:1L (fun x -> x))) + +let () = + let open S.Chain_pinned_col in + run "chain_pinned: pick id (WHERE pins the whole chain, no joins dropped)" (fun () -> + ignore (List.select () id ~label:"x" (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/chains.t/run.t b/test/cram/test_build_dynamic_join/chains.t/run.t new file mode 100644 index 00000000..34ba8955 --- /dev/null +++ b/test/cram/test_build_dynamic_join/chains.t/run.t @@ -0,0 +1,67 @@ +Transitive chains: a child's ON reference to a droppable parent is a parent +edge, not a blocker; closures are accumulated child first (two-level, +three-level, diamond). + +Generated code matches the golden file: + + $ cat chains.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > chains.ml + $ diff chains.ml chains.compare.ml + +Runtime (print_impl mock): a pick renders only the joins its closure needs, +the parent is emitted once: + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c chains.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe chains.cmo print_impl.cmo run.ml + $ ./run.exe + === chain: pick id === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === chain: pick bio === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === chain: pick url (pulls profiles transitively) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = ? + [SQL] SELECT a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === chain: pick all === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id, p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = ? + [SQL] SELECT u.id, p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === chain3: pick label (pulls the whole ancestor chain) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE u.id = ? + [SQL] SELECT b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === chain3: pick url (badges not pulled) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[6]: SELECT a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = ? + [SQL] SELECT a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === diamond: pick url (one branch) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[7]: SELECT a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = ? + [SQL] SELECT a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === diamond: pick label (other branch) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[8]: SELECT b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN badges b ON b.id = p.user_id WHERE u.id = ? + [SQL] SELECT b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN badges b ON b.id = p.user_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === diamond: pick both (parent emitted once) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[9]: SELECT a.url, b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = p.user_id WHERE u.id = ? + [SQL] SELECT a.url, b.label FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = p.user_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === chain_pinned: pick id (WHERE pins the whole chain, no joins dropped) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[10]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE b.label = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = p.avatar_id LEFT JOIN badges b ON b.id = a.badge_id WHERE b.label = 'x' + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/dune b/test/cram/test_build_dynamic_join/dune new file mode 100644 index 00000000..cebc0ef9 --- /dev/null +++ b/test/cram/test_build_dynamic_join/dune @@ -0,0 +1,4 @@ +(cram + (deps + %{bin:sqlgg} + ../print_impl.ml)) diff --git a/test/cram/test_build_dynamic_join/example.t/example.compare.ml b/test/cram/test_build_dynamic_join/example.t/example.compare.ml new file mode 100644 index 00000000..5f08c64d --- /dev/null +++ b/test/cram/test_build_dynamic_join/example.t/example.compare.ml @@ -0,0 +1,215 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module User_info_col = struct + type source = Billing | Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let name = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text row idx, idx + 1)); + column = ("u.name"); + count = 0; + } + let email = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text row idx, idx + 1)); + column = ("u.email"); + count = 0; + } + let created_at = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Datetime row idx, idx + 1)); + column = ("u.created_at"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let avatar_url = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.avatar_url"); + count = 0; + } + let location = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.location"); + count = 0; + } + let website = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.website"); + count = 0; + } + let plan = + lift [Billing] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("b.plan"); + count = 0; + } + let paid_until = + lift [Billing] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Datetime_nullable row idx, idx + 1)); + column = ("b.paid_until"); + count = 0; + } + let balance = + lift [Billing] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int_nullable row idx, idx + 1)); + column = ("b.balance"); + count = 0; + } + + let select db (col : _ t) ~org callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p org; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ "\n\ +FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Billing col.deps then " LEFT JOIN billing b ON b.user_id = u.id" else "") ^ "\n\ +WHERE u.org_id = ? AND u.deleted = FALSE") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~org callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p org; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ "\n\ +FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Billing col.deps then " LEFT JOIN billing b ON b.user_id = u.id" else "") ^ "\n\ +WHERE u.org_id = ? AND u.deleted = FALSE") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~org callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p org; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ "\n\ +FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Billing col.deps then " LEFT JOIN billing b ON b.user_id = u.id" else "") ^ "\n\ +WHERE u.org_id = ? AND u.deleted = FALSE") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (\n\ + id INT PRIMARY KEY,\n\ + org_id INT NOT NULL,\n\ + name TEXT NOT NULL,\n\ + email TEXT NOT NULL,\n\ + created_at TIMESTAMP NOT NULL,\n\ + deleted BOOLEAN NOT NULL\n\ +)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (\n\ + user_id INT PRIMARY KEY,\n\ + bio TEXT,\n\ + avatar_url TEXT,\n\ + location TEXT,\n\ + website TEXT\n\ +)") T.no_params + + let create_billing db = + T.execute db ("CREATE TABLE billing (\n\ + user_id INT PRIMARY KEY,\n\ + plan TEXT NOT NULL,\n\ + paid_until DATETIME,\n\ + balance INT NOT NULL\n\ +)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/example.t/example.sql b/test/cram/test_build_dynamic_join/example.t/example.sql new file mode 100644 index 00000000..f6242ca4 --- /dev/null +++ b/test/cram/test_build_dynamic_join/example.t/example.sql @@ -0,0 +1,30 @@ +CREATE TABLE users ( + id INT PRIMARY KEY, + org_id INT NOT NULL, + name TEXT NOT NULL, + email TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + deleted BOOLEAN NOT NULL +); +CREATE TABLE profiles ( + user_id INT PRIMARY KEY, + bio TEXT, + avatar_url TEXT, + location TEXT, + website TEXT +); +CREATE TABLE billing ( + user_id INT PRIMARY KEY, + plan TEXT NOT NULL, + paid_until DATETIME, + balance INT NOT NULL +); +-- [sqlgg] dynamic_select=true +-- @user_info +SELECT u.id, u.name, u.email, u.created_at, + p.bio, p.avatar_url, p.location, p.website, + b.plan, b.paid_until, b.balance +FROM users u +LEFT JOIN profiles p ON p.user_id = u.id +LEFT JOIN billing b ON b.user_id = u.id +WHERE u.org_id = @org AND u.deleted = FALSE; diff --git a/test/cram/test_build_dynamic_join/example.t/run.ml b/test/cram/test_build_dynamic_join/example.t/run.ml new file mode 100644 index 00000000..574dd44f --- /dev/null +++ b/test/cram/test_build_dynamic_join/example.t/run.ml @@ -0,0 +1,20 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Example = Example.Sqlgg(Print_impl) + +let () = + let open Example.User_info_col in + run "brief: name + email -> no joins" (fun () -> + ignore (List.select () (let+ n = name and+ e = email in (n, e)) ~org:1L (fun x -> x))); + run "card: name + bio + avatar_url -> profiles only" (fun () -> + ignore (List.select () (let+ n = name and+ b = bio and+ a = avatar_url in (n, b, a)) ~org:1L (fun x -> x))); + run "brief + billing, no profile: name + plan -> billing only" (fun () -> + ignore (List.select () (let+ n = name and+ pl = plan in (n, pl)) ~org:1L (fun x -> x))); + run "admin: everything -> both joins" (fun () -> + ignore (List.select () + (let+ n = name and+ b = bio and+ pl = plan and+ bal = balance in (n, b, pl, bal)) + ~org:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/example.t/run.t b/test/cram/test_build_dynamic_join/example.t/run.t new file mode 100644 index 00000000..e3a9beac --- /dev/null +++ b/test/cram/test_build_dynamic_join/example.t/run.t @@ -0,0 +1,52 @@ +The README/PR example: one wide reusable query definition acts as a whole +family of queries — pick fewer columns and the SQL narrows accordingly, +dropping the joins whose columns were not picked. + +Generated code matches the golden file: + + $ cat example.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > example.ml + $ diff example.ml example.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c example.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe example.cmo print_impl.cmo run.ml + $ ./run.exe + === brief: name + email -> no joins === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.name, u.email + FROM users u + WHERE u.org_id = ? AND u.deleted = FALSE + [SQL] SELECT u.name, u.email + FROM users u + WHERE u.org_id = 1 AND u.deleted = FALSE + [MOCK] Returning 0 rows + === card: name + bio + avatar_url -> profiles only === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u.name, p.bio, p.avatar_url + FROM users u LEFT JOIN profiles p ON p.user_id = u.id + WHERE u.org_id = ? AND u.deleted = FALSE + [SQL] SELECT u.name, p.bio, p.avatar_url + FROM users u LEFT JOIN profiles p ON p.user_id = u.id + WHERE u.org_id = 1 AND u.deleted = FALSE + [MOCK] Returning 0 rows + === brief + billing, no profile: name + plan -> billing only === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.name, b.plan + FROM users u LEFT JOIN billing b ON b.user_id = u.id + WHERE u.org_id = ? AND u.deleted = FALSE + [SQL] SELECT u.name, b.plan + FROM users u LEFT JOIN billing b ON b.user_id = u.id + WHERE u.org_id = 1 AND u.deleted = FALSE + [MOCK] Returning 0 rows + === admin: everything -> both joins === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.name, p.bio, b.plan, b.balance + FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN billing b ON b.user_id = u.id + WHERE u.org_id = ? AND u.deleted = FALSE + [SQL] SELECT u.name, p.bio, b.plan, b.balance + FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN billing b ON b.user_id = u.id + WHERE u.org_id = 1 AND u.deleted = FALSE + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/join_kinds.t/join_kinds.compare.ml b/test/cram/test_build_dynamic_join/join_kinds.t/join_kinds.compare.ml new file mode 100644 index 00000000..d39ecdd6 --- /dev/null +++ b/test/cram/test_build_dynamic_join/join_kinds.t/join_kinds.compare.ml @@ -0,0 +1,810 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Inner_join_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Join_using_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p USING (user_id) WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p USING (user_id) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p USING (user_id) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Join_natural_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u NATURAL LEFT JOIN profiles p WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u NATURAL LEFT JOIN profiles p WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u NATURAL LEFT JOIN profiles p WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Using_after_candidate_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let amount = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int_nullable row idx, idx + 1)); + column = ("o.amount"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id JOIN orders o USING (bio) WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id JOIN orders o USING (bio) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id JOIN orders o USING (bio) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Natural_after_candidate_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let amount = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int_nullable row idx, idx + 1)); + column = ("o.amount"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id NATURAL JOIN orders o WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id NATURAL JOIN orders o WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id NATURAL JOIN orders o WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Right_join_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int_nullable row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u RIGHT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u RIGHT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u RIGHT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Comma_join_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u, profiles p WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u, profiles p WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u, profiles p WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Implicit_before_candidate_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let status = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("s.status"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u JOIN shipments s USING (user_id)" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u JOIN shipments s USING (user_id)" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u JOIN shipments s USING (user_id)" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT, user_id INT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + let create_orders db = + T.execute db ("CREATE TABLE orders (bio TEXT, amount INT)") T.no_params + + let create_shipments db = + T.execute db ("CREATE TABLE shipments (user_id INT, status TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/join_kinds.t/join_kinds.sql b/test/cram/test_build_dynamic_join/join_kinds.t/join_kinds.sql new file mode 100644 index 00000000..da337fbf --- /dev/null +++ b/test/cram/test_build_dynamic_join/join_kinds.t/join_kinds.sql @@ -0,0 +1,28 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT, user_id INT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +CREATE TABLE orders (bio TEXT, amount INT); +CREATE TABLE shipments (user_id INT, status TEXT); +-- [sqlgg] dynamic_select=true +-- @inner_join +SELECT u.id, p.bio FROM users u JOIN profiles p ON p.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @join_using +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p USING (user_id) WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @join_natural +SELECT u.id, p.bio FROM users u NATURAL LEFT JOIN profiles p WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @using_after_candidate +SELECT u.id, p.bio, o.amount FROM users u LEFT JOIN profiles p ON p.user_id = u.id JOIN orders o USING (bio) WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @natural_after_candidate +SELECT u.id, p.bio, o.amount FROM users u LEFT JOIN profiles p ON p.user_id = u.id NATURAL JOIN orders o WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @right_join +SELECT u.id, p.bio FROM users u RIGHT JOIN profiles p ON p.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @comma_join +SELECT u.id, p.bio FROM users u, profiles p WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @implicit_before_candidate +SELECT u.id, p.bio, s.status FROM users u JOIN shipments s USING (user_id) LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = @uid; diff --git a/test/cram/test_build_dynamic_join/join_kinds.t/run.ml b/test/cram/test_build_dynamic_join/join_kinds.t/run.ml new file mode 100644 index 00000000..db44d56c --- /dev/null +++ b/test/cram/test_build_dynamic_join/join_kinds.t/run.ml @@ -0,0 +1,40 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Join_kinds = Join_kinds.Sqlgg(Print_impl) + +let () = + let open Join_kinds.Inner_join_col in + run "join_kinds/inner: pick id -> join kept (INNER)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Join_using_col in + run "join_kinds/using: pick id -> join kept (USING)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Join_natural_col in + run "join_kinds/natural: pick id -> join kept (NATURAL)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Using_after_candidate_col in + run "join_kinds/using_after: pick id -> candidate kept (later USING)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Natural_after_candidate_col in + run "join_kinds/natural_after: pick id -> candidate kept (later NATURAL)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Right_join_col in + run "join_kinds/right_join: pick id -> join kept (RIGHT)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Comma_join_col in + run "join_kinds/comma_join: pick id -> join kept (comma join)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Join_kinds.Implicit_before_candidate_col in + run "join_kinds/implicit_before: pick id -> join dropped (USING before candidate)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "join_kinds/implicit_before: pick bio -> join present" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/join_kinds.t/run.t b/test/cram/test_build_dynamic_join/join_kinds.t/run.t new file mode 100644 index 00000000..ac04df3a --- /dev/null +++ b/test/cram/test_build_dynamic_join/join_kinds.t/run.t @@ -0,0 +1,61 @@ +Join kinds: only LEFT JOIN ... ON is a candidate (INNER removes rows, RIGHT +flips sides, comma join has no ON, USING/NATURAL have no ON to analyse). +USING/NATURAL only blocks candidates that come before it, not after. + +Generated code matches the golden file: + + $ cat join_kinds.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > join_kinds.ml + $ diff join_kinds.ml join_kinds.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c join_kinds.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe join_kinds.cmo print_impl.cmo run.ml + $ ./run.exe + === join_kinds/inner: pick id -> join kept (INNER) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/using: pick id -> join kept (USING) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u.id FROM users u LEFT JOIN profiles p USING (user_id) WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p USING (user_id) WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/natural: pick id -> join kept (NATURAL) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.id FROM users u NATURAL LEFT JOIN profiles p WHERE u.id = ? + [SQL] SELECT u.id FROM users u NATURAL LEFT JOIN profiles p WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/using_after: pick id -> candidate kept (later USING) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id JOIN orders o USING (bio) WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id JOIN orders o USING (bio) WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/natural_after: pick id -> candidate kept (later NATURAL) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id NATURAL JOIN orders o WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id NATURAL JOIN orders o WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/right_join: pick id -> join kept (RIGHT) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[6]: SELECT u.id FROM users u RIGHT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u RIGHT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/comma_join: pick id -> join kept (comma join) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[7]: SELECT u.id FROM users u, profiles p WHERE u.id = ? + [SQL] SELECT u.id FROM users u, profiles p WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/implicit_before: pick id -> join dropped (USING before candidate) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[8]: SELECT u.id FROM users u JOIN shipments s USING (user_id) WHERE u.id = ? + [SQL] SELECT u.id FROM users u JOIN shipments s USING (user_id) WHERE u.id = 1 + [MOCK] Returning 0 rows + === join_kinds/implicit_before: pick bio -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[9]: SELECT p.bio FROM users u JOIN shipments s USING (user_id) LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT p.bio FROM users u JOIN shipments s USING (user_id) LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/key_shapes.t/key_shapes.compare.ml b/test/cram/test_build_dynamic_join/key_shapes.t/key_shapes.compare.ml new file mode 100644 index 00000000..02b2c160 --- /dev/null +++ b/test/cram/test_build_dynamic_join/key_shapes.t/key_shapes.compare.ml @@ -0,0 +1,331 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Unique_key_col = struct + type source = Accounts + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let label = + lift [Accounts] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.label"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Accounts col.deps then " LEFT JOIN accounts a ON a.email = u.email" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Accounts col.deps then " LEFT JOIN accounts a ON a.email = u.email" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Accounts col.deps then " LEFT JOIN accounts a ON a.email = u.email" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Composite_partial_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let title = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("m.title"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN memberships m ON m.org = u.org WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN memberships m ON m.org = u.org WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN memberships m ON m.org = u.org WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Composite_full_col = struct + type source = Memberships + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let title = + lift [Memberships] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("m.title"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Memberships col.deps then " LEFT JOIN memberships m ON m.org = u.org AND m.dept = u.dept" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Memberships col.deps then " LEFT JOIN memberships m ON m.org = u.org AND m.dept = u.dept" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Memberships col.deps then " LEFT JOIN memberships m ON m.org = u.org AND m.dept = u.dept" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT, email TEXT, org INT, dept INT)") T.no_params + + let create_accounts db = + T.execute db ("CREATE TABLE accounts (id INT PRIMARY KEY, email TEXT UNIQUE, label TEXT)") T.no_params + + let create_memberships db = + T.execute db ("CREATE TABLE memberships (org INT, dept INT, title TEXT, PRIMARY KEY (org, dept))") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/key_shapes.t/key_shapes.sql b/test/cram/test_build_dynamic_join/key_shapes.t/key_shapes.sql new file mode 100644 index 00000000..fcd6a422 --- /dev/null +++ b/test/cram/test_build_dynamic_join/key_shapes.t/key_shapes.sql @@ -0,0 +1,12 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT, email TEXT, org INT, dept INT); +CREATE TABLE accounts (id INT PRIMARY KEY, email TEXT UNIQUE, label TEXT); +CREATE TABLE memberships (org INT, dept INT, title TEXT, PRIMARY KEY (org, dept)); +-- [sqlgg] dynamic_select=true +-- @unique_key +SELECT u.id, a.label FROM users u LEFT JOIN accounts a ON a.email = u.email WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @composite_partial +SELECT u.id, m.title FROM users u LEFT JOIN memberships m ON m.org = u.org WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @composite_full +SELECT u.id, m.title FROM users u LEFT JOIN memberships m ON m.org = u.org AND m.dept = u.dept WHERE u.id = @uid; diff --git a/test/cram/test_build_dynamic_join/key_shapes.t/run.ml b/test/cram/test_build_dynamic_join/key_shapes.t/run.ml new file mode 100644 index 00000000..186efc8e --- /dev/null +++ b/test/cram/test_build_dynamic_join/key_shapes.t/run.ml @@ -0,0 +1,21 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Key_shapes = Key_shapes.Sqlgg(Print_impl) + +let () = + let open Key_shapes.Unique_key_col in + run "key_shapes/unique: pick id -> join dropped (UNIQUE key)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "key_shapes/unique: pick label -> join present" (fun () -> ignore (List.select () label ~uid:1L (fun x -> x))) + +let () = + let open Key_shapes.Composite_partial_col in + run "key_shapes/composite_partial: pick id -> join kept" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Key_shapes.Composite_full_col in + run "key_shapes/composite_full: pick id -> join dropped" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "key_shapes/composite_full: pick title -> join present" (fun () -> ignore (List.select () title ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/key_shapes.t/run.t b/test/cram/test_build_dynamic_join/key_shapes.t/run.t new file mode 100644 index 00000000..8a21967a --- /dev/null +++ b/test/cram/test_build_dynamic_join/key_shapes.t/run.t @@ -0,0 +1,39 @@ +Key shapes: non-PK UNIQUE works, composite PK only when ALL parts are equated. + +Generated code matches the golden file: + + $ cat key_shapes.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > key_shapes.ml + $ diff key_shapes.ml key_shapes.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c key_shapes.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe key_shapes.cmo print_impl.cmo run.ml + $ ./run.exe + === key_shapes/unique: pick id -> join dropped (UNIQUE key) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === key_shapes/unique: pick label -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT a.label FROM users u LEFT JOIN accounts a ON a.email = u.email WHERE u.id = ? + [SQL] SELECT a.label FROM users u LEFT JOIN accounts a ON a.email = u.email WHERE u.id = 1 + [MOCK] Returning 0 rows + === key_shapes/composite_partial: pick id -> join kept === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.id FROM users u LEFT JOIN memberships m ON m.org = u.org WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN memberships m ON m.org = u.org WHERE u.id = 1 + [MOCK] Returning 0 rows + === key_shapes/composite_full: pick id -> join dropped === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === key_shapes/composite_full: pick title -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT m.title FROM users u LEFT JOIN memberships m ON m.org = u.org AND m.dept = u.dept WHERE u.id = ? + [SQL] SELECT m.title FROM users u LEFT JOIN memberships m ON m.org = u.org AND m.dept = u.dept WHERE u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/multi.t/multi.compare.ml b/test/cram/test_build_dynamic_join/multi.t/multi.compare.ml new file mode 100644 index 00000000..cc1e951c --- /dev/null +++ b/test/cram/test_build_dynamic_join/multi.t/multi.compare.ml @@ -0,0 +1,352 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Two_indep_col = struct + type source = Avatars | Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let url = + lift [Avatars] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Avatars col.deps then " LEFT JOIN avatars a ON a.id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Subq_in_on_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let url = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("a.url"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = (SELECT MAX(id) FROM avatars) WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = (SELECT MAX(id) FROM avatars) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = (SELECT MAX(id) FROM avatars) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Same_twice_col = struct + type source = Profiles_p1 | Profiles_p2 + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio1 = + lift [Profiles_p1] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p1.bio"); + count = 0; + } + let bio2 = + lift [Profiles_p2] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p2.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles_p1 col.deps then " LEFT JOIN profiles p1 ON p1.user_id = u.id" else "") ^ (if List.mem Profiles_p2 col.deps then " LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles_p1 col.deps then " LEFT JOIN profiles p1 ON p1.user_id = u.id" else "") ^ (if List.mem Profiles_p2 col.deps then " LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles_p1 col.deps then " LEFT JOIN profiles p1 ON p1.user_id = u.id" else "") ^ (if List.mem Profiles_p2 col.deps then " LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT, mentor_id INT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + let create_avatars db = + T.execute db ("CREATE TABLE avatars (id INT PRIMARY KEY, url TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/multi.t/multi.sql b/test/cram/test_build_dynamic_join/multi.t/multi.sql new file mode 100644 index 00000000..a7a1c5b9 --- /dev/null +++ b/test/cram/test_build_dynamic_join/multi.t/multi.sql @@ -0,0 +1,12 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT, mentor_id INT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +CREATE TABLE avatars (id INT PRIMARY KEY, url TEXT); +-- [sqlgg] dynamic_select=true +-- @two_indep +SELECT u.id, p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @subq_in_on +SELECT u.id, p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = (SELECT MAX(id) FROM avatars) WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @same_twice +SELECT u.id, p1.bio AS bio1, p2.bio AS bio2 FROM users u LEFT JOIN profiles p1 ON p1.user_id = u.id LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id WHERE u.id = @uid; diff --git a/test/cram/test_build_dynamic_join/multi.t/run.ml b/test/cram/test_build_dynamic_join/multi.t/run.ml new file mode 100644 index 00000000..0eea16af --- /dev/null +++ b/test/cram/test_build_dynamic_join/multi.t/run.ml @@ -0,0 +1,26 @@ +module S = Multi.Sqlgg(Print_impl) + +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +let () = + let open S.Two_indep_col in + run "two_indep: pick id (no joins)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "two_indep: pick bio (profiles only)" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))); + run "two_indep: pick url (avatars only)" (fun () -> ignore (List.select () url ~uid:1L (fun x -> x))); + run "two_indep: pick bio+url (both)" (fun () -> + ignore (List.select () (let+ b = bio and+ u = url in (b, u)) ~uid:1L (fun x -> x))) + +let () = + let open S.Subq_in_on_col in + run "subq_in_on: pick id -> everything kept (subquery in another join's ON)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open S.Same_twice_col in + run "same_twice: pick bio1 (p1 only)" (fun () -> ignore (List.select () bio1 ~uid:1L (fun x -> x))); + run "same_twice: pick bio2 (p2 only)" (fun () -> ignore (List.select () bio2 ~uid:1L (fun x -> x))); + run "same_twice: pick both" (fun () -> + ignore (List.select () (let+ a = bio1 and+ b = bio2 in (a, b)) ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/multi.t/run.t b/test/cram/test_build_dynamic_join/multi.t/run.t new file mode 100644 index 00000000..0f70ccc5 --- /dev/null +++ b/test/cram/test_build_dynamic_join/multi.t/run.t @@ -0,0 +1,56 @@ +Multiple joins in one FROM: two independent droppables, a subquery in another +join's ON (conservative keep of everything), the same table joined twice +(distinct constructors per alias). + +Generated code matches the golden file: + + $ cat multi.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > multi.ml + $ diff multi.ml multi.compare.ml + +Runtime (print_impl mock): each pick renders only its own join: + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c multi.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe multi.cmo print_impl.cmo run.ml + $ ./run.exe + === two_indep: pick id (no joins) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === two_indep: pick bio (profiles only) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === two_indep: pick url (avatars only) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT a.url FROM users u LEFT JOIN avatars a ON a.id = u.id WHERE u.id = ? + [SQL] SELECT a.url FROM users u LEFT JOIN avatars a ON a.id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === two_indep: pick bio+url (both) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = u.id WHERE u.id = ? + [SQL] SELECT p.bio, a.url FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === subq_in_on: pick id -> everything kept (subquery in another join's ON) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = (SELECT MAX(id) FROM avatars) WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN avatars a ON a.id = (SELECT MAX(id) FROM avatars) WHERE u.id = 1 + [MOCK] Returning 0 rows + === same_twice: pick bio1 (p1 only) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[6]: SELECT p1.bio FROM users u LEFT JOIN profiles p1 ON p1.user_id = u.id WHERE u.id = ? + [SQL] SELECT p1.bio FROM users u LEFT JOIN profiles p1 ON p1.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === same_twice: pick bio2 (p2 only) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[7]: SELECT p2.bio FROM users u LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id WHERE u.id = ? + [SQL] SELECT p2.bio FROM users u LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === same_twice: pick both === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[8]: SELECT p1.bio, p2.bio FROM users u LEFT JOIN profiles p1 ON p1.user_id = u.id LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id WHERE u.id = ? + [SQL] SELECT p1.bio, p2.bio FROM users u LEFT JOIN profiles p1 ON p1.user_id = u.id LEFT JOIN profiles p2 ON p2.user_id = u.mentor_id WHERE u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/on_shapes.t/on_shapes.compare.ml b/test/cram/test_build_dynamic_join/on_shapes.t/on_shapes.compare.ml new file mode 100644 index 00000000..e4420c2c --- /dev/null +++ b/test/cram/test_build_dynamic_join/on_shapes.t/on_shapes.compare.ml @@ -0,0 +1,831 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Param_in_on_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~b ~uid callback = + let set_params stmt = + let p = T.start_params stmt (2 + col.count) in + col.set p; + T.set_param_Text p b; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = ? WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~b ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (2 + col.count) in + col.set p; + T.set_param_Text p b; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = ? WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~b ~uid callback = + let set_params stmt = + let p = T.start_params stmt (2 + col.count) in + col.set p; + T.set_param_Text p b; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = ? WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Extra_const_on_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x'" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x'" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x'" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module On_inequality_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id > u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id > u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id > u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module No_alias_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("profiles.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles ON profiles.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles ON profiles.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles ON profiles.user_id = u.id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Flipped_on_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON u.id = p.user_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON u.id = p.user_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON u.id = p.user_id" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Const_key_on_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = 5" else "") ^ " WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = 5" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = 5" else "") ^ " WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Or_in_on_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id OR p.user_id = 0 WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id OR p.user_id = 0 WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id OR p.user_id = 0 WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Subq_own_on_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = (SELECT MAX(id) FROM users) WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = (SELECT MAX(id) FROM users) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = (SELECT MAX(id) FROM users) WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/on_shapes.t/on_shapes.sql b/test/cram/test_build_dynamic_join/on_shapes.t/on_shapes.sql new file mode 100644 index 00000000..ea6151bd --- /dev/null +++ b/test/cram/test_build_dynamic_join/on_shapes.t/on_shapes.sql @@ -0,0 +1,26 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +-- [sqlgg] dynamic_select=true +-- @param_in_on +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = @b WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @extra_const_on +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x' WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @on_inequality +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id > u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @no_alias +SELECT u.id, profiles.bio FROM users u LEFT JOIN profiles ON profiles.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @flipped_on +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON u.id = p.user_id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @const_key_on +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = 5 WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @or_in_on +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id OR p.user_id = 0 WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @subq_own_on +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = (SELECT MAX(id) FROM users) WHERE u.id = @uid; diff --git a/test/cram/test_build_dynamic_join/on_shapes.t/run.ml b/test/cram/test_build_dynamic_join/on_shapes.t/run.ml new file mode 100644 index 00000000..0b3c85ae --- /dev/null +++ b/test/cram/test_build_dynamic_join/on_shapes.t/run.ml @@ -0,0 +1,43 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module On_shapes = On_shapes.Sqlgg(Print_impl) + +let () = + let open On_shapes.Param_in_on_col in + run "on_shapes/param_in_on: pick id -> join kept (param in ON)" (fun () -> ignore (List.select () id ~b:"x" ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.Extra_const_on_col in + run "on_shapes/extra_const_on: pick id -> join dropped" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "on_shapes/extra_const_on: pick bio -> join present" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.On_inequality_col in + run "on_shapes/inequality: pick id -> join kept" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.No_alias_col in + run "on_shapes/no_alias: pick id -> join dropped" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "on_shapes/no_alias: pick bio -> join present" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.Flipped_on_col in + run "on_shapes/flipped_on: pick id -> join dropped (operand order does not matter)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "on_shapes/flipped_on: pick bio -> join present" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.Const_key_on_col in + run "on_shapes/const_key_on: pick id -> join dropped (key equated to a constant)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))); + run "on_shapes/const_key_on: pick bio -> join present" (fun () -> ignore (List.select () bio ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.Or_in_on_col in + run "on_shapes/or_in_on: pick id -> join kept (OR can match many rows)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open On_shapes.Subq_own_on_col in + run "on_shapes/subq_own_on: pick id -> join kept (subquery in own ON)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/on_shapes.t/run.t b/test/cram/test_build_dynamic_join/on_shapes.t/run.t new file mode 100644 index 00000000..72d5db6d --- /dev/null +++ b/test/cram/test_build_dynamic_join/on_shapes.t/run.t @@ -0,0 +1,77 @@ +ON shapes: parameter in ON keeps the join, an extra constant conjunct does not, +inequality keeps it, a table without an alias is matched by its own name, +operand order in the key equation does not matter, a key equated to a constant +is droppable, OR and a subquery in the candidate's own ON keep the join. + +Generated code matches the golden file: + + $ cat on_shapes.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > on_shapes.ml + $ diff on_shapes.ml on_shapes.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c on_shapes.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe on_shapes.cmo print_impl.cmo run.ml + $ ./run.exe + === on_shapes/param_in_on: pick id -> join kept (param in ON) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = ? WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x' WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/extra_const_on: pick id -> join dropped === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/extra_const_on: pick bio -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x' WHERE u.id = ? + [SQL] SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id AND p.bio = 'x' WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/inequality: pick id -> join kept === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id > u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id > u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/no_alias: pick id -> join dropped === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/no_alias: pick bio -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[6]: SELECT profiles.bio FROM users u LEFT JOIN profiles ON profiles.user_id = u.id WHERE u.id = ? + [SQL] SELECT profiles.bio FROM users u LEFT JOIN profiles ON profiles.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/flipped_on: pick id -> join dropped (operand order does not matter) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[7]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/flipped_on: pick bio -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[8]: SELECT p.bio FROM users u LEFT JOIN profiles p ON u.id = p.user_id WHERE u.id = ? + [SQL] SELECT p.bio FROM users u LEFT JOIN profiles p ON u.id = p.user_id WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/const_key_on: pick id -> join dropped (key equated to a constant) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[9]: SELECT u.id FROM users u WHERE u.id = ? + [SQL] SELECT u.id FROM users u WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/const_key_on: pick bio -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[10]: SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = 5 WHERE u.id = ? + [SQL] SELECT p.bio FROM users u LEFT JOIN profiles p ON p.user_id = 5 WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/or_in_on: pick id -> join kept (OR can match many rows) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[11]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id OR p.user_id = 0 WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id OR p.user_id = 0 WHERE u.id = 1 + [MOCK] Returning 0 rows + === on_shapes/subq_own_on: pick id -> join kept (subquery in own ON) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[12]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = (SELECT MAX(id) FROM users) WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = (SELECT MAX(id) FROM users) WHERE u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/outside_refs.t/outside_refs.compare.ml b/test/cram/test_build_dynamic_join/outside_refs.t/outside_refs.compare.ml new file mode 100644 index 00000000..704cf8c4 --- /dev/null +++ b/test/cram/test_build_dynamic_join/outside_refs.t/outside_refs.compare.ml @@ -0,0 +1,659 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Ref_in_group_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY p.bio") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY p.bio") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY p.bio") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Ref_in_order_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id ORDER BY p.bio") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id ORDER BY p.bio") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id ORDER BY p.bio") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Ref_in_having_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY u.id HAVING MAX(p.user_id) > 0") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY u.id HAVING MAX(p.user_id) > 0") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY u.id HAVING MAX(p.user_id) > 0") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Complex_proj_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let shout = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("CONCAT(p.bio, '!')"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Subq_in_where_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id IN (SELECT user_id FROM profiles)") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id IN (SELECT user_id FROM profiles)") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id IN (SELECT user_id FROM profiles)") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Unqualified_where_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE bio = 'x'") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE bio = 'x'") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE bio = 'x'") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Join_unreferenced_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let name = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("u.name"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/outside_refs.t/outside_refs.sql b/test/cram/test_build_dynamic_join/outside_refs.t/outside_refs.sql new file mode 100644 index 00000000..8968a54f --- /dev/null +++ b/test/cram/test_build_dynamic_join/outside_refs.t/outside_refs.sql @@ -0,0 +1,23 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +-- [sqlgg] dynamic_select=true +-- @ref_in_group +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY p.bio; +-- [sqlgg] dynamic_select=true +-- @ref_in_order +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id ORDER BY p.bio; +-- [sqlgg] dynamic_select=true +-- @ref_in_having +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY u.id HAVING MAX(p.user_id) > 0; +-- [sqlgg] dynamic_select=true +-- @complex_proj +SELECT u.id, CONCAT(p.bio, '!') AS shout FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @subq_in_where +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id IN (SELECT user_id FROM profiles); +-- [sqlgg] dynamic_select=true +-- @unqualified_where +SELECT u.id, p.bio FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE bio = 'x'; +-- [sqlgg] dynamic_select=true +-- @join_unreferenced +SELECT u.id, u.name FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = @uid; diff --git a/test/cram/test_build_dynamic_join/outside_refs.t/run.ml b/test/cram/test_build_dynamic_join/outside_refs.t/run.ml new file mode 100644 index 00000000..92dec98c --- /dev/null +++ b/test/cram/test_build_dynamic_join/outside_refs.t/run.ml @@ -0,0 +1,35 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Outside_refs = Outside_refs.Sqlgg(Print_impl) + +let () = + let open Outside_refs.Ref_in_group_col in + run "outside_refs/group: pick id -> join kept (GROUP BY)" (fun () -> ignore (List.select () id (fun x -> x))) + +let () = + let open Outside_refs.Ref_in_order_col in + run "outside_refs/order: pick id -> join kept (ORDER BY)" (fun () -> ignore (List.select () id (fun x -> x))) + +let () = + let open Outside_refs.Ref_in_having_col in + run "outside_refs/having: pick id -> join kept (HAVING)" (fun () -> ignore (List.select () id (fun x -> x))) + +let () = + let open Outside_refs.Complex_proj_col in + run "outside_refs/complex_proj: pick id -> join kept (complex expr)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Outside_refs.Subq_in_where_col in + run "outside_refs/subq_in_where: pick id -> join kept (subquery in WHERE)" (fun () -> ignore (List.select () id (fun x -> x))) + +let () = + let open Outside_refs.Unqualified_where_col in + run "outside_refs/unqualified: pick id -> join kept (unqualified ref)" (fun () -> ignore (List.select () id (fun x -> x))) + +let () = + let open Outside_refs.Join_unreferenced_col in + run "outside_refs/unreferenced: pick id -> join rendered statically" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/outside_refs.t/run.t b/test/cram/test_build_dynamic_join/outside_refs.t/run.t new file mode 100644 index 00000000..64387229 --- /dev/null +++ b/test/cram/test_build_dynamic_join/outside_refs.t/run.t @@ -0,0 +1,52 @@ +References outside the projection (GROUP BY / ORDER BY / HAVING / complex +projection expression / subquery in WHERE / unqualified column) pin the join; +a droppable join never referenced by the projection gets no hole and stays +static. + +Generated code matches the golden file: + + $ cat outside_refs.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > outside_refs.ml + $ diff outside_refs.ml outside_refs.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c outside_refs.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe outside_refs.cmo print_impl.cmo run.ml + $ ./run.exe + === outside_refs/group: pick id -> join kept (GROUP BY) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY p.bio + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY p.bio + [MOCK] Returning 0 rows + === outside_refs/order: pick id -> join kept (ORDER BY) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id ORDER BY p.bio + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id ORDER BY p.bio + [MOCK] Returning 0 rows + === outside_refs/having: pick id -> join kept (HAVING) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY u.id HAVING MAX(p.user_id) > 0 + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id GROUP BY u.id HAVING MAX(p.user_id) > 0 + [MOCK] Returning 0 rows + === outside_refs/complex_proj: pick id -> join kept (complex expr) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === outside_refs/subq_in_where: pick id -> join kept (subquery in WHERE) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id IN (SELECT user_id FROM profiles) + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id IN (SELECT user_id FROM profiles) + [MOCK] Returning 0 rows + === outside_refs/unqualified: pick id -> join kept (unqualified ref) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[6]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE bio = 'x' + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE bio = 'x' + [MOCK] Returning 0 rows + === outside_refs/unreferenced: pick id -> join rendered statically === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[7]: SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN profiles p ON p.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/self_join.t/run.ml b/test/cram/test_build_dynamic_join/self_join.t/run.ml new file mode 100644 index 00000000..e79ba142 --- /dev/null +++ b/test/cram/test_build_dynamic_join/self_join.t/run.ml @@ -0,0 +1,16 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Self_join = Self_join.Sqlgg(Print_impl) + +let () = + let open Self_join.Bad_col in + run "self_join/bad: pick id -> join kept (non-unique self key)" (fun () -> ignore (List.select () id (fun x -> x))) + +let () = + let open Self_join.Good_col in + run "self_join/good: pick id -> join dropped (PK self key)" (fun () -> ignore (List.select () id (fun x -> x))); + run "self_join/good: pick name -> join present" (fun () -> ignore (List.select () name (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/self_join.t/run.t b/test/cram/test_build_dynamic_join/self_join.t/run.t new file mode 100644 index 00000000..8c013467 --- /dev/null +++ b/test/cram/test_build_dynamic_join/self_join.t/run.t @@ -0,0 +1,30 @@ +Self-joins are matched by their alias key, not the bare table name: non-unique +key keeps the join, PK self-join is droppable. + +Generated code matches the golden file: + + $ cat self_join.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > self_join.ml + $ diff self_join.ml self_join.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c self_join.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe self_join.cmo print_impl.cmo run.ml + $ ./run.exe + === self_join/bad: pick id -> join kept (non-unique self key) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u1.id FROM users u1 LEFT JOIN users u2 ON u2.manager_id = u1.id + [SQL] SELECT u1.id FROM users u1 LEFT JOIN users u2 ON u2.manager_id = u1.id + [MOCK] Returning 0 rows + === self_join/good: pick id -> join dropped (PK self key) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u1.id FROM users u1 + [SQL] SELECT u1.id FROM users u1 + [MOCK] Returning 0 rows + === self_join/good: pick name -> join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u2.name FROM users u1 LEFT JOIN users u2 ON u2.id = u1.manager_id + [SQL] SELECT u2.name FROM users u1 LEFT JOIN users u2 ON u2.id = u1.manager_id + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/self_join.t/self_join.compare.ml b/test/cram/test_build_dynamic_join/self_join.t/self_join.compare.ml new file mode 100644 index 00000000..33088610 --- /dev/null +++ b/test/cram/test_build_dynamic_join/self_join.t/self_join.compare.ml @@ -0,0 +1,210 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Bad_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u1.id"); + count = 0; + } + let name = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("u2.name"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u1 LEFT JOIN users u2 ON u2.manager_id = u1.id") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u1 LEFT JOIN users u2 ON u2.manager_id = u1.id") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.count) in + col.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u1 LEFT JOIN users u2 ON u2.manager_id = u1.id") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Good_col = struct + type source = Users + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u1.id"); + count = 0; + } + let name = + lift [Users] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("u2.name"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.projection.count) in + col.projection.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u1" ^ (if List.mem Users col.deps then " LEFT JOIN users u2 ON u2.id = u1.manager_id" else "")) + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.projection.count) in + col.projection.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u1" ^ (if List.mem Users col.deps then " LEFT JOIN users u2 ON u2.id = u1.manager_id" else "")) + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.projection.count) in + col.projection.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM users u1" ^ (if List.mem Users col.deps then " LEFT JOIN users u2 ON u2.id = u1.manager_id" else "")) + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT, manager_id INT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/self_join.t/self_join.sql b/test/cram/test_build_dynamic_join/self_join.t/self_join.sql new file mode 100644 index 00000000..074c38e0 --- /dev/null +++ b/test/cram/test_build_dynamic_join/self_join.t/self_join.sql @@ -0,0 +1,7 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT, manager_id INT); +-- [sqlgg] dynamic_select=true +-- @bad +SELECT u1.id, u2.name FROM users u1 LEFT JOIN users u2 ON u2.manager_id = u1.id; +-- [sqlgg] dynamic_select=true +-- @good +SELECT u1.id, u2.name FROM users u1 LEFT JOIN users u2 ON u2.id = u1.manager_id; diff --git a/test/cram/test_build_dynamic_join/subquery_sources.t/run.ml b/test/cram/test_build_dynamic_join/subquery_sources.t/run.ml new file mode 100644 index 00000000..74b3896e --- /dev/null +++ b/test/cram/test_build_dynamic_join/subquery_sources.t/run.ml @@ -0,0 +1,24 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Subquery_sources = Subquery_sources.Sqlgg(Print_impl) + +let () = + let open Subquery_sources.Join_subq_source_col in + run "subquery_sources/plain: pick id -> join kept (subquery source)" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Subquery_sources.Subq_join_dup_col in + run "subquery_sources/cross_dup: pick id -> join kept" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Subquery_sources.Subq_union_dup_col in + run "subquery_sources/union_dup: pick id -> join kept" (fun () -> ignore (List.select () id ~uid:1L (fun x -> x))) + +let () = + let open Subquery_sources.Subq_base_join_col in + run "subquery_sources/subq_base: pick id -> table join dropped" (fun () -> ignore (List.select () id (fun x -> x))); + run "subquery_sources/subq_base: pick bio -> table join present" (fun () -> ignore (List.select () bio (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/subquery_sources.t/run.t b/test/cram/test_build_dynamic_join/subquery_sources.t/run.t new file mode 100644 index 00000000..38737a8c --- /dev/null +++ b/test/cram/test_build_dynamic_join/subquery_sources.t/run.t @@ -0,0 +1,42 @@ +Subquery sources: a LEFT JOIN of a subquery is never droppable (its columns +inherit the underlying table's UNIQUE/PRIMARY marks but the subquery may +multiply rows); a subquery as the BASE source does not poison a droppable +table join on top of it. + +Generated code matches the golden file: + + $ cat subquery_sources.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > subquery_sources.ml + $ diff subquery_sources.ml subquery_sources.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c subquery_sources.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe subquery_sources.cmo print_impl.cmo run.ml + $ ./run.exe + === subquery_sources/plain: pick id -> join kept (subquery source) === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === subquery_sources/cross_dup: pick id -> join kept === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u.id FROM users u LEFT JOIN (SELECT p.user_id, p.bio FROM profiles p, users x) s ON s.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN (SELECT p.user_id, p.bio FROM profiles p, users x) s ON s.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === subquery_sources/union_dup: pick id -> join kept === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.id FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles UNION ALL SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ? + [SQL] SELECT u.id FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles UNION ALL SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = 1 + [MOCK] Returning 0 rows + === subquery_sources/subq_base: pick id -> table join dropped === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT s.id FROM (SELECT id FROM users) s + [SQL] SELECT s.id FROM (SELECT id FROM users) s + [MOCK] Returning 0 rows + === subquery_sources/subq_base: pick bio -> table join present === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[5]: SELECT p.bio FROM (SELECT id FROM users) s LEFT JOIN profiles p ON p.user_id = s.id + [SQL] SELECT p.bio FROM (SELECT id FROM users) s LEFT JOIN profiles p ON p.user_id = s.id + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/subquery_sources.t/subquery_sources.compare.ml b/test/cram/test_build_dynamic_join/subquery_sources.t/subquery_sources.compare.ml new file mode 100644 index 00000000..a48ba552 --- /dev/null +++ b/test/cram/test_build_dynamic_join/subquery_sources.t/subquery_sources.compare.ml @@ -0,0 +1,404 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Join_subq_source_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("s.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Subq_join_dup_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("s.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT p.user_id, p.bio FROM profiles p, users x) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT p.user_id, p.bio FROM profiles p, users x) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT p.user_id, p.bio FROM profiles p, users x) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Subq_union_dup_col = struct + type 'a t = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + let pure x = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + } + + let apply f a = { + set = (fun p -> f.set p; a.set p); + read = (fun row idx -> + let (vf, i1) = f.read row idx in + let (va, i2) = a.read row i1 in + (vf va, i2)); + column = (match f.column, a.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.count + a.count; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + let id = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let bio = + { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("s.bio"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles UNION ALL SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles UNION ALL SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.count) in + col.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.column ^ " FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles UNION ALL SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + module Subq_base_join_col = struct + type source = Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("s.id"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.projection.count) in + col.projection.set p; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ " FROM (SELECT id FROM users) s" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = s.id" else "")) + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) callback acc = + let set_params stmt = + let p = T.start_params stmt (0 + col.projection.count) in + col.projection.set p; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM (SELECT id FROM users) s" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = s.id" else "")) + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) callback = + let set_params stmt = + let p = T.start_params stmt (0 + col.projection.count) in + col.projection.set p; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ " FROM (SELECT id FROM users) s" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = s.id" else "")) + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/subquery_sources.t/subquery_sources.sql b/test/cram/test_build_dynamic_join/subquery_sources.t/subquery_sources.sql new file mode 100644 index 00000000..6173922b --- /dev/null +++ b/test/cram/test_build_dynamic_join/subquery_sources.t/subquery_sources.sql @@ -0,0 +1,14 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +-- [sqlgg] dynamic_select=true +-- @join_subq_source +SELECT u.id, s.bio FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @subq_join_dup +SELECT u.id, s.bio FROM users u LEFT JOIN (SELECT p.user_id, p.bio FROM profiles p, users x) s ON s.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @subq_union_dup +SELECT u.id, s.bio FROM users u LEFT JOIN (SELECT user_id, bio FROM profiles UNION ALL SELECT user_id, bio FROM profiles) s ON s.user_id = u.id WHERE u.id = @uid; +-- [sqlgg] dynamic_select=true +-- @subq_base_join +SELECT s.id, p.bio FROM (SELECT id FROM users) s LEFT JOIN profiles p ON p.user_id = s.id; diff --git a/test/cram/test_build_dynamic_join/whitespace.t/run.ml b/test/cram/test_build_dynamic_join/whitespace.t/run.ml new file mode 100644 index 00000000..efd3e65b --- /dev/null +++ b/test/cram/test_build_dynamic_join/whitespace.t/run.ml @@ -0,0 +1,20 @@ +let run label f = + Printf.printf "=== %s ===\n%!" label; + Print_impl.clear_mock_responses (); + Print_impl.setup_select_response []; + f () + +module Whitespace = Whitespace.Sqlgg(Print_impl) + +let () = + let open Whitespace.Ws_col in + run "ws: pick id -> both joins dropped, no gaps" (fun () -> + ignore (List.select () id ~uid:1L (fun x -> x))); + run "ws: pick id + bio -> profiles kept, billing gap collapsed" (fun () -> + ignore (List.select () (let+ i = id and+ b = bio in (i, b)) ~uid:1L (fun x -> x))); + run "ws: pick id + plan -> profiles gap collapsed, billing kept" (fun () -> + ignore (List.select () (let+ i = id and+ p = plan in (i, p)) ~uid:1L (fun x -> x))); + run "ws: pick all -> both joins, single spaces" (fun () -> + ignore (List.select () + (let+ i = id and+ b = bio and+ p = plan in (i, b, p)) + ~uid:1L (fun x -> x))) diff --git a/test/cram/test_build_dynamic_join/whitespace.t/run.t b/test/cram/test_build_dynamic_join/whitespace.t/run.t new file mode 100644 index 00000000..5f1ea534 --- /dev/null +++ b/test/cram/test_build_dynamic_join/whitespace.t/run.t @@ -0,0 +1,52 @@ +Whitespace around dropped joins: a dropped join takes its leading whitespace +with it, a kept one is spliced back with a single space, string literals stay +untouched. + +Generated code matches the golden file: + + $ cat whitespace.sql | sqlgg -no-header -gen caml_io -params unnamed -gen caml -dialect mysql - > whitespace.ml + $ diff whitespace.ml whitespace.compare.ml + +Runtime (print_impl mock): + + $ cp ../../print_impl.ml . + $ ocamlfind ocamlc -package sqlgg.traits -I . -c print_impl.ml + $ ocamlfind ocamlc -package sqlgg.traits,sqlgg -I . -c whitespace.ml + $ ocamlfind ocamlc -package unix,sqlgg.traits -I . -linkpkg -o run.exe whitespace.cmo print_impl.cmo run.ml + $ ./run.exe + === ws: pick id -> both joins dropped, no gaps === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[1]: SELECT u.id + FROM users u + WHERE u.note = ' two spaces inside ' AND u.id = ? + [SQL] SELECT u.id + FROM users u + WHERE u.note = ' two spaces inside ' AND u.id = 1 + [MOCK] Returning 0 rows + === ws: pick id + bio -> profiles kept, billing gap collapsed === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[2]: SELECT u.id, p.bio + FROM users u LEFT JOIN profiles p ON p.user_id = u.id + WHERE u.note = ' two spaces inside ' AND u.id = ? + [SQL] SELECT u.id, p.bio + FROM users u LEFT JOIN profiles p ON p.user_id = u.id + WHERE u.note = ' two spaces inside ' AND u.id = 1 + [MOCK] Returning 0 rows + === ws: pick id + plan -> profiles gap collapsed, billing kept === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[3]: SELECT u.id, b.plan + FROM users u LEFT JOIN billing b ON b.user_id = u.id + WHERE u.note = ' two spaces inside ' AND u.id = ? + [SQL] SELECT u.id, b.plan + FROM users u LEFT JOIN billing b ON b.user_id = u.id + WHERE u.note = ' two spaces inside ' AND u.id = 1 + [MOCK] Returning 0 rows + === ws: pick all -> both joins, single spaces === + [MOCK SELECT] Connection type: [> `RO ] + [MOCK] PREPARE[4]: SELECT u.id, p.bio, b.plan + FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN billing b ON b.user_id = u.id + WHERE u.note = ' two spaces inside ' AND u.id = ? + [SQL] SELECT u.id, p.bio, b.plan + FROM users u LEFT JOIN profiles p ON p.user_id = u.id LEFT JOIN billing b ON b.user_id = u.id + WHERE u.note = ' two spaces inside ' AND u.id = 1 + [MOCK] Returning 0 rows diff --git a/test/cram/test_build_dynamic_join/whitespace.t/whitespace.compare.ml b/test/cram/test_build_dynamic_join/whitespace.t/whitespace.compare.ml new file mode 100644 index 00000000..fb5fa405 --- /dev/null +++ b/test/cram/test_build_dynamic_join/whitespace.t/whitespace.compare.ml @@ -0,0 +1,148 @@ +module Sqlgg (T : Sqlgg_traits.M) = struct + + module IO = Sqlgg_io.Blocking + module Ws_col = struct + type source = Billing | Profiles + + type 'a projection = { + set: T.params -> unit; + read: T.row -> int -> 'a * int; + column: string; + count: int; + } + + type 'a t = { + projection: 'a projection; + deps: source list; + } + + let pure x = { + projection = { + set = (fun _p -> ()); + read = (fun _row idx -> (x, idx)); + column = ""; + count = 0; + }; + deps = []; + } + + let apply f a = { + projection = { + set = (fun p -> f.projection.set p; a.projection.set p); + read = (fun row idx -> + let (vf, i1) = f.projection.read row idx in + let (va, i2) = a.projection.read row i1 in + (vf va, i2)); + column = (match f.projection.column, a.projection.column with + | "", c | c, "" -> c + | c1, c2 -> c1 ^ ", " ^ c2); + count = f.projection.count + a.projection.count; + }; + deps = f.deps @ List.filter (fun d -> not (List.mem d f.deps)) a.deps; + } + + let map f a = apply (pure f) a + + let (let+) t f = map f t + let (and+) a b = apply (map (fun a b -> (a, b)) a) b + + let lift deps projection = { projection; deps } + let id = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Int row idx, idx + 1)); + column = ("u.id"); + count = 0; + } + let name = + lift [] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("u.name"); + count = 0; + } + let bio = + lift [Profiles] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("p.bio"); + count = 0; + } + let plan = + lift [Billing] { + set = (fun _p -> ()); + read = (fun row idx -> (T.get_column_Text_nullable row idx, idx + 1)); + column = ("b.plan"); + count = 0; + } + + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + T.select db + ("SELECT " ^ col.projection.column ^ "\n\ +FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Billing col.deps then " LEFT JOIN billing b ON b.user_id = u.id" else "") ^ "\n\ +WHERE u.note = ' two spaces inside ' AND u.id = ?") + set_params (fun row -> let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) + + module Fold = struct + let select db (col : _ t) ~uid callback acc = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref acc in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ "\n\ +FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Billing col.deps then " LEFT JOIN billing b ON b.user_id = u.id" else "") ^ "\n\ +WHERE u.note = ' two spaces inside ' AND u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col !r_acc))) + (fun () -> IO.return !r_acc) + + end (* module Fold *) + + module List = struct + let select db (col : _ t) ~uid callback = + let set_params stmt = + let p = T.start_params stmt (1 + col.projection.count) in + col.projection.set p; + T.set_param_Int p uid; + T.finish_params p + in + let r_acc = ref [] in + IO.(>>=) (T.select db + ("SELECT " ^ col.projection.column ^ "\n\ +FROM users u" ^ (if List.mem Profiles col.deps then " LEFT JOIN profiles p ON p.user_id = u.id" else "") ^ (if List.mem Billing col.deps then " LEFT JOIN billing b ON b.user_id = u.id" else "") ^ "\n\ +WHERE u.note = ' two spaces inside ' AND u.id = ?") + set_params (fun row -> r_acc := (let (__sqlgg_r_col, __sqlgg_idx_after_col) = col.projection.read row 0 in callback + __sqlgg_r_col) :: !r_acc)) + (fun () -> IO.return (List.rev !r_acc)) + + end (* module List *) + + end + + + let create_users db = + T.execute db ("CREATE TABLE users (id INT PRIMARY KEY, name TEXT, note TEXT)") T.no_params + + let create_profiles db = + T.execute db ("CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT)") T.no_params + + let create_billing db = + T.execute db ("CREATE TABLE billing (user_id INT PRIMARY KEY, plan TEXT)") T.no_params + + module Fold = struct + end (* module Fold *) + + module List = struct + end (* module List *) +end (* module Sqlgg *) diff --git a/test/cram/test_build_dynamic_join/whitespace.t/whitespace.sql b/test/cram/test_build_dynamic_join/whitespace.t/whitespace.sql new file mode 100644 index 00000000..fbcc5f8b --- /dev/null +++ b/test/cram/test_build_dynamic_join/whitespace.t/whitespace.sql @@ -0,0 +1,12 @@ +CREATE TABLE users (id INT PRIMARY KEY, name TEXT, note TEXT); +CREATE TABLE profiles (user_id INT PRIMARY KEY, bio TEXT); +CREATE TABLE billing (user_id INT PRIMARY KEY, plan TEXT); +-- [sqlgg] dynamic_select=true +-- @ws +SELECT u.id, u.name, + p.bio, + b.plan +FROM users u +LEFT JOIN profiles p ON p.user_id = u.id +LEFT JOIN billing b ON b.user_id = u.id +WHERE u.note = ' two spaces inside ' AND u.id = @uid; diff --git a/test/cram/test_build_dynamic_subquery/dyn_subq.sql b/test/cram/test_build_dynamic_subquery/dyn_subq.sql new file mode 100644 index 00000000..3870243e --- /dev/null +++ b/test/cram/test_build_dynamic_subquery/dyn_subq.sql @@ -0,0 +1,26 @@ +CREATE TABLE products (id INT PRIMARY KEY, name TEXT, price INT); +CREATE TABLE users (id INT PRIMARY KEY, name TEXT); +CREATE TABLE orders (id INT PRIMARY KEY, user_id INT, total INT); + +-- Pass-through (SELECT *) over a subquery source: dynamic columns are pushed down +-- INTO the subquery projection. +-- [sqlgg] dynamic_select=true +-- @star_over_subq +SELECT * FROM (SELECT id, name, price FROM products WHERE price > @min) AS sub; + +-- Pass-through via explicit src.* over a subquery source. +-- [sqlgg] dynamic_select=true +-- @star_alias_over_subq +SELECT sub.* FROM (SELECT id, name, price FROM products) AS sub; + +-- Pass-through over a LEFT JOIN subquery: nullability of joined columns is honest, +-- and the dynamic projection still penetrates the subquery. +-- [sqlgg] dynamic_select=true +-- @star_over_join_subq +SELECT * FROM (SELECT u.id AS uid, u.name AS uname, o.total AS ototal FROM users u LEFT JOIN orders o ON o.user_id = u.id) AS sub; + +-- Non-pass-through: the outer query selects specific columns, so the dynamic stays +-- at the outer level and the subquery projection remains fixed. +-- [sqlgg] dynamic_select=true +-- @cols_over_subq +SELECT sub.id, sub.name FROM (SELECT id, name, price FROM products) AS sub WHERE sub.id = @id; diff --git a/test/cram/test_build_dynamic_subquery/test_run.ml b/test/cram/test_build_dynamic_subquery/test_run.ml new file mode 100644 index 00000000..52d6e7c7 --- /dev/null +++ b/test/cram/test_build_dynamic_subquery/test_run.ml @@ -0,0 +1,78 @@ +(* test_run.ml - exercise dynamic select over subquery sources. + + For each scenario we pick different runtime columns and let the printing mock + traits implementation echo the final SQL ([SQL] ...), so the test shows exactly + how the chosen columns are spliced into the (sub)query. *) + +open Printf + +module M (T: Sqlgg_traits.M with + type Types.Int.t = int64 and + type Types.Text.t = string and + type Types.Decimal.t = float and + type Types.Any.t = string) = struct + + module Sql = Output.Sqlgg(T) + open Sql + + let prep () = + Print_ocaml_impl.clear_mock_responses (); + Print_ocaml_impl.setup_select_response [] + + (* Group 1: pass-through SELECT * over a subquery -> pushdown into the subquery *) + module Test1 = struct + open Sql.Star_over_subq_col + + let run c = + printf "[1.1] pick id\n"; prep (); ignore (List.select c id ~min:10L (fun x -> x)); + printf "[1.2] pick name\n"; prep (); ignore (List.select c name ~min:10L (fun x -> x)); + printf "[1.3] pick price\n"; prep (); ignore (List.select c price ~min:10L (fun x -> x)); + printf "[1.4] pick id + name + price\n"; prep (); + ignore (List.select c (let+ i = id and+ n = name and+ p = price in (i, n, p)) ~min:10L (fun x -> x)) + end + + (* Group 2: pass-through via explicit sub.* *) + module Test2 = struct + open Sql.Star_alias_over_subq_col + + let run c = + printf "[2.1] pick name\n"; prep (); ignore (List.select c name (fun x -> x)); + printf "[2.2] pick id + price\n"; prep (); + ignore (List.select c (let+ i = id and+ p = price in (i, p)) (fun x -> x)) + end + + (* Group 3: pass-through over a LEFT JOIN subquery (nullable joined columns) *) + module Test3 = struct + open Sql.Star_over_join_subq_col + + let run c = + printf "[3.1] pick uid\n"; prep (); ignore (List.select c uid (fun x -> x)); + printf "[3.2] pick ototal (nullable)\n"; prep (); ignore (List.select c ototal (fun x -> x)); + printf "[3.3] pick uid + uname + ototal\n"; prep (); + ignore (List.select c (let+ a = uid and+ b = uname and+ d = ototal in (a, b, d)) (fun x -> x)) + end + + (* Group 4: non-pass-through outer -> dynamic stays outside, subquery fixed *) + module Test4 = struct + open Sql.Cols_over_subq_col + + let run c = + printf "[4.1] pick id\n"; prep (); ignore (List.select c id ~id:1L (fun x -> x)); + printf "[4.2] pick id + name\n"; prep (); + ignore (List.select c (let+ i = id and+ n = name in (i, n)) ~id:1L (fun x -> x)) + end + + let run_all c = + printf "--- Group 1: SELECT * over subquery (pushdown) ---\n"; Test1.run c; + printf "--- Group 2: SELECT sub.* over subquery (pushdown) ---\n"; Test2.run c; + printf "--- Group 3: SELECT * over LEFT JOIN subquery ---\n"; Test3.run c; + printf "--- Group 4: non-pass-through (dynamic stays outside) ---\n"; Test4.run c +end + +module Test = M(Print_ocaml_impl) + +let () = + let con = () in + printf "Dynamic Select over Subquery Sources\n"; + printf "%s\n" (String.make 50 '='); + Test.run_all con