From 4bca226b2ba336dc032a85baa0b1844a62f39788 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 6 Oct 2026 01:56:07 +0200 Subject: [PATCH 1/3] CREATE MATERIALIZED VIEW lowercases double-quoted identifiers (view name and aliases) --- src/core/parser_parse.cpp | 12 +++--- src/core/sql_utils.cpp | 6 ++- test/sql/quoted_identifier_case.test | 59 ++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 test/sql/quoted_identifier_case.test diff --git a/src/core/parser_parse.cpp b/src/core/parser_parse.cpp index c22574d9..fc8096f1 100644 --- a/src/core/parser_parse.cpp +++ b/src/core/parser_parse.cpp @@ -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 SET REFRESH EVERY '' | 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 + @@ -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(); } @@ -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); } diff --git a/src/core/sql_utils.cpp b/src/core/sql_utils.cpp index 58f34863..ba0d43c4 100644 --- a/src/core/sql_utils.cpp +++ b/src/core/sql_utils.cpp @@ -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(); @@ -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*;$"), ""); } diff --git a/test/sql/quoted_identifier_case.test b/test/sql/quoted_identifier_case.test new file mode 100644 index 00000000..0dd89f19 --- /dev/null +++ b/test/sql/quoted_identifier_case.test @@ -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 From 62e5756e65c85b4b78522320807d89ac6379a8c6 Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 6 Oct 2026 03:21:26 +0200 Subject: [PATCH 2/3] CREATE MATERIALIZED VIEW lowercases double-quoted identifiers (view name and aliases) --- src/core/sql_utils.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/sql_utils.cpp b/src/core/sql_utils.cpp index ba0d43c4..88841f6f 100644 --- a/src/core/sql_utils.cpp +++ b/src/core/sql_utils.cpp @@ -391,8 +391,8 @@ string SqlUtils::EscapeSingleQuotes(const string &input) { } void SqlUtils::ReplaceMaterializedView(string &query) { - query = std::regex_replace(query, std::regex("\\bmaterialized\\s+view\\b", std::regex::icase), - "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*;$"), ""); } From 2b03a5e7617f5f956bcdf3852d62e0fcdd9edf3e Mon Sep 17 00:00:00 2001 From: ila Date: Tue, 6 Oct 2026 23:27:05 +0200 Subject: [PATCH 3/3] CREATE MATERIALIZED VIEW lowercases double-quoted identifiers (view name and aliases) --- src/core/sql_utils.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/sql_utils.cpp b/src/core/sql_utils.cpp index 88841f6f..7b8650bb 100644 --- a/src/core/sql_utils.cpp +++ b/src/core/sql_utils.cpp @@ -472,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(match.position()) + static_cast(match.length()));