Skip to content

Commit 5e7bd61

Browse files
committed
Also support explicit backtick quote for MySql
1 parent 8e70049 commit 5e7bd61

File tree

4 files changed

+109
-23
lines changed

4 files changed

+109
-23
lines changed

mug-safesql/src/main/java/com/google/mu/safesql/OrderBy.java

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Locale;
1010
import java.util.Map;
1111
import java.util.Set;
12+
import java.util.function.Function;
1213
import java.util.stream.Collector;
1314
import java.util.stream.Collectors;
1415
import java.util.stream.IntStream;
@@ -18,21 +19,41 @@
1819
import com.google.mu.util.stream.BiStream;
1920

2021
final class OrderBy {
21-
private static final StringFormat QUOTED_IDENTIFIER = new StringFormat("\"{identifier}\"");
22+
private static final StringFormat DOUBLE_QUOTED = new StringFormat("\"{identifier}\"");
23+
private static final StringFormat BACKTICK_QUOTED = new StringFormat("`{identifier}`");
24+
private static final StringFormat.Template<SafeSql> DOUBLE_QUOTE_IT = SafeSql.template("\"{identifier}\"");
25+
private static final StringFormat.Template<SafeSql> BACKTICK_IT = SafeSql.template("`{identifier}`");
2226

2327
final String columnName;
28+
private final Function<String, SafeSql> asIdentifier;
2429
private final boolean descending;
2530

26-
private OrderBy(String columnName, boolean descending) {
27-
this.columnName =
28-
QUOTED_IDENTIFIER.parse(columnName, identifier -> identifier).orElse(columnName);
31+
private OrderBy(String columnName, Function<String, SafeSql> asIdentifier, boolean descending) {
32+
this.columnName = columnName;
33+
this.asIdentifier = asIdentifier;
2934
this.descending = descending;
3035
}
36+
37+
private static OrderBy by(String columnName, boolean descending) {
38+
OrderBy orderBy =
39+
DOUBLE_QUOTED.parse(columnName, id -> new OrderBy(id, DOUBLE_QUOTE_IT::with, descending))
40+
.orElse(null);
41+
if (orderBy != null) {
42+
return orderBy;
43+
}
44+
orderBy =
45+
BACKTICK_QUOTED.parse(columnName, id -> new OrderBy(id, BACKTICK_IT::with, descending))
46+
.orElse(null);
47+
if (orderBy != null) {
48+
return orderBy;
49+
}
50+
return new OrderBy(columnName, DOUBLE_QUOTE_IT::with, descending);
51+
}
3152

3253
SafeSql sql() {
3354
return descending
34-
? SafeSql.of("\"{column}\" DESC", columnName)
35-
: SafeSql.of("\"{column}\"", columnName);
55+
? SafeSql.of("{column} DESC", asIdentifier.apply(columnName))
56+
: asIdentifier.apply(columnName);
3657
}
3758

3859
static Collector<String, ?, List<OrderBy>> parsingToDistinctOrderByList() {
@@ -69,7 +90,7 @@ private static SafeSql after(Map<String, ?> columnValues, List<OrderBy> orderByL
6990
}
7091

7192
private SafeSql is(SafeSql relativeTo, Object pageEnd) {
72-
return SafeSql.of("\"{column}\" {relative_to} {page_end}", columnName, relativeTo, pageEnd);
93+
return SafeSql.of("{column} {relative_to} {page_end}", asIdentifier.apply(columnName), relativeTo, pageEnd);
7394
}
7495

7596
private SafeSql after() {
@@ -78,9 +99,9 @@ private SafeSql after() {
7899

79100
private static OrderBy parse(String orderBy) {
80101
orderBy = orderBy.trim();
81-
return last('"').or(Substring.BEGINNING).then(last(Character::isWhitespace))
102+
return last(c -> c == '"' || c == '`').or(Substring.BEGINNING).then(last(Character::isWhitespace))
82103
.splitThenTrim(orderBy)
83-
.map((name, dir) -> new OrderBy(name, dir.toUpperCase(Locale.US).equals("DESC")))
84-
.orElse(new OrderBy(orderBy, false));
104+
.map((name, dir) -> by(name, dir.toUpperCase(Locale.US).equals("DESC")))
105+
.orElse(by(orderBy, false));
85106
}
86107
}

mug-safesql/src/main/java/com/google/mu/safesql/SafeSql.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -982,9 +982,9 @@ public <T> Optional<T> queryForOne(
982982
* If {@code orderByKeys} is empty, the paginated query will use {@code OFFSET} keyword
983983
* instead of the sort key columns to specify the next page offset.
984984
*
985-
* <p>The {@code orderByKeys} column names are automatically double-quoted to avoid clashing
986-
* with keywords or spaces in identifiers.
987-
* If your DB is case sensitive, make sure you pass in the accurate case.
985+
* <p>By default, the {@code orderByKeys} column names are automatically double-quoted to
986+
* avoid clashing with keywords or space in identifiers. If you are on MySQL,
987+
* you can backtick-quote your column name before passing it in {@code orderByKeys}.
988988
*
989989
* <p>The method is lazy. Paginated queries are only sent when the returned stream is consumed.
990990
* The caller should close the {@code connection} after done, which will close the cached
@@ -1023,9 +1023,9 @@ public <T> Stream<List<T>> paginateLazily(
10231023
* If {@code orderByKeys} is empty, the paginated query will use {@code OFFSET} keyword
10241024
* instead of the sort key columns to specify the next page offset.
10251025
*
1026-
* <p>The {@code orderByKeys} column names are automatically double-quoted to avoid clashing
1027-
* with keywords or spaces in identifiers.
1028-
* If your DB is case sensitive, make sure you pass in the accurate case.
1026+
* <p>By default, the {@code orderByKeys} column names are automatically double-quoted to
1027+
* avoid clashing with keywords or space in identifiers. If you are on MySQL,
1028+
* you can backtick-quote your column name before passing it in {@code orderByKeys}.
10291029
*
10301030
* <p>The method is lazy. Paginated queries are only sent when the returned stream is consumed.
10311031
* The caller should close the {@code connection} after done, which will close the cached
@@ -1067,9 +1067,9 @@ public <T> Stream<List<T>> paginateLazily(
10671067
* If {@code orderByKeys} is empty, the paginated query will use {@code OFFSET} keyword
10681068
* instead of the sort key columns to specify the next page offset.
10691069
*
1070-
* <p>The {@code orderByKeys} column names are automatically double-quoted to avoid clashing
1071-
* with keywords or spaces in identifiers.
1072-
* If your DB is case sensitive, make sure you pass in the accurate case.
1070+
* <p>By default, the {@code orderByKeys} column names are automatically double-quoted to
1071+
* avoid clashing with keywords or space in identifiers. If you are on MySQL,
1072+
* you can backtick-quote your column name before passing it in {@code orderByKeys}.
10731073
*
10741074
* <p>The method is lazy. Paginated queries are only sent when the returned stream is consumed.
10751075
* The caller should close the {@code connection} after done, which will close the cached
@@ -1108,9 +1108,9 @@ public <T> Stream<List<T>> paginateLazily(
11081108
* If {@code orderByKeys} is empty, the paginated query will use {@code OFFSET} keyword
11091109
* instead of the sort key columns to specify the next page offset.
11101110
*
1111-
* <p>The {@code orderByKeys} column names are automatically double-quoted to avoid clashing
1112-
* with keywords or spaces in identifiers.
1113-
* If your DB is case sensitive, make sure you pass in the accurate case.
1111+
* <p>By default, the {@code orderByKeys} column names are automatically double-quoted to
1112+
* avoid clashing with keywords or space in identifiers. If you are on MySQL,
1113+
* you can backtick-quote your column name before passing it in {@code orderByKeys}.
11141114
*
11151115
* <p>The method is lazy. Paginated queries are only sent when the returned stream is consumed.
11161116
* The caller should close the {@code connection} after done, which will close the cached

mug-safesql/src/test/java/com/google/mu/safesql/OrderByTest.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class OrderByTest {
4040
.containsExactly(SafeSql.of("\"foo\""));
4141
}
4242

43-
@Test public void parsingToDistinctOrderByList_columnQuuoted() {
43+
@Test public void parsingToDistinctOrderByList_columDoubleQuoted() {
4444
assertThat(Stream.of("\"foo\"").collect(parsingToDistinctOrderByList()).stream().map(OrderBy::sql))
4545
.containsExactly(SafeSql.of("\"foo\""));
4646
assertThat(Stream.of("\"last name\"").collect(parsingToDistinctOrderByList()).stream().map(OrderBy::sql))
@@ -49,6 +49,15 @@ public class OrderByTest {
4949
.containsExactly(SafeSql.of("\"last name\" DESC"));
5050
}
5151

52+
@Test public void parsingToDistinctOrderByList_columBacktickQuoted() {
53+
assertThat(Stream.of("`foo`").collect(parsingToDistinctOrderByList()).stream().map(OrderBy::sql))
54+
.containsExactly(SafeSql.of("`foo`"));
55+
assertThat(Stream.of("`last name`").collect(parsingToDistinctOrderByList()).stream().map(OrderBy::sql))
56+
.containsExactly(SafeSql.of("`last name`"));
57+
assertThat(Stream.of("`last name` desc").collect(parsingToDistinctOrderByList()).stream().map(OrderBy::sql))
58+
.containsExactly(SafeSql.of("`last name` DESC"));
59+
}
60+
5261
@Test public void parsingToDistinctOrderByList_twoColumns() {
5362
assertThat(Stream.of("foo", "bar").collect(parsingToDistinctOrderByList()).stream().map(OrderBy::sql))
5463
.containsExactly(SafeSql.of("\"foo\""), SafeSql.of("\"bar\""));
@@ -106,4 +115,16 @@ public class OrderByTest {
106115
"((\"id\" > {id})) OR ((\"id\" = {id}) AND (\"name\" < {name})) OR ((\"id\" = {id}) AND (\"name\" = {name}) AND (\"time\" > {time}))",
107116
id, id, name, id, name, /* time */ 101));
108117
}
118+
119+
@Test public void after_columnNameBacktickQuoted() {
120+
List<OrderBy> orderBy = Stream.of("`id` desc").collect(parsingToDistinctOrderByList());
121+
ImmutableMap<String, Integer> last = ImmutableMap.of("id", 10);
122+
assertThat(OrderBy.after(last, orderBy)).isEqualTo(SafeSql.of("((`id` < {id}))", 10));
123+
}
124+
125+
@Test public void after_columnNameDoubleQuoted() {
126+
List<OrderBy> orderBy = Stream.of("\"id\" desc").collect(parsingToDistinctOrderByList());
127+
ImmutableMap<String, Integer> last = ImmutableMap.of("id", 10);
128+
assertThat(OrderBy.after(last, orderBy)).isEqualTo(SafeSql.of("((\"id\" < {id}))", 10));
129+
}
109130
}

mug-safesql/src/test/java/com/google/mu/safesql/SafeSqlDbTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,50 @@ protected DatabaseOperation getTearDownOperation() {
11631163
assertThat(stream.collect(toList())).isEmpty();
11641164
}
11651165

1166+
@Test public void paginateLazily_withOrderBy_nullValueNotSupported() throws Exception {
1167+
ZonedDateTime barTime = ZonedDateTime.of(2024, 11, 1, 10, 20, 30, 0, ZoneId.of("UTC"));
1168+
assertThat(
1169+
SafeSql.of("insert into ITEMS(id, title, time) VALUES({id}, {title}, {time})", testId(), "bar", barTime)
1170+
.update(connection()))
1171+
.isEqualTo(1);
1172+
assertThat(
1173+
SafeSql.of("insert into ITEMS(id, title, time) VALUES({id}, {title}, {time})", testId() + 1, "bar", barTime.plusSeconds(1))
1174+
.update(connection()))
1175+
.isEqualTo(1);
1176+
Stream<List<Item>> stream =
1177+
SafeSql.of(
1178+
"select ITEM_UUID, TIME, TITLE from ITEMS where ID between {from} and {to}",
1179+
/* from */ testId(), /* to */ testId() + 1)
1180+
.paginateLazily(connection(), asList("TITLE", "TIME desc", "ITEM_UUID"), 1, Item.class);
1181+
NullPointerException thrown = assertThrows(NullPointerException.class, () -> stream.collect(toList()));
1182+
assertThat(thrown).hasMessageThat().contains("ITEM_UUID");
1183+
}
1184+
1185+
@Test public void paginateLazily_explicitBacktickQuote() throws Exception {
1186+
ZonedDateTime barTime = ZonedDateTime.of(2024, 11, 1, 10, 20, 30, 0, ZoneId.of("UTC"));
1187+
assertThat(
1188+
SafeSql.of("insert into ITEMS(id, title, time) VALUES({id}, {title}, {time})", testId(), "bar", barTime)
1189+
.update(connection()))
1190+
.isEqualTo(1);
1191+
assertThat(
1192+
SafeSql.of("insert into ITEMS(id, title, time) VALUES({id}, {title}, {time})", testId() + 1, "bar", barTime.plusSeconds(1))
1193+
.update(connection()))
1194+
.isEqualTo(1);
1195+
assertThat(
1196+
SafeSql.of("insert into ITEMS(id, title, time) VALUES({id}, {title}, {time})", testId() + 2, "baz", barTime)
1197+
.update(connection()))
1198+
.isEqualTo(1);
1199+
Stream<List<Item>> stream =
1200+
SafeSql.of(
1201+
"select ID, TIME, TITLE from ITEMS where ID between {from} and {to} ORDER BY ID",
1202+
/* from */ testId(), /* to */ testId() + 2)
1203+
.paginateLazily(connection(), asList("`title`", "`time` desc", "`id`"), 2, Item.class);
1204+
assertThat(stream.collect(toList()))
1205+
.containsExactly(
1206+
asList(new Item(testId() + 1, "bar", barTime.toInstant().plusSeconds(1)), new Item(testId(), "bar", barTime.toInstant())),
1207+
asList(new Item(testId() + 2, "baz", barTime.toInstant())));
1208+
}
1209+
11661210
static class Item {
11671211
private final int id;
11681212
private final String title;

0 commit comments

Comments
 (0)