Skip to content

Commit

Permalink
Added support of jdbc_table macro
Browse files Browse the repository at this point in the history
  • Loading branch information
alex268 committed Nov 27, 2024
1 parent 2222ad7 commit 6958183
Show file tree
Hide file tree
Showing 5 changed files with 335 additions and 1 deletion.
87 changes: 87 additions & 0 deletions jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ public String parseSQL() throws SQLException {
fragmentStart = i;
}
}

// Process JDBC_TABLE (?, ?, ... )
if (i < chars.length && detectJdbcArgs && isConvertJdbcInToList) {
if (parseJdbcTableKeyword(chars, keywordStart, keywordLength)) {
parsed.append(chars, fragmentStart, keywordStart - fragmentStart);
fragmentStart = keywordStart;
int updated = parseJdbcTableListParameters(chars, i, statement);
if (updated != i) {
i = updated;
fragmentStart = updated;
}
}
}
} else {
boolean skipped = false;
if (isDetectQueryType) {
Expand Down Expand Up @@ -381,6 +394,63 @@ private int parseInListParameters(char[] query, int offset, QueryStatement st) {
return start;
}

private int parseJdbcTableListParameters(char[] query, int offset, QueryStatement st) {
int start = offset;
int listStartedAt = -1;
int listSize = 0;
boolean waitPrm = false;
while (offset < query.length) {
char ch = query[offset];
switch (ch) {
case '(': // start of list
if (listStartedAt >= 0) {
return start;
}
listStartedAt = offset;
waitPrm = true;
break;
case ',':
if (listStartedAt < 0 || waitPrm) {
return start;
}
waitPrm = true;
break;
case '?' :
if (!waitPrm || (offset + 1 < query.length && query[offset + 1] == '?')) {
return start;
}
listSize++;
waitPrm = false;
break;
case ')':
if (waitPrm || listSize == 0 || listStartedAt < 0) {
return start;
}

String name = nextJdbcPrmName();
parsed.append(" AS_TABLE(");
parsed.append(name);
parsed.append(")");
st.addJdbcPrmFactory(JdbcPrm.jdbcTableListOrm(name, listSize));
return offset + 1;
case '-': // possibly -- style comment
offset = parseLineComment(query, offset);
break;
case '/': // possibly /* */ style comment
offset = parseBlockComment(query, offset);
break;
default:
if (!Character.isWhitespace(query[offset])) {
return start;
}
break;
}
offset++;
}

return start;
}

private static int parseSingleQuotes(final char[] query, int offset) {
// treat backslashes as escape characters
while (++offset < query.length) {
Expand Down Expand Up @@ -657,4 +727,21 @@ private static boolean parseInKeyword(char[] query, int offset, int length) {
return (query[offset] | 32) == 'i'
&& (query[offset + 1] | 32) == 'n';
}

private static boolean parseJdbcTableKeyword(char[] query, int offset, int length) {
if (length != 10) {
return false;
}

return (query[offset] | 32) == 'j'
&& (query[offset + 1] | 32) == 'd'
&& (query[offset + 2] | 32) == 'b'
&& (query[offset + 3] | 32) == 'c'
&& (query[offset + 4]) == '_'
&& (query[offset + 5] | 32) == 't'
&& (query[offset + 6] | 32) == 'a'
&& (query[offset + 7] | 32) == 'b'
&& (query[offset + 8] | 32) == 'l'
&& (query[offset + 9] | 32) == 'e';
}
}
136 changes: 136 additions & 0 deletions jdbc/src/main/java/tech/ydb/jdbc/query/params/AsTableJdbcPrm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package tech.ydb.jdbc.query.params;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import tech.ydb.jdbc.YdbConst;
import tech.ydb.jdbc.common.TypeDescription;
import tech.ydb.jdbc.impl.YdbTypes;
import tech.ydb.table.query.Params;
import tech.ydb.table.values.ListType;
import tech.ydb.table.values.OptionalType;
import tech.ydb.table.values.StructType;
import tech.ydb.table.values.Type;
import tech.ydb.table.values.Value;
import tech.ydb.table.values.VoidValue;

/**
*
* @author Aleksandr Gorshenin
*/
public class AsTableJdbcPrm {
private static final Value<?> NULL = VoidValue.of();
private static final String COLUMN_NAME = "x";

private final String listName;
private final List<Item> items = new ArrayList<>();
private TypeDescription type;

public AsTableJdbcPrm(String listName, int listSize) {
this.listName = listName;
for (int idx = 0; idx < listSize; idx++) {
items.add(new Item(listName, idx));
}
}

public List<? extends JdbcPrm> toJdbcPrmList() {
return items;
}

private Value<?> buildList() throws SQLException {
if (type == null) {
throw new SQLException(YdbConst.PARAMETER_TYPE_UNKNOWN);
}

boolean hasNull = false;
for (Item item: items) {
if (item.value == null) {
throw new SQLException(YdbConst.MISSING_VALUE_FOR_PARAMETER + item.name);
}
hasNull = hasNull || item.value == NULL;
}

List<Value<?>> values = new ArrayList<>();
if (!hasNull) {
StructType structType = StructType.of(COLUMN_NAME, type.ydbType());
for (Item item: items) {
values.add(structType.newValue(COLUMN_NAME, item.value));
}
return ListType.of(structType).newValue(values);
}

OptionalType optional = type.ydbType().makeOptional();
StructType structType = StructType.of(COLUMN_NAME, optional);
for (Item item: items) {
if (item.value == NULL) {
values.add(structType.newValue(COLUMN_NAME, optional.emptyValue()));
} else {
values.add(structType.newValue(COLUMN_NAME, item.value.makeOptional()));
}
}

return ListType.of(structType).newValue(values);

}

private class Item implements JdbcPrm {
private final String name;
private final int index;
private Value<?> value = null;

Item(String listName, int index) {
this.name = listName + "[" + index + "]";
this.index = index;
}

@Override
public String getName() {
return name;
}

@Override
public TypeDescription getType() {
return type;
}

@Override
public void setValue(Object obj, int sqlType) throws SQLException {
if (type == null) {
Type ydbType = YdbTypes.findType(obj, sqlType);
if (ydbType == null) {
if (obj == null) {
value = NULL;
return;
} else {
throw new SQLException(String.format(YdbConst.PARAMETER_TYPE_UNKNOWN, sqlType, obj));
}
}

type = TypeDescription.of(ydbType);
}

if (obj == null) {
value = NULL;
return;
}

value = type.setters().toValue(obj);
}

@Override
public void copyToParams(Params params) throws SQLException {
if (index == 0) { // first prm
params.put(listName, buildList());
}
}

@Override
public void reset() {
value = null;
if (index == items.size() - 1) { // last prm reset type
type = null;
}
}
}
}
4 changes: 4 additions & 0 deletions jdbc/src/main/java/tech/ydb/jdbc/query/params/JdbcPrm.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ static Factory uint64Prm(String name) {
static Factory inListOrm(String name, int count) {
return () -> new InListJdbcPrm(name, count).toJdbcPrmList();
}

static Factory jdbcTableListOrm(String name, int count) {
return () -> new AsTableJdbcPrm(name, count).toJdbcPrmList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,85 @@ public void inListTest(boolean convertInToList) throws SQLException {
}
}

@Test
public void jdbcTableListTest() throws SQLException {
String arg2Name = "$jp1[1]";
String upsert = TEST_TABLE.upsertOne(SqlQueries.JdbcQuery.STANDARD, "c_Text", "Text");
String selectByIds = TEST_TABLE.withTableName(
"select count(*) from jdbc_table(?,?) as j join #tableName t on t.key=j.x"
);
String selectByValue = TEST_TABLE.withTableName(
"select count(*) from jdbc_table(?,?) as j join #tableName t on t.c_Text=j.x"
);

try (PreparedStatement ps = jdbc.connection().prepareStatement(upsert)) {
ps.setInt(1, 1);
ps.setString(2, "1");
ps.addBatch();

ps.setInt(1, 2);
ps.setString(2, null);
ps.addBatch();

ps.setInt(1, 3);
ps.setString(2, "3");
ps.addBatch();

ps.setInt(1, 4);
ps.setString(2, "null");
ps.addBatch();

ps.executeBatch();
}

try (PreparedStatement ps = jdbc.connection().prepareStatement(selectByIds)) {
ps.setInt(1, 1);
ExceptionAssert.sqlException("Missing value for parameter: " + arg2Name, ps::executeQuery);

ps.setInt(1, 1);
ps.setInt(2, 2);
assertResultSetCount(ps.executeQuery(), 2);

ps.setInt(1, 1);
ps.setInt(2, 5);
assertResultSetCount(ps.executeQuery(), 1);

ps.setInt(1, 1);
ExceptionAssert.sqlException("Cannot cast [class java.lang.String: text] to [Int32]", () -> {
ps.setString(2, "text");
});
}

try (PreparedStatement ps = jdbc.connection().prepareStatement(selectByValue)) {
ps.setString(1, null);
ExceptionAssert.sqlException("Missing value for parameter: " + arg2Name, ps::executeQuery);

ps.setString(1, null);
ps.setString(2, null);
assertResultSetCount(ps.executeQuery(), 0);

ps.setString(1, "1");
ps.setString(2, null);
assertResultSetCount(ps.executeQuery(), 1);

ps.setString(1, null);
ps.setString(2, "2");
assertResultSetCount(ps.executeQuery(), 0);

ps.setString(1, "1");
ps.setString(2, "1");
assertResultSetCount(ps.executeQuery(), 2);

ps.setString(1, "1");
ps.setString(2, "2");
assertResultSetCount(ps.executeQuery(), 1);

ps.setString(1, "1");
ps.setString(2, "3");
assertResultSetCount(ps.executeQuery(), 2);
}
}

@ParameterizedTest(name = "with {0}")
@EnumSource(SqlQueries.JdbcQuery.class)
public void int32Test(SqlQueries.JdbcQuery query) throws SQLException {
Expand Down
30 changes: 29 additions & 1 deletion jdbc/src/test/java/tech/ydb/jdbc/query/YdbQueryParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,35 @@ public void inListParameterTest(String query, String parsed) throws SQLException
}
}

@ParameterizedTest(name = "[{index}] {0} has in list parameter")
@ParameterizedTest(name = "[{index}] {0} has as_table list parameter")
@CsvSource(value = {
"'select * from jdbc_table(?) as t join test_table on id=t.x'"
+ "@'select * from AS_TABLE($jp1) as t join test_table on id=t.x'",
"'select * from jdbc_table(?,\n?, ?, \t?)'"
+ "@'select * from AS_TABLE($jp1)'",
"'select * from JDbc_Table (?--comment\n,?,?/**other /** inner */ comment*/)'"
+ "@'select * from AS_TABLE($jp1)'",
}, delimiter = '@')
public void jdbcTableinListParameterTest(String query, String parsed) throws SQLException {
YdbQueryParser parser = new YdbQueryParser(query, true, true, true);
Assertions.assertEquals(parsed, parser.parseSQL());

Assertions.assertEquals(1, parser.getStatements().size());

QueryStatement statement = parser.getStatements().get(0);
Assertions.assertEquals(QueryType.DATA_QUERY, statement.getType());

Assertions.assertTrue(statement.hasJdbcParameters());
int idx = 0;
for (JdbcPrm.Factory factory : statement.getJdbcPrmFactories()) {
for (JdbcPrm prm: factory.create()) {
Assertions.assertEquals("$jp1[" + idx + "]", prm.getName());
idx++;
}
}
}

@ParameterizedTest(name = "[{index}] {0} has not in list parameter")
@CsvSource(value = {
"'select * from test_table where id in (?)'"
+ "@'select * from test_table where id in ($jp1)'",
Expand Down

0 comments on commit 6958183

Please sign in to comment.