Skip to content

Commit 879c24a

Browse files
committed
Support conditional subquery operator for SafeQuery and SafeSql
1 parent b5907dd commit 879c24a

File tree

5 files changed

+166
-4
lines changed

5 files changed

+166
-4
lines changed

mug-errorprone/src/test/java/com/google/mu/errorprone/StringFormatPlaceholderNamesCheckTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,27 +232,27 @@ public void trailingSpacesNotAllowed() {
232232
}
233233

234234
@Test
235-
public void trailingSpacesBeforeEqualSignIgnored() {
235+
public void equalSignBeforeArrow() {
236236
helper
237237
.addSourceLines(
238238
"Test.java",
239239
"import com.google.mu.util.StringFormat;",
240240
"class Test {",
241241
" private static final StringFormat FORMAT =",
242-
" new StringFormat(\"{shows_id => id,}\");",
242+
" new StringFormat(\"{pattern=a->b}\");",
243243
"}")
244244
.doTest();
245245
}
246246

247247
@Test
248-
public void trailingSpacesBeforeArrowIgnored() {
248+
public void trailingSpacesBeforeArrowSignIgnored() {
249249
helper
250250
.addSourceLines(
251251
"Test.java",
252252
"import com.google.mu.util.StringFormat;",
253253
"class Test {",
254254
" private static final StringFormat FORMAT =",
255-
" new StringFormat(\"{shows_id -> id,}\");",
255+
" new StringFormat(\"{only_active -> status = 'ACTIVE'}\");",
256256
"}")
257257
.doTest();
258258
}

mug-guava/src/main/java/com/google/mu/safesql/SafeQuery.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616

1717
import static com.google.common.base.CharMatcher.anyOf;
1818
import static com.google.common.base.CharMatcher.javaIsoControl;
19+
import static com.google.common.base.CharMatcher.whitespace;
1920
import static com.google.common.base.Preconditions.checkArgument;
2021
import static com.google.common.base.Preconditions.checkNotNull;
2122
import static com.google.mu.safesql.InternalCollectors.skippingEmpty;
2223
import static com.google.mu.safesql.TrustedTypes.TRUSTED_SQL_TYPE_NAME;
2324
import static com.google.mu.safesql.TrustedTypes.isTrusted;
25+
import static com.google.mu.util.Substring.first;
2426
import static com.google.mu.util.Substring.prefix;
2527
import static com.google.mu.util.Substring.suffix;
2628
import static java.util.stream.Collectors.collectingAndThen;
@@ -68,6 +70,13 @@
6870
* injection. It can be used for databases that don't support JDBC parameterization, or if you
6971
* prefer dynamic SQL over parameterization for debugging reasons etc.
7072
*
73+
* <p>API Note: it's common that a piece of the query needs to be conditionally guarded, you can use
74+
* the conditional query operator {@code ->} in the placeholder, for example:
75+
*
76+
* <pre>{@code
77+
* SafeQuery.of("SELECT {shows_id -> id,} name FROM tbl", showsId());
78+
* }</pre>
79+
*
7180
* <p>This class is Android compatible.
7281
*
7382
* @since 7.0
@@ -423,6 +432,23 @@ protected SafeQuery translateLiteral(Substring.Match placeholder, Object value)
423432

424433
private String fillInPlaceholder(Substring.Match placeholder, Object value) {
425434
validatePlaceholder(placeholder);
435+
Substring.Match conditional = first("->").in(placeholder.skip(1, 1).toString()).orElse(null);
436+
if (conditional != null) {
437+
checkArgument(
438+
!placeholder.isImmediatelyBetween("`", "`"),
439+
"boolean placeholder {%s->} shouldn't be backtick quoted",
440+
conditional.before());
441+
checkArgument(
442+
value != null,
443+
"boolean placeholder {%s->} cannot be used with a null value",
444+
conditional.before());
445+
checkArgument(
446+
value instanceof Boolean,
447+
"boolean placeholder {%s->} can only be used with a boolean value; %s encountered.",
448+
conditional.before(),
449+
value.getClass().getName());
450+
return (Boolean) value ? whitespace().trimFrom(conditional.after()) : "";
451+
}
426452
if (value instanceof Iterable) {
427453
Iterable<?> iterable = skipEmptySubqueries((Iterable<?>) value);
428454
if (placeholder.isImmediatelyBetween("`", "`")) { // If backquoted, it's a list of symbols

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.mu.safesql;
1616

1717
import static com.google.common.base.CharMatcher.breakingWhitespace;
18+
import static com.google.common.base.CharMatcher.whitespace;
1819
import static com.google.common.base.Preconditions.checkArgument;
1920
import static com.google.common.base.Preconditions.checkNotNull;
2021
import static com.google.common.base.Preconditions.checkState;
@@ -183,6 +184,23 @@
183184
* convenience methods, {@code statement.setObject(1, "%" + criteria.firstName().get() + "%")}
184185
* will be called to populate the PreparedStatement.
185186
*
187+
* <dl><dt><STRONG>Trivial Conditional Subqueries</STRONG></dt></dl>
188+
*
189+
* SafeSql's template syntax is designed to avoid control flows that could obfuscate SQL. Instead,
190+
* complex control flow such as {@code if-else}, nested {@code if}, loops etc. should be performed
191+
* in Java and passed in as subqueries.
192+
*
193+
* <p>That said, for trivial conditional subqueries such as selecting a column only if a flag is
194+
* enabled, you can use the special conditional subquery operator {@code ->} in the template:
195+
*
196+
* <pre>{@code
197+
* SafeSql sql = SafeSql.of(
198+
* "SELECT {shows_email -> email,} name FROM Users", showsEmail());
199+
* }</pre>
200+
*
201+
* The text after the {@code ->} operator is the conditional subquery that's only included if
202+
* {@code showEmail()} returns true.
203+
*
186204
* <dl><dt><STRONG>Parameterize by Column Names or Table Names</STRONG></dt></dl>
187205
*
188206
* Sometimes you may wish to parameterize by table names, column names etc.
@@ -456,6 +474,27 @@ public static Template<SafeSql> template(@CompileTimeConstant String template) {
456474
class SqlWriter {
457475
void writePlaceholder(Substring.Match placeholder, Object value) {
458476
String paramName = rejectQuestionMark(placeholder.skip(1, 1).toString().trim());
477+
Substring.Match conditional = first("->").in(paramName).orElse(null);
478+
if (conditional != null) {
479+
checkArgument(
480+
!placeholder.isImmediatelyBetween("`", "`"),
481+
"boolean placeholder {%s->} shouldn't be backtick quoted",
482+
conditional.before());
483+
checkArgument(
484+
value != null,
485+
"boolean placeholder {%s->} cannot be used with a null value",
486+
conditional.before());
487+
checkArgument(
488+
value instanceof Boolean,
489+
"boolean placeholder {%s->} can only be used with a boolean value; %s encountered.",
490+
conditional.before(),
491+
value.getClass().getName());
492+
builder.appendSql(texts.pop());
493+
if ((Boolean) value) {
494+
builder.appendSql(whitespace().trimFrom(conditional.after()));
495+
}
496+
return;
497+
}
459498
if (value instanceof Iterable) {
460499
Iterator<?> elements = ((Iterable<?>) value).iterator();
461500
checkArgument(elements.hasNext(), "%s cannot be empty list", placeholder);

mug-guava/src/test/java/com/google/mu/safesql/SafeQueryTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,51 @@ public void booleanPlaceholder() {
142142
.isEqualTo(SafeQuery.of("SELECT * FROM tbl WHERE status in (TRUE, FALSE)"));
143143
}
144144

145+
@Test
146+
public void conditionalOperator_evaluateToTrue() {
147+
boolean showsId = true;
148+
assertThat(SafeQuery.of("SELECT {shows_id->id,} name FROM tbl", showsId))
149+
.isEqualTo(SafeQuery.of("SELECT id, name FROM tbl"));
150+
}
151+
152+
@Test
153+
public void conditionalOperator_evaluateToFalse() {
154+
boolean showsId = false;
155+
assertThat(SafeQuery.of("SELECT {shows_id->id,} name FROM tbl", showsId))
156+
.isEqualTo(SafeQuery.of("SELECT name FROM tbl"));
157+
}
158+
159+
@Test
160+
public void conditionalOperator_nonBooleanArg_disallowed() {
161+
IllegalArgumentException thrown =
162+
assertThrows(
163+
IllegalArgumentException.class,
164+
() -> SafeQuery.of("SELECT {shows_id->id,} name FROM tbl", SafeQuery.of("showsId")));
165+
assertThat(thrown).hasMessageThat().contains("{shows_id->");
166+
assertThat(thrown).hasMessageThat().contains("SafeQuery");
167+
}
168+
169+
@Test
170+
public void conditionalOperator_nullArg_disallowed() {
171+
Boolean showsId = null;
172+
IllegalArgumentException thrown =
173+
assertThrows(
174+
IllegalArgumentException.class,
175+
() -> SafeQuery.of("SELECT {shows_id->id,} name FROM tbl", showsId));
176+
assertThat(thrown).hasMessageThat().contains("{shows_id->");
177+
assertThat(thrown).hasMessageThat().contains("null");
178+
}
179+
180+
@Test
181+
public void conditionalOperator_cannotBeBacktickQuoted() {
182+
IllegalArgumentException thrown =
183+
assertThrows(
184+
IllegalArgumentException.class,
185+
() -> SafeQuery.of("SELECT `{shows_id->id}` name FROM tbl", true));
186+
assertThat(thrown).hasMessageThat().contains("{shows_id->");
187+
assertThat(thrown).hasMessageThat().contains("backtick quoted");
188+
}
189+
145190
@Test
146191
@SuppressWarnings("SafeQueryArgsCheck")
147192
public void charPlaceholder_notAllowed() {

mug-guava/src/test/java/com/google/mu/safesql/SafeSqlTest.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,58 @@ public void singleIntParameter() {
4444
assertThat(sql.getParameters()).containsExactly(123);
4545
}
4646

47+
@Test
48+
public void singleBoolParameter() {
49+
SafeSql sql = SafeSql.of("select {bool}", true);
50+
assertThat(sql.toString()).isEqualTo("select ?");
51+
assertThat(sql.getParameters()).containsExactly(true);
52+
}
53+
54+
@Test
55+
public void conditionalOperator_evaluateToTrue() {
56+
boolean showsId = true;
57+
assertThat(SafeSql.of("SELECT {shows_id->id,} name FROM tbl", showsId))
58+
.isEqualTo(SafeSql.of("SELECT id, name FROM tbl"));
59+
}
60+
61+
@Test
62+
public void conditionalOperator_evaluateToFalse() {
63+
boolean showsId = false;
64+
assertThat(SafeSql.of("SELECT {shows_id->id,} name FROM tbl", showsId))
65+
.isEqualTo(SafeSql.of("SELECT name FROM tbl"));
66+
}
67+
68+
@Test
69+
public void conditionalOperator_nonBooleanArg_disallowed() {
70+
IllegalArgumentException thrown =
71+
assertThrows(
72+
IllegalArgumentException.class,
73+
() -> SafeSql.of("SELECT {shows_id->id,} name FROM tbl", SafeQuery.of("showsId")));
74+
assertThat(thrown).hasMessageThat().contains("{shows_id->");
75+
assertThat(thrown).hasMessageThat().contains("SafeQuery");
76+
}
77+
78+
@Test
79+
public void conditionalOperator_nullArg_disallowed() {
80+
Boolean showsId = null;
81+
IllegalArgumentException thrown =
82+
assertThrows(
83+
IllegalArgumentException.class,
84+
() -> SafeSql.of("SELECT {shows_id->id,} name FROM tbl", showsId));
85+
assertThat(thrown).hasMessageThat().contains("{shows_id->");
86+
assertThat(thrown).hasMessageThat().contains("null");
87+
}
88+
89+
@Test
90+
public void conditionalOperator_cannotBeBacktickQuoted() {
91+
IllegalArgumentException thrown =
92+
assertThrows(
93+
IllegalArgumentException.class,
94+
() -> SafeSql.of("SELECT `{shows_id->id}` name FROM tbl", true));
95+
assertThat(thrown).hasMessageThat().contains("{shows_id->");
96+
assertThat(thrown).hasMessageThat().contains("backtick quoted");
97+
}
98+
4799
@Test
48100
public void backquotedEnumParameter() {
49101
SafeSql sql = SafeSql.of(

0 commit comments

Comments
 (0)