Skip to content

Commit 958b56e

Browse files
db: named parameter shortcut in case of single unique placeholder (#799)
1 parent fb62252 commit 958b56e

9 files changed

Lines changed: 123 additions & 13 deletions

File tree

webtau-db/src/main/java/org/testingisdocumenting/webtau/db/Database.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public DbQuery createQuery(String query, Map<String, Object> params) {
4646
return QueryRunnerUtils.createQuery(dataSource, query, params);
4747
}
4848

49+
public <E> DbQuery createQuery(String query, E singleParam) {
50+
return QueryRunnerUtils.createQuery(dataSource, query, DbNamedParamsQuery.singleNoNameParam(singleParam));
51+
}
52+
4953
public TableData queryTableData(String query) {
5054
return queryTableData(query, Collections.emptyMap());
5155
}

webtau-db/src/main/java/org/testingisdocumenting/webtau/db/DatabaseFacade.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public DbQuery createQuery(String query, Map<String, Object> params) {
4848
return from(getPrimaryDataSource()).createQuery(query, params);
4949
}
5050

51+
public <E> DbQuery createQuery(String query, E singleParam) {
52+
return from(getPrimaryDataSource()).createQuery(query, singleParam);
53+
}
54+
5155
public TableData queryTableData(String query) {
5256
return from(getPrimaryDataSource()).queryTableData(query);
5357
}

webtau-db/src/main/java/org/testingisdocumenting/webtau/db/DbNamedParamsQuery.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,31 @@ class DbNamedParamsQuery {
2323
private final Object[] valuesArray;
2424
private final String namedParamsQuery;
2525
private final Map<String, Object> params;
26+
private final Set<String> uniqueParamNames;
2627

2728
private final StringBuilder currentNamedParam;
2829
private final StringBuilder questionMarksQuery;
2930

31+
private final boolean hasSingleNoNameParameter;
32+
3033
private boolean insideParamName;
3134
private boolean insideSingleQuote;
3235
private boolean insideDoubleQuote;
3336

37+
public DbNamedParamsQuery(String namedParamsQuery, Object singleParam) {
38+
this(namedParamsQuery, singleNoNameParam(singleParam));
39+
}
40+
3441
public DbNamedParamsQuery(String namedParamsQuery, Map<String, Object> params) {
3542
this.namedParamsQuery = namedParamsQuery;
43+
this.uniqueParamNames = new TreeSet<>();
3644
this.values = new ArrayList<>();
3745
this.currentNamedParam = new StringBuilder();
3846
this.questionMarksQuery = new StringBuilder();
3947
this.insideParamName = false;
4048
this.insideSingleQuote = false;
4149
this.insideDoubleQuote = false;
50+
this.hasSingleNoNameParameter = params.size() == 1 && params.keySet().iterator().next().equals("");
4251
this.params = params;
4352

4453
convertToQuestionMarks();
@@ -58,6 +67,22 @@ public Object[] getQuestionMarksValues() {
5867
return valuesArray;
5968
}
6069

70+
public Map<String, Object> effectiveParams() {
71+
if (hasSingleNoNameParameter) {
72+
return Collections.singletonMap(uniqueParamNames.iterator().next(), params.values().iterator().next());
73+
}
74+
75+
return params;
76+
}
77+
78+
public boolean isEmpty() {
79+
return params.isEmpty();
80+
}
81+
82+
static Map<String, Object> singleNoNameParam(Object singleParam) {
83+
return Collections.singletonMap("", singleParam);
84+
}
85+
6186
private void convertToQuestionMarks() {
6287
char c;
6388

@@ -77,7 +102,8 @@ private void convertToQuestionMarks() {
77102
continue;
78103
}
79104

80-
if (insideParamName && Character.isAlphabetic(c)) {
105+
if (insideParamName &&
106+
(Character.isAlphabetic(c) || (Character.isDigit(c) && currentNamedParam.length() > 0))) {
81107
currentNamedParam.append(c);
82108
} else if (insideParamName) {
83109
handleCurrentParamName();
@@ -93,7 +119,10 @@ private void convertToQuestionMarks() {
93119
}
94120

95121
private void handleCurrentParamName() {
96-
Object valueToAppend = valueByName(currentNamedParam.toString());
122+
String paramName = currentNamedParam.toString();
123+
uniqueParamNames.add(paramName);
124+
125+
Object valueToAppend = valueByName(paramName);
97126
if (valueToAppend instanceof Iterable) {
98127
handleIterableParam((Iterable<?>) valueToAppend);
99128
} else {
@@ -123,6 +152,15 @@ private void handleSingleParam(Object valueToAppend) {
123152
}
124153

125154
private Object valueByName(String name) {
155+
if (hasSingleNoNameParameter) {
156+
if (uniqueParamNames.size() > 1) {
157+
throw new IllegalArgumentException("one no-name parameter value was provided, " +
158+
"but have seen multiple placeholders: " + uniqueParamNames);
159+
}
160+
161+
return params.values().iterator().next();
162+
}
163+
126164
Object value = params.get(name);
127165
if (value == null) {
128166
throw new IllegalArgumentException("No parameter value found: " + name);

webtau-db/src/main/java/org/testingisdocumenting/webtau/db/QueryRunnerUtils.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ static DbQuery createQuery(LabeledDataSource dataSource, String query, Map<Strin
3434
QueryRunner run = new QueryRunner(dataSource.getDataSource());
3535
MapListHandler handler = new MapListHandler();
3636

37-
return new DbQuery(dataSource.getLabel(),() -> runQuery(run, handler, query, params), query, params);
37+
DbNamedParamsQuery namedParamsQuery = new DbNamedParamsQuery(query, params);
38+
39+
return new DbQuery(dataSource.getLabel(),
40+
() -> runQuery(run, handler, namedParamsQuery),
41+
query, namedParamsQuery.effectiveParams());
3842
}
3943

4044
static int runUpdate(DataSource dataSource, String query) {
@@ -58,14 +62,12 @@ static int runUpdate(DataSource dataSource, String query, Map<String, Object> pa
5862

5963
private static List<Map<String, Object>> runQuery(QueryRunner runner,
6064
MapListHandler handler,
61-
String query,
62-
Map<String, Object> params) {
65+
DbNamedParamsQuery namedParamsQuery) {
6366
try {
64-
if (params.isEmpty()) {
65-
return runner.query(query, handler);
67+
if (namedParamsQuery.isEmpty()) {
68+
return runner.query(namedParamsQuery.getQuestionMarksQuery(), handler);
6669
}
6770

68-
DbNamedParamsQuery namedParamsQuery = new DbNamedParamsQuery(query, params);
6971
return runner.query(namedParamsQuery.getQuestionMarksQuery(),
7072
handler,
7173
namedParamsQuery.getQuestionMarksValues());

webtau-db/src/test/groovy/org/testingisdocumenting/webtau/db/DatabaseBaseTest.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class DatabaseBaseTest {
7272
String definition =
7373
"""CREATE TABLE PRICES (
7474
id varchar(255),
75+
external_id varchar(255),
7576
description varchar(255),
7677
available bool,
7778
type varchar(255),

webtau-db/src/test/groovy/org/testingisdocumenting/webtau/db/DatabaseFacadeTest.groovy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ class DatabaseFacadeTest extends DatabaseBaseTest {
144144
// query with where clause end
145145
}
146146

147+
@Test
148+
void "query table to match one row using single param shortcut and assert against map"() {
149+
setupPrices()
150+
// query with where clause start
151+
def prices = db.createQuery("select * from PRICES where id=:id or external_id=:id", "id1")
152+
prices.should == [ID: "id1", "DESCRIPTION": "nice set", PRICE: 1000]
153+
// query with where clause end
154+
}
155+
147156
@Test
148157
void "query table to match multiple row and assert against map"() {
149158
setupPrices()

webtau-db/src/test/groovy/org/testingisdocumenting/webtau/db/DatabaseStepReporterTest.groovy

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ class DatabaseStepReporterTest extends DatabaseBaseTest implements StepReporter
5353
fullMessage.should contain("select price from PRICES where id=:id with {id=id1} doesn't equal 2000")
5454
}
5555

56+
@Test
57+
void "query result comparison step should capture query and params in case of single param"() {
58+
setupPrices()
59+
60+
def price = db.createQuery("select price from PRICES where id=:id", 'id1')
61+
price.should == 1000
62+
63+
def fullMessage = stepMessages.join('\n')
64+
fullMessage.should contain("select price from PRICES where id=:id with {id=id1} equals 1000")
65+
}
66+
5667
@Test
5768
void "query result comparison step should not capture params when no params ara passed"() {
5869
setupPrices()

webtau-db/src/test/groovy/org/testingisdocumenting/webtau/db/DbNamedParamsQueryTest.groovy

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ package org.testingisdocumenting.webtau.db
1818

1919
import org.junit.Test
2020

21+
import static org.testingisdocumenting.webtau.Matchers.code
22+
import static org.testingisdocumenting.webtau.Matchers.throwException
23+
2124
class DbNamedParamsQueryTest {
2225
@Test
2326
void "convert named params to question marks outside quotes"() {
@@ -40,12 +43,37 @@ class DbNamedParamsQueryTest {
4043
}
4144

4245
@Test
43-
void "convert placeholder into multiple question marks in case of array"() {
46+
void "handles parameter names with numeric prefix"() {
47+
def query = new DbNamedParamsQuery(
48+
"select * from table where id=:id1 or id=:id2", [id1: 1, id2: 2])
49+
50+
query.questionMarksQuery.should == "select * from table where id=? or id=?"
51+
query.questionMarksValues.should == [1, 2]
52+
}
53+
54+
@Test
55+
void "validates missing params"() {
56+
code {
57+
new DbNamedParamsQuery(
58+
"select * from table where count>:count and date<:date and last_count<:count",
59+
[count: 10])
60+
} should throwException("No parameter value found: date")
61+
}
62+
63+
@Test
64+
void "understands single parameter value when there is only one unique placeholder"() {
4465
def query = new DbNamedParamsQuery(
45-
"select * from table where id in (:ids)",
46-
[ids: [1, 2, 3, 10]])
66+
"select * from table where id=:id or another_id=:id", 25)
4767

48-
query.questionMarksQuery.should == "select * from table where id in (?, ?, ?, ?)"
49-
query.questionMarksValues.should == [1, 2, 3, 10]
68+
query.questionMarksQuery.should == "select * from table where id=? or another_id=?"
69+
query.questionMarksValues.should == [25, 25]
70+
}
71+
72+
@Test
73+
void "validates mismatch between single parameter and named placeholders"() {
74+
code {
75+
new DbNamedParamsQuery(
76+
"select * from table where id=:id1 or another_id=:id2", 25)
77+
} should throwException("one no-name parameter value was provided, but have seen multiple placeholders: [id1, id2]")
5078
}
5179
}

webtau-docs/znai/database/data-query.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ To query all data from a table use:
4040
excludeStartEnd: true
4141
}
4242

43+
# Named Parameter Shortcut
44+
45+
If your query uses a single unique placeholder name, you can pass a regular value instead of a `java.util.Map`
46+
47+
:include-groovy: org/testingisdocumenting/webtau/db/DatabaseFacadeTest.groovy {
48+
entry: "query table to match one row using single param shortcut and assert against map",
49+
title: "use regular value in case of one unique placeholder name",
50+
bodyOnly: true,
51+
startLine: "query with where clause start",
52+
endLine: "query with where clause end",
53+
excludeStartEnd: true
54+
}
55+
4356
# Lazy Declaration
4457

4558
`createQuery` doesn't query database at the call time. It defines a query to be used later.

0 commit comments

Comments
 (0)