Skip to content

Commit c16b705

Browse files
authored
Added support of jdbc_table macro (#93)
2 parents 2222ad7 + 6958183 commit c16b705

File tree

5 files changed

+335
-1
lines changed

5 files changed

+335
-1
lines changed

jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ public String parseSQL() throws SQLException {
166166
fragmentStart = i;
167167
}
168168
}
169+
170+
// Process JDBC_TABLE (?, ?, ... )
171+
if (i < chars.length && detectJdbcArgs && isConvertJdbcInToList) {
172+
if (parseJdbcTableKeyword(chars, keywordStart, keywordLength)) {
173+
parsed.append(chars, fragmentStart, keywordStart - fragmentStart);
174+
fragmentStart = keywordStart;
175+
int updated = parseJdbcTableListParameters(chars, i, statement);
176+
if (updated != i) {
177+
i = updated;
178+
fragmentStart = updated;
179+
}
180+
}
181+
}
169182
} else {
170183
boolean skipped = false;
171184
if (isDetectQueryType) {
@@ -381,6 +394,63 @@ private int parseInListParameters(char[] query, int offset, QueryStatement st) {
381394
return start;
382395
}
383396

397+
private int parseJdbcTableListParameters(char[] query, int offset, QueryStatement st) {
398+
int start = offset;
399+
int listStartedAt = -1;
400+
int listSize = 0;
401+
boolean waitPrm = false;
402+
while (offset < query.length) {
403+
char ch = query[offset];
404+
switch (ch) {
405+
case '(': // start of list
406+
if (listStartedAt >= 0) {
407+
return start;
408+
}
409+
listStartedAt = offset;
410+
waitPrm = true;
411+
break;
412+
case ',':
413+
if (listStartedAt < 0 || waitPrm) {
414+
return start;
415+
}
416+
waitPrm = true;
417+
break;
418+
case '?' :
419+
if (!waitPrm || (offset + 1 < query.length && query[offset + 1] == '?')) {
420+
return start;
421+
}
422+
listSize++;
423+
waitPrm = false;
424+
break;
425+
case ')':
426+
if (waitPrm || listSize == 0 || listStartedAt < 0) {
427+
return start;
428+
}
429+
430+
String name = nextJdbcPrmName();
431+
parsed.append(" AS_TABLE(");
432+
parsed.append(name);
433+
parsed.append(")");
434+
st.addJdbcPrmFactory(JdbcPrm.jdbcTableListOrm(name, listSize));
435+
return offset + 1;
436+
case '-': // possibly -- style comment
437+
offset = parseLineComment(query, offset);
438+
break;
439+
case '/': // possibly /* */ style comment
440+
offset = parseBlockComment(query, offset);
441+
break;
442+
default:
443+
if (!Character.isWhitespace(query[offset])) {
444+
return start;
445+
}
446+
break;
447+
}
448+
offset++;
449+
}
450+
451+
return start;
452+
}
453+
384454
private static int parseSingleQuotes(final char[] query, int offset) {
385455
// treat backslashes as escape characters
386456
while (++offset < query.length) {
@@ -657,4 +727,21 @@ private static boolean parseInKeyword(char[] query, int offset, int length) {
657727
return (query[offset] | 32) == 'i'
658728
&& (query[offset + 1] | 32) == 'n';
659729
}
730+
731+
private static boolean parseJdbcTableKeyword(char[] query, int offset, int length) {
732+
if (length != 10) {
733+
return false;
734+
}
735+
736+
return (query[offset] | 32) == 'j'
737+
&& (query[offset + 1] | 32) == 'd'
738+
&& (query[offset + 2] | 32) == 'b'
739+
&& (query[offset + 3] | 32) == 'c'
740+
&& (query[offset + 4]) == '_'
741+
&& (query[offset + 5] | 32) == 't'
742+
&& (query[offset + 6] | 32) == 'a'
743+
&& (query[offset + 7] | 32) == 'b'
744+
&& (query[offset + 8] | 32) == 'l'
745+
&& (query[offset + 9] | 32) == 'e';
746+
}
660747
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package tech.ydb.jdbc.query.params;
2+
3+
import java.sql.SQLException;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
import tech.ydb.jdbc.YdbConst;
8+
import tech.ydb.jdbc.common.TypeDescription;
9+
import tech.ydb.jdbc.impl.YdbTypes;
10+
import tech.ydb.table.query.Params;
11+
import tech.ydb.table.values.ListType;
12+
import tech.ydb.table.values.OptionalType;
13+
import tech.ydb.table.values.StructType;
14+
import tech.ydb.table.values.Type;
15+
import tech.ydb.table.values.Value;
16+
import tech.ydb.table.values.VoidValue;
17+
18+
/**
19+
*
20+
* @author Aleksandr Gorshenin
21+
*/
22+
public class AsTableJdbcPrm {
23+
private static final Value<?> NULL = VoidValue.of();
24+
private static final String COLUMN_NAME = "x";
25+
26+
private final String listName;
27+
private final List<Item> items = new ArrayList<>();
28+
private TypeDescription type;
29+
30+
public AsTableJdbcPrm(String listName, int listSize) {
31+
this.listName = listName;
32+
for (int idx = 0; idx < listSize; idx++) {
33+
items.add(new Item(listName, idx));
34+
}
35+
}
36+
37+
public List<? extends JdbcPrm> toJdbcPrmList() {
38+
return items;
39+
}
40+
41+
private Value<?> buildList() throws SQLException {
42+
if (type == null) {
43+
throw new SQLException(YdbConst.PARAMETER_TYPE_UNKNOWN);
44+
}
45+
46+
boolean hasNull = false;
47+
for (Item item: items) {
48+
if (item.value == null) {
49+
throw new SQLException(YdbConst.MISSING_VALUE_FOR_PARAMETER + item.name);
50+
}
51+
hasNull = hasNull || item.value == NULL;
52+
}
53+
54+
List<Value<?>> values = new ArrayList<>();
55+
if (!hasNull) {
56+
StructType structType = StructType.of(COLUMN_NAME, type.ydbType());
57+
for (Item item: items) {
58+
values.add(structType.newValue(COLUMN_NAME, item.value));
59+
}
60+
return ListType.of(structType).newValue(values);
61+
}
62+
63+
OptionalType optional = type.ydbType().makeOptional();
64+
StructType structType = StructType.of(COLUMN_NAME, optional);
65+
for (Item item: items) {
66+
if (item.value == NULL) {
67+
values.add(structType.newValue(COLUMN_NAME, optional.emptyValue()));
68+
} else {
69+
values.add(structType.newValue(COLUMN_NAME, item.value.makeOptional()));
70+
}
71+
}
72+
73+
return ListType.of(structType).newValue(values);
74+
75+
}
76+
77+
private class Item implements JdbcPrm {
78+
private final String name;
79+
private final int index;
80+
private Value<?> value = null;
81+
82+
Item(String listName, int index) {
83+
this.name = listName + "[" + index + "]";
84+
this.index = index;
85+
}
86+
87+
@Override
88+
public String getName() {
89+
return name;
90+
}
91+
92+
@Override
93+
public TypeDescription getType() {
94+
return type;
95+
}
96+
97+
@Override
98+
public void setValue(Object obj, int sqlType) throws SQLException {
99+
if (type == null) {
100+
Type ydbType = YdbTypes.findType(obj, sqlType);
101+
if (ydbType == null) {
102+
if (obj == null) {
103+
value = NULL;
104+
return;
105+
} else {
106+
throw new SQLException(String.format(YdbConst.PARAMETER_TYPE_UNKNOWN, sqlType, obj));
107+
}
108+
}
109+
110+
type = TypeDescription.of(ydbType);
111+
}
112+
113+
if (obj == null) {
114+
value = NULL;
115+
return;
116+
}
117+
118+
value = type.setters().toValue(obj);
119+
}
120+
121+
@Override
122+
public void copyToParams(Params params) throws SQLException {
123+
if (index == 0) { // first prm
124+
params.put(listName, buildList());
125+
}
126+
}
127+
128+
@Override
129+
public void reset() {
130+
value = null;
131+
if (index == items.size() - 1) { // last prm reset type
132+
type = null;
133+
}
134+
}
135+
}
136+
}

jdbc/src/main/java/tech/ydb/jdbc/query/params/JdbcPrm.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ static Factory uint64Prm(String name) {
3636
static Factory inListOrm(String name, int count) {
3737
return () -> new InListJdbcPrm(name, count).toJdbcPrmList();
3838
}
39+
40+
static Factory jdbcTableListOrm(String name, int count) {
41+
return () -> new AsTableJdbcPrm(name, count).toJdbcPrmList();
42+
}
3943
}

jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,85 @@ public void inListTest(boolean convertInToList) throws SQLException {
614614
}
615615
}
616616

617+
@Test
618+
public void jdbcTableListTest() throws SQLException {
619+
String arg2Name = "$jp1[1]";
620+
String upsert = TEST_TABLE.upsertOne(SqlQueries.JdbcQuery.STANDARD, "c_Text", "Text");
621+
String selectByIds = TEST_TABLE.withTableName(
622+
"select count(*) from jdbc_table(?,?) as j join #tableName t on t.key=j.x"
623+
);
624+
String selectByValue = TEST_TABLE.withTableName(
625+
"select count(*) from jdbc_table(?,?) as j join #tableName t on t.c_Text=j.x"
626+
);
627+
628+
try (PreparedStatement ps = jdbc.connection().prepareStatement(upsert)) {
629+
ps.setInt(1, 1);
630+
ps.setString(2, "1");
631+
ps.addBatch();
632+
633+
ps.setInt(1, 2);
634+
ps.setString(2, null);
635+
ps.addBatch();
636+
637+
ps.setInt(1, 3);
638+
ps.setString(2, "3");
639+
ps.addBatch();
640+
641+
ps.setInt(1, 4);
642+
ps.setString(2, "null");
643+
ps.addBatch();
644+
645+
ps.executeBatch();
646+
}
647+
648+
try (PreparedStatement ps = jdbc.connection().prepareStatement(selectByIds)) {
649+
ps.setInt(1, 1);
650+
ExceptionAssert.sqlException("Missing value for parameter: " + arg2Name, ps::executeQuery);
651+
652+
ps.setInt(1, 1);
653+
ps.setInt(2, 2);
654+
assertResultSetCount(ps.executeQuery(), 2);
655+
656+
ps.setInt(1, 1);
657+
ps.setInt(2, 5);
658+
assertResultSetCount(ps.executeQuery(), 1);
659+
660+
ps.setInt(1, 1);
661+
ExceptionAssert.sqlException("Cannot cast [class java.lang.String: text] to [Int32]", () -> {
662+
ps.setString(2, "text");
663+
});
664+
}
665+
666+
try (PreparedStatement ps = jdbc.connection().prepareStatement(selectByValue)) {
667+
ps.setString(1, null);
668+
ExceptionAssert.sqlException("Missing value for parameter: " + arg2Name, ps::executeQuery);
669+
670+
ps.setString(1, null);
671+
ps.setString(2, null);
672+
assertResultSetCount(ps.executeQuery(), 0);
673+
674+
ps.setString(1, "1");
675+
ps.setString(2, null);
676+
assertResultSetCount(ps.executeQuery(), 1);
677+
678+
ps.setString(1, null);
679+
ps.setString(2, "2");
680+
assertResultSetCount(ps.executeQuery(), 0);
681+
682+
ps.setString(1, "1");
683+
ps.setString(2, "1");
684+
assertResultSetCount(ps.executeQuery(), 2);
685+
686+
ps.setString(1, "1");
687+
ps.setString(2, "2");
688+
assertResultSetCount(ps.executeQuery(), 1);
689+
690+
ps.setString(1, "1");
691+
ps.setString(2, "3");
692+
assertResultSetCount(ps.executeQuery(), 2);
693+
}
694+
}
695+
617696
@ParameterizedTest(name = "with {0}")
618697
@EnumSource(SqlQueries.JdbcQuery.class)
619698
public void int32Test(SqlQueries.JdbcQuery query) throws SQLException {

jdbc/src/test/java/tech/ydb/jdbc/query/YdbQueryParserTest.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,35 @@ public void inListParameterTest(String query, String parsed) throws SQLException
269269
}
270270
}
271271

272-
@ParameterizedTest(name = "[{index}] {0} has in list parameter")
272+
@ParameterizedTest(name = "[{index}] {0} has as_table list parameter")
273+
@CsvSource(value = {
274+
"'select * from jdbc_table(?) as t join test_table on id=t.x'"
275+
+ "@'select * from AS_TABLE($jp1) as t join test_table on id=t.x'",
276+
"'select * from jdbc_table(?,\n?, ?, \t?)'"
277+
+ "@'select * from AS_TABLE($jp1)'",
278+
"'select * from JDbc_Table (?--comment\n,?,?/**other /** inner */ comment*/)'"
279+
+ "@'select * from AS_TABLE($jp1)'",
280+
}, delimiter = '@')
281+
public void jdbcTableinListParameterTest(String query, String parsed) throws SQLException {
282+
YdbQueryParser parser = new YdbQueryParser(query, true, true, true);
283+
Assertions.assertEquals(parsed, parser.parseSQL());
284+
285+
Assertions.assertEquals(1, parser.getStatements().size());
286+
287+
QueryStatement statement = parser.getStatements().get(0);
288+
Assertions.assertEquals(QueryType.DATA_QUERY, statement.getType());
289+
290+
Assertions.assertTrue(statement.hasJdbcParameters());
291+
int idx = 0;
292+
for (JdbcPrm.Factory factory : statement.getJdbcPrmFactories()) {
293+
for (JdbcPrm prm: factory.create()) {
294+
Assertions.assertEquals("$jp1[" + idx + "]", prm.getName());
295+
idx++;
296+
}
297+
}
298+
}
299+
300+
@ParameterizedTest(name = "[{index}] {0} has not in list parameter")
273301
@CsvSource(value = {
274302
"'select * from test_table where id in (?)'"
275303
+ "@'select * from test_table where id in ($jp1)'",

0 commit comments

Comments
 (0)