{"id":31762264,"url":"https://github.com/andreypopp/ch_queries","last_synced_at":"2026-03-05T02:01:06.532Z","repository":{"id":307348395,"uuid":"1029233873","full_name":"andreypopp/ch_queries","owner":"andreypopp","description":"A library to generate ClickHouse SQL queries in a typesafe way with OCaml","archived":false,"fork":false,"pushed_at":"2026-02-19T08:11:44.000Z","size":945,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-02-19T13:34:59.278Z","etag":null,"topics":["clickhouse","dsl","ocaml","querybuilder","sql"],"latest_commit_sha":null,"homepage":"","language":"OCaml","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andreypopp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-30T18:11:43.000Z","updated_at":"2026-02-19T08:11:47.000Z","dependencies_parsed_at":"2026-02-19T10:09:44.258Z","dependency_job_id":null,"html_url":"https://github.com/andreypopp/ch_queries","commit_stats":null,"previous_names":["andreypopp/ch_queries"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/andreypopp/ch_queries","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreypopp%2Fch_queries","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreypopp%2Fch_queries/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreypopp%2Fch_queries/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreypopp%2Fch_queries/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andreypopp","download_url":"https://codeload.github.com/andreypopp/ch_queries/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreypopp%2Fch_queries/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30106121,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-05T01:39:18.192Z","status":"online","status_checked_at":"2026-03-05T02:00:06.710Z","response_time":93,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clickhouse","dsl","ocaml","querybuilder","sql"],"created_at":"2025-10-09T22:17:31.687Z","updated_at":"2026-03-05T02:01:06.527Z","avatar_url":"https://github.com/andreypopp.png","language":"OCaml","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ch_queries\n\n**WARNING: EXPERIMENTAL, DO NOT USE**\n\nA library to generate ClickHouse SQL queries in a typesafe way with OCaml.\n\n## ch_queries.ppx\n\nThe main mode of using ch_queries is through the `ch_queries.ppx` preprocessor.\nIt allows you to write queries in SQL-like syntax. The preprocessor will expand\nthe queries into OCaml code which uses the `Ch_queries` library which provides type\nsafe combinators for query generation.\n\n```ocaml\n# #require \"ch_queries\";;\n# #require \"ch_queries.ppx\";;\n```\n\nThe `ch_queries.ppx` preprocessor will generate code assuming database schema\nis defined in OCaml code (usually you'd want this to be generated automatically\nfrom DDL or some other definition):\n\n```ocaml\nopen Ch_queries\n\nmodule Ch_database = struct\n  module Db = struct\n    type users = \u003c\n      id : (non_null, int number) expr;\n      name : (non_null, string) expr;\n      is_active : (non_null, bool) expr;\n    \u003e\n    let users =\n      let scope ~alias =\n        let col name = unsafe_col alias name in\n        object\n          method id : (non_null, int number) expr = col \"id\"\n          method name : (non_null, string) expr = col \"name\"\n          method is_active : (non_null, bool) expr = col \"is_active\"\n        end\n      in\n      from_table ~db:\"public\" ~table:\"users\" scope\n  end\nend\n```\n\n## `%q` - queries\n\nThe `%q` syntax form is used to define queries:\n```ocaml\n# let users = {%q|SELECT users.id, users.name FROM db.users|};;\nval users :\n  \u003c id : (non_null, int number) expr; name : (non_null, string) expr \u003e scope\n  select = \u003cabstr\u003e\n```\n\n\u003e [!IMPORTANT]\n\u003e To reference columns from tables/subqueries in the `FROM` clause, you need to\n\u003e use fully qualified names, e.g. `users.id` instead of just `id`.\n\u003e\n\u003e The unqualified names are only allowed outside of the `SELECT` fields (but\n\u003e allowed in `WHERE`, `GROUP BY`, etc) and they resolve to the columns defined\n\u003e in `SELECT` fields.\n\nThe `$param` and `$.param` syntax is used for parameters.\n\nRegular `$param` syntax is used to splice in expressions defined outside of the\nquery while `$.param` syntax is used to build expressions in the current query\nscope:\n```ocaml\n# let users ~field ~where = {%q|\n    SELECT users.id AS id, users.name AS name, $field::String AS extra_field\n    FROM db.users\n    WHERE users.is_active AND $.where\n  |}\nval users :\n  field:(non_null, string) expr -\u003e\n  where:(\u003c extra_field : (non_null, string) expr;\n           id : (non_null, int number) expr; name : (non_null, string) expr;\n           users : \u003c id : (non_null, int number) expr;\n                     is_active : (non_null, bool) expr;\n                     name : (non_null, string) expr \u003e\n                   scope \u003e -\u003e\n         (non_null, bool) expr) -\u003e\n  \u003c extra_field : (non_null, string) expr; id : (non_null, int number) expr;\n    name : (non_null, string) expr \u003e\n  scope select = \u003cfun\u003e\n```\n\nFinally to generate SQL from the query, one needs to define what exactly to\nselect and how to parse each column:\n```ocaml\n# let sql, parse_row = Ch_queries.query {%q|SELECT users.id FROM db.users|} Row.(fun __q -\u003e col {%e|q.id|} Parse.int);;\nval sql : string = \"SELECT users.id AS id FROM public.users AS users\"\nval parse_row : json list -\u003e int = \u003cfun\u003e\n```\n\n### Parameter syntax\n\nThe full set of param forms:\n\n| Syntax | Meaning |\n|--------|---------|\n| `$param` | Splice an expression |\n| `$.param` | Build an expression in a current query scope |\n| `$Mod.param` | Same as `$param` but with a module path (longident) |\n| `$.Mod.param` | Same as `$.param` but with a module path (longident) |\n| `?$.param` | Optional scope-aware parameter |\n\nParams `$.param` and `$.Mod.param` allow to build expression in the current\nquery scope. The identifiers which param refer to are functions which receive\nthe current query scope and return an expression.\n\nLongident syntax allows referencing values from other modules directly. Module\ncomponents must start with an uppercase letter (e.g. `$Foo.Bar.baz`,\n`$.Foo.Bar.baz`).\n\nOptional `?$.param` parameters can only be used immediately in `WHERE`,\n`HAVING` and other clasues. If they eval to `None`, the clause will be omitted\nfrom the query entirely.\n\n## `%ch.query_and_map` - queries with row parsing\n\nThe `%ch.query_and_map` syntax form is a convenient way to define a query along\nwith a row-mapping function. It returns a tuple `(sql, map)` where `sql` is the\ngenerated SQL string and `map` parses rows:\n```ocaml\n# let sql, map = {%ch.query_and_map|\n    SELECT users.id::Int32 AS id, users.is_active::Bool AS is_active\n    FROM db.users\n  |};;\nval sql : string =\n  \"SELECT users.id AS id, users.is_active AS is_active FROM public.users AS users\"\nval map : json list -\u003e (id:int -\u003e is_active:bool -\u003e '_weak1) -\u003e '_weak1 =\n  \u003cfun\u003e\n```\n\nThe `map` function takes a JSON row and a callback that receives the parsed\nfields as labeled arguments.\n\n### Type annotations\n\nFields must have type annotations (e.g., `::Int32`, `::Bool`, `::String`) to\nspecify how they should be parsed. Fields without type annotation default to\n`Any`, which returns raw JSON:\n```ocaml\n# let sql, map = {%ch.query_and_map|\n    SELECT users.name AS data FROM db.users\n  |};;\nval sql : string = \"SELECT users.name AS data FROM public.users AS users\"\nval map : json list -\u003e (data:json -\u003e '_weak2) -\u003e '_weak2 = \u003cfun\u003e\n```\n\n## `%e` - expressions, `%s` - scopes\n\nThere's a `%e` syntax form which allows you to define standalone expressions, which\ncan be spliced into queries later:\n```ocaml\n# let two = {%e|1+1|};;\nval two : (non_null, int number) expr = \u003cabstr\u003e\n```\n\nRemember than query parameters are non just plain expressions but functions\nwhich take the query scope and return an expression. It is useful (and\nsometimes required) to annotate scope types explicitly. For that we have `%s`\nsyntax form:\n```ocaml\n# let is_deleted {%s|q (is_active Bool, ...)|} = {%e|NOT q.is_active|};;\nval is_deleted :\n  \u003c q : \u003c is_active : (non_null, bool) expr; .. \u003e scope \u003e -\u003e\n  (non_null, bool) expr = \u003cfun\u003e\n```\n\nNote that `q` in `q.is_active` is being resolved in the current scope, thus\nreusable expressions are usually defined as functions from scopes to\nexpressions.\n\n\u003e [!IMPORTANT]\n\u003e In most cases it is required to annotate types of the arguments of reusable\n\u003e expressions with `_ Ch_queries.scope`. This is to force the type inference to\n\u003e infer the polymorphic type for scopes.\n\n## `%eu` - expressions, unsafely\n\nSometimes you need to construct an expression using syntax which is not\nsupported by ch_queries.ppx. In this case you can use the `%eu` syntax:\n```ocaml\n# let expr {%s|q ...|} = {%eu|q.name || ' ' || q.surname|};;\nval expr :\n  \u003c q : \u003c name : ('a, 'b) expr; surname : ('c, 'd) expr; .. \u003e scope \u003e -\u003e\n  ('e, 'f) expr = \u003cfun\u003e\n```\n\nSuch syntax recognizes only `q.name` and `$param` constructs and passes the rest\nas-is.\n\n\u003e [!IMPORTANT]\n\u003e The parameters and the result of `%eu` expressions are inferred to have \"any\n\u003e expression\" type. Consider putting additional type constraints on them to\n\u003e avoid spreading unsafety to other parts of the code.\n\n## `%t` - types\n\nFinally, the `%t` syntax form is used as a shortcut to define DSL types:\n```ocaml\n# type ch_uint64 = {%t|UInt64|};;\ntype ch_uint64 = (non_null, uint64 number) expr\n# type ch_nullable_string = {%t|Nullable(String)|};;\ntype ch_nullable_string = (null, string) expr\n# type ch_array_string = {%t|Array(String)|};;\ntype ch_array_string = (non_null, (non_null, string) Ch_queries.array) expr\n```\n\nCan also be used for scope types:\n```ocaml\n# type user_scope = {%t| (id UInt64, name Nullable(String)) |};;\ntype user_scope =\n    \u003c id : (non_null, uint64 number) expr; name : (null, string) expr \u003e scope\n```\n\nScope types can also be acquired by referencing scopes of database tables:\n```ocaml\n# type users = {%t|db.users|};;\ntype users = Ch_database.Db.users scope\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreypopp%2Fch_queries","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandreypopp%2Fch_queries","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreypopp%2Fch_queries/lists"}