diff --git a/Tests/LibSQL/TestSqlStatementExecution.cpp b/Tests/LibSQL/TestSqlStatementExecution.cpp index ef049b8ae7..562d6e5c0b 100644 --- a/Tests/LibSQL/TestSqlStatementExecution.cpp +++ b/Tests/LibSQL/TestSqlStatementExecution.cpp @@ -510,4 +510,109 @@ TEST_CASE(select_with_order_by_column_not_in_result) EXPECT_EQ(rows[4].row[0].to_string(), "Test_1"); } +TEST_CASE(select_with_limit) +{ + ScopeGuard guard([]() { unlink(db_name); }); + auto database = SQL::Database::construct(db_name); + EXPECT(!database->open().is_error()); + create_table(database); + for (auto count = 0; count < 100; count++) { + auto result = execute(database, + String::formatted("INSERT INTO TestSchema.TestTable ( TextColumn, IntColumn ) VALUES ( 'Test_{}', {} );", count, count)); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->inserted() == 1); + } + auto result = execute(database, "SELECT TextColumn, IntColumn FROM TestSchema.TestTable LIMIT 10;"); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->has_results()); + auto rows = result->results(); + EXPECT_EQ(rows.size(), 10u); +} + +TEST_CASE(select_with_limit_and_offset) +{ + ScopeGuard guard([]() { unlink(db_name); }); + auto database = SQL::Database::construct(db_name); + EXPECT(!database->open().is_error()); + create_table(database); + for (auto count = 0; count < 100; count++) { + auto result = execute(database, + String::formatted("INSERT INTO TestSchema.TestTable ( TextColumn, IntColumn ) VALUES ( 'Test_{}', {} );", count, count)); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->inserted() == 1); + } + auto result = execute(database, "SELECT TextColumn, IntColumn FROM TestSchema.TestTable LIMIT 10 OFFSET 10;"); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->has_results()); + auto rows = result->results(); + EXPECT_EQ(rows.size(), 10u); +} + +TEST_CASE(select_with_order_limit_and_offset) +{ + ScopeGuard guard([]() { unlink(db_name); }); + auto database = SQL::Database::construct(db_name); + EXPECT(!database->open().is_error()); + create_table(database); + for (auto count = 0; count < 100; count++) { + auto result = execute(database, + String::formatted("INSERT INTO TestSchema.TestTable ( TextColumn, IntColumn ) VALUES ( 'Test_{}', {} );", count, count)); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->inserted() == 1); + } + auto result = execute(database, "SELECT TextColumn, IntColumn FROM TestSchema.TestTable ORDER BY IntColumn LIMIT 10 OFFSET 10;"); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->has_results()); + auto rows = result->results(); + EXPECT_EQ(rows.size(), 10u); + EXPECT_EQ(rows[0].row[1].to_int().value(), 10); + EXPECT_EQ(rows[1].row[1].to_int().value(), 11); + EXPECT_EQ(rows[2].row[1].to_int().value(), 12); + EXPECT_EQ(rows[3].row[1].to_int().value(), 13); + EXPECT_EQ(rows[4].row[1].to_int().value(), 14); + EXPECT_EQ(rows[5].row[1].to_int().value(), 15); + EXPECT_EQ(rows[6].row[1].to_int().value(), 16); + EXPECT_EQ(rows[7].row[1].to_int().value(), 17); + EXPECT_EQ(rows[8].row[1].to_int().value(), 18); + EXPECT_EQ(rows[9].row[1].to_int().value(), 19); +} + +TEST_CASE(select_with_limit_out_of_bounds) +{ + ScopeGuard guard([]() { unlink(db_name); }); + auto database = SQL::Database::construct(db_name); + EXPECT(!database->open().is_error()); + create_table(database); + for (auto count = 0; count < 100; count++) { + auto result = execute(database, + String::formatted("INSERT INTO TestSchema.TestTable ( TextColumn, IntColumn ) VALUES ( 'Test_{}', {} );", count, count)); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->inserted() == 1); + } + auto result = execute(database, "SELECT TextColumn, IntColumn FROM TestSchema.TestTable LIMIT 500;"); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->has_results()); + auto rows = result->results(); + EXPECT_EQ(rows.size(), 100u); +} + +TEST_CASE(select_with_offset_out_of_bounds) +{ + ScopeGuard guard([]() { unlink(db_name); }); + auto database = SQL::Database::construct(db_name); + EXPECT(!database->open().is_error()); + create_table(database); + for (auto count = 0; count < 100; count++) { + auto result = execute(database, + String::formatted("INSERT INTO TestSchema.TestTable ( TextColumn, IntColumn ) VALUES ( 'Test_{}', {} );", count, count)); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->inserted() == 1); + } + auto result = execute(database, "SELECT TextColumn, IntColumn FROM TestSchema.TestTable LIMIT 10 OFFSET 200;"); + EXPECT(result->error().code == SQL::SQLErrorCode::NoError); + EXPECT(result->has_results()); + auto rows = result->results(); + EXPECT_EQ(rows.size(), 0u); +} + } diff --git a/Userland/Libraries/LibSQL/AST/Select.cpp b/Userland/Libraries/LibSQL/AST/Select.cpp index 9db06f02ab..8e9f2da9a8 100644 --- a/Userland/Libraries/LibSQL/AST/Select.cpp +++ b/Userland/Libraries/LibSQL/AST/Select.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -69,7 +70,7 @@ RefPtr Select::execute(ExecutionContext& context) const rows.remove(0); auto table_rows_or_error = context.database->select_all(*table); if (table_rows_or_error.is_error()) - return SQLResult::construct(SQLCommand::Create, SQLErrorCode::InternalError, table_rows_or_error.error()); + return SQLResult::construct(SQLCommand::Select, SQLErrorCode::InternalError, table_rows_or_error.error()); for (auto& table_row : table_rows_or_error.value()) { auto new_row = cartesian_row; new_row.extend(table_row); @@ -114,6 +115,31 @@ RefPtr Select::execute(ExecutionContext& context) const } context.result->insert(tuple, sort_key); } + + if (m_limit_clause != nullptr) { + size_t limit_value = NumericLimits::max(); + auto limit = m_limit_clause->limit_expression()->evaluate(context); + if (!limit.is_null()) { + auto limit_value_maybe = limit.to_u32(); + if (!limit_value_maybe.has_value()) { + return SQLResult::construct(SQLCommand::Select, SQLErrorCode::SyntaxError, "LIMIT clause must evaluate to an integer value"); + } + limit_value = limit_value_maybe.value(); + } + size_t offset_value = 0; + if (m_limit_clause->offset_expression() != nullptr) { + auto offset = m_limit_clause->offset_expression()->evaluate(context); + if (!offset.is_null()) { + auto offset_value_maybe = offset.to_u32(); + if (!offset_value_maybe.has_value()) { + return SQLResult::construct(SQLCommand::Select, SQLErrorCode::SyntaxError, "OFFSET clause must evaluate to an integer value"); + } + offset_value = offset_value_maybe.value(); + } + } + context.result->limit(offset_value, limit_value); + } + return context.result; } diff --git a/Userland/Libraries/LibSQL/CMakeLists.txt b/Userland/Libraries/LibSQL/CMakeLists.txt index 5bc6b4dc47..3abf929a3d 100644 --- a/Userland/Libraries/LibSQL/CMakeLists.txt +++ b/Userland/Libraries/LibSQL/CMakeLists.txt @@ -21,6 +21,7 @@ set(SOURCES Row.cpp Serializer.cpp SQLClient.cpp + SQLResult.cpp TreeNode.cpp Tuple.cpp Value.cpp diff --git a/Userland/Libraries/LibSQL/SQLResult.cpp b/Userland/Libraries/LibSQL/SQLResult.cpp new file mode 100644 index 0000000000..fc1550247f --- /dev/null +++ b/Userland/Libraries/LibSQL/SQLResult.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022, Jan de Visser + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace SQL { + +void SQLResult::insert(Tuple const& row, Tuple const& sort_key) +{ + m_has_results = true; + m_result_set.insert_row(row, sort_key); +} + +void SQLResult::limit(size_t offset, size_t limit) +{ + if (offset > 0) { + if (offset > m_result_set.size()) { + m_result_set.clear(); + return; + } + m_result_set.remove(0, offset); + } + if (m_result_set.size() > limit) { + m_result_set.remove(limit, m_result_set.size() - limit); + } +} + +} diff --git a/Userland/Libraries/LibSQL/SQLResult.h b/Userland/Libraries/LibSQL/SQLResult.h index 6847da8a62..c0bc4a73ea 100644 --- a/Userland/Libraries/LibSQL/SQLResult.h +++ b/Userland/Libraries/LibSQL/SQLResult.h @@ -111,12 +111,8 @@ class SQLResult : public Core::Object { C_OBJECT(SQLResult) public: - void insert(Tuple const& row, Tuple const& sort_key) - { - m_has_results = true; - m_result_set.insert_row(row, sort_key); - } - + void insert(Tuple const& row, Tuple const& sort_key); + void limit(size_t offset, size_t limit); SQLCommand command() const { return m_command; } int updated() const { return m_update_count; } int inserted() const { return m_insert_count; }