LibSQL: Implement LIKE SQL expressions

This commit is contained in:
Guilherme Gonçalves 2021-12-29 11:47:29 -03:00 committed by Ali Mohammad Pur
parent e957c078d5
commit f91d471843
Notes: sideshowbarker 2024-07-19 17:11:18 +09:00
5 changed files with 133 additions and 1 deletions

View file

@ -409,6 +409,7 @@ if (BUILD_LAGOM)
list(REMOVE_ITEM LIBSQL_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/../../Userland/Libraries/LibSQL/SQLClient.cpp")
lagom_lib(SQL sql
SOURCES ${LIBSQL_SOURCES}
LIBS LagomRegex
)
# TextCodec

View file

@ -337,4 +337,77 @@ TEST_CASE(select_inner_join)
EXPECT_EQ(row[2].to_string(), "Test_12");
}
TEST_CASE(select_with_like)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = SQL::Database::construct(db_name);
EXPECT(!database->open().is_error());
create_table(database);
auto result = execute(database,
"INSERT INTO TestSchema.TestTable ( TextColumn, IntColumn ) VALUES "
"( 'Test+1', 42 ), "
"( 'Test+2', 43 ), "
"( 'Test+3', 44 ), "
"( 'Test+4', 45 ), "
"( 'Test+5', 46 ), "
"( 'Another+Test_6', 47 );");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->inserted() == 6);
// Simple match
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE 'Test+1';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 1u);
// Use % to match most rows
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE 'T%';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 5u);
// Same as above but invert the match
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn NOT LIKE 'T%';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 1u);
// Use _ and % to match all rows
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE '%e_t%';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 6u);
// Use escape to match a single row. The escape character happens to be a
// Regex metacharacter, let's make sure we don't get confused by that.
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE '%Test^_%' ESCAPE '^';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 1u);
// Same as above but escape the escape character happens to be a SQL
// metacharacter - we want to make sure it's treated as an escape.
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE '%Test__%' ESCAPE '_';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 1u);
// (Unnecessarily) escaping a character that happens to be a Regex
// metacharacter should have no effect.
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE 'Test:+_' ESCAPE ':';");
EXPECT(result->error().code == SQL::SQLErrorCode::NoError);
EXPECT(result->has_results());
EXPECT_EQ(result->results().size(), 5u);
// Make sure we error out if the ESCAPE is empty
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE '%' ESCAPE '';");
EXPECT(result->error().code == SQL::SQLErrorCode::SyntaxError);
EXPECT(!result->has_results());
// Make sure we error out if the ESCAPE has more than a single character
result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE TextColumn LIKE '%' ESCAPE 'whf';");
EXPECT(result->error().code == SQL::SQLErrorCode::SyntaxError);
EXPECT(!result->has_results());
}
}

View file

@ -636,6 +636,7 @@ public:
MatchOperator type() const { return m_type; }
const RefPtr<Expression>& escape() const { return m_escape; }
virtual Value evaluate(ExecutionContext&) const override;
private:
MatchOperator m_type;

View file

@ -4,11 +4,14 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibRegex/Regex.h>
#include <LibSQL/AST/AST.h>
#include <LibSQL/Database.h>
namespace SQL::AST {
static const String s_posix_basic_metacharacters = ".^$*[]+\\";
Value Expression::evaluate(ExecutionContext&) const
{
return Value::null();
@ -194,4 +197,58 @@ Value ColumnNameExpression::evaluate(ExecutionContext& context) const
return Value::null();
}
Value MatchExpression::evaluate(ExecutionContext& context) const
{
if (context.result->has_error())
return Value::null();
switch (type()) {
case MatchOperator::Like: {
Value lhs_value = lhs()->evaluate(context);
Value rhs_value = rhs()->evaluate(context);
char escape_char = '\0';
if (escape()) {
auto escape_str = escape()->evaluate(context).to_string();
if (escape_str.length() != 1) {
context.result->set_error(SQLErrorCode::SyntaxError, "ESCAPE should be a single character");
return Value::null();
}
escape_char = escape_str[0];
}
// Compile the pattern into a simple regex.
// https://sqlite.org/lang_expr.html#the_like_glob_regexp_and_match_operators
bool escaped = false;
AK::StringBuilder builder;
builder.append('^');
for (auto c : rhs_value.to_string()) {
if (escape() && c == escape_char && !escaped) {
escaped = true;
} else if (s_posix_basic_metacharacters.contains(c)) {
escaped = false;
builder.append('\\');
builder.append(c);
} else if (c == '_' && !escaped) {
builder.append('.');
} else if (c == '%' && !escaped) {
builder.append(".*");
} else {
escaped = false;
builder.append(c);
}
}
builder.append('$');
// FIXME: We should probably cache this regex.
auto regex = Regex<PosixBasic>(builder.build());
auto result = regex.match(lhs_value.to_string(), PosixFlags::Insensitive | PosixFlags::Unicode);
return Value(invert_expression() ? !result.success : result.success);
}
case MatchOperator::Glob:
case MatchOperator::Match:
case MatchOperator::Regexp:
default:
VERIFY_NOT_REACHED();
}
return Value::null();
}
}

View file

@ -31,4 +31,4 @@ set(GENERATED_SOURCES
)
serenity_lib(LibSQL sql)
target_link_libraries(LibSQL LibCore LibSyntax)
target_link_libraries(LibSQL LibCore LibSyntax LibRegex)