Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propose a fragment concat operator that ensures a whitespace in between #2166

Merged
merged 2 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
jatcwang marked this conversation as resolved.
Show resolved Hide resolved
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
```