From 1500479a1dd1336782ef609f8e950f8ed49138d1 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 23 Apr 2021 22:18:35 -0400 Subject: LibSQL: Parse ALTER TABLE statement There are 4 forms an ALTER TABLE statement can take, and each are very distinct, so they each get their own AST node class. --- Userland/Libraries/LibSQL/AST.h | 76 +++++++++++ Userland/Libraries/LibSQL/Forward.h | 5 + Userland/Libraries/LibSQL/Parser.cpp | 40 +++++- Userland/Libraries/LibSQL/Parser.h | 1 + .../LibSQL/Tests/TestSqlStatementParser.cpp | 141 +++++++++++++++++++++ 5 files changed, 262 insertions(+), 1 deletion(-) diff --git a/Userland/Libraries/LibSQL/AST.h b/Userland/Libraries/LibSQL/AST.h index 1e1f2714b6..8f71d26e17 100644 --- a/Userland/Libraries/LibSQL/AST.h +++ b/Userland/Libraries/LibSQL/AST.h @@ -720,6 +720,82 @@ private: bool m_is_error_if_table_exists; }; +class AlterTable : public Statement { +public: + const String& schema_name() const { return m_schema_name; } + const String& table_name() const { return m_table_name; } + +protected: + AlterTable(String schema_name, String table_name) + : m_schema_name(move(schema_name)) + , m_table_name(move(table_name)) + { + } + +private: + String m_schema_name; + String m_table_name; +}; + +class RenameTable : public AlterTable { +public: + RenameTable(String schema_name, String table_name, String new_table_name) + : AlterTable(move(schema_name), move(table_name)) + , m_new_table_name(move(new_table_name)) + { + } + + const String& new_table_name() const { return m_new_table_name; } + +private: + String m_new_table_name; +}; + +class RenameColumn : public AlterTable { +public: + RenameColumn(String schema_name, String table_name, String column_name, String new_column_name) + : AlterTable(move(schema_name), move(table_name)) + , m_column_name(move(column_name)) + , m_new_column_name(move(new_column_name)) + { + } + + const String& column_name() const { return m_column_name; } + const String& new_column_name() const { return m_new_column_name; } + +private: + String m_column_name; + String m_new_column_name; +}; + +class AddColumn : public AlterTable { +public: + AddColumn(String schema_name, String table_name, NonnullRefPtr column) + : AlterTable(move(schema_name), move(table_name)) + , m_column(move(column)) + { + } + + const NonnullRefPtr& column() const { return m_column; } + +private: + NonnullRefPtr m_column; +}; + +class DropColumn : public AlterTable { +public: + DropColumn(String schema_name, String table_name, String column_name) + : AlterTable(move(schema_name), move(table_name)) + , m_column_name(move(column_name)) + { + } + + const String& column_name() const { return m_column_name; } + +private: + String m_column_name; +}; + class DropTable : public Statement { public: DropTable(String schema_name, String table_name, bool is_error_if_table_does_not_exist) diff --git a/Userland/Libraries/LibSQL/Forward.h b/Userland/Libraries/LibSQL/Forward.h index 3f83078e40..14c08d0923 100644 --- a/Userland/Libraries/LibSQL/Forward.h +++ b/Userland/Libraries/LibSQL/Forward.h @@ -7,6 +7,8 @@ #pragma once namespace SQL { +class AddColumn; +class AlterTable; class ASTNode; class BetweenExpression; class BinaryOperatorExpression; @@ -21,6 +23,7 @@ class CommonTableExpression; class CommonTableExpressionList; class CreateTable; class Delete; +class DropColumn; class DropTable; class ErrorExpression; class ErrorStatement; @@ -45,6 +48,8 @@ class NumericLiteral; class OrderingTerm; class Parser; class QualifiedTableName; +class RenameColumn; +class RenameTable; class ResultColumn; class ReturningClause; class Select; diff --git a/Userland/Libraries/LibSQL/Parser.cpp b/Userland/Libraries/LibSQL/Parser.cpp index de96359776..eb9fdd9215 100644 --- a/Userland/Libraries/LibSQL/Parser.cpp +++ b/Userland/Libraries/LibSQL/Parser.cpp @@ -34,6 +34,8 @@ NonnullRefPtr Parser::parse_statement() switch (m_parser_state.m_token.type()) { case TokenType::Create: return parse_create_table_statement(); + case TokenType::Alter: + return parse_alter_table_statement(); case TokenType::Drop: return parse_drop_table_statement(); case TokenType::Insert: @@ -45,7 +47,7 @@ NonnullRefPtr Parser::parse_statement() case TokenType::Select: return parse_select_statement({}); default: - expected("CREATE, DROP, INSERT, UPDATE, DELETE, or SELECT"); + expected("CREATE, ALTER, DROP, INSERT, UPDATE, DELETE, or SELECT"); return create_ast_node(); } } @@ -102,6 +104,42 @@ NonnullRefPtr Parser::parse_create_table_statement() return create_ast_node(move(schema_name), move(table_name), move(column_definitions), is_temporary, is_error_if_table_exists); } +NonnullRefPtr Parser::parse_alter_table_statement() +{ + // https://sqlite.org/lang_altertable.html + consume(TokenType::Alter); + consume(TokenType::Table); + + String schema_name; + String table_name; + parse_schema_and_table_name(schema_name, table_name); + + if (consume_if(TokenType::Add)) { + consume_if(TokenType::Column); // COLUMN is optional. + auto column = parse_column_definition(); + return create_ast_node(move(schema_name), move(table_name), move(column)); + } + + if (consume_if(TokenType::Drop)) { + consume_if(TokenType::Column); // COLUMN is optional. + auto column = consume(TokenType::Identifier).value(); + return create_ast_node(move(schema_name), move(table_name), move(column)); + } + + consume(TokenType::Rename); + + if (consume_if(TokenType::To)) { + auto new_table_name = consume(TokenType::Identifier).value(); + return create_ast_node(move(schema_name), move(table_name), move(new_table_name)); + } + + consume_if(TokenType::Column); // COLUMN is optional. + auto column_name = consume(TokenType::Identifier).value(); + consume(TokenType::To); + auto new_column_name = consume(TokenType::Identifier).value(); + return create_ast_node(move(schema_name), move(table_name), move(column_name), move(new_column_name)); +} + NonnullRefPtr Parser::parse_drop_table_statement() { // https://sqlite.org/lang_droptable.html diff --git a/Userland/Libraries/LibSQL/Parser.h b/Userland/Libraries/LibSQL/Parser.h index 43463508ad..e3a832986c 100644 --- a/Userland/Libraries/LibSQL/Parser.h +++ b/Userland/Libraries/LibSQL/Parser.h @@ -53,6 +53,7 @@ private: NonnullRefPtr parse_statement(); NonnullRefPtr parse_statement_with_expression_list(RefPtr); NonnullRefPtr parse_create_table_statement(); + NonnullRefPtr parse_alter_table_statement(); NonnullRefPtr parse_drop_table_statement(); NonnullRefPtr parse_insert_statement(RefPtr); NonnullRefPtr parse_update_statement(RefPtr); diff --git a/Userland/Libraries/LibSQL/Tests/TestSqlStatementParser.cpp b/Userland/Libraries/LibSQL/Tests/TestSqlStatementParser.cpp index a7456d3444..7438311bdc 100644 --- a/Userland/Libraries/LibSQL/Tests/TestSqlStatementParser.cpp +++ b/Userland/Libraries/LibSQL/Tests/TestSqlStatementParser.cpp @@ -125,6 +125,147 @@ TEST_CASE(create_table) validate("CREATE TABLE test ( column1 varchar(1e3) );", {}, "test", { { "column1", "varchar", { 1000 } } }); } +TEST_CASE(alter_table) +{ + // This test case only contains common error cases of the AlterTable subclasses. + EXPECT(parse("ALTER").is_error()); + EXPECT(parse("ALTER TABLE").is_error()); + EXPECT(parse("ALTER TABLE table").is_error()); + EXPECT(parse("ALTER TABLE table;").is_error()); +} + +TEST_CASE(alter_table_rename_table) +{ + EXPECT(parse("ALTER TABLE table RENAME").is_error()); + EXPECT(parse("ALTER TABLE table RENAME TO").is_error()); + EXPECT(parse("ALTER TABLE table RENAME TO new_table").is_error()); + + auto validate = [](StringView sql, StringView expected_schema, StringView expected_table, StringView expected_new_table) { + auto result = parse(sql); + EXPECT(!result.is_error()); + + auto statement = result.release_value(); + EXPECT(is(*statement)); + + const auto& alter = static_cast(*statement); + EXPECT_EQ(alter.schema_name(), expected_schema); + EXPECT_EQ(alter.table_name(), expected_table); + EXPECT_EQ(alter.new_table_name(), expected_new_table); + }; + + validate("ALTER TABLE table RENAME TO new_table;", {}, "table", "new_table"); + validate("ALTER TABLE schema.table RENAME TO new_table;", "schema", "table", "new_table"); +} + +TEST_CASE(alter_table_rename_column) +{ + EXPECT(parse("ALTER TABLE table RENAME").is_error()); + EXPECT(parse("ALTER TABLE table RENAME COLUMN").is_error()); + EXPECT(parse("ALTER TABLE table RENAME COLUMN column").is_error()); + EXPECT(parse("ALTER TABLE table RENAME COLUMN column TO").is_error()); + EXPECT(parse("ALTER TABLE table RENAME COLUMN column TO new_column").is_error()); + EXPECT(parse("ALTER TABLE table RENAME column").is_error()); + EXPECT(parse("ALTER TABLE table RENAME column TO").is_error()); + EXPECT(parse("ALTER TABLE table RENAME column TO new_column").is_error()); + + auto validate = [](StringView sql, StringView expected_schema, StringView expected_table, StringView expected_column, StringView expected_new_column) { + auto result = parse(sql); + EXPECT(!result.is_error()); + + auto statement = result.release_value(); + EXPECT(is(*statement)); + + const auto& alter = static_cast(*statement); + EXPECT_EQ(alter.schema_name(), expected_schema); + EXPECT_EQ(alter.table_name(), expected_table); + EXPECT_EQ(alter.column_name(), expected_column); + EXPECT_EQ(alter.new_column_name(), expected_new_column); + }; + + validate("ALTER TABLE table RENAME column TO new_column;", {}, "table", "column", "new_column"); + validate("ALTER TABLE table RENAME COLUMN column TO new_column;", {}, "table", "column", "new_column"); + validate("ALTER TABLE schema.table RENAME column TO new_column;", "schema", "table", "column", "new_column"); + validate("ALTER TABLE schema.table RENAME COLUMN column TO new_column;", "schema", "table", "column", "new_column"); +} + +TEST_CASE(alter_table_add_column) +{ + EXPECT(parse("ALTER TABLE table ADD").is_error()); + EXPECT(parse("ALTER TABLE table ADD COLUMN").is_error()); + EXPECT(parse("ALTER TABLE table ADD COLUMN column").is_error()); + + struct Column { + StringView name; + StringView type; + Vector signed_numbers {}; + }; + + auto validate = [](StringView sql, StringView expected_schema, StringView expected_table, Column expected_column) { + auto result = parse(sql); + EXPECT(!result.is_error()); + + auto statement = result.release_value(); + EXPECT(is(*statement)); + + const auto& alter = static_cast(*statement); + EXPECT_EQ(alter.schema_name(), expected_schema); + EXPECT_EQ(alter.table_name(), expected_table); + + const auto& column = alter.column(); + EXPECT_EQ(column->name(), expected_column.name); + + const auto& type_name = column->type_name(); + EXPECT_EQ(type_name->name(), expected_column.type); + + const auto& signed_numbers = type_name->signed_numbers(); + EXPECT_EQ(signed_numbers.size(), expected_column.signed_numbers.size()); + + for (size_t j = 0; j < signed_numbers.size(); ++j) { + double signed_number = signed_numbers[j].value(); + double expected_signed_number = expected_column.signed_numbers[j]; + EXPECT_EQ(signed_number, expected_signed_number); + } + }; + + validate("ALTER TABLE test ADD column1;", {}, "test", { "column1", "BLOB" }); + validate("ALTER TABLE test ADD column1 int;", {}, "test", { "column1", "int" }); + validate("ALTER TABLE test ADD column1 varchar;", {}, "test", { "column1", "varchar" }); + validate("ALTER TABLE test ADD column1 varchar(255);", {}, "test", { "column1", "varchar", { 255 } }); + validate("ALTER TABLE test ADD column1 varchar(255, 123);", {}, "test", { "column1", "varchar", { 255, 123 } }); + + validate("ALTER TABLE schema.test ADD COLUMN column1;", "schema", "test", { "column1", "BLOB" }); + validate("ALTER TABLE schema.test ADD COLUMN column1 int;", "schema", "test", { "column1", "int" }); + validate("ALTER TABLE schema.test ADD COLUMN column1 varchar;", "schema", "test", { "column1", "varchar" }); + validate("ALTER TABLE schema.test ADD COLUMN column1 varchar(255);", "schema", "test", { "column1", "varchar", { 255 } }); + validate("ALTER TABLE schema.test ADD COLUMN column1 varchar(255, 123);", "schema", "test", { "column1", "varchar", { 255, 123 } }); +} + +TEST_CASE(alter_table_drop_column) +{ + EXPECT(parse("ALTER TABLE table DROP").is_error()); + EXPECT(parse("ALTER TABLE table DROP COLUMN").is_error()); + EXPECT(parse("ALTER TABLE table DROP column").is_error()); + EXPECT(parse("ALTER TABLE table DROP COLUMN column").is_error()); + + auto validate = [](StringView sql, StringView expected_schema, StringView expected_table, StringView expected_column) { + auto result = parse(sql); + EXPECT(!result.is_error()); + + auto statement = result.release_value(); + EXPECT(is(*statement)); + + const auto& alter = static_cast(*statement); + EXPECT_EQ(alter.schema_name(), expected_schema); + EXPECT_EQ(alter.table_name(), expected_table); + EXPECT_EQ(alter.column_name(), expected_column); + }; + + validate("ALTER TABLE table DROP column;", {}, "table", "column"); + validate("ALTER TABLE table DROP COLUMN column;", {}, "table", "column"); + validate("ALTER TABLE schema.table DROP column;", "schema", "table", "column"); + validate("ALTER TABLE schema.table DROP COLUMN column;", "schema", "table", "column"); +} + TEST_CASE(drop_table) { EXPECT(parse("DROP").is_error()); -- cgit v1.2.3