diff options
Diffstat (limited to 'Userland/Libraries/LibSQL')
-rw-r--r-- | Userland/Libraries/LibSQL/AST.h | 81 | ||||
-rw-r--r-- | Userland/Libraries/LibSQL/Forward.h | 4 | ||||
-rw-r--r-- | Userland/Libraries/LibSQL/Parser.cpp | 114 | ||||
-rw-r--r-- | Userland/Libraries/LibSQL/Parser.h | 4 | ||||
-rw-r--r-- | Userland/Libraries/LibSQL/Tests/TestSqlParser.cpp | 82 |
5 files changed, 284 insertions, 1 deletions
diff --git a/Userland/Libraries/LibSQL/AST.h b/Userland/Libraries/LibSQL/AST.h index e90fcb7777..0bf1d33e8c 100644 --- a/Userland/Libraries/LibSQL/AST.h +++ b/Userland/Libraries/LibSQL/AST.h @@ -101,6 +101,62 @@ private: NonnullRefPtr<TypeName> m_type_name; }; +class CommonTableExpression : public ASTNode { +public: + CommonTableExpression(String table_name, Vector<String> column_names) + : m_table_name(move(table_name)) + , m_column_names(move(column_names)) + { + } + + const String& table_name() const { return m_table_name; } + const Vector<String>& column_names() const { return m_column_names; } + +private: + String m_table_name; + Vector<String> m_column_names; +}; + +class QualifiedTableName : public ASTNode { +public: + QualifiedTableName(String schema_name, String table_name, String alias) + : m_schema_name(move(schema_name)) + , m_table_name(move(table_name)) + , m_alias(move(alias)) + { + } + + const String& schema_name() const { return m_schema_name; } + const String& table_name() const { return m_table_name; } + const String& alias() const { return m_alias; } + +private: + String m_schema_name; + String m_table_name; + String m_alias; +}; + +class ReturningClause : public ASTNode { +public: + struct ColumnClause { + NonnullRefPtr<Expression> expression; + String column_alias; + }; + + ReturningClause() = default; + + explicit ReturningClause(Vector<ColumnClause> columns) + : m_columns(move(columns)) + { + } + + bool return_all_columns() const { return m_columns.is_empty(); }; + const Vector<ColumnClause>& columns() const { return m_columns; } + +private: + Vector<ColumnClause> m_columns; +}; + //================================================================================================== // Expressions //================================================================================================== @@ -495,4 +551,29 @@ private: bool m_is_error_if_table_does_not_exist; }; +class Delete : public Statement { +public: + Delete(bool recursive, RefPtr<CommonTableExpression> common_table_expression, NonnullRefPtr<QualifiedTableName> qualified_table_name, RefPtr<Expression> where_clause, RefPtr<ReturningClause> returning_clause) + : m_recursive(recursive) + , m_common_table_expression(move(common_table_expression)) + , m_qualified_table_name(move(qualified_table_name)) + , m_where_clause(move(where_clause)) + , m_returning_clause(move(returning_clause)) + { + } + + bool recursive() const { return m_recursive; } + const RefPtr<CommonTableExpression>& common_table_expression() const { return m_common_table_expression; } + const NonnullRefPtr<QualifiedTableName>& qualified_table_name() const { return m_qualified_table_name; } + const RefPtr<Expression>& where_clause() const { return m_where_clause; } + const RefPtr<ReturningClause>& returning_clause() const { return m_returning_clause; } + +private: + bool m_recursive; + RefPtr<CommonTableExpression> m_common_table_expression; + NonnullRefPtr<QualifiedTableName> m_qualified_table_name; + RefPtr<Expression> m_where_clause; + RefPtr<ReturningClause> m_returning_clause; +}; + } diff --git a/Userland/Libraries/LibSQL/Forward.h b/Userland/Libraries/LibSQL/Forward.h index 0430a97bb5..b05cbe342b 100644 --- a/Userland/Libraries/LibSQL/Forward.h +++ b/Userland/Libraries/LibSQL/Forward.h @@ -37,7 +37,9 @@ class ChainedExpression; class CollateExpression; class ColumnDefinition; class ColumnNameExpression; +class CommonTableExpression; class CreateTable; +class Delete; class DropTable; class ErrorExpression; class ErrorStatement; @@ -55,6 +57,8 @@ class NullExpression; class NullLiteral; class NumericLiteral; class Parser; +class QualifiedTableName; +class ReturningClause; class SignedNumber; class Statement; class StringLiteral; diff --git a/Userland/Libraries/LibSQL/Parser.cpp b/Userland/Libraries/LibSQL/Parser.cpp index e35398db15..aa97202f5e 100644 --- a/Userland/Libraries/LibSQL/Parser.cpp +++ b/Userland/Libraries/LibSQL/Parser.cpp @@ -41,8 +41,11 @@ NonnullRefPtr<Statement> Parser::next_statement() return parse_create_table_statement(); case TokenType::Drop: return parse_drop_table_statement(); + case TokenType::Delete: + case TokenType::With: + return parse_delete_statement(); default: - expected("CREATE or DROP"); + expected("CREATE, DROP, or DELETE"); return create_ast_node<ErrorStatement>(); } } @@ -125,6 +128,34 @@ NonnullRefPtr<DropTable> Parser::parse_drop_table_statement() return create_ast_node<DropTable>(move(schema_name), move(table_name), is_error_if_table_does_not_exist); } +NonnullRefPtr<Delete> Parser::parse_delete_statement() +{ + // https://sqlite.org/lang_delete.html + + bool recursive = false; + RefPtr<CommonTableExpression> common_table_expression; + if (consume_if(TokenType::With)) { + recursive = consume_if(TokenType::Recursive); + common_table_expression = parse_common_table_expression(); + } + + consume(TokenType::Delete); + consume(TokenType::From); + auto qualified_table_name = parse_qualified_table_name(); + + RefPtr<Expression> where_clause; + if (consume_if(TokenType::Where)) + where_clause = parse_expression(); + + RefPtr<ReturningClause> returning_clause; + if (match(TokenType::Returning)) + returning_clause = parse_returning_clause(); + + consume(TokenType::SemiColon); + + return create_ast_node<Delete>(recursive, move(common_table_expression), move(qualified_table_name), move(where_clause), move(returning_clause)); +} + NonnullRefPtr<Expression> Parser::parse_expression() { // https://sqlite.org/lang_expr.html @@ -620,6 +651,87 @@ NonnullRefPtr<SignedNumber> Parser::parse_signed_number() return create_ast_node<SignedNumber>(0); } +NonnullRefPtr<CommonTableExpression> Parser::parse_common_table_expression() +{ + // https://sqlite.org/syntax/common-table-expression.html + auto table_name = consume(TokenType::Identifier).value(); + + Vector<String> column_names; + if (consume_if(TokenType::ParenOpen)) { + do { + column_names.append(consume(TokenType::Identifier).value()); + if (match(TokenType::ParenClose)) + break; + + consume(TokenType::Comma); + } while (!match(TokenType::Eof)); + + consume(TokenType::ParenClose); + } + + consume(TokenType::As); + consume(TokenType::ParenOpen); + // FIXME: Parse "select-stmt". + consume(TokenType::ParenClose); + + return create_ast_node<CommonTableExpression>(move(table_name), move(column_names)); +} + +NonnullRefPtr<QualifiedTableName> Parser::parse_qualified_table_name() +{ + // https://sqlite.org/syntax/qualified-table-name.html + String schema_or_table_name = consume(TokenType::Identifier).value(); + String schema_name; + String table_name; + + if (consume_if(TokenType::Period)) { + schema_name = move(schema_or_table_name); + table_name = consume(TokenType::Identifier).value(); + } else { + table_name = move(schema_or_table_name); + } + + String alias; + if (consume_if(TokenType::As)) + alias = consume(TokenType::Identifier).value(); + + // Note: The qualified-table-name spec may include an "INDEXED BY index-name" or "NOT INDEXED" clause. This is a SQLite extension + // "designed to help detect undesirable query plan changes during regression testing", and "application developers are admonished + // to omit all use of INDEXED BY during application design, implementation, testing, and tuning". Our implementation purposefully + // omits parsing INDEXED BY for now until there is good reason to add support. + + return create_ast_node<QualifiedTableName>(move(schema_name), move(table_name), move(alias)); +} + +NonnullRefPtr<ReturningClause> Parser::parse_returning_clause() +{ + // https://sqlite.org/syntax/returning-clause.html + consume(TokenType::Returning); + + if (consume_if(TokenType::Asterisk)) + return create_ast_node<ReturningClause>(); + + Vector<ReturningClause::ColumnClause> columns; + + do { + auto expression = parse_expression(); + + consume_if(TokenType::As); // 'AS' is optional. + + String column_alias; + if (match(TokenType::Identifier)) + column_alias = consume().value(); + + columns.append({ move(expression), move(column_alias) }); + if (!match(TokenType::Comma)) + break; + + consume(TokenType::Comma); + } while (!match(TokenType::Eof)); + + return create_ast_node<ReturningClause>(move(columns)); +} + Token Parser::consume() { auto old_token = m_parser_state.m_token; diff --git a/Userland/Libraries/LibSQL/Parser.h b/Userland/Libraries/LibSQL/Parser.h index 9b0ea5d2ab..41c526a4d3 100644 --- a/Userland/Libraries/LibSQL/Parser.h +++ b/Userland/Libraries/LibSQL/Parser.h @@ -72,6 +72,7 @@ private: NonnullRefPtr<CreateTable> parse_create_table_statement(); NonnullRefPtr<DropTable> parse_drop_table_statement(); + NonnullRefPtr<Delete> parse_delete_statement(); NonnullRefPtr<Expression> parse_primary_expression(); NonnullRefPtr<Expression> parse_secondary_expression(NonnullRefPtr<Expression> primary); @@ -93,6 +94,9 @@ private: NonnullRefPtr<ColumnDefinition> parse_column_definition(); NonnullRefPtr<TypeName> parse_type_name(); NonnullRefPtr<SignedNumber> parse_signed_number(); + NonnullRefPtr<CommonTableExpression> parse_common_table_expression(); + NonnullRefPtr<QualifiedTableName> parse_qualified_table_name(); + NonnullRefPtr<ReturningClause> parse_returning_clause(); Token consume(); Token consume(TokenType type); diff --git a/Userland/Libraries/LibSQL/Tests/TestSqlParser.cpp b/Userland/Libraries/LibSQL/Tests/TestSqlParser.cpp index c7701c641d..fee4c0cf80 100644 --- a/Userland/Libraries/LibSQL/Tests/TestSqlParser.cpp +++ b/Userland/Libraries/LibSQL/Tests/TestSqlParser.cpp @@ -157,4 +157,86 @@ TEST_CASE(drop_table) validate("DROP TABLE IF EXISTS test;", {}, "test", false); } +TEST_CASE(delete_) +{ + EXPECT(parse("DELETE").is_error()); + EXPECT(parse("DELETE FROM").is_error()); + EXPECT(parse("DELETE FROM table").is_error()); + EXPECT(parse("DELETE FROM table WHERE").is_error()); + EXPECT(parse("DELETE FROM table WHERE 15").is_error()); + EXPECT(parse("DELETE FROM table WHERE 15 RETURNING").is_error()); + EXPECT(parse("DELETE FROM table WHERE 15 RETURNING *").is_error()); + EXPECT(parse("DELETE FROM table WHERE (');").is_error()); + EXPECT(parse("WITH DELETE FROM table;").is_error()); + EXPECT(parse("WITH table DELETE FROM table;").is_error()); + EXPECT(parse("WITH table AS DELETE FROM table;").is_error()); + EXPECT(parse("WITH RECURSIVE table DELETE FROM table;").is_error()); + EXPECT(parse("WITH RECURSIVE table AS DELETE FROM table;").is_error()); + + struct SelectedTable { + bool recursive { false }; + StringView table_name {}; + Vector<StringView> column_names {}; + }; + + auto validate = [](StringView sql, SelectedTable expected_selected_table, StringView expected_schema, StringView expected_table, StringView expected_alias, bool expect_where_clause, bool expect_returning_clause, Vector<StringView> expected_returned_column_aliases) { + auto result = parse(sql); + EXPECT(!result.is_error()); + + auto statement = result.release_value(); + EXPECT(is<SQL::Delete>(*statement)); + + const auto& delete_ = static_cast<const SQL::Delete&>(*statement); + EXPECT_EQ(delete_.recursive(), expected_selected_table.recursive); + + const auto& common_table_expression = delete_.common_table_expression(); + EXPECT_EQ(common_table_expression.is_null(), expected_selected_table.table_name.is_empty()); + if (common_table_expression) { + EXPECT_EQ(common_table_expression->table_name(), expected_selected_table.table_name); + EXPECT_EQ(common_table_expression->column_names().size(), expected_selected_table.column_names.size()); + for (size_t i = 0; i < common_table_expression->column_names().size(); ++i) + EXPECT_EQ(common_table_expression->column_names()[i], expected_selected_table.column_names[i]); + } + + const auto& qualified_table_name = delete_.qualified_table_name(); + EXPECT_EQ(qualified_table_name->schema_name(), expected_schema); + EXPECT_EQ(qualified_table_name->table_name(), expected_table); + EXPECT_EQ(qualified_table_name->alias(), expected_alias); + + const auto& where_clause = delete_.where_clause(); + EXPECT_EQ(where_clause.is_null(), !expect_where_clause); + if (where_clause) + EXPECT(!is<SQL::ErrorExpression>(*where_clause)); + + const auto& returning_clause = delete_.returning_clause(); + EXPECT_EQ(returning_clause.is_null(), !expect_returning_clause); + if (returning_clause) { + EXPECT_EQ(returning_clause->columns().size(), expected_returned_column_aliases.size()); + + for (size_t i = 0; i < returning_clause->columns().size(); ++i) { + const auto& column = returning_clause->columns()[i]; + const auto& expected_column_alias = expected_returned_column_aliases[i]; + + EXPECT(!is<SQL::ErrorExpression>(*column.expression)); + EXPECT_EQ(column.column_alias, expected_column_alias); + } + } + }; + + validate("DELETE FROM table;", {}, {}, "table", {}, false, false, {}); + validate("DELETE FROM schema.table;", {}, "schema", "table", {}, false, false, {}); + validate("DELETE FROM schema.table AS alias;", {}, "schema", "table", "alias", false, false, {}); + validate("DELETE FROM table WHERE (1 == 1);", {}, {}, "table", {}, true, false, {}); + validate("DELETE FROM table RETURNING *;", {}, {}, "table", {}, false, true, {}); + validate("DELETE FROM table RETURNING column;", {}, {}, "table", {}, false, true, { {} }); + validate("DELETE FROM table RETURNING column AS alias;", {}, {}, "table", {}, false, true, { "alias" }); + validate("DELETE FROM table RETURNING column1 AS alias1, column2 AS alias2;", {}, {}, "table", {}, false, true, { "alias1", "alias2" }); + + // FIXME: When parsing of SELECT statements are supported, the common-table-expressions below will become invalid due to the empty "AS ()" clause. + validate("WITH table AS () DELETE FROM table;", { false, "table", {} }, {}, "table", {}, false, false, {}); + validate("WITH table (column) AS () DELETE FROM table;", { false, "table", { "column" } }, {}, "table", {}, false, false, {}); + validate("WITH table (column1, column2) AS () DELETE FROM table;", { false, "table", { "column1", "column2" } }, {}, "table", {}, false, false, {}); + validate("WITH RECURSIVE table AS () DELETE FROM table;", { true, "table", {} }, {}, "table", {}, false, false, {}); +} + TEST_MAIN(SqlParser) |