Skip to content

Commit

Permalink
Merge pull request #2166 from satorg/new-fragment-ops
Browse files Browse the repository at this point in the history
Propose a fragment concat operator that ensures a whitespace in between
  • Loading branch information
jatcwang authored Feb 3, 2025
2 parents 34ca0df + 26e3f63 commit a3b4087
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 3 deletions.
17 changes: 17 additions & 0 deletions modules/core/src/main/scala/doobie/util/fragment.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ object fragment {
def ++(fb: Fragment): Fragment =
new Fragment(sql + fb.sql, elems ++ fb.elems, pos orElse fb.pos)

/** Concatenate this fragment with another, yielding a larger fragment and making sure there is at least one
* whitespace between the source fragments.
*/
def +~+(that: Fragment): Fragment = {
val res =
if (sql.isEmpty) that.sql
else if (that.sql.isEmpty) sql
else if (sql.last.isWhitespace || that.sql.head.isWhitespace) sql + that.sql
else sql + " " + that.sql

new Fragment(
res,
elems ++ that.elems,
pos orElse that.pos
)
}

def stripMargin(marginChar: Char): Fragment =
new Fragment(sql.stripMargin(marginChar), elems, pos)

Expand Down
21 changes: 19 additions & 2 deletions modules/core/src/test/scala/doobie/util/FragmentSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

package doobie.util

import cats.syntax.all.*
import cats.effect.IO
import cats.syntax.all.*
import doobie.*
import doobie.Fragment.const0
import doobie.implicits.*
import doobie.testutils.VoidExtensions
import munit.CatsEffectSuite
Expand All @@ -33,9 +34,25 @@ class FragmentSuite extends CatsEffectSuite {
assertEquals(fr"foo $a $b bar".query[Unit].sql, "foo ? ? bar ")
}

test("Fragment must concatenate properly") {
test("Fragment builders DO NOT treat backslashes as escape characters") {
assertEquals(fr0"foo\\bar".query[Unit].sql, """foo\\bar""")
assertEquals(fr0"foo\nbar".query[Unit].sql, """foo\nbar""")
}
test("Fragment must concatenate properly with `++`") {
assertEquals((fr"foo" ++ fr"bar $a baz").query[Unit].sql, "foo bar ? baz ")
}
test("Fragment must concatenate properly enforcing at least 1 whitespace in between with `+~+`") {
// Since `fr"..."` do not parse backslashes as escape characters, so we need to use `const0` to test them.
assertEquals((const0("") +~+ const0("bar")).query[Unit].sql, "bar")
assertEquals((const0("foo") +~+ const0("")).query[Unit].sql, "foo")
assertEquals((const0("foo") +~+ const0("bar")).query[Unit].sql, "foo bar")
assertEquals((const0("foo ") +~+ const0("bar")).query[Unit].sql, "foo bar")
assertEquals((const0("foo\n") +~+ const0("bar")).query[Unit].sql, "foo\nbar")
assertEquals((const0("foo") +~+ const0(" bar")).query[Unit].sql, "foo bar")
assertEquals((const0("foo") +~+ const0("\tbar")).query[Unit].sql, "foo\tbar")
assertEquals((const0("foo ") +~+ const0(" bar")).query[Unit].sql, "foo bar")
assertEquals((const0("foo\r") +~+ const0("\fbar")).query[Unit].sql, "foo\r\fbar")
}

test("Fragment must interpolate fragments properly") {
assertEquals(fr"foo ${fr0"bar $a baz"}".query[Unit].sql, "foo bar ? baz ")
Expand Down
18 changes: 17 additions & 1 deletion modules/docs/src/main/mdoc/docs/08-Fragments.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,23 @@ fr0"IN (" ++ List(1, 2, 3).map(n => fr0"$n").intercalate(fr",") ++ fr")"
```
Note that the `sql` interpolator is simply an alias for `fr0`.

Additionally, you can use the `+~+` operator to concatenate two fragments, ensuring at least one space between them.
This is useful when you want to maintain proper spacing without worrying about trailing spaces in individual fragments.

```scala mdoc
import Fragment.const0

// Assume we don't know (or don't want to worry) if `codeCondFrag` or `populationCondFrag` end with whitespaces or not.
def codeCondFrag: Fragment = fr0"code = 'USA'"
def populationCondFrag: Fragment = fr0"population > 1000000"

const0("SELECT code, name, population FROM country\n") +~+ // a newline will be preserved, no extra whitespace added
fr0"WHERE" +~+ codeCondFrag +~+ fr0"AND" +~+ populationCondFrag +~+
fr0"ORDER BY population DESC"
```

In the above example, spaces will be added between fragments where needed automatically.

### The `Fragments` Module

The `Fragments` module provides some combinators for common patterns when working with fragments. The following example illustrates a few of them. See the Scaladoc or source for more information.
Expand Down Expand Up @@ -147,4 +164,3 @@ select(None, None, Nil, 10).check.unsafeRunSync() // no filters
select(Some("U%"), None, Nil, 10).check.unsafeRunSync() // one filter
select(Some("U%"), Some(12345), List("FRA", "GBR"), 10).check.unsafeRunSync() // three filters
```

0 comments on commit a3b4087

Please sign in to comment.