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
12 changes: 7 additions & 5 deletions src/core/parser_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,18 @@ ParserExtensionParseResult MaterializedViewParserExtension::ParseFunction(Parser
}

ParserExtensionParseResult ParseMaterializedViewStatement(const string &query, SqlDialect input_dialect) {
auto query_lower = SqlUtils::SQLToLowercase(StringUtil::Replace(query, ";", ""));
// Keep the original case: double-quoted identifiers must retain it. Keyword detection is case-insensitive.
auto query_lower = StringUtil::Replace(query, ";", "");
StringUtil::Trim(query_lower);
// Strip SQL line comments (-- to end of line) before whitespace normalization.
// RemoveRedundantWhitespaces collapses '\n' to ' ', which would turn
// "-- comment\n rest" into "-- comment rest" where the rest is eaten by the comment.
SqlUtils::StripLineComments(query_lower);
SqlUtils::RemoveRedundantWhitespaces(query_lower);
const string keyword_probe = StringUtil::Lower(query_lower);

// Handle ALTER MATERIALIZED VIEW <name> SET REFRESH EVERY '<interval>' | SET REFRESH MANUAL
if (StringUtil::Contains(query_lower, "alter materialized view")) {
if (StringUtil::Contains(keyword_probe, "alter materialized view")) {
const string identifier = "(?:\"(?:[^\"]|\"\")*\"|[a-zA-Z_][a-zA-Z0-9_$]*)";
const string qualified_identifier = identifier + "(?:\\s*\\.\\s*" + identifier + "){0,2}";
std::regex alter_re("^alter\\s+materialized\\s+view\\s+(" + qualified_identifier +
Expand Down Expand Up @@ -132,8 +134,8 @@ ParserExtensionParseResult ParseMaterializedViewStatement(const string &query, S
return ParserExtensionParseResult(std::move(parse_data));
}

if (!StringUtil::Contains(query_lower, "create materialized view") &&
!StringUtil::Contains(query_lower, "create or replace materialized view")) {
if (!StringUtil::Contains(keyword_probe, "create materialized view") &&
!StringUtil::Contains(keyword_probe, "create or replace materialized view")) {
return ParserExtensionParseResult();
}

Expand All @@ -145,7 +147,7 @@ ParserExtensionParseResult ParseMaterializedViewStatement(const string &query, S
if (std::regex_search(query_lower, or_replace_re)) {
is_replace = true;
// Strip "or replace" so the rest of the pipeline sees "create materialized view"
query_lower = std::regex_replace(query_lower, std::regex("\\bor\\s+replace\\s+"), "");
query_lower = std::regex_replace(query_lower, std::regex("\\bor\\s+replace\\s+", std::regex::icase), "");
SqlUtils::RemoveRedundantWhitespaces(query_lower);
}

Expand Down
9 changes: 6 additions & 3 deletions src/core/sql_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ string SqlUtils::ExtractTableName(const string &sql) {
}
// Matches: CREATE TABLE [IF NOT EXISTS] name|"quoted name" (AS ...|(...))
std::regex table_name_regex(
R"re(create\s+table\s+(?:if\s+not\s+exists\s+)?("(?:[^"]+)"|[a-zA-Z0-9_.]+)(?:\s*\([^)]*\)|\s+as\s+(.*)))re");
R"re(create\s+table\s+(?:if\s+not\s+exists\s+)?("(?:[^"]+)"|[a-zA-Z0-9_.]+)(?:\s*\([^)]*\)|\s+as\s+(.*)))re",
std::regex::icase);
std::smatch match;
if (std::regex_search(sql, match, table_name_regex)) {
auto name = match[1].str();
Expand All @@ -390,7 +391,8 @@ string SqlUtils::EscapeSingleQuotes(const string &input) {
}

void SqlUtils::ReplaceMaterializedView(string &query) {
query = std::regex_replace(query, std::regex("\\bmaterialized\\s+view\\b"), "table if not exists");
query =
std::regex_replace(query, std::regex("\\bmaterialized\\s+view\\b", std::regex::icase), "table if not exists");
query = regex_replace(query, std::regex("\\s*;$"), "");
}

Expand Down Expand Up @@ -470,7 +472,8 @@ string SqlUtils::ExtractViewQuery(string &query) {
// Match only up through "AS " — then return everything after as a raw substr so
// multi-line CTE bodies (which contain '\n') are not truncated by regex '.' semantics.
std::regex rgx_create_view(
R"re(create\s+(table|materialized view)\s+(?:if\s+not\s+exists\s+)?("(?:[^"]+)"|[a-zA-Z0-9_.]+)\s+as\s+)re");
R"re(create\s+(table|materialized view)\s+(?:if\s+not\s+exists\s+)?("(?:[^"]+)"|[a-zA-Z0-9_.]+)\s+as\s+)re",
std::regex::icase);
std::smatch match;
if (std::regex_search(query, match, rgx_create_view)) {
return query.substr(static_cast<size_t>(match.position()) + static_cast<size_t>(match.length()));
Expand Down
59 changes: 59 additions & 0 deletions test/sql/quoted_identifier_case.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# name: test/sql/quoted_identifier_case.test
# description: Quoted mixed-case MV names and aliases keep their case, like CREATE VIEW
# group: [sql]

require openivm

statement ok
CREATE TABLE "Orders" ("CustomerId" INT, "Amount" INT);

statement ok
INSERT INTO "Orders" VALUES (1, 10), (1, 5), (2, 7);

statement ok
CREATE MATERIALIZED VIEW "OrderTotals" AS SELECT "CustomerId", SUM("Amount") AS "TotalAmount" FROM "Orders" GROUP BY "CustomerId";

statement ok
CREATE VIEW "PlainTotals" AS SELECT "CustomerId", SUM("Amount") AS "TotalAmount" FROM "Orders" GROUP BY "CustomerId";

query TT
SELECT table_name, column_name FROM information_schema.columns WHERE table_name ILIKE '%totals' ORDER BY table_name, ordinal_position;
----
OrderTotals CustomerId
OrderTotals TotalAmount
PlainTotals CustomerId
PlainTotals TotalAmount

query T
SELECT view_sql_name FROM openivm_views WHERE view_sql_name ILIKE 'ordertotals';
----
OrderTotals

statement ok
INSERT INTO "Orders" VALUES (1, 100), (3, 1);

statement ok
PRAGMA refresh('OrderTotals');

query II
SELECT "CustomerId", "TotalAmount" FROM "OrderTotals" ORDER BY 1;
----
1 115
2 7
3 1

query I
SELECT COUNT(*) FROM ((SELECT * FROM "OrderTotals" EXCEPT ALL SELECT * FROM "PlainTotals") UNION ALL (SELECT * FROM "PlainTotals" EXCEPT ALL SELECT * FROM "OrderTotals"));
----
0

# Mixed-case keywords and OR REPLACE
statement ok
Create Or Replace Materialized View "OrderTotals" AS SELECT "CustomerId", SUM("Amount") AS "TotalAmount", COUNT(*) AS "RowCount" FROM "Orders" GROUP BY "CustomerId";

query T
SELECT column_name FROM information_schema.columns WHERE table_name = 'OrderTotals' ORDER BY ordinal_position;
----
CustomerId
TotalAmount
RowCount
Loading