diff --git a/Tests/LibSQL/TestSqlStatementExecution.cpp b/Tests/LibSQL/TestSqlStatementExecution.cpp index e4c21138f287bf..adb95670ac1bf2 100644 --- a/Tests/LibSQL/TestSqlStatementExecution.cpp +++ b/Tests/LibSQL/TestSqlStatementExecution.cpp @@ -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); +} + } diff --git a/Tests/LibSQL/TestSqlStatementParser.cpp b/Tests/LibSQL/TestSqlStatementParser.cpp index 8e03c00bf2dc7e..2eb485cdfbb387 100644 --- a/Tests/LibSQL/TestSqlStatementParser.cpp +++ b/Tests/LibSQL/TestSqlStatementParser.cpp @@ -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 signed_numbers {}; + bool unique = false; }; auto validate = [](StringView sql, StringView expected_schema, StringView expected_table, Vector expected_columns, bool expected_is_temporary = false, bool expected_is_error_if_table_exists = true) { @@ -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); @@ -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) diff --git a/Userland/Libraries/LibSQL/AST/AST.h b/Userland/Libraries/LibSQL/AST/AST.h index e007c93cdc6063..8ab7b8df08bd9b 100644 --- a/Userland/Libraries/LibSQL/AST/AST.h +++ b/Userland/Libraries/LibSQL/AST/AST.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Tim Flynn - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * Copyright (c) 2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause @@ -71,18 +71,21 @@ class TypeName : public ASTNode { class ColumnDefinition : public ASTNode { public: - ColumnDefinition(ByteString name, NonnullRefPtr type_name) + ColumnDefinition(ByteString name, NonnullRefPtr 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 const& type_name() const { return m_type_name; } + bool unique() const { return m_unique; } private: ByteString m_name; NonnullRefPtr m_type_name; + bool m_unique { false }; }; class CommonTableExpression : public ASTNode { diff --git a/Userland/Libraries/LibSQL/AST/CreateTable.cpp b/Userland/Libraries/LibSQL/AST/CreateTable.cpp index a5f383c691d490..6b32843fc59d75 100644 --- a/Userland/Libraries/LibSQL/AST/CreateTable.cpp +++ b/Userland/Libraries/LibSQL/AST/CreateTable.cpp @@ -28,7 +28,7 @@ ResultOr 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()) { diff --git a/Userland/Libraries/LibSQL/AST/Describe.cpp b/Userland/Libraries/LibSQL/AST/Describe.cpp index ecdf1b90cab566..24c43e9f530a4a 100644 --- a/Userland/Libraries/LibSQL/AST/Describe.cpp +++ b/Userland/Libraries/LibSQL/AST/Describe.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ diff --git a/Userland/Libraries/LibSQL/AST/Insert.cpp b/Userland/Libraries/LibSQL/AST/Insert.cpp index 233395040a9710..87c21284a8f17b 100644 --- a/Userland/Libraries/LibSQL/AST/Insert.cpp +++ b/Userland/Libraries/LibSQL/AST/Insert.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Jan de Visser - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ diff --git a/Userland/Libraries/LibSQL/AST/Parser.cpp b/Userland/Libraries/LibSQL/AST/Parser.cpp index 38d2298e72cec9..e4ec64ed813a7f 100644 --- a/Userland/Libraries/LibSQL/AST/Parser.cpp +++ b/Userland/Libraries/LibSQL/AST/Parser.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Tim Flynn - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ @@ -901,9 +901,24 @@ NonnullRefPtr Parser::parse_column_definition() // https://www.sqlite.org/datatype3.html: If no type is specified then the column has affinity BLOB. : create_ast_node("BLOB", Vector> {}); - // 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(move(name), move(type_name)); + return create_ast_node(move(name), move(type_name), is_unique); } NonnullRefPtr Parser::parse_type_name() diff --git a/Userland/Libraries/LibSQL/AST/Parser.h b/Userland/Libraries/LibSQL/AST/Parser.h index 2ec593548974b8..f527b534fc5e88 100644 --- a/Userland/Libraries/LibSQL/AST/Parser.h +++ b/Userland/Libraries/LibSQL/AST/Parser.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Tim Flynn - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ diff --git a/Userland/Libraries/LibSQL/AST/Token.h b/Userland/Libraries/LibSQL/AST/Token.h index aec43bead1a3c3..0530bc95049eb8 100644 --- a/Userland/Libraries/LibSQL/AST/Token.h +++ b/Userland/Libraries/LibSQL/AST/Token.h @@ -1,7 +1,7 @@ /* * Copyright (c) 2021, Tim Flynn * Copyright (c) 2021, Jan de Visser - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ diff --git a/Userland/Libraries/LibSQL/Database.cpp b/Userland/Libraries/LibSQL/Database.cpp index 3c3fa16091b3c5..1f2e9146effdf7 100644 --- a/Userland/Libraries/LibSQL/Database.cpp +++ b/Userland/Libraries/LibSQL/Database.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Jan de Visser - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ @@ -200,10 +200,12 @@ ErrorOr> Database::match(TableDef& table, Key const& key) return ret; } -ErrorOr Database::insert(Row& row) +ResultOr 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()); @@ -218,7 +220,7 @@ ErrorOr Database::insert(Row& row) return {}; } -ErrorOr Database::remove(Row& row) +ResultOr Database::remove(Row& row) { auto& table = row.table(); VERIFY(m_table_cache.get(table.key().hash()).has_value()); @@ -249,10 +251,12 @@ ErrorOr Database::remove(Row& row) return {}; } -ErrorOr Database::update(Row& tuple) +ResultOr 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); @@ -261,4 +265,32 @@ ErrorOr Database::update(Row& tuple) return {}; } +ResultOr 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(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 {}; +} + } diff --git a/Userland/Libraries/LibSQL/Database.h b/Userland/Libraries/LibSQL/Database.h index 863e2327f8e1cb..13b50feb0a3ba1 100644 --- a/Userland/Libraries/LibSQL/Database.h +++ b/Userland/Libraries/LibSQL/Database.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Jan de Visser - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ @@ -43,12 +43,13 @@ class Database : public RefCounted { ErrorOr> select_all(TableDef&); ErrorOr> match(TableDef&, Key const&); - ErrorOr insert(Row&); - ErrorOr remove(Row&); - ErrorOr update(Row&); + ResultOr insert(Row&); + ResultOr remove(Row&); + ResultOr update(Row&); private: explicit Database(NonnullRefPtr); + ResultOr check_unique_constraints(Row const& row); bool m_open { false }; NonnullRefPtr m_heap; diff --git a/Userland/Libraries/LibSQL/Meta.cpp b/Userland/Libraries/LibSQL/Meta.cpp index caeae9d2bac11c..4950154ce67fbf 100644 --- a/Userland/Libraries/LibSQL/Meta.cpp +++ b/Userland/Libraries/LibSQL/Meta.cpp @@ -72,6 +72,7 @@ Key ColumnDef::key() const key["column_number"] = column_number(); key["column_name"] = name(); key["column_type"] = to_underlying(type()); + key["unique"] = unique() ? 1 : 0; return key; } @@ -96,6 +97,7 @@ NonnullRefPtr ColumnDef::index_def() s_index_def->append_column("column_number", SQLType::Integer, Order::Ascending); s_index_def->append_column("column_name", SQLType::Text, Order::Ascending); s_index_def->append_column("column_type", SQLType::Integer, Order::Ascending); + s_index_def->append_column("unique", SQLType::Integer, Order::Ascending); } return s_index_def; } @@ -200,9 +202,10 @@ Key TableDef::key() const return key; } -void TableDef::append_column(ByteString name, SQLType sql_type) +void TableDef::append_column(ByteString name, SQLType sql_type, bool unique) { auto column = ColumnDef::create(this, num_columns(), move(name), sql_type).release_value_but_fixme_should_propagate_errors(); + column->set_unique(unique); m_columns.append(column); } @@ -211,7 +214,16 @@ void TableDef::append_column(Key const& column) auto column_type = column["column_type"].to_int>(); VERIFY(column_type.has_value()); - append_column(column["column_name"].to_byte_string(), static_cast(*column_type)); + auto column_def = ColumnDef::create(this, num_columns(), column["column_name"].to_byte_string(), static_cast(*column_type)).release_value_but_fixme_should_propagate_errors(); + + // Set constraints based on the serialized data + if (column.has("unique")) { + auto unique = column["unique"].to_int(); + if (unique.has_value()) + column_def->set_unique(*unique != 0); + } + + m_columns.append(column_def); } Key TableDef::make_key(SchemaDef const& schema_def) diff --git a/Userland/Libraries/LibSQL/Meta.h b/Userland/Libraries/LibSQL/Meta.h index 997752680d31da..f6374ae74174d0 100644 --- a/Userland/Libraries/LibSQL/Meta.h +++ b/Userland/Libraries/LibSQL/Meta.h @@ -77,6 +77,8 @@ class ColumnDef : public Relation { size_t column_number() const { return m_index; } void set_not_null(bool can_not_be_null) { m_not_null = can_not_be_null; } bool not_null() const { return m_not_null; } + void set_unique(bool is_unique) { m_unique = is_unique; } + bool unique() const { return m_unique; } void set_default_value(Value const& default_value); Value const& default_value() const { return m_default; } @@ -90,6 +92,7 @@ class ColumnDef : public Relation { size_t m_index; SQLType m_type { SQLType::Text }; bool m_not_null { false }; + bool m_unique { false }; Value m_default; }; @@ -133,7 +136,7 @@ class TableDef : public Relation { static ErrorOr> create(SchemaDef*, ByteString); Key key() const override; - void append_column(ByteString, SQLType); + void append_column(ByteString name, SQLType sql_type, bool unique = false); void append_column(Key const&); size_t num_columns() { return m_columns.size(); } size_t num_indexes() { return m_indexes.size(); } diff --git a/Userland/Libraries/LibSQL/Result.h b/Userland/Libraries/LibSQL/Result.h index 0aacf2bfbd0574..aca73c7f26492b 100644 --- a/Userland/Libraries/LibSQL/Result.h +++ b/Userland/Libraries/LibSQL/Result.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, Jan de Visser - * Copyright (c) 2021, Mahmoud Mandour + * Copyright (c) 2025, Mahmoud Abumandour * * SPDX-License-Identifier: BSD-2-Clause */ @@ -65,7 +65,8 @@ constexpr char const* command_tag(SQLCommand command) S(StatementUnavailable, "Statement with id '{}' Unavailable") \ S(SyntaxError, "Syntax Error") \ S(TableDoesNotExist, "Table '{}' does not exist") \ - S(TableExists, "Table '{}' already exist") + S(TableExists, "Table '{}' already exist") \ + S(UniqueConstraintViolated, "Unique constraint violated on column '{}'") enum class SQLErrorCode { #undef __ENUMERATE_SQL_ERROR @@ -130,3 +131,11 @@ template using ResultOr = ErrorOr; } + +template<> +struct AK::Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, SQL::Result const& result) + { + return Formatter::format(builder, result.error_string()); + } +};