Skip to content
Open
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
118 changes: 118 additions & 0 deletions Tests/LibSQL/TestSqlStatementExecution.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1060,4 +1060,122 @@ TEST_CASE(update_all_rows)
}
}

TEST_CASE(insert_unique_values_success)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = MUST(SQL::Database::create(db_name));
MUST(database->open());

create_schema(database);
execute(database, "CREATE TABLE TestSchema.TestTable ( IntColumn integer, TextColumn text UNIQUE, SecondTextColumn text );");

auto result = execute(database, "INSERT INTO TestSchema.TestTable VALUES (1, 'Test_1', 'Alice');");
EXPECT_EQ(result.size(), 1u);

result = execute(database, "INSERT INTO TestSchema.TestTable VALUES (2, 'Test_2', 'Bob');");
EXPECT_EQ(result.size(), 1u);

result = execute(database, "SELECT * FROM TestSchema.TestTable ORDER BY IntColumn;");
EXPECT_EQ(result.size(), 2u);
EXPECT_EQ(result[0].row[1].to_byte_string(), "Test_1");
EXPECT_EQ(result[1].row[1].to_byte_string(), "Test_2");
}

TEST_CASE(insert_duplicate_unique_value_fails)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = MUST(SQL::Database::create(db_name));
MUST(database->open());

create_schema(database);
execute(database, "CREATE TABLE TestSchema.TestTable ( IntColumn integer, TextColumn text UNIQUE, SecondTextColumn text );");

execute(database, "INSERT INTO TestSchema.TestTable VALUES (1, 'Test_1', 'Alice');");

auto result = try_execute(database, "INSERT INTO TestSchema.TestTable VALUES (2, 'Test_1', 'Bob');");
EXPECT(result.is_error());
EXPECT_EQ(result.release_error().error(), SQL::SQLErrorCode::UniqueConstraintViolated);
}

TEST_CASE(update_to_duplicate_unique_value_fails)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = MUST(SQL::Database::create(db_name));
MUST(database->open());

create_schema(database);
execute(database, "CREATE TABLE TestSchema.TestTable ( IntColumn integer, TextColumn text UNIQUE, SecondTextColumn text );");

execute(database, "INSERT INTO TestSchema.TestTable VALUES (1, 'Test_1', 'Alice');");
execute(database, "INSERT INTO TestSchema.TestTable VALUES (2, 'Test_2', 'Bob');");

auto result = try_execute(database, "UPDATE TestSchema.TestTable SET TextColumn='Test_1' WHERE IntColumn=2;");
EXPECT(result.is_error());
EXPECT_EQ(result.release_error().error(), SQL::SQLErrorCode::UniqueConstraintViolated);
}

TEST_CASE(update_unique_value_to_new_value_succeeds)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = MUST(SQL::Database::create(db_name));
MUST(database->open());

create_schema(database);
execute(database, "CREATE TABLE TestSchema.TestTable ( IntColumn integer, TextColumn text UNIQUE, SecondTextColumn text );");

execute(database, "INSERT INTO TestSchema.TestTable VALUES (1, 'Test_1', 'Alice');");
execute(database, "INSERT INTO TestSchema.TestTable VALUES (2, 'Test_2', 'Bob');");

auto result = execute(database, "UPDATE TestSchema.TestTable SET TextColumn='Test_3' WHERE IntColumn=2;");
EXPECT_EQ(result.size(), 1u);

result = execute(database, "SELECT TextColumn FROM TestSchema.TestTable WHERE IntColumn=2;");
EXPECT_EQ(result.size(), 1u);
EXPECT_EQ(result[0].row[0].to_byte_string(), "Test_3");
}

TEST_CASE(unique_constraint_with_mixed_data_types)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = MUST(SQL::Database::create(db_name));
MUST(database->open());

create_schema(database);
execute(database, "CREATE TABLE TestSchema.TestTable ( IntColumn integer UNIQUE, FloatColumn float, TextColumn text );");

execute(database, "INSERT INTO TestSchema.TestTable VALUES (100, 85.5, 'Test_1');");
execute(database, "INSERT INTO TestSchema.TestTable VALUES (200, 92.0, 'Test_2');");

auto result = try_execute(database, "INSERT INTO TestSchema.TestTable VALUES (100, 78.5, 'Test_3');");
EXPECT(result.is_error());
EXPECT_EQ(result.release_error().error(), SQL::SQLErrorCode::UniqueConstraintViolated);
}

TEST_CASE(multiple_unique_constraints_on_different_columns)
{
ScopeGuard guard([]() { unlink(db_name); });
auto database = MUST(SQL::Database::create(db_name));
MUST(database->open());

create_schema(database);
execute(database, "CREATE TABLE TestSchema.TestTable ( IntColumn integer UNIQUE, TextColumn text UNIQUE, SecondTextColumn text UNIQUE );");

execute(database, "INSERT INTO TestSchema.TestTable VALUES (1, 'Test_1', 'Value_1');");

auto result = try_execute(database, "INSERT INTO TestSchema.TestTable VALUES (1, 'Test_2', 'Value_2');");
EXPECT(result.is_error());
EXPECT_EQ(result.release_error().error(), SQL::SQLErrorCode::UniqueConstraintViolated);

result = try_execute(database, "INSERT INTO TestSchema.TestTable VALUES (2, 'Test_1', 'Value_3');");
EXPECT(result.is_error());
EXPECT_EQ(result.release_error().error(), SQL::SQLErrorCode::UniqueConstraintViolated);

result = try_execute(database, "INSERT INTO TestSchema.TestTable VALUES (3, 'Test_3', 'Value_1');");
EXPECT(result.is_error());
EXPECT_EQ(result.release_error().error(), SQL::SQLErrorCode::UniqueConstraintViolated);

auto success_result = execute(database, "INSERT INTO TestSchema.TestTable VALUES (2, 'Test_2', 'Value_2');");
EXPECT_EQ(success_result.size(), 1u);
}

}
10 changes: 10 additions & 0 deletions Tests/LibSQL/TestSqlStatementParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ TEST_CASE(create_table)
EXPECT(parse("CREATE TABLE test ( column1 varchar(0xzzz) )"sv).is_error());
EXPECT(parse("CREATE TABLE test ( column1 int ) AS SELECT * FROM table_name;"sv).is_error());
EXPECT(parse("CREATE TABLE test AS SELECT * FROM table_name ( column1 int ) ;"sv).is_error());
EXPECT(parse("CREATE TABLE test ( column1 UNIQUE )"sv).is_error());
EXPECT(parse("CREATE TABLE test ( column1 text UNIQUE )"sv).is_error());

struct Column {
StringView name;
StringView type;
Vector<double> signed_numbers {};
bool unique = false;
};

auto validate = [](StringView sql, StringView expected_schema, StringView expected_table, Vector<Column> expected_columns, bool expected_is_temporary = false, bool expected_is_error_if_table_exists = true) {
Expand All @@ -89,6 +92,7 @@ TEST_CASE(create_table)
auto const& column = columns[i];
auto const& expected_column = expected_columns[i];
EXPECT_EQ(column->name(), expected_column.name);
EXPECT_EQ(column->unique(), expected_column.unique);

auto const& type_name = column->type_name();
EXPECT_EQ(type_name->name(), expected_column.type);
Expand Down Expand Up @@ -124,6 +128,12 @@ TEST_CASE(create_table)
validate("CREATE TABLE test ( column1 varchar(0xff) );"sv, {}, "TEST"sv, { { "COLUMN1"sv, "VARCHAR"sv, { 255 } } });
validate("CREATE TABLE test ( column1 varchar(3.14) );"sv, {}, "TEST"sv, { { "COLUMN1"sv, "VARCHAR"sv, { 3.14 } } });
validate("CREATE TABLE test ( column1 varchar(1e3) );"sv, {}, "TEST"sv, { { "COLUMN1"sv, "VARCHAR"sv, { 1000 } } });

validate("CREATE TABLE test ( IntColumn integer, TextColumn text UNIQUE, SecondTextColumn text );"sv, {}, "TEST"sv, { { "INTCOLUMN"sv, "INTEGER"sv, {}, false }, { "TEXTCOLUMN"sv, "TEXT"sv, {}, true }, { "SECONDTEXTCOLUMN"sv, "TEXT"sv, {}, false } });
validate("CREATE TABLE test ( IntColumn integer UNIQUE, TextColumn text UNIQUE, SecondTextColumn text UNIQUE );"sv, {}, "TEST"sv, { { "INTCOLUMN"sv, "INTEGER"sv, {}, true }, { "TEXTCOLUMN"sv, "TEXT"sv, {}, true }, { "SECONDTEXTCOLUMN"sv, "TEXT"sv, {}, true } });
validate("CREATE TABLE test ( IntColumn integer UNIQUE, TextColumn text, SecondTextColumn text );"sv, {}, "TEST"sv, { { "INTCOLUMN"sv, "INTEGER"sv, {}, true }, { "TEXTCOLUMN"sv, "TEXT"sv, {}, false }, { "SECONDTEXTCOLUMN"sv, "TEXT"sv, {}, false } });
validate("CREATE TABLE test ( IntColumn integer, TextColumn text CONSTRAINT unique_text UNIQUE, SecondTextColumn text );"sv, {}, "TEST"sv, { { "INTCOLUMN"sv, "INTEGER"sv, {}, false }, { "TEXTCOLUMN"sv, "TEXT"sv, {}, true }, { "SECONDTEXTCOLUMN"sv, "TEXT"sv, {}, false } });
validate("CREATE TABLE test ( IntColumn integer CONSTRAINT unique_int UNIQUE, TextColumn text CONSTRAINT unique_text UNIQUE );"sv, {}, "TEST"sv, { { "INTCOLUMN"sv, "INTEGER"sv, {}, true }, { "TEXTCOLUMN"sv, "TEXT"sv, {}, true } });
}

TEST_CASE(alter_table)
Expand Down
7 changes: 5 additions & 2 deletions Userland/Libraries/LibSQL/AST/AST.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Tim Flynn <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
Expand Down Expand Up @@ -71,18 +71,21 @@ class TypeName : public ASTNode {

class ColumnDefinition : public ASTNode {
public:
ColumnDefinition(ByteString name, NonnullRefPtr<TypeName> type_name)
ColumnDefinition(ByteString name, NonnullRefPtr<TypeName> type_name, bool unique = false)
: m_name(move(name))
, m_type_name(move(type_name))
, m_unique(unique)
{
}

ByteString const& name() const { return m_name; }
NonnullRefPtr<TypeName> const& type_name() const { return m_type_name; }
bool unique() const { return m_unique; }

private:
ByteString m_name;
NonnullRefPtr<TypeName> m_type_name;
bool m_unique { false };
};

class CommonTableExpression : public ASTNode {
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibSQL/AST/CreateTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ ResultOr<ResultSet> CreateTable::execute(ExecutionContext& context) const
else
return Result { SQLCommand::Create, SQLErrorCode::InvalidType, column->type_name()->name() };

table_def->append_column(column->name(), type);
table_def->append_column(column->name(), type, column->unique());
}

if (auto result = context.database->add_table(*table_def); result.is_error()) {
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibSQL/AST/Describe.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibSQL/AST/Insert.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Jan de Visser <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down
21 changes: 18 additions & 3 deletions Userland/Libraries/LibSQL/AST/Parser.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Tim Flynn <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down Expand Up @@ -901,9 +901,24 @@ NonnullRefPtr<ColumnDefinition> Parser::parse_column_definition()
// https://www.sqlite.org/datatype3.html: If no type is specified then the column has affinity BLOB.
: create_ast_node<TypeName>("BLOB", Vector<NonnullRefPtr<SignedNumber>> {});

// FIXME: Parse "column-constraint".
// Parse column constraints
bool is_unique = false;
while (match(TokenType::Unique) || match(TokenType::Constraint)) {
if (consume_if(TokenType::Unique)) {
is_unique = true;
} else if (consume_if(TokenType::Constraint)) {
// Skip constraint name if present
if (match(TokenType::Identifier))
consume();

if (consume_if(TokenType::Unique)) {
is_unique = true;
}
// TODO: Add support for other constraints like NOT NULL, PRIMARY KEY, etc.
}
}

return create_ast_node<ColumnDefinition>(move(name), move(type_name));
return create_ast_node<ColumnDefinition>(move(name), move(type_name), is_unique);
}

NonnullRefPtr<TypeName> Parser::parse_type_name()
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibSQL/AST/Parser.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Tim Flynn <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down
2 changes: 1 addition & 1 deletion Userland/Libraries/LibSQL/AST/Token.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2021, Tim Flynn <[email protected]>
* Copyright (c) 2021, Jan de Visser <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down
44 changes: 38 additions & 6 deletions Userland/Libraries/LibSQL/Database.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Jan de Visser <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down Expand Up @@ -200,10 +200,12 @@ ErrorOr<Vector<Row>> Database::match(TableDef& table, Key const& key)
return ret;
}

ErrorOr<void> Database::insert(Row& row)
ResultOr<void> Database::insert(Row& row)
{
VERIFY(m_table_cache.get(row.table().key().hash()).has_value());
// TODO: implement table constraints such as unique, foreign key, etc.

// TODO implement other table constraints such as foreign key, default, etc.
TRY(check_unique_constraints(row));

row.set_block_index(m_heap->request_new_block_index());
row.set_next_block_index(row.table().block_index());
Expand All @@ -218,7 +220,7 @@ ErrorOr<void> Database::insert(Row& row)
return {};
}

ErrorOr<void> Database::remove(Row& row)
ResultOr<void> Database::remove(Row& row)
{
auto& table = row.table();
VERIFY(m_table_cache.get(table.key().hash()).has_value());
Expand Down Expand Up @@ -249,10 +251,12 @@ ErrorOr<void> Database::remove(Row& row)
return {};
}

ErrorOr<void> Database::update(Row& tuple)
ResultOr<void> Database::update(Row& tuple)
{
VERIFY(m_table_cache.get(tuple.table().key().hash()).has_value());
// TODO: implement table constraints such as unique, foreign key, etc.

// TODO: implement other table constraints such as foreign key, default, etc.
TRY(check_unique_constraints(tuple));

m_serializer.reset();
m_serializer.serialize_and_write<Tuple>(tuple);
Expand All @@ -261,4 +265,32 @@ ErrorOr<void> Database::update(Row& tuple)
return {};
}

ResultOr<void> Database::check_unique_constraints(Row const& row)
{
auto const& table = row.table();

// Check each column for unique constraints
for (auto const& column : table.columns()) {
if (!column->unique())
continue;

auto column_value = row[column->name()];
if (column_value.is_null())
continue; // NULL values are allowed in UNIQUE columns (multiple NULLs are OK)

auto all_rows = TRY(select_all(const_cast<TableDef&>(table)));
for (auto const& existing_row : all_rows) {
if (existing_row.block_index() == row.block_index())
continue;

auto existing_value = existing_row[column->name()];
if (!existing_value.is_null() && existing_value == column_value) {
return Result { SQLCommand::Insert, SQLErrorCode::UniqueConstraintViolated, column->name() };
}
}
}

return {};
}

}
9 changes: 5 additions & 4 deletions Userland/Libraries/LibSQL/Database.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Jan de Visser <[email protected]>
* Copyright (c) 2021, Mahmoud Mandour <[email protected]>
* Copyright (c) 2025, Mahmoud Abumandour <[email protected]>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
Expand Down Expand Up @@ -43,12 +43,13 @@ class Database : public RefCounted<Database> {

ErrorOr<Vector<Row>> select_all(TableDef&);
ErrorOr<Vector<Row>> match(TableDef&, Key const&);
ErrorOr<void> insert(Row&);
ErrorOr<void> remove(Row&);
ErrorOr<void> update(Row&);
ResultOr<void> insert(Row&);
ResultOr<void> remove(Row&);
ResultOr<void> update(Row&);

private:
explicit Database(NonnullRefPtr<Heap>);
ResultOr<void> check_unique_constraints(Row const& row);

bool m_open { false };
NonnullRefPtr<Heap> m_heap;
Expand Down
Loading
Loading