Skip to content

Commit 985b9fd

Browse files
committed
Merged for javadoc
2 parents f8140b5 + a4df945 commit 985b9fd

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

mug-safesql/src/main/java/com/google/mu/safesql/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,16 @@ And if you like explicitness, consider quoting all string-typed placeholders:
309309
either it's a table/column name and needs identifier-quotes, or it's a string value,
310310
which you can single-quote like `'{user_name}'`. It doesn't change runtime behavior,
311311
but makes the SQL read less ambiguous.
312+
313+
---
314+
315+
## Summary
316+
317+
In a nutshell, use `SafeSql` if any of the following applies to you:
318+
319+
* You are a large enterprise. Relying on developer vigilance to avoid SQL injection isn't an option. Instead, you need **systematically enforced safety**.
320+
* You prefer to write actual SQL, and appreciate the ability to directly **copy-paste queries** between your code and the database console for easy debugging and testing.
321+
* A low learning curve and a *what-you-see-is-what-you-get* (WYSIWYG) approach are important to you. No Domain Specific Language (DSL) to learn.
322+
* You need to parameterize by **table names**, **column names**, all while preventing injection risks.
323+
* You have **dynamic and complex subqueries** to compose. And you find it error prone managing the subquery parameters manually.
324+
* You value **compile-time semantic error prevention** so you won't accidentally use `user.name()` in a place intended for `{ssn}`, even if both are strings.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
## When to Use ParameterizedQuery?
2+
3+
### If your Spanner query is simple enough…
4+
If you’re just running a straightforward query with a couple of parameters, the native `Statement` API is fine:
5+
```java
6+
Statement stmt = Statement.newBuilder("SELECT name FROM Users WHERE id = @id")
7+
.bind("id").to(user.id())
8+
.build();
9+
```
10+
11+
It gets more difficult when the query grows complex.
12+
13+
---
14+
15+
### 1. **Dynamic subqueries or conditional fragments**
16+
17+
**Native Spanner:**
18+
```java
19+
StringBuilder sql = new StringBuilder("SELECT e.id, e.name");
20+
if (includeManager) {
21+
sql.append(", e.manager_id, m.name as manager_name");
22+
}
23+
sql.append(" FROM Employees e");
24+
if (includeManager) {
25+
sql.append(" LEFT JOIN Employees m ON e.manager_id = m.id");
26+
}
27+
sql.append(" WHERE e.id = @id");
28+
Statement stmt = Statement.newBuilder(sql.toString())
29+
.bind("id").to(user.id())
30+
.build();
31+
```
32+
- **Risks:**
33+
- String concatenation (SQL injection risk)
34+
- Your SQL is fragmented and error-prone
35+
36+
**With ParameterizedQuery:**
37+
```java
38+
ParameterizedQuery.of(
39+
"""
40+
SELECT e.id, e.name
41+
{include_manager? -> , e.manager_id, m.name AS manager_name}
42+
FROM Employees e
43+
{include_manager? -> LEFT JOIN Employees m ON e.manager_id = m.id}
44+
WHERE e.id = {id}
45+
""",
46+
includeManager, user.id(), includeManager);
47+
```
48+
- No string concatenation
49+
- No SQL injection risk
50+
- **Minimal SQL fragmentation**—the entire query stays in one piece, making it easier to read, reason about, and maintain
51+
52+
---
53+
54+
### 2. **Managing subquery parameters**
55+
56+
**Native Spanner:**
57+
You must manually gather and merge parameter maps from every subquery, invent unique names, and ensure consistency—tedious and error-prone.
58+
- **Risks:**
59+
- Manual parameter map management
60+
- Name clashes between subquery parameters
61+
- Loss of type safety and consistency
62+
63+
**With ParameterizedQuery:**
64+
```java
65+
ParameterizedQuery toSqlFilter(Expr expr) {
66+
return switch (expr) {
67+
case MatchExpr(String column, String value) ->
68+
ParameterizedQuery.of("`{column}` = '{value}'", column, value);
69+
case AndExpr(Expr left, Expr right) ->
70+
ParameterizedQuery.of("({left}) AND ({right})", toSqlFilter(left), toSqlFilter(right));
71+
case OrExpr(Expr left, Expr right) ->
72+
ParameterizedQuery.of("({left}) OR ({right})", toSqlFilter(left), toSqlFilter(right));
73+
};
74+
}
75+
76+
ParameterizedQuery query = ParameterizedQuery.of(
77+
"SELECT * FROM Foo WHERE {filter}", toSqlFilter(expr));
78+
```
79+
- Each subquery manages its own parameters, which are merged automatically. No name clashes, no manual work.
80+
81+
---
82+
83+
### 3. **Parameterizing table/column names**
84+
85+
**Native Spanner:**
86+
```java
87+
List<String> keyColumns = ...;
88+
String sql = "SELECT " + keyColumns + ", COUNT(*) FROM Users GROUP BY " + keyColumns.stream().join(", ");
89+
Statement stmt = Statement.of(sql);
90+
```
91+
- **Risks:**
92+
- If any of `keyColumns` comes from the user, injection can happen
93+
- String concatenation
94+
95+
**With ParameterizedQuery:**
96+
```java
97+
List<String> keyColumns = ...;
98+
ParameterizedQuery.of(
99+
"SELECT `{key_columns}`, COUNT(*) AS cnt FROM Users GROUP BY `{key_columns}`",
100+
keyColumns
101+
);
102+
```
103+
- Backtick-quoted placeholders are strictly validated as identifiers. No string concat or injection risk.
104+
105+
---
106+
107+
### 4. **Semantic error prevention**
108+
109+
Traditional frameworks only check parameter types (e.g., “this parameter is a String”),
110+
so it’s easy to accidentally pass the wrong value if two fields share the same type.
111+
And out-of-order bugs are subtle and hard to catch, even at runtime.
112+
113+
**With ParameterizedQuery, semantic mistakes are caught at compile time:**
114+
```java
115+
// Suppose your template is:
116+
ParameterizedQuery.of("SELECT * FROM Users WHERE ssn = '{ssn}'", user.name());
117+
// ^ placeholder expects ssn, but passed name()
118+
119+
// Compile-time error! The plugin detects the semantic mismatch, even though both are String.
120+
```
121+
- Catches real-world semantic errors that would silently pass in other APIs, making your code safer from human mistakes.

mug-spanner/src/test/java/com/google/mu/spanner/ParameterizedQuerySpannerTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.junit.AfterClass;
1010
import org.junit.BeforeClass;
1111
import org.junit.ClassRule;
12+
import org.junit.Ignore;
1213
import org.junit.Test;
1314
import org.junit.runner.RunWith;
1415
import org.junit.runners.JUnit4;
@@ -28,6 +29,7 @@
2829
import com.google.cloud.spanner.SpannerOptions;
2930

3031
@RunWith(JUnit4.class)
32+
@Ignore("Run manually with Docker image")
3133
public class ParameterizedQuerySpannerTest {
3234

3335
@ClassRule

0 commit comments

Comments
 (0)