diff --git a/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java b/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java index b7c5511..661c478 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/YdbConst.java @@ -66,7 +66,8 @@ public final class YdbConst { public static final String INVALID_ROW = "Current row index is out of bounds: "; public static final String BULKS_UNSUPPORTED = "BULK mode is available only for prepared statement with one UPSERT"; public static final String INVALID_BATCH_COLUMN = "Cannot prepared batch request: cannot find a column"; - public static final String BULKS_DESCRIBE_ERROR = "Cannot parse BULK upsert: "; + public static final String BULK_DESCRIBE_ERROR = "Cannot parse BULK upsert: "; + public static final String BULK_NOT_SUPPORT_RETURNING = "BULK query doesn't support RETURNING"; public static final String METADATA_RS_UNSUPPORTED_IN_PS = "ResultSet metadata is not supported " + "in prepared statements"; public static final String CANNOT_UNWRAP_TO = "Cannot unwrap to "; diff --git a/jdbc/src/main/java/tech/ydb/jdbc/context/StaticQueryResult.java b/jdbc/src/main/java/tech/ydb/jdbc/context/StaticQueryResult.java index 820f0b9..1413dc0 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/context/StaticQueryResult.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/context/StaticQueryResult.java @@ -34,15 +34,24 @@ public class StaticQueryResult implements YdbQueryResult { private static class ExpressionResult { private final int updateCount; private final YdbResultSet resultSet; + private final YdbResultSet generatedKeys; ExpressionResult(int updateCount) { this.updateCount = updateCount; this.resultSet = null; + this.generatedKeys = null; } - ExpressionResult(YdbResultSet resultSet) { + ExpressionResult(int updateCount, YdbResultSet keys) { + this.updateCount = updateCount; + this.resultSet = null; + this.generatedKeys = keys; + } + + ExpressionResult(YdbResultSet result) { this.updateCount = -1; - this.resultSet = resultSet; + this.resultSet = result; + this.generatedKeys = null; } } @@ -59,6 +68,13 @@ public StaticQueryResult(YdbQuery query, List list) { results.add(NO_UPDATED); continue; } + + if (exp.hasUpdateWithGenerated() && idx < list.size()) { + results.add(new ExpressionResult(1, list.get(idx))); + idx++; + continue; + } + if (exp.hasUpdateCount()) { results.add(HAS_UPDATED); continue; @@ -124,6 +140,14 @@ public YdbResultSet getCurrentResultSet() { return results.get(resultIndex).resultSet; } + @Override + public YdbResultSet getGeneratedKeys() { + if (results == null || resultIndex >= results.size()) { + return null; + } + return results.get(resultIndex).generatedKeys; + } + @Override public boolean getMoreResults(int current) throws SQLException { if (results == null || resultIndex >= results.size()) { diff --git a/jdbc/src/main/java/tech/ydb/jdbc/context/StreamQueryResult.java b/jdbc/src/main/java/tech/ydb/jdbc/context/StreamQueryResult.java index c071c91..0d72bc9 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/context/StreamQueryResult.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/context/StreamQueryResult.java @@ -219,6 +219,12 @@ public YdbResultSet getCurrentResultSet() throws SQLException { } } + @Override + public YdbResultSet getGeneratedKeys() throws SQLException { + // TODO: Implement + return null; + } + @Override public boolean hasResultSets() throws SQLException { if (resultIndex >= resultIndexes.length) { diff --git a/jdbc/src/main/java/tech/ydb/jdbc/context/YdbContext.java b/jdbc/src/main/java/tech/ydb/jdbc/context/YdbContext.java index b4eb548..6c0bef1 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/context/YdbContext.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/context/YdbContext.java @@ -29,6 +29,7 @@ import tech.ydb.jdbc.exception.ExceptionFactory; import tech.ydb.jdbc.impl.YdbTracerImpl; import tech.ydb.jdbc.impl.YdbTracerNone; +import tech.ydb.jdbc.query.QueryKey; import tech.ydb.jdbc.query.QueryType; import tech.ydb.jdbc.query.YdbPreparedQuery; import tech.ydb.jdbc.query.YdbQuery; @@ -83,7 +84,7 @@ public class YdbContext implements AutoCloseable { private final String prefixPath; private final String prefixPragma; - private final Cache queriesCache; + private final Cache queriesCache; private final Cache statsCache; private final Cache> queryParamsCache; private final Cache tableDescribeCache; @@ -348,22 +349,21 @@ public > T withRequestTimeout(T bui return builder.withRequestTimeout(operation); } - public YdbQuery parseYdbQuery(String sql) throws SQLException { - return YdbQuery.parseQuery(sql, queryOptions, types); + public YdbQuery parseYdbQuery(String query) throws SQLException { + return YdbQuery.parseQuery(new QueryKey(query), queryOptions, types); } - public YdbQuery findOrParseYdbQuery(String sql) throws SQLException { + public YdbQuery findOrParseYdbQuery(QueryKey key) throws SQLException { if (queriesCache == null) { - return parseYdbQuery(sql); + return YdbQuery.parseQuery(key, queryOptions, types); } - YdbQuery cached = queriesCache.getIfPresent(sql); + YdbQuery cached = queriesCache.getIfPresent(key); if (cached == null) { - cached = parseYdbQuery(sql); - queriesCache.put(sql, cached); + cached = YdbQuery.parseQuery(key, queryOptions, types); + queriesCache.put(key, cached); } - return cached; } @@ -432,16 +432,19 @@ public YdbPreparedQuery findOrPrepareParams(YdbQuery query, YdbPrepareMode mode) tableDescribeCache.put(tablePath, description); } else { if (type == QueryType.BULK_QUERY) { - throw new SQLException(YdbConst.BULKS_DESCRIBE_ERROR + result.getStatus()); + throw new SQLException(YdbConst.BULK_DESCRIBE_ERROR + result.getStatus()); } } } if (type == QueryType.BULK_QUERY) { + if (query.getReturning() != null) { + throw new SQLException(YdbConst.BULK_NOT_SUPPORT_RETURNING); + } return BulkUpsertQuery.build(types, tablePath, query.getYqlBatcher().getColumns(), description); } if (description != null) { - BatchedQuery params = BatchedQuery.createAutoBatched(types, query.getYqlBatcher(), description); + BatchedQuery params = BatchedQuery.createAutoBatched(types, query, description); if (params != null) { return params; } diff --git a/jdbc/src/main/java/tech/ydb/jdbc/impl/BaseYdbStatement.java b/jdbc/src/main/java/tech/ydb/jdbc/impl/BaseYdbStatement.java index 0601755..5450d69 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/impl/BaseYdbStatement.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/impl/BaseYdbStatement.java @@ -244,7 +244,7 @@ public void setCursorName(String name) throws SQLFeatureNotSupportedException { @Override public ResultSet getGeneratedKeys() throws SQLException { - return null; // -- + return state.getGeneratedKeys(); } @Override diff --git a/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbConnectionImpl.java b/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbConnectionImpl.java index 02cba57..f537183 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbConnectionImpl.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbConnectionImpl.java @@ -29,6 +29,7 @@ import tech.ydb.jdbc.context.YdbContext; import tech.ydb.jdbc.context.YdbExecutor; import tech.ydb.jdbc.context.YdbValidator; +import tech.ydb.jdbc.query.QueryKey; import tech.ydb.jdbc.query.YdbPreparedQuery; import tech.ydb.jdbc.query.YdbQuery; @@ -213,15 +214,15 @@ public YdbStatement createStatement(int resultSetType, int resultSetConcurrency, } @Override - public YdbPreparedStatement prepareStatement(String origSql, int resultSetType, int resultSetConcurrency, + public YdbPreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { checkStatementParams(resultSetType, resultSetConcurrency, resultSetHoldability); - return prepareStatement(origSql, resultSetType, null, YdbPrepareMode.AUTO); + return prepareStatement(new QueryKey(sql), resultSetType, YdbPrepareMode.AUTO); } @Override public YdbPreparedStatement prepareStatement(String sql, YdbPrepareMode mode) throws SQLException { - return prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, null, mode); + return prepareStatement(new QueryKey(sql), ResultSet.TYPE_SCROLL_INSENSITIVE, mode); } @Override @@ -245,18 +246,18 @@ public YdbPreparedStatement prepareStatement(String sql, String[] columnNames) t if (columnNames == null || columnNames.length == 0) { return prepareStatement(sql); } - return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, columnNames, YdbPrepareMode.AUTO); + return prepareStatement(new QueryKey(sql, columnNames), ResultSet.TYPE_FORWARD_ONLY, YdbPrepareMode.AUTO); } - private YdbPreparedStatement prepareStatement(String sql, int type, String[] columnNames, YdbPrepareMode mode) + private YdbPreparedStatement prepareStatement(QueryKey key, int resultSetType, YdbPrepareMode mode) throws SQLException { validator.clearWarnings(); ctx.getTracer().trace("prepare statement"); - YdbQuery query = ctx.findOrParseYdbQuery(sql); + YdbQuery query = ctx.findOrParseYdbQuery(key); YdbPreparedQuery params = ctx.findOrPrepareParams(query, mode); ctx.getTracer().trace("create prepared statement"); - return new YdbPreparedStatementImpl(this, query, params, type); + return new YdbPreparedStatementImpl(this, query, params, resultSetType); } @Override diff --git a/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbQueryResult.java b/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbQueryResult.java index 32e908c..8754bce 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbQueryResult.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/impl/YdbQueryResult.java @@ -20,6 +20,11 @@ public YdbResultSet getCurrentResultSet() throws SQLException { return null; } + @Override + public YdbResultSet getGeneratedKeys() throws SQLException { + return null; + } + @Override public boolean hasResultSets() throws SQLException { return false; @@ -36,6 +41,7 @@ public void close() throws SQLException { } int getUpdateCount() throws SQLException; YdbResultSet getCurrentResultSet() throws SQLException; + YdbResultSet getGeneratedKeys() throws SQLException; boolean hasResultSets() throws SQLException; boolean getMoreResults(int current) throws SQLException; diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/QueryKey.java b/jdbc/src/main/java/tech/ydb/jdbc/query/QueryKey.java new file mode 100644 index 0000000..fce186f --- /dev/null +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/QueryKey.java @@ -0,0 +1,72 @@ +package tech.ydb.jdbc.query; + +import java.util.Objects; + +/** + * + * @author Aleksandr Gorshenin + */ +public class QueryKey { + private final String query; + private final String returning; + + public QueryKey(String query) { + this.query = query; + this.returning = null; + } + + public QueryKey(String query, String[] columnNames) { + this.query = query; + this.returning = buildReturning(columnNames); + } + + public String getQuery() { + return query; + } + + public String getReturning() { + return returning; + } + + @Override + public int hashCode() { + return Objects.hash(query, returning); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + QueryKey other = (QueryKey) obj; + return Objects.equals(this.query, other.query) && Objects.equals(this.returning, other.returning); + } + + private static String buildReturning(String[] columnNames) { + StringBuilder sb = new StringBuilder(); + sb.append("RETURNING "); + if (columnNames.length == 1 && columnNames[0].charAt(0) == '*') { + sb.append('*'); + return sb.toString(); + } + + for (int col = 0; col < columnNames.length; col++) { + String columnName = columnNames[col]; + if (col > 0) { + sb.append(", "); + } + + if (columnName.charAt(0) == '`') { + sb.append(columnName); + } else { + sb.append('`').append(columnName).append('`'); + } + } + + return sb.toString(); + } +} diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/QueryStatement.java b/jdbc/src/main/java/tech/ydb/jdbc/query/QueryStatement.java index 8d31012..e985653 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/query/QueryStatement.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/QueryStatement.java @@ -16,6 +16,7 @@ public class QueryStatement { private final QueryCmd command; private final List parameters = new ArrayList<>(); private boolean hasReturinng = false; + private boolean hasGenerated = false; public QueryStatement(QueryType custom, QueryType baseType, QueryCmd command) { this.queryType = custom != null ? custom : baseType; @@ -46,10 +47,18 @@ public void setHasReturning(boolean hasReturning) { this.hasReturinng = hasReturning; } + public void setHasGenerated(boolean hasGenerated) { + this.hasGenerated = hasGenerated; + } + public boolean hasUpdateCount() { return (command == QueryCmd.INSERT_UPSERT || command == QueryCmd.UPDATE_REPLACE_DELETE) && !hasReturinng; } + public boolean hasUpdateWithGenerated() { + return hasGenerated; + } + public boolean hasResults() { return command == QueryCmd.SELECT || hasReturinng; } diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQuery.java b/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQuery.java index cb5a059..ec516ac 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQuery.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQuery.java @@ -13,7 +13,7 @@ * @author Aleksandr Gorshenin */ public class YdbQuery { - private final String originQuery; + private final QueryKey key; private final String preparedYQL; private final List statements; private final YqlBatcher batcher; @@ -21,8 +21,8 @@ public class YdbQuery { private final QueryType type; private final boolean isPlainYQL; - YdbQuery(String originQuery, String preparedYQL, List stats, YqlBatcher batcher, QueryType type) { - this.originQuery = originQuery; + YdbQuery(QueryKey key, String preparedYQL, List stats, YqlBatcher batcher, QueryType type) { + this.key = key; this.preparedYQL = preparedYQL; this.statements = stats; this.type = type; @@ -48,7 +48,11 @@ public boolean isPlainYQL() { } public String getOriginQuery() { - return originQuery; + return key.getQuery(); + } + + public String getReturning() { + return key.getReturning(); } public String getPreparedYql() { @@ -59,7 +63,7 @@ public List getStatements() { return statements; } - public static YdbQuery parseQuery(String query, YdbQueryProperties opts, YdbTypes types) throws SQLException { + public static YdbQuery parseQuery(QueryKey query, YdbQueryProperties opts, YdbTypes types) throws SQLException { YdbQueryParser parser = new YdbQueryParser(types, query, opts); String preparedYQL = parser.parseSQL(); 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 25941a5..a15c0c1 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/YdbQueryParser.java @@ -22,20 +22,27 @@ public class YdbQueryParser { private final boolean isForceJdbcParamters; private final boolean isConvertJdbcInToList; - private final List statements = new ArrayList<>(); - private final YqlBatcher batcher = new YqlBatcher(); private final String origin; + private final String returning; private final StringBuilder parsed; private final YdbTypes types; + private final List statements = new ArrayList<>(); + private final YqlBatcher batcher = new YqlBatcher(); + private int jdbcPrmIndex = 0; - public YdbQueryParser(YdbTypes types, String origin, YdbQueryProperties props) { + public YdbQueryParser(YdbTypes types, String query, YdbQueryProperties props) { + this(types, new QueryKey(query), props); + } + + public YdbQueryParser(YdbTypes types, QueryKey key, YdbQueryProperties props) { this.isDetectQueryType = props.isDetectQueryType(); this.isDetectJdbcParameters = props.isDetectJdbcParameters(); this.isForceJdbcParamters = props.isForceJdbcParameters(); this.isConvertJdbcInToList = props.isReplaceJdbcInByYqlList(); - this.origin = origin; + this.origin = key.getQuery(); + this.returning = key.getReturning(); this.parsed = new StringBuilder(origin.length() + 10); this.types = types; } @@ -300,6 +307,7 @@ public String parseSQL() throws SQLException { case ';': batcher.readSemiColon(); if (parenLevel == 0) { + addReturning(parsed, statement); statement = null; type = null; detectJdbcArgs = false; @@ -315,9 +323,23 @@ public String parseSQL() throws SQLException { parsed.append(chars, fragmentStart, chars.length - fragmentStart); } + addReturning(parsed, statement); + return parsed.toString(); } + private void addReturning(StringBuilder parsed, QueryStatement st) throws SQLException { + if (st == null || returning == null || st.hasResults()) { + return; + } + if (st.getCmd() != QueryCmd.INSERT_UPSERT && st.getCmd() != QueryCmd.UPDATE_REPLACE_DELETE) { + return; + } + + st.setHasGenerated(true); + parsed.append("\n").append(returning); + } + private String nextJdbcPrmName() { while (true) { jdbcPrmIndex += 1; diff --git a/jdbc/src/main/java/tech/ydb/jdbc/query/params/BatchedQuery.java b/jdbc/src/main/java/tech/ydb/jdbc/query/params/BatchedQuery.java index 97d84d5..21b029d 100644 --- a/jdbc/src/main/java/tech/ydb/jdbc/query/params/BatchedQuery.java +++ b/jdbc/src/main/java/tech/ydb/jdbc/query/params/BatchedQuery.java @@ -230,9 +230,11 @@ public static BatchedQuery tryCreateBatched(YdbTypes types, YdbQuery query, Map< return new BatchedQuery(null, query.getPreparedYql(), listName, descriptions); } - public static BatchedQuery createAutoBatched(YdbTypes types, YqlBatcher batcher, TableDescription description) + public static BatchedQuery createAutoBatched(YdbTypes types, YdbQuery query, TableDescription description) throws SQLException { + YqlBatcher batcher = query.getYqlBatcher(); + // DELETE and UPDATE may be batched only if WHERE contains only primary key columns if (batcher.getCommand() == YqlBatcher.Cmd.DELETE || batcher.getCommand() == YqlBatcher.Cmd.UPDATE) { Set primaryKey = new HashSet<>(description.getPrimaryKeys()); @@ -267,10 +269,12 @@ public static BatchedQuery createAutoBatched(YdbTypes types, YqlBatcher batcher, idx++; } - return new BatchedQuery(simpleQuery(batcher, params), batchQuery(batcher, params), "$batch", params); + String simple = simpleQuery(batcher, params, query.getReturning()); + String batched = batchQuery(batcher, params, query.getReturning()); + return new BatchedQuery(simple, batched, "$batch", params); } - private static String batchQuery(YqlBatcher batcher, ParamDescription[] params) { + private static String batchQuery(YqlBatcher batcher, ParamDescription[] params, String returning) { StringBuilder sb = new StringBuilder(); sb.append("DECLARE $batch AS List YdbQuery.parseQuery(sql, opts, new YdbTypes(false)), + () -> YdbQuery.parseQuery(new QueryKey(sql), opts, new YdbTypes(false)), "Mix type query must throw SQLException" ); Assertions.assertEquals("Query cannot contain expressions with different types: " + types, ex.getMessage());