Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;

/**
* Prepared statement over the Hrana transport.
Expand Down Expand Up @@ -105,11 +104,18 @@ public int executeUpdate() throws SQLException {
}

private static boolean returnsResultSet(String sql) {
List<Token> tokens = tokenize(sql);
if (tokens.isEmpty()) {
if (sql == null) {
return false;
}
List<SqlTokenizer.Token> tokens = SqlTokenizer.tokenize(sql);
if (tokens == null) {
return false;
}
return switch (tokens.get(0).word()) {
String first = SqlTokenizer.wordAt(tokens, 0, 0);
if (first == null) {
return false;
}
return switch (first) {
case "SELECT", "VALUES", "EXPLAIN" -> true;
case "PRAGMA" -> pragmaReturnsResultSet(tokens);
case "INSERT", "UPDATE", "DELETE", "REPLACE" -> hasTopLevelReturning(tokens, 1);
Expand All @@ -118,23 +124,22 @@ private static boolean returnsResultSet(String sql) {
};
}

private static boolean pragmaReturnsResultSet(List<Token> tokens) {
private static boolean pragmaReturnsResultSet(List<SqlTokenizer.Token> tokens) {
for (int i = 1; i < tokens.size(); i++) {
Token token = tokens.get(i);
if (token.depth() == 0 && token.word().equals("=")) {
if (SqlTokenizer.isSymbolAt(tokens, i, "=", 0)) {
return false;
}
}
return true;
}

private static boolean withReturnsResultSet(List<Token> tokens) {
private static boolean withReturnsResultSet(List<SqlTokenizer.Token> tokens) {
for (int i = 1; i < tokens.size(); i++) {
Token token = tokens.get(i);
if (token.depth() != 0) {
String word = SqlTokenizer.wordAt(tokens, i, 0);
if (word == null) {
continue;
}
switch (token.word()) {
switch (word) {
case "SELECT", "VALUES" -> {
return true;
}
Expand All @@ -148,98 +153,15 @@ private static boolean withReturnsResultSet(List<Token> tokens) {
return false;
}

private static boolean hasTopLevelReturning(List<Token> tokens, int from) {
private static boolean hasTopLevelReturning(List<SqlTokenizer.Token> tokens, int from) {
for (int i = from; i < tokens.size(); i++) {
Token token = tokens.get(i);
if (token.depth() == 0 && token.word().equals("RETURNING")) {
if (SqlTokenizer.isWordAt(tokens, i, 0, "RETURNING")) {
return true;
}
}
return false;
}

private static List<Token> tokenize(String sql) {
List<Token> tokens = new ArrayList<>();
if (sql == null) {
return tokens;
}
int depth = 0;
int i = 0;
int n = sql.length();
while (i < n) {
char c = sql.charAt(i);
if (Character.isWhitespace(c)) {
i++;
} else if (c == '-' && i + 1 < n && sql.charAt(i + 1) == '-') {
i += 2;
while (i < n && sql.charAt(i) != '\n') {
i++;
}
} else if (c == '/' && i + 1 < n && sql.charAt(i + 1) == '*') {
i += 2;
while (i + 1 < n && !(sql.charAt(i) == '*' && sql.charAt(i + 1) == '/')) {
i++;
}
i = Math.min(n, i + 2);
} else if (c == '\'' || c == '"' || c == '`') {
i = skipQuoted(sql, i, c);
} else if (c == '[') {
i++;
while (i < n && sql.charAt(i) != ']') {
i++;
}
if (i < n) {
i++;
}
} else if (c == '(') {
depth++;
i++;
} else if (c == ')') {
depth = Math.max(0, depth - 1);
i++;
} else if (c == '=') {
tokens.add(new Token("=", depth));
i++;
} else if (isWordStart(c)) {
int start = i;
i++;
while (i < n && isWordPart(sql.charAt(i))) {
i++;
}
tokens.add(new Token(sql.substring(start, i).toUpperCase(Locale.ROOT), depth));
} else {
i++;
}
}
return tokens;
}

private static int skipQuoted(String sql, int start, char quote) {
int i = start + 1;
int n = sql.length();
while (i < n) {
if (sql.charAt(i) == quote) {
if (i + 1 < n && sql.charAt(i + 1) == quote) {
i += 2;
continue;
}
return i + 1;
}
i++;
}
return n;
}

private static boolean isWordStart(char c) {
return Character.isLetter(c) || c == '_';
}

private static boolean isWordPart(char c) {
return Character.isLetterOrDigit(c) || c == '_' || c == '$';
}

private record Token(String word, int depth) {}

@Override
public boolean execute() throws SQLException {
return executeResult(sql, executeCurrent());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
* Statement over the Hrana transport.
Expand Down Expand Up @@ -80,6 +81,9 @@ public class LibsqlStatement implements Statement {
private int maxRows = 0;
private boolean escapeProcessing = true;

private static final Set<String> STATEMENT_KEYWORDS =
Set.of("SELECT", "VALUES", "INSERT", "REPLACE", "UPDATE", "DELETE");

public LibsqlStatement(LibsqlConnection connection) {
this.connection = connection;
}
Expand Down Expand Up @@ -402,89 +406,20 @@ protected void clearGeneratedKeys() throws SQLException {
}

private static boolean isInsertSql(String sql) {
int start = skipWhitespace(sql, 0);
if (keywordAt(sql, start, "INSERT") || keywordAt(sql, start, "REPLACE")) {
return true;
}
if (!keywordAt(sql, start, "WITH")) {
return false;
}
int afterWith = start + "WITH".length();
if (afterWith >= sql.length() || !Character.isWhitespace(sql.charAt(afterWith))) {
List<SqlTokenizer.Token> tokens = SqlTokenizer.tokenize(sql);
if (tokens == null) {
return false;
}

int depth = 0;
int i = afterWith;
while (i < sql.length()) {
char c = sql.charAt(i);
if (c == '-' && i + 1 < sql.length() && sql.charAt(i + 1) == '-') {
i += 2;
while (i < sql.length() && sql.charAt(i) != '\n') {
i++;
}
} else if (c == '/' && i + 1 < sql.length() && sql.charAt(i + 1) == '*') {
i += 2;
while (i + 1 < sql.length()
&& !(sql.charAt(i) == '*' && sql.charAt(i + 1) == '/')) {
i++;
}
i = Math.min(sql.length(), i + 2);
} else if (c == '\'' || c == '"' || c == '`') {
i = skipQuoted(sql, i, c);
} else if (c == '[') {
i++;
while (i < sql.length() && sql.charAt(i) != ']') {
i++;
}
if (i < sql.length()) {
i++;
}
} else if (c == '(') {
depth++;
i++;
} else if (c == ')') {
depth = Math.max(0, depth - 1);
i++;
if (depth == 0) {
int next = skipWhitespace(sql, i);
if (keywordAt(sql, next, "INSERT") || keywordAt(sql, next, "REPLACE")) {
return true;
}
}
} else {
i++;
}
}
return false;
}

private static int skipWhitespace(String sql, int start) {
int i = start;
while (i < sql.length() && Character.isWhitespace(sql.charAt(i))) {
i++;
String first = SqlTokenizer.wordAt(tokens, 0, 0);
if ("INSERT".equals(first) || "REPLACE".equals(first)) {
return true;
}
return i;
}

private static int skipQuoted(String sql, int start, char quote) {
int i = start + 1;
while (i < sql.length()) {
if (sql.charAt(i) == quote) {
if (i + 1 < sql.length() && sql.charAt(i + 1) == quote) {
i += 2;
continue;
}
return i + 1;
}
i++;
if (!"WITH".equals(first)) {
return false;
}
return sql.length();
}

private static boolean keywordAt(String sql, int index, String keyword) {
return index + keyword.length() <= sql.length()
&& sql.regionMatches(true, index, keyword, 0, keyword.length());
int index = SqlTokenizer.indexOfAnyWord(tokens, 1, tokens.size(), 0, STATEMENT_KEYWORDS);
String statement = SqlTokenizer.wordAt(tokens, index, 0);
return "INSERT".equals(statement) || "REPLACE".equals(statement);
}

private ResultSet emptyGeneratedKeys() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,107 @@ void autoCommitInsertReturnsLastInsertRowidFromStmtResult() throws SQLException
}
}

@Test
void insertOrReplaceReturnsGeneratedKey() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(8L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("insert or replace into k (id, v) values (8, 'a')");

try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isEqualTo(8L);
}
}
}

@Test
void commentsAroundInsertLeaveTheKeyDetectionIntact() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(4L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("/* seed */ insert into k (v) values ('a')");
try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isEqualTo(4L);
}

statement.executeUpdate("-- seed\ninsert into k (v) values ('b')");
try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isEqualTo(4L);
}
}
}

@Test
void commentBetweenCteAndInsertLeavesTheKeyDetectionIntact() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(12L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("with cte as (select lower('--') as v)"
+ " /* insert */ insert into k (v) select v from cte");

try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isEqualTo(12L);
}
}
}

@Test
void quotedIdentifierNamedInsertDoesNotCreateGeneratedKeys() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(21L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("with \"insert\" as (select 1) select * from \"insert\"");

try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isFalse();
}
}
}

@Test
void cteUpdateDoesNotCreateGeneratedKeys() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(22L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("with cte as (select 1) update k set v = 'a'");

try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isFalse();
}
}
}

@Test
void quotedTableNameStillReportsTheInsertKey() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(23L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("insert into [k] (v) values ('a')");

try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isTrue();
assertThat(keys.getLong(1)).isEqualTo(23L);
}
}
}

@Test
void unbalancedSqlLeavesNoGeneratedKeys() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(24L));

try (Statement statement = connection.createStatement()) {
statement.executeUpdate("insert into k (v values ('a')");

try (ResultSet keys = statement.getGeneratedKeys()) {
assertThat(keys.next()).isFalse();
}
}
}

@Test
void statementReplaceReturnsGeneratedKey() throws SQLException {
RecordingConnection connection = new RecordingConnection(insertResult(7L));
Expand Down
Loading
Loading