如何在OCaml中使用Caqti序列化运行时的Postgres查询?

huangapple go评论66阅读模式
英文:

How to serialize runtime Postgres queries in OCaml using Caqti?

问题

以下是您要翻译的内容:

我有一个使用情景,用户以字符串形式向我提出查询,执行查询,然后将结果以 JSON 形式返回。

我正在使用 Caqti 库,它允许我执行类型安全的查询,问题在于查询的类型取决于查询本身,在我的情况下在编译时是未知的。

我通过 HTTP 请求接收查询,并需要以 JSON 格式响应结果,为简化起见,假设所有列都是字符串是可以接受的。在这种情况下,一个具有类型 string -> string list list 的函数足够,它接收查询作为 string 并将结果作为 string list list 返回。

-- 更新 --

我创建了一个示例,由于这需要使用 Dune,我将其放在了这个 Gist 中:https://gist.github.com/dhilst/f1ac36163ba08bbeb8cf6fa806b6aaa2
要运行它,您可以使用 dune init project foo 初始化一个项目,然后将来自 Gist 的 .ml 文件粘贴到 Dune 创建的 bin/main.ml 中,并用 Gist 中的 dune 文件替换 bin/dune 文件。

与问题相关的部分如下:

我想从用户那里得到一个 SQL 查询的字符串

  let query: string = Sys.argv.(1) in (* 这是用户的查询 *)

然后我想运行这个查询,并接收一个 string string list,其中包含值。可以通过以下函数完成

val do_query (query : string) : (string list list, string) Lwt_result.t

其中 string list 是一行数据,string list list 是一组行。我使用 string list 是因为我不知道它会有多少列。

现在的问题是,在 Caqti 中,我必须表示我的查询的类型。在示例中,我有一个包含这些列的表 (id int, content text),我可以这样表示查询 SELECT id, content FROM todos

  Caqti_type.(unit ->* (tup2 int string))
    "SELECT id, content FROM todos"

我的问题是,我不知道用户会输入什么查询,所以我不知道要在上面的函数中放什么,也就是我的问题所在。

-- 更新 --

我使用了 Richard 的想法,但我没有创建一个函数,而是使用了 to_jsonb。解决方案是像这样定义 user_query

let user_query q : (unit, string, [ `Many | `One | `Zero ]) Caqti_request.t =
  let cte = Format.sprintf {|
    WITH q AS (%s)
    SELECT to_jsonb(q.*)::text FROM q
  |} q in
  Caqti_type.(unit ->* string) cte

现在它将每一行输出为 JSON。这甚至更好,因为我可以逐行发送结果,这样就不需要将所有内容都存储在内存中。

我在此处更新了 Gist:https://gist.github.com/dhilst/d0f878cc19b43c78136d69e035f5370d

英文:

I have a use case where I receive a query from the user as a string. execute the query, and return the output as json.

I'm using the Caqti library, which let me do type safe queries, the problem is that the type of the query depends on the query, which is unknown at compilation time in my case.

I'm receiving the query in a HTTP request and need to respond with the results in JSON, for simplicity, assuming that all columns are strings is admissible. In such case a function with the type string -> string list list which receives the query as string and returns the results as string list list is enough.

-- update --

I created an example, since this requires dune I put it in this gist: https://gist.github.com/dhilst/f1ac36163ba08bbeb8cf6fa806b6aaa2
To run you can initialize a project with dune init project foo, then paste the .ml file from gist in bin/main.ml created by dune, and replace the bin/dune file with the dune file from the gist.

The relevant parts for the question are:

I want to get a string from the user which is a SQL query

  let query: string = Sys.argv.(1) in (* this is the user query *)

Then I want to run this query and receive a string string list with the values. Which may me done by this function

val do_query (query : string) : (string list list, string) Lwt_result.t

where string list is a single row, and string list list is a list of rows. I'm using string list because I don't know how many columns it would have.

Now the problem is that in Caqti I have to express the type of my query. In the example I have a table todos with these columns (id int, content text), I can express the type of the query SELECT id, content FROM todos like this

  Caqti_type.(unit ->* (tup2 int string))
    "SELECT id, content FROM todos"

My problem is that I don't know the query user will input so I can't know what to put in Caqti_type.( ??? ) user_query

In the example I left a function like this

let user_query q =
  Caqti_type.(unit ->* unit (* ??? what I put here? *)) q

q is a string with the user query (which I don't know), I don't know what to put in place of the second unit in the above function, that is precisely my problem.

-- update --

I use Richard's idea but instead of creating a function I used to_jsonb. The solution was to define user_query like this

let user_query q : (unit, string, [ `Many | `One | `Zero ]) Caqti_request.t =
  let cte = Format.sprintf {|
    WITH q AS (%s)
    SELECT to_jsonb(q.*)::text FROM q
  |} q in
  Caqti_type.(unit ->* string) cte

Now it outputs each row as JSON. This is even better because I can send the results row by row so that I don't need to put it all in memory.

I update the gist here: https://gist.github.com/dhilst/d0f878cc19b43c78136d69e035f5370d

答案1

得分: 3

好的,以下是翻译好的部分:

"OK, so anything involving directly executing SQL from a user makes me very nervous, but presuming you know what you are doing..."

"让PostgreSQL与您一起工作,而不是与静态类型抗争。如果您想要返回JSON,只需让PostgreSQL返回JSON。"

"Write a plpgsql function to accept your SQL string, wrap it in the aggregation and use EXECUTE to run the composed query."

  • "编写一个plpgsql函数来接受您的SQL字符串,将其包装在聚合中,并使用EXECUTE运行组合查询。"

请注意,上述链接没有被翻译,因为它们是URL链接,无需翻译。

英文:

OK, so anything involving directly executing SQL from a user makes me very nervous, but presuming you know what you are doing...

Get PostgreSQL to work with you on this, rather than fighting the static typing. If you want JSON returned then just get PostgreSQL to return JSON.

WITH source_rows AS (
    SELECT i, CURRENT_DATE + i AS dt FROM generate_series(0, 2) i
)
SELECT jsonb_agg(row_to_json(source_rows.*))
FROM source_rows;

┌────────────────────────────────────────────────────────────────────────────────────────────┐
│                                         jsonb_agg                                          │
├────────────────────────────────────────────────────────────────────────────────────────────┤
│ [{"i": 0, "dt": "2023-07-03"}, {"i": 1, "dt": "2023-07-04"}, {"i": 2, "dt": "2023-07-05"}] │
└────────────────────────────────────────────────────────────────────────────────────────────┘
(1 row)

Write a plpgsql function to accept your SQL string, wrap it in the aggregation and use EXECUTE to run the composed query.

huangapple
  • 本文由 发表于 2023年7月3日 02:47:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76600339.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定