Skip to content

Commit a1ac232

Browse files
authored
Handle some params edge cases (#10)
1 parent 83b0224 commit a1ac232

File tree

2 files changed

+219
-1
lines changed

2 files changed

+219
-1
lines changed

lib/req_ch.ex

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,67 @@ defmodule ReqCH do
177177
end
178178

179179
defp prepare_params(params) do
180-
Enum.map(params, fn {key, value} -> {"param_#{key}", value} end)
180+
Enum.map(params, fn {key, value} -> {"param_#{key}", prepare_param_value(value)} end)
181+
end
182+
183+
defp prepare_param_value(text) when is_binary(text) do
184+
escapes = [{"\\", "\\\\"}, {"\t", "\\\t"}, {"\n", "\\\n"}]
185+
186+
Enum.reduce(escapes, text, fn {pattern, replacement}, text ->
187+
String.replace(text, pattern, replacement)
188+
end)
189+
end
190+
191+
defp prepare_param_value(%DateTime{} = datetime) do
192+
unix_microseconds =
193+
datetime
194+
|> DateTime.shift_zone!("Etc/UTC")
195+
|> DateTime.to_unix(:microsecond)
196+
197+
unix_seconds = unix_microseconds / 1_000_000
198+
unix_seconds_trunc = trunc(unix_seconds)
199+
200+
if unix_seconds_trunc == unix_seconds do
201+
unix_seconds_trunc
202+
else
203+
:erlang.float_to_binary(unix_seconds, decimals: 6)
204+
end
205+
end
206+
207+
defp prepare_param_value(array) when is_list(array) do
208+
elements = Enum.map(array, &prepare_array_param_value/1)
209+
IO.iodata_to_binary([?[, Enum.intersperse(elements, ?,), ?]])
210+
end
211+
212+
defp prepare_param_value(tuple) when is_tuple(tuple) do
213+
elements = Enum.map(Tuple.to_list(tuple), &prepare_array_param_value/1)
214+
IO.iodata_to_binary([?(, Enum.intersperse(elements, ?,), ?)])
215+
end
216+
217+
defp prepare_param_value(struct) when is_struct(struct), do: to_string(struct)
218+
219+
defp prepare_param_value(map) when is_map(map) do
220+
elements = Enum.map(Map.to_list(map), &prepare_map_param_value/1)
221+
IO.iodata_to_binary([?{, Enum.intersperse(elements, ?,), ?}])
222+
end
223+
224+
defp prepare_param_value(other), do: to_string(other)
225+
226+
defp prepare_array_param_value(text) when is_binary(text) do
227+
text = prepare_param_value(text)
228+
[?', String.replace(text, "'", "''"), ?']
229+
end
230+
231+
defp prepare_array_param_value(%s{} = param) when s in [Date, NaiveDateTime] do
232+
[?', to_string(param), ?']
233+
end
234+
235+
defp prepare_array_param_value(other), do: prepare_param_value(other)
236+
237+
defp prepare_map_param_value({key, value}) do
238+
key = prepare_array_param_value(key)
239+
value = prepare_array_param_value(value)
240+
[key, ?:, value]
181241
end
182242

183243
@valid_formats [:tsv, :csv, :json, :explorer]

test/req_ch_test.exs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,164 @@ defmodule ReqCHTest do
213213
"""
214214
end
215215

216+
test "a query with Date(Time) params" do
217+
utc_now = ~U[2024-12-20 05:44:53.855679Z]
218+
utc_now_seconds = DateTime.truncate(utc_now, :second)
219+
utc_today = DateTime.to_date(utc_now)
220+
221+
params = [
222+
utc_now: utc_now,
223+
utc_now_seconds: utc_now_seconds,
224+
utc_today: utc_today
225+
]
226+
227+
response =
228+
ReqCH.query!(
229+
ReqCH.new(),
230+
"""
231+
SELECT
232+
{utc_now:DateTime64(6)},
233+
{utc_now_seconds:DateTime},
234+
{utc_today:Date}
235+
FORMAT
236+
JSONCompact
237+
""",
238+
params
239+
)
240+
241+
assert response.status == 200
242+
243+
assert response.body["data"] == [
244+
[
245+
"2024-12-20 05:44:53.855679",
246+
"2024-12-20 05:44:53",
247+
"2024-12-20"
248+
]
249+
]
250+
end
251+
252+
# https://clickhouse.com/docs/en/interfaces/http#tabs-in-url-parameters
253+
# https://clickhouse.com/docs/en/interfaces/formats#tabseparated-data-formatting
254+
test "a query with tabs and newlines" do
255+
params = [
256+
tab: "a\tb",
257+
newline: "c\nd",
258+
both: "a\tb\nc\t\nd"
259+
]
260+
261+
response =
262+
ReqCH.query!(
263+
ReqCH.new(),
264+
"SELECT {tab:String}, {newline:String}, {both:String} FORMAT JSONCompact",
265+
params
266+
)
267+
268+
assert response.status == 200
269+
assert response.body["data"] == [["a\tb", "c\nd", "a\tb\nc\t\nd"]]
270+
end
271+
272+
test "a query with arrays" do
273+
params = [
274+
array: ["a", "b", "c", "a\tb\nc\t\nd"],
275+
empty_array: [],
276+
nested_array: [["a", "b"], ["c", "d"], ["a\tb\nc\t\nd"]],
277+
date_array: [~D[2024-12-20], ~D[2024-12-21]]
278+
]
279+
280+
response =
281+
ReqCH.query!(
282+
ReqCH.new(),
283+
"""
284+
SELECT
285+
{array:Array(String)},
286+
{empty_array:Array(String)},
287+
{nested_array:Array(Array(String))},
288+
{date_array:Array(Date)}
289+
FORMAT
290+
JSONCompact
291+
""",
292+
params
293+
)
294+
295+
assert response.status == 200
296+
297+
assert response.body["data"] == [
298+
[
299+
["a", "b", "c", "a\tb\nc\t\nd"],
300+
[],
301+
[["a", "b"], ["c", "d"], ["a\tb\nc\t\nd"]],
302+
["2024-12-20", "2024-12-21"]
303+
]
304+
]
305+
end
306+
307+
test "a query with tuples" do
308+
params = [
309+
tuple: {1, "a", ~D[2024-12-20]},
310+
empty_tuple: {},
311+
nested_tuple: {{1, "a"}, {2, "b"}},
312+
date_tuple: {~D[2024-12-20], ~D[2024-12-21]}
313+
]
314+
315+
response =
316+
ReqCH.query!(
317+
ReqCH.new(),
318+
"""
319+
SELECT
320+
{tuple:Tuple(UInt8, String, Date)},
321+
{empty_tuple:Tuple()},
322+
{nested_tuple:Tuple(Tuple(UInt8, String), Tuple(UInt8, String))},
323+
{date_tuple:Tuple(Date, Date)}
324+
""",
325+
params
326+
)
327+
328+
assert response.status == 200
329+
330+
assert response.body ==
331+
"""
332+
(1,'a','2024-12-20')\t\
333+
()\t\
334+
((1,'a'),(2,'b'))\t\
335+
('2024-12-20','2024-12-21')
336+
"""
337+
end
338+
339+
test "a query with maps" do
340+
params = [
341+
map: %{"a" => 1, "b" => 2},
342+
empty_map: %{},
343+
nested_map: %{"a" => %{"b" => 1}, "c" => %{"d" => 2}},
344+
date_map: %{"a" => ~D[2024-12-20], "b" => ~D[2024-12-21]}
345+
]
346+
347+
response =
348+
ReqCH.query!(
349+
ReqCH.new(),
350+
"""
351+
SELECT
352+
{map:Map(String, UInt8)},
353+
{empty_map:Map(String, UInt8)},
354+
{nested_map:Map(String, Map(String, UInt8))},
355+
{date_map:Map(String, Date)}
356+
FORMAT
357+
JSONCompact
358+
""",
359+
params
360+
)
361+
362+
assert response.status == 200
363+
364+
assert response.body["data"] == [
365+
[
366+
%{"a" => 1, "b" => 2},
367+
%{},
368+
%{"a" => %{"b" => 1}, "c" => %{"d" => 2}},
369+
%{"a" => "2024-12-20", "b" => "2024-12-21"}
370+
]
371+
]
372+
end
373+
216374
test "a query with unknown database" do
217375
assert {:ok, %Req.Response{} = response} =
218376
ReqCH.query(

0 commit comments

Comments
 (0)