diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java b/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java index 8cde00c..f9a732c 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java @@ -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) { @@ -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) { @@ -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'; + } } diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/params/AsTableJdbcPrm.java b/jdbc/src/main/java/tech/ydb/jdbc/query/params/AsTableJdbcPrm.java new file mode 100644 index 0000000..e25777b --- /dev/null +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/params/AsTableJdbcPrm.java @@ -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 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 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> 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; + } + } + } +} diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/params/JdbcPrm.java b/jdbc/src/main/java/tech/ydb/jdbc/query/params/JdbcPrm.java index 7d10a53..ecbdbee 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/query/params/JdbcPrm.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/params/JdbcPrm.java @@ -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(); + } } diff --git a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java index d98c73e..f0e44a6 100644 --- a/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java +++ b/jdbc/src/test/java/tech/ydb/jdbc/impl/YdbPreparedStatementTest.java @@ -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 { diff --git a/jdbc/src/test/java/tech/ydb/jdbc/query/YdbQueryParserTest.java b/jdbc/src/test/java/tech/ydb/jdbc/query/YdbQueryParserTest.java index c0cf533..5597a52 100644 --- a/jdbc/src/test/java/tech/ydb/jdbc/query/YdbQueryParserTest.java +++ b/jdbc/src/test/java/tech/ydb/jdbc/query/YdbQueryParserTest.java @@ -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)'",