From 66a6d8a22130a6f52d40c2649e728ef763c8d358 Mon Sep 17 00:00:00 2001 From: Niols Date: Sat, 4 Jul 2026 11:50:51 +0200 Subject: [PATCH 1/2] Add tests for PostgreSQL param numbering in reusable queries Add tests covering `include: reuse` queries spliced as CTEs via `WITH x AS &ref`, compiled with `-params postgresql`. The expected output captures the current behaviour, where each fragment restarts `$N` numbering at `$1` so the outer query's placeholders collide with the reused query's. --- .../cram/reusable_queries_postgresql_params.t | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 test/cram/reusable_queries_postgresql_params.t diff --git a/test/cram/reusable_queries_postgresql_params.t b/test/cram/reusable_queries_postgresql_params.t new file mode 100644 index 00000000..802cdb51 --- /dev/null +++ b/test/cram/reusable_queries_postgresql_params.t @@ -0,0 +1,176 @@ +PostgreSQL parameter numbering in reusable queries, distinct parameters: + + $ sqlgg -no-header -gen caml -params postgresql -dialect postgresql - <<'EOF' 2>/dev/null | awk '/^ let get_users/{p=1} p&&/^$/{exit} p{print}' + > CREATE TABLE person ( + > id INT PRIMARY KEY, + > user_id INT NOT NULL, + > name TEXT NOT NULL + > ); + > CREATE TABLE "user" ( + > id INT PRIMARY KEY, + > username TEXT NOT NULL + > ); + > -- @get_persons | include: reuse + > SELECT * + > FROM person + > WHERE name LIKE @name; + > -- @get_users + > WITH person AS &get_persons + > SELECT * + > FROM "user" + > JOIN person ON person.user_id = "user".id + > WHERE "user".id > @min_id; + > EOF + let get_users db ~name ~min_id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~username:(T.get_column_Text stmt 1) + ~id0:(T.get_column_Int stmt 2) + ~user_id:(T.get_column_Int stmt 3) + ~name:(T.get_column_Text stmt 4) + in + let set_params stmt = + let p = T.start_params stmt (2) in + T.set_param_Text p name; + T.set_param_Int p min_id; + T.finish_params p + in + T.select db ("WITH person AS (SELECT *\n\ + FROM person\n\ + WHERE name LIKE $1)\n\ + SELECT *\n\ + FROM \"user\"\n\ + JOIN person ON person.user_id = \"user\".id\n\ + WHERE \"user\".id > $1") set_params invoke_callback + +PostgreSQL parameter numbering in reusable queries, shared parameter: + + $ sqlgg -no-header -gen caml -params postgresql -dialect postgresql - <<'EOF' 2>/dev/null | awk '/^ let get_users/{p=1} p&&/^$/{exit} p{print}' + > CREATE TABLE person ( + > id INT PRIMARY KEY, + > user_id INT NOT NULL, + > name TEXT NOT NULL + > ); + > CREATE TABLE "user" ( + > id INT PRIMARY KEY, + > username TEXT NOT NULL + > ); + > -- @get_persons | include: reuse + > SELECT * + > FROM person + > WHERE + > name LIKE @name + > AND person.id > @min_id; + > -- @get_users + > WITH person AS &get_persons + > SELECT * + > FROM "user" + > JOIN person ON person.user_id = "user".id + > WHERE + > username LIKE @username + > AND "user".id > @min_id; + > EOF + let get_users db ~name ~username ~min_id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~username:(T.get_column_Text stmt 1) + ~id0:(T.get_column_Int stmt 2) + ~user_id:(T.get_column_Int stmt 3) + ~name:(T.get_column_Text stmt 4) + in + let set_params stmt = + let p = T.start_params stmt (4) in + T.set_param_Text p name; + T.set_param_Int p min_id; + T.set_param_Text p username; + T.set_param_Int p min_id; + T.finish_params p + in + T.select db ("WITH person AS (SELECT *\n\ + FROM person\n\ + WHERE\n\ + name LIKE $1\n\ + AND person.id > $2)\n\ + SELECT *\n\ + FROM \"user\"\n\ + JOIN person ON person.user_id = \"user\".id\n\ + WHERE\n\ + username LIKE $1\n\ + AND \"user\".id > $2") set_params invoke_callback + +PostgreSQL parameter numbering with two reusable queries in one outer query: + + $ sqlgg -no-header -gen caml -params postgresql -dialect postgresql - <<'EOF' 2>/dev/null | awk '/^ let two_ctes/{p=1} p&&/^$/{exit} p{print}' + > CREATE TABLE person ( + > id INT PRIMARY KEY, + > user_id INT NOT NULL, + > name TEXT NOT NULL + > ); + > CREATE TABLE "user" ( + > id INT PRIMARY KEY, + > username TEXT NOT NULL + > ); + > -- @get_persons | include: reuse + > SELECT * FROM person WHERE name LIKE @name; + > -- @get_admins | include: reuse + > SELECT * FROM "user" WHERE username LIKE @admin; + > -- @two_ctes + > WITH p AS &get_persons, a AS &get_admins + > SELECT p.id, a.username + > FROM p JOIN a ON a.id = p.user_id + > WHERE p.id > @min_id; + > EOF + let two_ctes db ~name ~admin ~min_id callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~username:(T.get_column_Text stmt 1) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + T.set_param_Text p admin; + T.set_param_Int p min_id; + T.finish_params p + in + T.select db ("WITH p AS (SELECT * FROM person WHERE name LIKE $1), a AS (SELECT * FROM \"user\" WHERE username LIKE $1)\n\ + SELECT p.id, a.username\n\ + FROM p JOIN a ON a.id = p.user_id\n\ + WHERE p.id > $1") set_params invoke_callback + +PostgreSQL parameter numbering with reusable queries threaded into each other: + + $ sqlgg -no-header -gen caml -params postgresql -dialect postgresql - <<'EOF' 2>/dev/null | awk '/^ let outer_q/{p=1} p&&/^$/{exit} p{print}' + > CREATE TABLE person ( + > id INT PRIMARY KEY, + > user_id INT NOT NULL, + > name TEXT NOT NULL + > ); + > -- @get_persons | include: reuse + > SELECT * FROM person WHERE name LIKE @name; + > -- @wrap_persons | include: reuse + > WITH inner_p AS &get_persons + > SELECT * FROM inner_p WHERE inner_p.id > @inner_min; + > -- @outer_q + > WITH p AS &wrap_persons + > SELECT * FROM p WHERE p.user_id > @outer_min; + > EOF + let outer_q db ~name ~inner_min ~outer_min callback = + let invoke_callback stmt = + callback + ~id:(T.get_column_Int stmt 0) + ~user_id:(T.get_column_Int stmt 1) + ~name:(T.get_column_Text stmt 2) + in + let set_params stmt = + let p = T.start_params stmt (3) in + T.set_param_Text p name; + T.set_param_Int p inner_min; + T.set_param_Int p outer_min; + T.finish_params p + in + T.select db ("WITH p AS (WITH inner_p AS (SELECT * FROM person WHERE name LIKE $1)\n\ + SELECT * FROM inner_p WHERE inner_p.id > $1)\n\ + SELECT * FROM p WHERE p.user_id > $1") set_params invoke_callback From 2a949ca6014e3052acae47f57e38aa56cd439a4f Mon Sep 17 00:00:00 2001 From: Niols Date: Sat, 4 Jul 2026 12:11:56 +0200 Subject: [PATCH 2/2] Fix numbering of PostgreSQL params across reusable queries When a reusable query (`include: reuse`) is spliced into an outer query as a CTE via `WITH x AS &ref`, its parameters and the outer query's parameters are emitted into a single statement and bound positionally by `set_params`. However, `substitute_vars` restarted the positional counter (`parami`) at 0 for the spliced fragment and then continued the outer query with the unchanged counter, so both fragments numbered their PostgreSQL `$N` placeholders from `$1`. Thread `parami` through the shared-query splice: `loop_and_squash` now takes the current counter and returns the advanced one, so numbering runs continuously across the whole statement. Only the PostgreSQL params mode was affected; named/oracle key by parameter name and unnamed uses `?`. --- src/gen.ml | 20 +++++++++---------- .../cram/reusable_queries_postgresql_params.t | 14 ++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/gen.ml b/src/gen.ml index 56c3e75d..42a90aad 100644 --- a/src/gen.ml +++ b/src/gen.ml @@ -90,7 +90,7 @@ and sql_dynamic_ctor = { let substitute_vars s vars subst_param = let rec loop s acc i parami vars = match vars with - | [] -> acc, i + | [] -> acc, i, parami | Sql.Single (param, _) :: tl -> let (i1,i2) = param.id.pos in assert (i2 > i1); @@ -116,7 +116,7 @@ let substitute_vars s vars subst_param = assert (i2 > i1); assert (i1 > i); let acc = - DynamicIn (name, kind, List.rev @@ fst @@ loop s [] i1 0 vars) :: + DynamicIn (name, kind, (let acc, _, _ = loop s [] i1 0 vars in List.rev acc)) :: Static (String.slice ~first:i ~last:i1 s) :: acc in @@ -161,7 +161,7 @@ let substitute_vars s vars subst_param = assert ((c2 = 0 && c1 = 1) || c2 > c1); assert (c1 > i); let pieces = - let (acc, last) = loop s [] c1 0 vars in + let (acc, last, _) = loop s [] c1 0 vars in let s_choice = let sql = [Static " ( "] @ List.rev(Static (String.slice ~first:last ~last:c2 s) :: acc) @ [Static " ) "]in let ctor = Sql.{ value=Some("Some"); pos=(0, 0); } in @@ -181,7 +181,7 @@ let substitute_vars s vars subst_param = assert (i2 > i1); assert (i1 > i); let shared_sql, (_: Sql.select_full) = Shared_queries.get id.value in - let raw_processed = loop_and_squash shared_sql shared_vars in + let raw_processed, parami = loop_and_squash ~parami shared_sql shared_vars in let processed_shared = [Static "("] @ raw_processed @ [Static ")"] in loop s (List.rev processed_shared @ Static (String.slice ~first:i ~last:i1 s) :: acc) i2 parami tl | DynamicSelect (name,ctors) :: tl -> @@ -191,7 +191,7 @@ let substitute_vars s vars subst_param = let sql = match args with | None | Some [] -> [Static (String.slice ~first:c1 ~last:c2 s)] | Some l -> - let (acc, last) = loop s [] (c1 - 1) 0 l in + let (acc, last, _) = loop s [] (c1 - 1) 0 l in let pieces = List.rev (Static (String.slice ~first:last ~last:c2 s) :: acc) in begin match pieces with | Static hd :: rest -> Static (String.slice ~first:1 hd) :: rest @@ -217,7 +217,7 @@ let substitute_vars s vars subst_param = match args with | None -> [Static ""] | Some l -> - let (acc, last) = loop s [] c1 0 l in + let (acc, last, _) = loop s [] c1 0 l in let body = List.rev (Static (String.slice ~first:last ~last:c2 s) :: acc) in squash [] ((Static " (" :: body) @ [Static ") "]) in @@ -225,17 +225,17 @@ let substitute_vars s vars subst_param = | Verbatim (n, v) -> { ctor = { value = Some n; pos = (0,0) }; args = Some []; sql = [Static v]; is_poly } end - and loop_and_squash sql vars = - let acc, last = loop sql [] 0 0 vars in + and loop_and_squash ?(parami=0) sql vars = + let acc, last, parami = loop sql [] 0 parami vars in let acc = List.rev (Static (String.slice ~first:last sql) :: acc) in - squash [] acc + squash [] acc, parami 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 + fst (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) diff --git a/test/cram/reusable_queries_postgresql_params.t b/test/cram/reusable_queries_postgresql_params.t index 802cdb51..d71315fb 100644 --- a/test/cram/reusable_queries_postgresql_params.t +++ b/test/cram/reusable_queries_postgresql_params.t @@ -42,7 +42,7 @@ PostgreSQL parameter numbering in reusable queries, distinct parameters: SELECT *\n\ FROM \"user\"\n\ JOIN person ON person.user_id = \"user\".id\n\ - WHERE \"user\".id > $1") set_params invoke_callback + WHERE \"user\".id > $2") set_params invoke_callback PostgreSQL parameter numbering in reusable queries, shared parameter: @@ -97,8 +97,8 @@ PostgreSQL parameter numbering in reusable queries, shared parameter: FROM \"user\"\n\ JOIN person ON person.user_id = \"user\".id\n\ WHERE\n\ - username LIKE $1\n\ - AND \"user\".id > $2") set_params invoke_callback + username LIKE $3\n\ + AND \"user\".id > $4") set_params invoke_callback PostgreSQL parameter numbering with two reusable queries in one outer query: @@ -135,10 +135,10 @@ PostgreSQL parameter numbering with two reusable queries in one outer query: T.set_param_Int p min_id; T.finish_params p in - T.select db ("WITH p AS (SELECT * FROM person WHERE name LIKE $1), a AS (SELECT * FROM \"user\" WHERE username LIKE $1)\n\ + T.select db ("WITH p AS (SELECT * FROM person WHERE name LIKE $1), a AS (SELECT * FROM \"user\" WHERE username LIKE $2)\n\ SELECT p.id, a.username\n\ FROM p JOIN a ON a.id = p.user_id\n\ - WHERE p.id > $1") set_params invoke_callback + WHERE p.id > $3") set_params invoke_callback PostgreSQL parameter numbering with reusable queries threaded into each other: @@ -172,5 +172,5 @@ PostgreSQL parameter numbering with reusable queries threaded into each other: T.finish_params p in T.select db ("WITH p AS (WITH inner_p AS (SELECT * FROM person WHERE name LIKE $1)\n\ - SELECT * FROM inner_p WHERE inner_p.id > $1)\n\ - SELECT * FROM p WHERE p.user_id > $1") set_params invoke_callback + SELECT * FROM inner_p WHERE inner_p.id > $2)\n\ + SELECT * FROM p WHERE p.user_id > $3") set_params invoke_callback