{"id":21663933,"url":"https://github.com/leviysoft/oolong","last_synced_at":"2025-04-12T00:00:26.010Z","repository":{"id":191915311,"uuid":"685578786","full_name":"leviysoft/oolong","owner":"leviysoft","description":"Compile-time query generation for document stores","archived":false,"fork":false,"pushed_at":"2025-01-29T22:15:53.000Z","size":260,"stargazers_count":20,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-25T19:51:07.042Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leviysoft.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-31T14:44:34.000Z","updated_at":"2025-01-29T22:15:54.000Z","dependencies_parsed_at":"2023-09-01T14:16:30.034Z","dependency_job_id":"65e7f952-21a7-495d-bb83-660ccc154ba1","html_url":"https://github.com/leviysoft/oolong","commit_stats":null,"previous_names":["leviysoft/oolong"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leviysoft%2Foolong","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leviysoft%2Foolong/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leviysoft%2Foolong/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leviysoft%2Foolong/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leviysoft","download_url":"https://codeload.github.com/leviysoft/oolong/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248497805,"owners_count":21113984,"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":[],"created_at":"2024-11-25T10:29:31.008Z","updated_at":"2025-04-12T00:00:25.957Z","avatar_url":"https://github.com/leviysoft.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Oolong\n\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.leviysoft/oolong-core_3.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.github.leviysoft%22%20AND%20a:%22oolong-core_3%22)\n\nOolong - compile-time query generation for document stores.\n\nThis library is insipred by [Quill](https://github.com/zio/zio-protoquill).\nEverything is implemented with Scala 3 macros. Scala 2 is not supported.\nAt the moment MongoDB is the only supported document store.\n\n## Community\n\n[Join us on Discord!](https://discord.gg/wjzXb4tEG2)\n\nIf you want to contribute please see our [guide for contributors](CONTRIBUTING.md).\n\n## Overview\n\nAll query generation is happening at compile-time. This means:\n1. Zero runtime overhead. You can enjoy the abstraction without worrying about performance.\n2. Debugging is straightforward because generated queries are displayed as compilation messages.\n\nWrite your queries as plain Scala lambdas and oolong will translate them into the target representation for your document store:\n\n```scala\nimport org.mongodb.scala.bson.BsonDocument\n\nimport oolong.dsl.*\nimport oolong.mongo.*\n\ncase class Person(name: String, address: Address)\n\ncase class Address(city: String)\n\nval q: BsonDocument = query[Person](p =\u003e p.name == \"Joe\" \u0026\u0026 p.address.city == \"Amsterdam\")\n\n// The generated query will be displayed during compilation:\n// {\"$and\": [{\"name\": {\"$eq\": \"Joe\"}}, {\"address.city\": {\"$eq\": \"Amsterdam\"}}]}\n  \n// ... Then you run the query by passing the generated BSON to mongo-scala-driver\n```\n\nUpdates are also supported:\n```scala\nval q: BsonDocument = update[Person](_\n\t.set(_.name, \"Alice\")\n\t.inc(_.age, 5)\n)\n// q is {\n// \t$set: { \"name\": \"Alice\" },\n//\t$inc: { \"age\": 5 }\n// }\n```\n\n## DSL of oolong\n\n### Supported MongoDB operators \n\n#### Query operators\n\nI Comparison query operators\n1. $eq\n\n```scala\nimport oolong.dsl.*\nimport oolong.mongo.*\n\ncase class Person(name: String, age: Int, email: Option[String])\n\nval q = query[Person](_.name == \"John\")\n// q is {\"name\": \"John\"}\n```\nIn oolong $eq query is transformed into its implicit form: `{ field: \u003cvalue\u003e }`, except when a field is queried more than once.\n\n2. $gt\n\n```scala\nval q = query[Person](_.age \u003e 18)\n// q is {\"age\": {\"$gt\": 18}}\n```\n\n3. $gte\n\n```scala\nval q = query[Person](_.age \u003e= 18)\n// q is {\"age\": {\"$gte\": 18}}\n```\n\n4. $in\n\n```scala\nval q = query[Person](p =\u003e List(18, 19, 20).contains(p.age))\n// q is {\"age\": {\"$in\": [18, 19, 20]}}\n```\n\n5. $lt\n\n```scala\nval q = query[Person](_.age \u003c 18)\n// q is {\"age\": {\"$lt\": 18}}\n```\n\n6. $lte\n\n```scala\nval q = query[Person](_.age \u003c= 18)\n// q is {\"age\": {\"$lte\": 18}}\n```\n\n7. $ne\n\n```scala\nval q = query[Person](_.name != \"John\")\n// q is {\"name\" : {\"$ne\": \"John\"}}\n```\n\n8. $nin\n\n```scala\nval q = query[Person](p =\u003e !List(18, 19, 20).contains(p.age))\n// q is {\"age\": {\"$nin\": [18, 19, 20]}}\n```\n\n9. $type\n\n```scala\nval q = query[Person](_.age.isInstance[MongoType.INT32])\n// q is {\"age\": { \"$type\": 16 }}\n```\n\n10. $mod\n\n```scala\nval q = query[Person](_.age % 4.5 == 2)\n// q is {\"age\": {\"$mod\": [4.5, 2]}}\n```\n\nAlso `$mod` is supported if `%` is defined in extension:\n\n```scala 3\ntrait NewType[T](using ev: Numeric[T]):\n  opaque type Type = T\n  given Numeric[Type] = ev\n  extension (nt: Type) def value: T = nt\n\nobject Number extends NewType[Int]:\n  extension (self: Number) def %(a: Int): Int = self.value % a\ntype Number = Number.Type\n\ncase class Human(age: Number)\nval q    = query[Human](_.age % 2 == 2)\n// q is {\"age\": {\"$mod\": [2, 2]}}\n```\n\nII Logical query operators\n\n1. $and\n\n```scala\nval q = query[Person](p =\u003e p.name == \"John\" \u0026\u0026 p.age \u003e= 18)\n// q is {\"name\" : \"John\", \"age\": {\"$gte\": 18}}\n```\nIf we query different fields the query is simplified as above. \n\n```scala\n//However, should we query the same field twice, we would observe the form with $and\nval q = query[Person](p =\u003e p.age != 33 \u0026\u0026 p.age \u003e= 18)\n// q is {\"$and\": [{\"age\": {\"$ne\": 33}}, {\"age\": {\"$gte\": 18}]}\n```\n2. $or\n\n```scala\nval q = query[Person](p =\u003e p.age != 33 || p.age \u003e= 18)\n// q is {\"or\": [{\"age\": {\"$ne\": 33}}, {\"age\": {\"$gte\": 18}]}\n```\n\n3. $not\n\n```scala\nval q = query[Person](p =\u003e !(p.age \u003c 18))\n// q is { \"age\": { \"$not\": { \"$lt\": 18 } } }\n```\n\nIII Element Query Operators\n\n1. $exists\n```scala\nval q = query[Person](_.email.isDefined)\n// q is { \"email\": { \"$exists\": true } }\nval q1 = query[Person](_.email.nonEmpty)\n// q1 is { \"email\": { \"$exists\": true } }\nval q2 = query[Person](_.email.isEmpty)\n// q2 is { \"email\": { \"$exists\": false } }\n```\n\nIV Evaluation Query Operators\n\n1. $regex\n\nThere are 4 ways to make a $regex query, that are supported in oolong, which are:\n```scala\nimport java.util.regex.Pattern\n\nval q = query[Person](_.email.!!.matches(\"(?ix)^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$\"))\n//q is {\"email\": {\"$regex\": \"^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$\", \"$options\": \"ix\"} \nval q1 = query[Person](p =\u003e Pattern.compile(\"(?ix)^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$\").matcher(p.email.!!).matches())\n//q1 is {\"email\": {\"$regex\": \"^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$\", \"$options\": \"ix\"}\nval q2 = query[Person](p =\u003e Pattern.compile(\"^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$\", Pattern.CASE_INSENSITIVE | Pattern.COMMENTS).matcher(p.email.!!).matches())\n//q2 is {\"email\": {\"$regex\": \"^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$\", \"$options\": \"ix\"}\nval q3 = query[Person](p =\u003e Pattern.matches(\"^[\\\\w-\\\\.]+@([\\\\w-]+\\\\.)+[\\\\w-]{2,4}$\", p.email.!!))\n//q3 is {\"email\": {\"$regex\": \"^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$\"}\n```\n\nV Array Query Operators\n\n1. $size\n\n```scala\nimport oolong.dsl.*\n\ncase class Course(studentNames: List[String])\n\nval q = query[Course](_.studentNames.size == 20)\nval q = query[Course](_.studentNames.length == 20)\n// q is {\"studentNames\": {\"$size\": 20}}\n```\n\n2. $elemMatch\n\n```scala\nimport oolong.dsl.*\n\ncase class Student(name: String, age: Int)\n\ncase class Course(students: List[Student], tutor: String)\n\nval q = query[Course](_.students.exists(_.age == 20)) // $elemMatch ommited when querying single field\n// q is {\"students.age\": 20}\n\nval q = query[Course](course =\u003e course.students.exists(st =\u003e st.age \u003e 20 \u0026\u0026 st.name == \"Pavel\"))\n// q is {\"students\": {\"$elemMatch\": {\"age\": {\"$gt\": 20}, \"name\": \"Pavel\"}}}\n\n```\n\n3. $all\n\n```scala\ncase class LotteryTicket(numbers: List[Int])\n\ninline def winningNumbers = List(4, 8, 15, 16, 23, 42)\n\nval q    = query[LotteryTicket](lt =\u003e winningNumbers.forall(lt.numbers.contains))\n// q is { \"numbers\": { \"$all\": [4, 8, 15, 16, 23, 42] } }\n```\n\n$all with $elemMatch\n```scala\ncase class LotteryTicket(numbers: List[Int], series: Long)\n\ncase class LotteryTickets(tickets: Vector[LotteryTicket])\n\nval q = query[LotteryTickets](lts =\u003e\n  lts.tickets.exists(_.numbers.size == 20) \u0026\u0026 lts.tickets.exists(ticket =\u003e\n    ticket.numbers.size == 10 \u0026\u0026 ticket.series == 99L\n  )\n)\n// q is { \"tickets\": { \"$all\": [{ \"$elemMatch\": { \"numbers\": { \"$size\": 20 } } }, { \"$elemMatch\": { \"numbers\": { \"$size\": 10 }, \"series\": 99 } }] } }\n```\n\n#### Update operators\n\nI Field Update Operators\n\n1. $inc\n```scala\nimport oolong.dsl.*\nimport oolong.mongo.*\n\ncase class Observation(count: Int, result: Long, name: String, threshold: Option[Int])\n\nval q = update[Observation](_.inc(_.count, 1))\n// q is {\"$set\": {\"count\": 1}}\n```\n\n2. $min\n```scala\nval q = update[Observation](_.min(_.result, 1))\n// q is {\"$min\": {\"result\": 1}}\n```\n\n3. $max\n```scala\nval q = update[Observation](_.max(_.result, 10))\n// q is {\"$min\": {\"result\": 1}}\n```\n\n4. $mul\n```scala\nval q = update[Observation](_.mul(_.result, 2))\n// q is {\"$mul\": {\"result\": 2}}\n```\n\n5. $rename\n```scala\nval q = update[Observation](_.rename(_.name, \"tag\"))\n// q is {\"$rename\": {\"name\": \"tag\"}}\n```\n\n6. $set\n```scala\nval q = update[Observation](_.set(_.count, 0))\n// q is {\"$set\": {\"count\": 0}}\n```\n\n7. $set\n```scala\nval q = update[Observation](_.set(_.count, 0))\n// q is {\"$set\": {\"count\": 0}}\n```\n\n7. $set\n```scala\nval q = update[Observation](_.setOnInsert(_.threshold, 100))\n// q is {\"$setOnInsert\": {\"threshold\": 100}}\n```\n\n8. $unset\n\n$unset can be used only to set None on Option fields\n```scala\nval q = update[Observation](_.unset(_.threshold))\n// q is {\"$unset\": {\"threshold\": \"\"}}\n```\n\nII Array update operators\n\n1. $addToSet\n\n```scala\ncase class Student(id: Int, courses: List[Int])\n\nval q = update[Student](_.addToSet(_.courses, 55))\n// q is {\"$addToSet\": {\"courses\": 55}}\n```\nIn order to append multiple values to array `addToSetAll` should be used:\n\n```scala\nval q = update[Student](_.addToSetAll(_.courses, List(42, 44, 53)))\n// q is {\"$addToSet\": {\"courses\": {$each: [42, 44, 53] }}}\n```\n\n2. $pop\n\n```scala\ncase class Student(id: Int, courses: List[Int])\n\nval q = update[Student](_.popHead(_.courses)) // removes the first element\n// q is {\"$pop\": {\"courses\": -1}}\n\nval q1 = update[Student](_.popLast(_.courses)) // removes the last element\n// q1 is {\"$pop\": {\"courses\": 1}}\n```\n\n3. $pull\n```scala\ncase class Student(id: Int, courses: List[Int])\n\nval q = update[Student](_.pull(_.courses, _ \u003e= 42)) \n// q is {\"$pull\": {\"courses\": {\"$gte\": 42}}}\n```\n4. $pullAll\n\n```scala\ncase class Student(id: Int, courses: List[Int])\n\nval q = update[Student](_.pullAll(_.courses, List(5, 10, 42)))\n// q is {\"$pullAll\": {\"courses\": [5, 10, 42]}}\n```\n\n#### Projection\n\n```scala 3\ncase class Passport(number: String, issueDate: LocalDate)\ncase class BirthInfo(country: String, date: LocalDate)\ncase class Student(name: String, lastName: String, passport: Passport, birthInfo: BirthInfo)\n\ncase class StudentDTO(name: String, lastName: String)\ncase class PassportDTO(number: String, issueDate: LocalDate)\ncase class BirthDateDTO(country: String, date: LocalDate)\n\nval proj = projection[Student, StudentDTO]\n// proj is {\"name\": 1, \"birthInfo.date\": 1, \"passport\": 1, \"lastName\": 1}\n```\n\n\n### QueryMeta\n\nIn order to rename fields in codecs and queries for type T the instance of QueryMeta[T] should be provided in the scope:\n\n```scala 3\n\nimport org.mongodb.scala.BsonDocument\n\nimport oolong.bson.BsonDecoder\nimport oolong.bson.BsonEncoder\nimport oolong.bson.given\nimport oolong.bson.meta.*\nimport oolong.bson.meta.QueryMeta\nimport oolong.dsl.*\nimport oolong.mongo.*\n\ncase class Person(name: String, address: Option[Address]) derives BsonEncoder, BsonDecoder\n\nobject Person:\n  inline given QueryMeta[Person] = queryMeta(_.name -\u003e \"lastName\")\n\ncase class Address(city: String) derives BsonEncoder, BsonDecoder\n\nval person = Person(\"Adams\", Some(Address(\"New York\")))\nval bson: BsonDocument = person.bson.asDocument()\nval json = bson.toJson\n// json is {\"lastName\": \"Adams\", \"address\": {\"city\": \"New York\"}}\n\n//also having QueryMeta[Person] affects filter and update queries:\nval q0: BsonDocument = query[Person](_.name == \"Johnson\")\n\n// The generated query will be:\n// {\"lastName\": \"Johnson\"}\n\nval q1: BsonDocument = update[Person](_\n  .set(_.name, \"Brook\")\n)\n// q1 is {\n// \t$set: { \"lastName\": \"Brook\" },\n// }\n```\n\nAll QueryMeta instances should be inline given instances to be used in macro. \nIf they are not given their presence will not have any effect on codecs and queries.\nAnd if they are not inline the error will be thrown during compilation:\n```Please, add `inline` to given QueryMeta[T]```\n\nIn addition to manual creation of QueryMeta instances, there are several existing instances of QueryMeta:\nQueryMeta.snakeCase\nQueryMeta.camelCase\nQueryMeta.upperCamelCase\n\nAlso they can be combined with manual fields renaming:\n```scala 3\n\nimport oolong.bson.BsonDecoder\nimport oolong.bson.BsonEncoder\nimport oolong.bson.given\nimport oolong.bson.meta.*\nimport oolong.bson.meta.QueryMeta\n\ncase class Student(firstName: String, lastName: String, previousUniversity: String) derives BsonEncoder, BsonDecoder\n\nobject Student:\n  inline given QueryMeta[Student] = QueryMeta.snakeCase.withRenaming(_.firstName -\u003e \"name\")\n\nval s = Student(\"Alexander\", \"Bloom\", \"MSU\")\nval bson = s.bson\n// bson printed form is: {\"name\": \"Alexander\", \"last_name\": \"Bloom\", \"previous_university\": \"MSU\"}\n```\n\nIf fields of a class `T` are not renamed, you don't need to provide any instance, even if some other class `U` has  a field of type `T`. \nMacro automatically searches for instances of QueryMeta for all fields, types of which are case classes, and if not found, assumes that fields are not renamed, and then continues doing it recursively \n\n### Working with Option[_]\n\nWhen we need to unwrap an `A` from `Option[A]`, we don't use `map` / `flatMap` / etc.\nWe use `!!` to reduce verbosity: \n```scala\ncase class Person(name: String, address: Option[Address])\n\ncase class Address(city: String)\n\nval q = query[Person](_.address.!!.city == \"Amsterdam\")\n```\n\nSimilar to Quill, Oolong provides a quoted DSL, which means that the code you write inside `query(...)` and `update` blocks never gets to execute.\nSince we don't have to worry about runtime exceptions, we can tell the compiler to relax and give us the type that we want.\n\n### Raw subquries\n\nIf you need to use a feature that's not supported by oolong, you can write the target subquery manually and combine it with the high level query DSL:\n```scala\nval q = query[Person](_.name == \"Joe\" \u0026\u0026 unchecked(\n  BsonDocument(Seq(\n    (\"address.city\", BsonDocument(Seq(\n      (\"$eq\", BsonString(\"Amsterdam\"))\n    )))\n  ))\n))\n```\n\n### Reusing queries\n\nIt's possible to reuse a query by defining an 'inline def':\n```scala\ninline def cityFilter(doc: Person) = doc.address.!!.city == \"Amsterdam\"\n\nval q = query[Person](p =\u003e p.name == \"Joe\" \u0026\u0026 cityFilter(p))\n```\n\n## Coming soon\n\n- elasticsearch support\n- aggregation pipelines for Mongo\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleviysoft%2Foolong","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleviysoft%2Foolong","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleviysoft%2Foolong/lists"}