{"id":13726279,"url":"https://github.com/roddyyaga/ppx_rapper","last_synced_at":"2025-05-07T21:31:54.157Z","repository":{"id":40454000,"uuid":"228683199","full_name":"roddyyaga/ppx_rapper","owner":"roddyyaga","description":"Syntax extension for writing SQL in OCaml","archived":false,"fork":false,"pushed_at":"2024-04-29T08:15:01.000Z","size":335,"stargazers_count":136,"open_issues_count":20,"forks_count":18,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-08-04T01:28:47.262Z","etag":null,"topics":["ocaml","reasonml","sql"],"latest_commit_sha":null,"homepage":"","language":"OCaml","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/roddyyaga.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","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}},"created_at":"2019-12-17T19:02:50.000Z","updated_at":"2024-07-21T20:00:15.000Z","dependencies_parsed_at":"2024-02-01T20:07:41.677Z","dependency_job_id":"ecafb5f5-643b-4a12-933e-06cdb5d18ddf","html_url":"https://github.com/roddyyaga/ppx_rapper","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roddyyaga%2Fppx_rapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roddyyaga%2Fppx_rapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roddyyaga%2Fppx_rapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/roddyyaga%2Fppx_rapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/roddyyaga","download_url":"https://codeload.github.com/roddyyaga/ppx_rapper/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224654201,"owners_count":17347691,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["ocaml","reasonml","sql"],"created_at":"2024-08-03T01:02:57.893Z","updated_at":"2025-05-07T21:31:54.143Z","avatar_url":"https://github.com/roddyyaga.png","language":"OCaml","funding_links":[],"categories":["OCaml","Databases"],"sub_categories":[],"readme":"![Build](https://github.com/roddyyaga/ppx_rapper/workflows/Build%20and%20test/badge.svg)\n\n# ppx_rapper\nAn extension that allows named parameters in SQL with types inferred, and syntax checking of SQL as a preprocessing\nstep. Like [ppx_mysql](https://github.com/issuu/ppx_mysql) but using [Caqti](https://github.com/paurkedal/ocaml-caqti). The name comes from the idea of\n[Dapper](https://github.com/StackExchange/Dapper) but with Records.\n\nThe syntax checking feature only works for PostgreSQL, but other features should work with other Caqti backends such as MariaDB and SQLite. If you are using a non-Postgres dialect you should use the `syntax_off` option to avoid spurious errors.\n\n## Installation\nYou can install `ppx_rapper` with opam:\n```\n$ opam install ppx_rapper ppx_rapper_lwt\n```\n(or `ppx_rapper_async` if you are using async instead).\n\nTo use in a project built with dune, add these lines to the relevant stanzas:\n```\n(libraries ppx_rapper_lwt)\n(preprocess (pps ppx_rapper))\n```\nor similar for async.\n\n## Example usage\n```ocaml\nlet my_query =\n  [%rapper\n    get_opt\n      {sql|\n      SELECT @int{id}, @string{username}, @bool{following}, @string?{bio}\n      FROM users\n      WHERE username \u003c\u003e %string{wrong_user} AND id \u003e %int{min_id}\n      |sql}]\n```\nturns into\n\n```ocaml\nlet my_query =\n  let query =\n    (let open Caqti_request in\n    find_opt)\n      ((let open Caqti_type in\n       t2 string int) [@ocaml.warning \"-33\"])\n      ((let open Caqti_type in\n       t2 int (t2 string (t2 bool (option string))))\n      [@ocaml.warning \"-33\"])\n      \"\\n\\\n      \\      SELECT id, username, following, bio\\n\\\n      \\      FROM users\\n\\\n      \\      WHERE username \u003c\u003e ? AND id \u003e ?\\n\\\n      \\      \"\n  in\n  let wrapped ~wrong_user ~min_id (module Db : Rapper_helper.CONNECTION) =\n    let f result =\n      let g (id, (username, (following, bio))) =\n        (id, username, following, bio)\n      in\n      let f =\n        (fun f x -\u003e match x with Some x -\u003e Some (f x) | None -\u003e None) g\n      in\n      match result with Ok x -\u003e Ok (f x) | Error e -\u003e Error e\n    in\n    Rapper_helper.map f (Db.find_opt query (wrong_user, min_id))\n  in\n  wrapped\n```\n\nFor further examples, see the `examples` directory.\n\n## Query functions\nQuery functions are\n- `execute` for queries that return 0 rows, represented as `()`\n- `get_one` for queries that return 1 rows, represented as a tuple/record\n- `get_opt` for queries that return 0 or 1 rows, represented as a tuple/record option\n- `get_many` for queries that may return any number of rows, represented as a list of tuples/records\n\nThese correspond to `exec`, `find`, `find_opt` and `collect` in `Caqti_request`.\n\nSince 1-tuples don't exist, single values are used instead for that case.\n\n## Parameters\n\nSyntax for input/output parameters is the same as ppx\\_mysql: `%type{name}` for\ninputs and `@type{name}` for outputs. The set of currently supported base types\noverlaps with `Caqti`'s: `int`,`int32`,`int64`, `string`, `octets`, `float`,\n`bool`, `pdate`, `ptime` and `ptime_span` are supported, in addition to `cdate`\nand `ctime`, provided by\n[caqti-type-calendar](https://paurkedal.github.io/ocaml-caqti/caqti-type-calendar/Caqti_type_calendar/index.html).\nOption types can be specified by appending a `?` to the type specification,\ne.g.`int?{id}`.\n\n### Custom types\n\nIn the style of `ppx_mysql`, `ppx_rapper` also provides (limited) support for\ncustom types via user-provided encoding and decoding functions. Consider the\nfollowing example, adapted from the `mysql_ppx`\n[section](ppx_mysql_custom_types) for the same feature:\n\n[ppx_mysql_custom_types]: https://github.com/issuu/ppx_mysql/blob/master/README.md#custom-types-and-deserialization-functions\n\n```ocaml\nmodule Suit : Rapper.CUSTOM = struct\n  type t = Clubs | Diamonds | Hearts | Spades\n\n  let t =\n    let encode = function\n      | Clubs -\u003e Ok \"c\"\n      | Diamonds -\u003e Ok \"d\"\n      | Hearts -\u003e Ok \"h\"\n      | Spades -\u003e Ok \"s\"\n    in\n    let decode = function\n      | \"c\" -\u003e Ok Clubs\n      | \"d\" -\u003e Ok Diamonds\n      | \"h\" -\u003e Ok Hearts\n      | \"s\" -\u003e Ok Spades\n      | _   -\u003e Error \"invalid suit\"\n    in\n    Caqti_type.(custom ~encode ~decode string)\nend\n\nlet get_cards =\n  [%rapper get_many\n   {sql| SELECT @int{id}, @Suit{suit} FROM cards WHERE suit \u003c\u003e %Suit{suit} |sql}]\n```\n\nThe syntax extension will recognize type specifications that start with an\nuppercase letter  -- `Suit` in our example -- and assume they refer to a module\n(available in the scope where the extension is evaluated) that implements the\n`Rapper.CUSTOM` signature, as listed below:\n\n```ocaml\nmodule type CUSTOM = sig\n  type t\n\n  val t : t Caqti_type.t\nend\n```\n\n_Note_: custom type support in this syntax extension is fairly limited and not\nmeant to be used for e.g. composite types in the output. If you intend to get\nthe return values for your query in a record, there's support for that with\nthe `record_out` option (described [below](#options)).\n\n### List support for input parameters\n\n`ppx_rapper` has limited support for queries that take a list of values as\ninput, through the special `%list{}` construct. An example is shown below:\n\n```ocaml\nlet users =\n  [%rapper\n    get_opt\n      {sql|\n      SELECT @int{id}, @string{username}, @bool{following}, @string?{bio}\n      FROM users\n      WHERE following = %bool{following} and username IN (%list{%int{ids}})\n      |sql}]\n```\n\nCurrent limitations for `list` include:\n\n- Only one `list` input parameter is supported at this time;\n- Generated Caqti queries are dynamically generated, and thus `oneshot` as per\n  the [documentation](https://paurkedal.github.io/ocaml-caqti/caqti/Caqti_request/index.html#how-to-dynamically-assemble-queries-and-parameters). Turning this off is not currently\n  supported, but please let us know if you have a use case for it.\n\n## Extension options\nIf `record_in` or `record_out` are given as options like so:\n```ocaml\nlet my_query =\n  [%rapper\n    get_opt\n      {sql|\n      SELECT @int{id}, @string{username}, @bool{following}, @string?{bio}\n      FROM users\n      WHERE username \u003c\u003e %string{wrong_user} AND id \u003e %int{min_id}\n      |sql}\n      record_in record_out]\n```\nthen the input and/or output of the query will be records. For the example above, they would have type `{id: int; wrong_user: string}` and `{id: int; username: string; following: bool; bio: string option}` respectively. The default non-record methods are labelled arguments and tuples respectively.\n\nInstead of `record_out` you can give `function_out`, in which case the first argument to the generated function should\nbe a function with labelled arguments of the types of the output parameters, like so:\n\n```ocaml\nlet show_user_names =\n  [%rapper\n    get_many {sql| SELECT @int{id}, @string{name} FROM users |sql} function_out]\n    (fun ~name ~id -\u003e Printf.sprintf \"User %d is called %s\" id name)\n```\n\nBy default, queries are syntax checked using [pg_query-ocaml](https://github.com/roddyyaga/pg_query-ocaml) and the\nextension will error if syntax checking fails. If you are using a non-Postgres SQL dialect or this gives a false positive error for a query it can be suppressed using the `syntax_off` option.\n\n## Multiple outputs\nWith the `record_out` or `function_out` option, an output parameter `@type{param_name}` will usually map to a record field name\nor labelled argument `param_name`. However, different behaviour occurs if there are output parameters containing dots.\nIn this case, multiple outputs will be produced. For example:\n\n```ocaml\nlet get_user_hat =\n  [%rapper\n    get_one\n      {sql|\n      SELECT @int{users.user_id}, @string{users.name},\n             @int{hats.hat_id}, @string{hats.colour}\n      FROM users\n      JOIN hats ON hats.hat_id = users.hat_id\n      WHERE users.id = 7\n      |sql}\n      record_out]\n```\n\nwill produce output with type `{ user_id: int; name: string} * { hat_id: int; colour: string}`. Similarly, with\n`function_out` the generated function will take a tuple of loading functions. Ordering of elements of these tuples is given by the order of their first output parameters in the query.\n\nNote that multiple outputs that share field names (for instance `@int{users.id}` and `@int{hats.id}` in the same query)\nwill not work with `record_out`, but will work fine with `function_out`.\n\n## Loading data with one-to-many relationships\nThe multiple outputs feature can be used with the runtime function `Rapper.load_many` to conveniently load entities with one-to-many relationships, as in the following example:\n\n```ocaml\nmodule Twoot = struct\n  type t = { id: int; content: string; likes: int }\n\n  let make ~id ~content ~likes = { id; content; likes }\nend\n\nmodule User = struct\n  type t = { id: int; name: string; twoots: Twoot.t list }\n\n  let make ~id ~name = { id; name; twoots = [] }\nend\n\nlet get_multiple_function_out () dbh =\n  let open Lwt_result.Infix in\n  [%rapper\n    get_many\n      {sql|\n      SELECT @int{users.id}, @string{users.name},\n             @int{twoots.id}, @string{twoots.content}, @int{twoots.likes}\n      FROM users\n      JOIN twoots ON twoots.user_id = users.id\n      ORDER BY users.id\n      |sql}\n      function_out]\n    (User.make, Twoot.make) () dbh\n  \u003e|= Rapper.load_many\n        (fst, fun { User.id; _ } -\u003e id)\n        [ (snd, fun user twoots -\u003e { user with twoots }) ]\n```\n\nHere, the query itself produces a list of tuples, where the first element is a user and the second element is one of\nthat user's \"twoots\". The query is sorted by user id, so all twoots belonging to one user are adjacent. Using\n`Rapper.load_many` produces a list of the unique users with the `twoots` field filled correctly.\n\n## Contributions\nContributions are very welcome!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froddyyaga%2Fppx_rapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Froddyyaga%2Fppx_rapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Froddyyaga%2Fppx_rapper/lists"}