From 7cbd20a05322d1e9ce4de7c067a448ecf61acebd Mon Sep 17 00:00:00 2001 From: dennis zhuang Date: Tue, 15 Sep 2026 08:59:30 +0000 Subject: [PATCH] fix(mysql): strip leading comments before the federated statement filter (#9156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(mysql): strip leading comments before the federated statement filter JDBC clients prefix every statement with a comment. DataGrip sends `/* ApplicationName=DataGrip */` in front of each one, and every pattern in the federated filter is anchored with `^`, so the prefix makes all of them miss. Two failures follow. `SET TRANSACTION READ WRITE` reaches the SQL parser and is rejected. Worse, the DBeaver-specific entries such as `^(/\* ApplicationName=(.*)SELECT @@(.*))` do match DataGrip's prefix, so `SELECT @@GLOBAL.event_scheduler` and `SHOW VARIABLES LIKE ...` are absorbed into a zero-column output, which the MySQL writer sends as an OK packet. The JDBC driver reports that the statement returned no cursor. Strip leading whitespace and comments before matching, and drop the ApplicationName-specific entries the stripping makes redundant. The three that had no unprefixed counterpart (`SHOW PLUGINS`, `SHOW ENGINES`, `SHOW @@...`) keep their behaviour as plain patterns. Also covers gaps found while probing the same path: - `BEGIN` is absorbed like `START TRANSACTION`/`COMMIT`/`ROLLBACK`. - `SHOW [GLOBAL|SESSION|LOCAL] VARIABLES|STATUS` parses; the scope is ignored because GreptimeDB keeps no global/session split. - `USER()`, `CURRENT_USER()`, `SYSTEM_USER()` and `SCHEMA()` are registered. Signed-off-by: Dennis Zhuang * fix(mysql): keep executable comments, reject multi-statement absorption Review follow-up on the comment stripping, plus the remaining MySQL compatibility gaps from #9155. `/*!...*/` is an executable comment: mysqldump emits its initialization as `/*!40101 SET NAMES ... */`, and the patterns match those verbatim. Stripping it left an empty statement, so nothing matched and the original SQL reached the parser, which rejects `SetNames` and `MultipleAssignments`. Leave executable comments in place. Absorbing a request also has to stop at a statement boundary, because every pattern ends in `(.*)`. `BEGIN; INSERT INTO t VALUES (1)` used to fail on the unsupported `BEGIN`; once `BEGIN` became absorbable the whole request would report success and write nothing. A request is now scanned for a second statement and handed to the query engine if it has one. The scan skips plain comments and string literals so a `;` inside either is not a boundary, and treats a `/*!...*/` that is not the request itself as a statement, since it carries SQL. `check()` now dispatches on the leading statement keyword, so INSERT, UPDATE, CREATE and ordinary SELECTs run no regex at all — this replaces the narrower hand-rolled INSERT shortcut. The statement scan runs only for a request the patterns already matched, which is always a short one. New compatibility surface: - `SELECT CURRENT_USER|SESSION_USER|SYSTEM_USER|USER` without parentheses, and `SELECT @var`, are answered here rather than in the parser. Both are anchored to the whole statement, so `SELECT user FROM t` still reads the column. - `information_schema.plugins`, `user_privileges` and `processlist` are registered as empty tables, like the other MySQL-shape tables around them. Sessions are reported through `information_schema.process_list` and `SHOW PROCESSLIST`; `processlist` carries the column shape only. Signed-off-by: Dennis Zhuang --------- Signed-off-by: Dennis Zhuang --- .../src/system_schema/information_schema.rs | 6 + .../information_memory_table.rs | 41 ++ .../information_schema/table_names.rs | 5 + src/common/catalog/src/consts.rs | 6 + src/common/function/src/system.rs | 7 +- src/common/function/src/system/database.rs | 88 ++- src/servers/src/mysql/federated.rs | 533 ++++++++++++++++-- src/sql/src/parsers/show_parser.rs | 44 ++ .../common/show/show_databases_tables.result | 9 + .../standalone/common/system/database.result | 24 + .../standalone/common/system/database.sql | 6 + .../common/system/information_schema.result | 26 + .../standalone/common/view/create.result | 3 + 13 files changed, 745 insertions(+), 53 deletions(-) diff --git a/src/catalog/src/system_schema/information_schema.rs b/src/catalog/src/system_schema/information_schema.rs index d0539a062c..8c3f4bf0f5 100644 --- a/src/catalog/src/system_schema/information_schema.rs +++ b/src/catalog/src/system_schema/information_schema.rs @@ -135,6 +135,9 @@ lazy_static! { GLOBAL_STATUS, SESSION_STATUS, PARTITIONS, + PLUGINS, + USER_PRIVILEGES, + PROCESSLIST, ]; } @@ -237,6 +240,9 @@ impl SystemSchemaProviderInner for InformationSchemaProvider { TABLE_PRIVILEGES => setup_memory_table!(TABLE_PRIVILEGES), GLOBAL_STATUS => setup_memory_table!(GLOBAL_STATUS), SESSION_STATUS => setup_memory_table!(SESSION_STATUS), + PLUGINS => setup_memory_table!(PLUGINS), + USER_PRIVILEGES => setup_memory_table!(USER_PRIVILEGES), + PROCESSLIST => setup_memory_table!(PROCESSLIST), KEY_COLUMN_USAGE => Some(Arc::new(InformationSchemaKeyColumnUsage::new( self.catalog_name.clone(), self.catalog_manager.clone(), diff --git a/src/catalog/src/system_schema/information_schema/information_memory_table.rs b/src/catalog/src/system_schema/information_schema/information_memory_table.rs index 56a84a0da1..847ad06dc3 100644 --- a/src/catalog/src/system_schema/information_schema/information_memory_table.rs +++ b/src/catalog/src/system_schema/information_schema/information_memory_table.rs @@ -383,6 +383,47 @@ pub(super) fn get_schema_columns(table_name: &str) -> (SchemaRef, Vec vec![], ), + // GreptimeDB has no pluggable components. + PLUGINS => ( + string_columns(&[ + "PLUGIN_NAME", + "PLUGIN_VERSION", + "PLUGIN_STATUS", + "PLUGIN_TYPE", + "PLUGIN_TYPE_VERSION", + "PLUGIN_LIBRARY", + "PLUGIN_LIBRARY_VERSION", + "PLUGIN_AUTHOR", + "PLUGIN_DESCRIPTION", + "PLUGIN_LICENSE", + "LOAD_OPTION", + ]), + vec![], + ), + + // Privileges are not exposed through `information_schema`, same as the other + // `*_privileges` tables above. + USER_PRIVILEGES => ( + string_columns(&["GRANTEE", "TABLE_CATALOG", "PRIVILEGE_TYPE", "IS_GRANTABLE"]), + vec![], + ), + + // Sessions are reported through `information_schema.process_list` and + // `SHOW PROCESSLIST`; this table carries the MySQL column shape only. + PROCESSLIST => ( + vec![ + bigint_column("ID"), + string_column("USER"), + string_column("HOST"), + string_column("DB"), + string_column("COMMAND"), + bigint_column("TIME"), + string_column("STATE"), + string_column("INFO"), + ], + vec![], + ), + _ => unreachable!("Unknown table in information_schema: {}", table_name), }; diff --git a/src/catalog/src/system_schema/information_schema/table_names.rs b/src/catalog/src/system_schema/information_schema/table_names.rs index f161fecb54..3e9b2e2d6d 100644 --- a/src/catalog/src/system_schema/information_schema/table_names.rs +++ b/src/catalog/src/system_schema/information_schema/table_names.rs @@ -55,3 +55,8 @@ pub const SSTS_INDEX_META: &str = "ssts_index_meta"; pub const TABLE_SEMANTICS: &str = "table_semantics"; pub const STATISTICS: &str = "statistics"; pub const RECYCLE_BIN: &str = "recycle_bin"; +pub const PLUGINS: &str = "plugins"; +pub const USER_PRIVILEGES: &str = "user_privileges"; +/// MySQL's session list. GreptimeDB reports its own sessions through [`PROCESS_LIST`]; +/// this table only exists so MySQL tooling finds the name and column shape it expects. +pub const PROCESSLIST: &str = "processlist"; diff --git a/src/common/catalog/src/consts.rs b/src/common/catalog/src/consts.rs index bc1ae5f38f..dd1b5fc3f0 100644 --- a/src/common/catalog/src/consts.rs +++ b/src/common/catalog/src/consts.rs @@ -122,6 +122,12 @@ pub const INFORMATION_SCHEMA_STATISTICS_TABLE_ID: u32 = 43; pub const INFORMATION_SCHEMA_RECYCLE_BIN_TABLE_ID: u32 = 44; /// id for information_schema.flow_statistics pub const INFORMATION_SCHEMA_FLOW_STATISTICS_TABLE_ID: u32 = 45; +/// id for information_schema.PLUGINS +pub const INFORMATION_SCHEMA_PLUGINS_TABLE_ID: u32 = 47; +/// id for information_schema.USER_PRIVILEGES +pub const INFORMATION_SCHEMA_USER_PRIVILEGES_TABLE_ID: u32 = 48; +/// id for information_schema.PROCESSLIST (for mysql) +pub const INFORMATION_SCHEMA_PROCESSLIST_TABLE_ID: u32 = 49; // ----- End of information_schema tables ----- diff --git a/src/common/function/src/system.rs b/src/common/function/src/system.rs index 0c1e5bee36..5d3a76ac4d 100644 --- a/src/common/function/src/system.rs +++ b/src/common/function/src/system.rs @@ -21,7 +21,8 @@ mod version; use build::BuildFunction; use database::{ - ConnectionIdFunction, DatabaseFunction, PgBackendPidFunction, ReadPreferenceFunction, + ConnectionIdFunction, CurrentUserFunction, DatabaseFunction, PgBackendPidFunction, + ReadPreferenceFunction, SchemaFunction, SystemUserFunction, UserFunction, }; use pg_catalog::PGCatalogFunction; use procedure_state::ProcedureStateFunction; @@ -37,6 +38,10 @@ impl SystemFunction { registry.register_scalar(BuildFunction::default()); registry.register_scalar(VersionFunction::default()); registry.register_scalar(DatabaseFunction::default()); + registry.register_scalar(SchemaFunction::default()); + registry.register_scalar(UserFunction::default()); + registry.register_scalar(CurrentUserFunction::default()); + registry.register_scalar(SystemUserFunction::default()); registry.register_scalar(ReadPreferenceFunction::default()); registry.register_scalar(PgBackendPidFunction::default()); registry.register_scalar(ConnectionIdFunction::default()); diff --git a/src/common/function/src/system/database.rs b/src/common/function/src/system/database.rs index b20ab1f883..1884c5525f 100644 --- a/src/common/function/src/system/database.rs +++ b/src/common/function/src/system/database.rs @@ -20,39 +20,91 @@ use crate::function::{Function, find_function_context}; use crate::system::define_nullary_udf; define_nullary_udf!(DatabaseFunction); +define_nullary_udf!(SchemaFunction); +define_nullary_udf!(UserFunction); +define_nullary_udf!(CurrentUserFunction); +define_nullary_udf!(SystemUserFunction); define_nullary_udf!(ReadPreferenceFunction); define_nullary_udf!(PgBackendPidFunction); define_nullary_udf!(ConnectionIdFunction); const DATABASE_FUNCTION_NAME: &str = "database"; +const SCHEMA_FUNCTION_NAME: &str = "schema"; +const USER_FUNCTION_NAME: &str = "user"; +const CURRENT_USER_FUNCTION_NAME: &str = "current_user"; +const SYSTEM_USER_FUNCTION_NAME: &str = "system_user"; const READ_PREFERENCE_FUNCTION_NAME: &str = "read_preference"; const PG_BACKEND_PID: &str = "pg_backend_pid"; const CONNECTION_ID: &str = "connection_id"; -impl Function for DatabaseFunction { - fn name(&self) -> &str { - DATABASE_FUNCTION_NAME - } +macro_rules! impl_current_schema_function { + ($name: ident, $fn_name: expr) => { + impl Function for $name { + fn name(&self) -> &str { + $fn_name + } - fn return_type(&self, _: &[DataType]) -> datafusion_common::Result { - Ok(DataType::Utf8View) - } + fn return_type(&self, _: &[DataType]) -> datafusion_common::Result { + Ok(DataType::Utf8View) + } - fn signature(&self) -> &Signature { - &self.signature - } + fn signature(&self) -> &Signature { + &self.signature + } - fn invoke_with_args( - &self, - args: ScalarFunctionArgs, - ) -> datafusion_common::Result { - let func_ctx = find_function_context(&args)?; - let db = func_ctx.query_ctx.current_schema(); + fn invoke_with_args( + &self, + args: ScalarFunctionArgs, + ) -> datafusion_common::Result { + let func_ctx = find_function_context(&args)?; + let db = func_ctx.query_ctx.current_schema(); - Ok(ColumnarValue::Scalar(ScalarValue::Utf8View(Some(db)))) - } + Ok(ColumnarValue::Scalar(ScalarValue::Utf8View(Some(db)))) + } + } + }; } +impl_current_schema_function!(DatabaseFunction, DATABASE_FUNCTION_NAME); +// MySQL's `SCHEMA()` is a synonym for `DATABASE()`. +impl_current_schema_function!(SchemaFunction, SCHEMA_FUNCTION_NAME); + +macro_rules! impl_current_user_function { + ($name: ident, $fn_name: expr) => { + impl Function for $name { + fn name(&self) -> &str { + $fn_name + } + + fn return_type(&self, _: &[DataType]) -> datafusion_common::Result { + Ok(DataType::Utf8View) + } + + fn signature(&self) -> &Signature { + &self.signature + } + + fn invoke_with_args( + &self, + args: ScalarFunctionArgs, + ) -> datafusion_common::Result { + let func_ctx = find_function_context(&args)?; + let user = func_ctx.query_ctx.current_user(); + + Ok(ColumnarValue::Scalar(ScalarValue::Utf8View(Some( + user.username().to_string(), + )))) + } + } + }; +} + +// GreptimeDB has no notion of a user switching identity mid-session, so `USER()`, +// `CURRENT_USER()` and `SYSTEM_USER()` all report the authenticated user. +impl_current_user_function!(UserFunction, USER_FUNCTION_NAME); +impl_current_user_function!(CurrentUserFunction, CURRENT_USER_FUNCTION_NAME); +impl_current_user_function!(SystemUserFunction, SYSTEM_USER_FUNCTION_NAME); + impl Function for ReadPreferenceFunction { fn name(&self) -> &str { READ_PREFERENCE_FUNCTION_NAME diff --git a/src/servers/src/mysql/federated.rs b/src/servers/src/mysql/federated.rs index fa3aec144c..e6c4890df9 100644 --- a/src/servers/src/mysql/federated.rs +++ b/src/servers/src/mysql/federated.rs @@ -31,23 +31,47 @@ use regex::bytes::RegexSet; use session::SessionRef; use session::context::QueryContextRef; -static SELECT_VAR_PATTERN: Lazy = Lazy::new(|| Regex::new("(?i)^(SELECT @@(.*))").unwrap()); -static MYSQL_CONN_JAVA_PATTERN: Lazy = - Lazy::new(|| Regex::new("(?i)^(/\\* mysql-connector-j(.*))").unwrap()); -static SHOW_LOWER_CASE_PATTERN: Lazy = - Lazy::new(|| Regex::new("(?i)^(SHOW VARIABLES LIKE 'lower_case_table_names'(.*))").unwrap()); -static SHOW_VARIABLES_LIKE_PATTERN: Lazy = - Lazy::new(|| Regex::new("(?i)^(SHOW VARIABLES( LIKE (.*))?)").unwrap()); +/// Matches the optional `GLOBAL`/`SESSION`/`LOCAL` scope MySQL accepts before `VARIABLES`. +const VARIABLES_SCOPE: &str = "(GLOBAL |SESSION |LOCAL )?"; + +static SELECT_VAR_PATTERN: Lazy = + Lazy::new(|| Regex::new("(?i)^(SELECT\\s+@@(.*))").unwrap()); +static SHOW_LOWER_CASE_PATTERN: Lazy = Lazy::new(|| { + Regex::new(&format!( + "(?i)^(SHOW {VARIABLES_SCOPE}VARIABLES LIKE 'lower_case_table_names'(.*))" + )) + .unwrap() +}); +static SHOW_VARIABLES_LIKE_PATTERN: Lazy = Lazy::new(|| { + Regex::new(&format!( + "(?i)^(SHOW {VARIABLES_SCOPE}VARIABLES( LIKE (.*))?)" + )) + .unwrap() +}); static SHOW_WARNINGS_PATTERN: Lazy = - Lazy::new(|| Regex::new("(?i)^(/\\* ApplicationName=.*)?SHOW WARNINGS").unwrap()); + Lazy::new(|| Regex::new("(?i)^(SHOW WARNINGS)").unwrap()); + +// Capture 1: a parenless session-user keyword. Capture 2: a user variable. Both parse as +// column references, which the planner then cannot resolve. Anchored at both ends so +// `SELECT user FROM t` still reads the column. +static SELECT_USER_OR_VAR_PATTERN: Lazy = Lazy::new(|| { + Regex::new( + "(?i)^SELECT\\s+(?:(CURRENT_USER|SESSION_USER|SYSTEM_USER|USER)|(@[a-z0-9_$.]+))\\s*;?\\s*$", + ) + .unwrap() +}); // SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP()); static SELECT_TIME_DIFF_FUNC_PATTERN: Lazy = Lazy::new(|| Regex::new("(?i)^(SELECT TIMEDIFF\\(NOW\\(\\), UTC_TIMESTAMP\\(\\)\\))").unwrap()); // sqlalchemy < 1.4.30 -static SHOW_SQL_MODE_PATTERN: Lazy = - Lazy::new(|| Regex::new("(?i)^(SHOW VARIABLES LIKE 'sql_mode'(.*))").unwrap()); +static SHOW_SQL_MODE_PATTERN: Lazy = Lazy::new(|| { + Regex::new(&format!( + "(?i)^(SHOW {VARIABLES_SCOPE}VARIABLES LIKE 'sql_mode'(.*))" + )) + .unwrap() +}); static OTHER_NOT_SUPPORTED_STMT: Lazy = Lazy::new(|| { RegexSet::new([ @@ -55,6 +79,7 @@ static OTHER_NOT_SUPPORTED_STMT: Lazy = Lazy::new(|| { "(?i)^(ROLLBACK(.*))", "(?i)^(COMMIT(.*))", "(?i)^(START(.*))", + "(?i)^(BEGIN(.*))", // Set. "(?i)^(SET NAMES(.*))", @@ -88,13 +113,9 @@ static OTHER_NOT_SUPPORTED_STMT: Lazy = Lazy::new(|| { "(?i)^(/\\*!40101 SET(.*) \\*/)$", // DBeaver. - "(?i)^(/\\* ApplicationName=(.*)SHOW PLUGINS)", - "(?i)^(/\\* ApplicationName=(.*)SHOW ENGINES)", - "(?i)^(/\\* ApplicationName=(.*)SELECT @@(.*))", - "(?i)^(/\\* ApplicationName=(.*)SHOW @@(.*))", - "(?i)^(/\\* ApplicationName=(.*)SET net_write_timeout(.*))", - "(?i)^(/\\* ApplicationName=(.*)SET SQL_SELECT_LIMIT(.*))", - "(?i)^(/\\* ApplicationName=(.*)SHOW VARIABLES(.*))", + "(?i)^(SHOW PLUGINS)", + "(?i)^(SHOW ENGINES)", + "(?i)^(SHOW @@(.*))", // pt-toolkit "(?i)^(/\\*!40101 SET(.*) \\*/)$", @@ -128,7 +149,7 @@ static VAR_VALUES: Lazy> = Lazy::new(|| { // Format: // |function_name| // |value| -fn select_function(name: &str, value: &str) -> RecordBatches { +fn select_function(name: &str, value: Option<&str>) -> RecordBatches { let schema = Arc::new(Schema::new(vec![ColumnSchema::new( name, ConcreteDataType::string_datatype(), @@ -227,16 +248,30 @@ fn select_variable(query: &str, query_context: QueryContextRef) -> Option Option { - if [&SELECT_VAR_PATTERN, &MYSQL_CONN_JAVA_PATTERN] - .iter() - .any(|r| r.is_match(query)) - { + if SELECT_VAR_PATTERN.is_match(query) { select_variable(query, query_context) } else { None } } +fn check_select_user_or_var(query: &str, query_context: QueryContextRef) -> Option { + let captures = SELECT_USER_OR_VAR_PATTERN.captures(query)?; + + let recordbatches = if let Some(keyword) = captures.get(1) { + let user = query_context.current_user(); + select_function(keyword.as_str(), Some(user.username())) + } else { + // `SET @var` is accepted and discarded, so a user variable is always unset, which + // MySQL reports as NULL. + let var = captures + .get(2) + .expect("one of the two groups always matches"); + select_function(var.as_str(), None) + }; + Some(Output::new_with_record_batches(recordbatches)) +} + fn check_show_variables(query: &str) -> Option { let recordbatches = if SHOW_SQL_MODE_PATTERN.is_match(query) { Some(show_variables( @@ -303,7 +338,7 @@ fn check_others(query: &str, _query_ctx: QueryContextRef) -> Option { let recordbatches = if SELECT_TIME_DIFF_FUNC_PATTERN.is_match(query) { Some(select_function( "TIMEDIFF(NOW(), UTC_TIMESTAMP())", - "00:00:00", + Some("00:00:00"), )) } else { None @@ -311,6 +346,164 @@ fn check_others(query: &str, _query_ctx: QueryContextRef) -> Option { recordbatches.map(Output::new_with_record_batches) } +/// Strips leading whitespace and SQL comments. +/// +/// All patterns above are anchored at the start of the statement, but JDBC clients such as +/// DataGrip and DBeaver prefix every statement they send with a `/* ApplicationName=... */` +/// comment. Without stripping it first, those statements miss every pattern and reach the +/// query engine, which rejects the ones this module exists to absorb. +fn strip_leading_comments(query: &str) -> &str { + let mut rest = query.trim_start(); + loop { + // A MySQL executable comment carries the statement itself — mysqldump emits its + // initialization as `/*!40101 SET NAMES ... */`. The patterns above match those + // verbatim, so the comment must survive. + if rest.starts_with("/*!") { + return rest; + } + if let Some(tail) = rest.strip_prefix("/*") { + // An unterminated block comment leaves no statement to match against. + let Some(end) = tail.find("*/") else { + return ""; + }; + rest = tail[end + 2..].trim_start(); + } else if rest.starts_with('#') + // MySQL only treats `--` as a comment when followed by whitespace. + || (rest.starts_with("--") + && rest[2..].chars().next().is_none_or(|c| c.is_whitespace())) + { + let Some(end) = rest.find('\n') else { + return ""; + }; + rest = rest[end + 1..].trim_start(); + } else { + return rest; + } + } +} + +/// The statement keywords that only [`OTHER_NOT_SUPPORTED_STMT`] matches. `SELECT` and +/// `SHOW` are dispatched separately below. +/// +/// Keep in sync with the patterns above: a statement whose leading keyword is absent from +/// this list and from that dispatch cannot match anything, and skips every regex. +const OTHER_LEADING_KEYWORDS: [&str; 7] = [ + "SET", "COMMIT", "ROLLBACK", "START", "BEGIN", "LOCK", "UNLOCK", +]; + +/// Returns the leading run of ASCII letters, which is the statement keyword for everything +/// this module matches. +fn leading_keyword(query: &str) -> &str { + let end = query + .find(|c: char| !c.is_ascii_alphabetic()) + .unwrap_or(query.len()); + &query[..end] +} + +/// Returns the index just past the line terminator at or after `from`. +fn line_comment_end(bytes: &[u8], from: usize) -> usize { + bytes[from..] + .iter() + .position(|c| *c == b'\n') + .map_or(bytes.len(), |p| from + p + 1) +} + +/// Returns the index just past the closing `quote` of the literal starting at `start`. +fn quoted_end(bytes: &[u8], start: usize, quote: u8) -> usize { + let mut i = start + 1; + while i < bytes.len() { + match bytes[i] { + // Backquoted identifiers take no backslash escapes. + b'\\' if quote != b'`' => i += 2, + c if c == quote => { + // A doubled quote is an escaped quote, not the end of the literal. + if bytes.get(i + 1) == Some("e) { + i += 2; + } else { + return i + 1; + } + } + _ => i += 1, + } + } + bytes.len() +} + +/// Returns true if another statement follows the first statement-level `;`. +/// +/// Expects [`strip_leading_comments`] to have run, so a leading comment is never the reason +/// an executable comment is rejected below. +/// +/// Every pattern here ends in `(.*)`, so absorbing a multi-statement request would discard +/// its trailing statements without executing them — `BEGIN; INSERT INTO t VALUES (1)` would +/// report success and write nothing. Such a request must reach the query engine, which +/// executes each statement. +/// +/// Plain comments, string literals and empty statements are skipped, so a `;` inside a +/// comment or a literal does not split the request, and `BEGIN; -- done` stays a single +/// statement. +/// +/// A `/*!...*/` executable comment carries a statement, so anything but a request that +/// starts with one — mysqldump's `/*!40101 SET NAMES ... */`, which the patterns match +/// whole — also counts as a trailing statement. +fn has_trailing_statement(query: &str) -> bool { + let bytes = query.as_bytes(); + let mut i = 0; + let mut seen_semicolon = false; + let mut seen_content = false; + + // Comparisons are all against ASCII bytes, which never occur inside a multi-byte UTF-8 + // sequence, so scanning by byte cannot mistake one for a delimiter. + while i < bytes.len() { + match bytes[i] { + b'/' if bytes.get(i + 1) == Some(&b'*') => { + if bytes.get(i + 2) == Some(&b'!') && seen_content { + return true; + } + i = match bytes[i + 2..].windows(2).position(|w| w == b"*/") { + Some(p) => i + 2 + p + 2, + // An unterminated comment runs to the end of the request. + None => bytes.len(), + }; + seen_content = true; + } + b'#' => { + i = line_comment_end(bytes, i); + seen_content = true; + } + // MySQL only treats `--` as a comment when followed by whitespace. + b'-' if bytes.get(i + 1) == Some(&b'-') + && bytes.get(i + 2).is_none_or(|c| c.is_ascii_whitespace()) => + { + i = line_comment_end(bytes, i); + seen_content = true; + } + quote @ (b'\'' | b'"' | b'`') => { + if seen_semicolon { + return true; + } + seen_content = true; + i = quoted_end(bytes, i, quote); + } + b';' => { + seen_semicolon = true; + seen_content = true; + i += 1; + } + c if c.is_ascii_whitespace() => i += 1, + _ => { + if seen_semicolon { + return true; + } + seen_content = true; + i += 1; + } + } + } + + false +} + // Check whether the query is a federated or driver setup command, // and return some faked results if there are any. pub(crate) fn check( @@ -318,21 +511,37 @@ pub(crate) fn check( query_ctx: QueryContextRef, session: SessionRef, ) -> Option { - // INSERT don't need MySQL federated check. We assume the query doesn't contain - // federated or driver setup command if it starts with a 'INSERT' statement. - let the_6th_index = query.char_indices().nth(6).map(|(i, _)| i); - if let Some(index) = the_6th_index - && query[..index].eq_ignore_ascii_case("INSERT") + let query = strip_leading_comments(query); + let keyword = leading_keyword(query); + + // Dispatch on the leading keyword so ordinary queries — INSERT, UPDATE, CREATE, and the + // `SELECT`s that carry real work — run as few regexes as possible. + let absorbed = if keyword.eq_ignore_ascii_case("SELECT") { + // First to check the query is like "select @@variables". + check_select_variable(query, query_ctx.clone()) + .or_else(|| check_select_user_or_var(query, query_ctx.clone())) + .or_else(|| check_others(query, query_ctx)) + } else if keyword.eq_ignore_ascii_case("SHOW") { + check_show_variables(query) + .or_else(|| check_show_warnings(query, &session)) + .or_else(|| check_others(query, query_ctx)) + } else if query.starts_with("/*!") + || OTHER_LEADING_KEYWORDS + .iter() + .any(|k| k.eq_ignore_ascii_case(keyword)) { + check_others(query, query_ctx) + } else { + return None; + }; + + // Only a request that is about to be absorbed needs the scan, and those are short. A + // query the patterns did not match never pays for it. + if absorbed.is_some() && has_trailing_statement(query) { return None; } - // First to check the query is like "select @@variables". - check_select_variable(query, query_ctx.clone()) - .or_else(|| check_show_variables(query)) - .or_else(|| check_show_warnings(query, &session)) - // Last check - .or_else(|| check_others(query, query_ctx)) + absorbed } #[cfg(test)] @@ -494,4 +703,260 @@ mod test { ); assert!(output.is_some()); } + + #[test] + fn test_check_select_user_or_var() { + let session = Arc::new(Session::new(None, Channel::Mysql, Default::default(), 0)); + + fn pretty(query: &str, session: &SessionRef) -> String { + let output = check(query, QueryContext::arc(), session.clone()) + .unwrap_or_else(|| panic!("{query} was not absorbed")); + let OutputData::RecordBatches(batches) = output.data else { + unreachable!() + }; + batches.pretty_print().unwrap() + } + + // The column name keeps the spelling the client sent. + assert_eq!( + pretty("SELECT CURRENT_USER", &session), + "\ ++--------------+ +| CURRENT_USER | ++--------------+ +| greptime | ++--------------+" + ); + assert_eq!( + pretty("select session_user;", &session), + "\ ++--------------+ +| session_user | ++--------------+ +| greptime | ++--------------+" + ); + + // A user variable is always unset. + assert_eq!( + pretty("SELECT @v", &session), + "\ ++----+ +| @v | ++----+ +| | ++----+" + ); + + // Anything that is not the whole statement must reach the query engine: these are + // column references, or real queries that happen to start with the same keyword. + for query in [ + "SELECT user FROM t", + "SELECT current_user, 1", + "SELECT @v FROM t", + "SELECT @v + 1", + "SELECT userid", + "SELECT 1", + ] { + assert!( + check(query, QueryContext::arc(), session.clone()).is_none(), + "{query} must not be absorbed" + ); + } + } + + /// A multi-statement request must reach the query engine. Absorbing it would report + /// success for the whole request while executing none of it. + #[test] + fn test_check_skips_multi_statement() { + let session = Arc::new(Session::new(None, Channel::Mysql, Default::default(), 0)); + for query in [ + "BEGIN; INSERT INTO t VALUES (1); COMMIT", + "BEGIN;\nINSERT INTO t VALUES (1)", + "START TRANSACTION; DELETE FROM t", + "COMMIT; INSERT INTO t VALUES (1)", + "SET NAMES utf8mb4; INSERT INTO t VALUES (1)", + "SELECT @@version; INSERT INTO t VALUES (1)", + ] { + assert!( + check(query, QueryContext::arc(), session.clone()).is_none(), + "{query} must not be absorbed" + ); + } + + // A trailing semicolon, comment or empty statement is still a single statement. + for query in [ + "BEGIN;", + "COMMIT; ", + "SET NAMES utf8mb4;\n", + "BEGIN; -- done", + "COMMIT; /* done */", + "BEGIN; # done", + "BEGIN;;", + "COMMIT; ; /* done */ ;", + "BEGIN; -- done\n", + ] { + assert!( + check(query, QueryContext::arc(), session.clone()).is_some(), + "{query} was not absorbed" + ); + } + + // A statement after a trailing comment still counts. + for query in [ + "BEGIN; -- go\nINSERT INTO t VALUES (1)", + "COMMIT; /* go */ INSERT INTO t VALUES (1)", + "BEGIN;; INSERT INTO t VALUES (1)", + // The `;` inside the comment does not end the statement; the one after it does. + "BEGIN /* previous delimiter; -- note */; INSERT INTO t VALUES (1)", + "SET NAMES 'a;b'; INSERT INTO t VALUES (1)", + // An executable comment carries a statement. + "BEGIN; /*! INSERT INTO t VALUES (1) */", + "BEGIN /*! INSERT INTO t VALUES (1) */", + "/*!40101 SET NAMES utf8mb4 */; INSERT INTO t VALUES (1)", + "/*!40101 SET NAMES utf8mb4 */ /*! INSERT INTO t VALUES (1) */", + ] { + assert!( + check(query, QueryContext::arc(), session.clone()).is_none(), + "{query} must not be absorbed" + ); + } + + // A `;` inside a comment or a literal is not a statement boundary. + for query in [ + "BEGIN /* previous delimiter; -- note */", + "BEGIN -- a; b", + "SET NAMES 'a;b'", + "SET NAMES \"a;b\"", + "SET NAMES 'it\\'s; here'", + "SET NAMES 'a;b';", + ] { + assert!( + check(query, QueryContext::arc(), session.clone()).is_some(), + "{query} was not absorbed" + ); + } + } + + #[test] + fn test_check_skips_non_federated_keywords() { + let session = Arc::new(Session::new(None, Channel::Mysql, Default::default(), 0)); + for query in [ + "INSERT INTO t VALUES (1)", + "UPDATE t SET a = 1", + "DELETE FROM t", + "CREATE TABLE t (ts TIMESTAMP TIME INDEX)", + "WITH x AS (SELECT 1) SELECT * FROM x", + "TQL EVAL (0, 10, '5s') up", + ] { + assert!( + check(query, QueryContext::arc(), session.clone()).is_none(), + "{query} must not be absorbed" + ); + } + } + + #[test] + fn test_strip_leading_comments() { + assert_eq!(strip_leading_comments("SELECT 1"), "SELECT 1"); + assert_eq!(strip_leading_comments(" \n\tSELECT 1"), "SELECT 1"); + assert_eq!( + strip_leading_comments("/* ApplicationName=DataGrip 2026.2.5 */ COMMIT"), + "COMMIT" + ); + assert_eq!(strip_leading_comments("/* a */ /* b */COMMIT"), "COMMIT"); + assert_eq!(strip_leading_comments("-- a comment\nCOMMIT"), "COMMIT"); + assert_eq!(strip_leading_comments("# a comment\nCOMMIT"), "COMMIT"); + // `--` without trailing whitespace is not a comment. + assert_eq!(strip_leading_comments("--x\nCOMMIT"), "--x\nCOMMIT"); + // Nothing left to match against. + assert_eq!(strip_leading_comments("/* unterminated"), ""); + assert_eq!(strip_leading_comments("-- trailing"), ""); + // Executable comments carry the statement and must survive. + assert_eq!( + strip_leading_comments("/*!40101 SET NAMES utf8mb4 */"), + "/*!40101 SET NAMES utf8mb4 */" + ); + assert_eq!( + strip_leading_comments("/* App */ /*!40101 SET NAMES utf8mb4 */"), + "/*!40101 SET NAMES utf8mb4 */" + ); + // Comments inside the statement are left alone; only the prefix is stripped. + assert_eq!( + strip_leading_comments("/* a */SELECT /* b */ 1"), + "SELECT /* b */ 1" + ); + } + + /// JDBC clients prefix every statement with a comment. Those statements must still reach + /// the federated handling, and the ones MySQL answers with a result set must keep doing so. + #[test] + fn test_check_comment_prefixed() { + let session = Arc::new(Session::new(None, Channel::Mysql, Default::default(), 0)); + let prefix = "/* ApplicationName=DataGrip 2026.2.5 */ "; + + for query in [ + "SET TRANSACTION READ WRITE", + "SET SESSION TRANSACTION READ ONLY", + "SET NAMES utf8mb4", + "BEGIN", + "START TRANSACTION", + "COMMIT", + "ROLLBACK", + // Covers the rest of OTHER_LEADING_KEYWORDS. + "LOCK TABLES t WRITE", + "UNLOCK TABLES", + ] { + let output = check( + &format!("{prefix}{query}"), + QueryContext::arc(), + session.clone(), + ); + let OutputData::RecordBatches(batches) = output + .unwrap_or_else(|| panic!("{query} was not absorbed")) + .data + else { + unreachable!() + }; + assert_eq!(batches.iter().map(|b| b.num_rows()).sum::(), 0); + } + + // mysqldump initialization arrives as executable comments, which the patterns match + // verbatim; stripping them would leave an empty statement and fail the import. + for query in [ + "/*!40101 SET NAMES utf8mb4 */", + "/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */", + "/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */", + "/*!80003 SET @OLD_x=1 */", + ] { + let output = check(query, QueryContext::arc(), session.clone()); + let OutputData::RecordBatches(batches) = output + .unwrap_or_else(|| panic!("{query} was not absorbed")) + .data + else { + unreachable!() + }; + assert_eq!(batches.iter().map(|b| b.num_rows()).sum::(), 0); + } + + // DataGrip reads the scheduler status through these two. A column-less output is written + // as an OK packet, which the JDBC driver reports as "statement has not returned cursor". + for query in [ + "SELECT @@GLOBAL.event_scheduler", + "SHOW GLOBAL VARIABLES LIKE 'event_scheduler'", + ] { + let output = check( + &format!("{prefix}{query}"), + QueryContext::arc(), + session.clone(), + ); + let OutputData::RecordBatches(batches) = output + .unwrap_or_else(|| panic!("{query} was not absorbed")) + .data + else { + unreachable!() + }; + assert!(!batches.schema().column_schemas().is_empty(), "{query}"); + } + } } diff --git a/src/sql/src/parsers/show_parser.rs b/src/sql/src/parsers/show_parser.rs index 075c17dabe..a039dceea6 100644 --- a/src/sql/src/parsers/show_parser.rs +++ b/src/sql/src/parsers/show_parser.rs @@ -40,6 +40,7 @@ impl ParserContext<'_> { if self.consume_token("TRIGGERS") { return self.parse_show_triggers(); } + self.consume_variables_scope(); if self.consume_token("DATABASES") || self.consume_token("SCHEMAS") { self.parse_show_databases(false) } else if self.matches_keyword(Keyword::TABLES) { @@ -147,6 +148,21 @@ impl ParserContext<'_> { } } + /// Consumes the `GLOBAL`/`SESSION`/`LOCAL` scope MySQL accepts in front of `VARIABLES` and + /// `STATUS`. GreptimeDB keeps no global/session split, so the scope is accepted and ignored. + fn consume_variables_scope(&mut self) { + let scoped = matches!( + self.parser.peek_token().token, + Token::Word(w) if matches!(w.keyword, Keyword::GLOBAL | Keyword::SESSION | Keyword::LOCAL) + ) && matches!( + self.parser.peek_nth_token(1).token, + Token::Word(w) if matches!(w.keyword, Keyword::VARIABLES | Keyword::STATUS) + ); + if scoped { + let _ = self.parser.next_token(); + } + } + fn parse_show_create_database(&mut self) -> Result { let raw_database_name = self.parse_object_name() @@ -1141,6 +1157,34 @@ mod tests { )); } + #[test] + fn test_show_variables_scope() { + for sql in [ + "SHOW STATUS", + "SHOW GLOBAL STATUS", + "SHOW SESSION STATUS", + "SHOW LOCAL STATUS", + ] { + let result = ParserContext::create_with_dialect( + sql, + &GreptimeDbDialect {}, + ParseOptions::default(), + ); + assert!( + matches!(result.unwrap()[0], Statement::ShowStatus(_)), + "{sql}" + ); + } + + // The scope is only swallowed in front of VARIABLES/STATUS. + let result = ParserContext::create_with_dialect( + "SHOW GLOBAL TABLES", + &GreptimeDbDialect {}, + ParseOptions::default(), + ); + assert!(result.is_err()); + } + fn parse_show_table_status(sql: &str) -> ShowTableStatus { let result = ParserContext::create_with_dialect(sql, &GreptimeDbDialect {}, ParseOptions::default()); diff --git a/tests/cases/standalone/common/show/show_databases_tables.result b/tests/cases/standalone/common/show/show_databases_tables.result index bdfd3b79ee..1c9e3a3246 100644 --- a/tests/cases/standalone/common/show/show_databases_tables.result +++ b/tests/cases/standalone/common/show/show_databases_tables.result @@ -46,8 +46,10 @@ SHOW TABLES; | optimizer_trace | | parameters | | partitions | +| plugins | | procedure_info | | process_list | +| processlist | | profiling | | referential_constraints | | region_info | @@ -65,6 +67,7 @@ SHOW TABLES; | table_privileges | | table_semantics | | tables | +| user_privileges | | views | +---------------------------------------+ @@ -100,8 +103,10 @@ SHOW FULL TABLES; | optimizer_trace | LOCAL TEMPORARY | | parameters | LOCAL TEMPORARY | | partitions | LOCAL TEMPORARY | +| plugins | LOCAL TEMPORARY | | procedure_info | LOCAL TEMPORARY | | process_list | LOCAL TEMPORARY | +| processlist | LOCAL TEMPORARY | | profiling | LOCAL TEMPORARY | | referential_constraints | LOCAL TEMPORARY | | region_info | LOCAL TEMPORARY | @@ -119,6 +124,7 @@ SHOW FULL TABLES; | table_privileges | LOCAL TEMPORARY | | table_semantics | LOCAL TEMPORARY | | tables | LOCAL TEMPORARY | +| user_privileges | LOCAL TEMPORARY | | views | LOCAL TEMPORARY | +---------------------------------------+-----------------+ @@ -148,8 +154,10 @@ SHOW TABLE STATUS; |optimizer_trace||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |parameters||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |partitions||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| +|plugins||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |procedure_info||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |process_list||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| +|processlist||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |profiling||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |referential_constraints||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |region_info||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| @@ -167,6 +175,7 @@ SHOW TABLE STATUS; |table_privileges||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |table_semantics||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |tables||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| +|user_privileges||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| |views||11|Fixed|0|0|0|0|0|0|0|DATETIME|DATETIME||utf8_bin|0||| +++++++++++++++++++ diff --git a/tests/cases/standalone/common/system/database.result b/tests/cases/standalone/common/system/database.result index 4a85ffadda..d49a511faa 100644 --- a/tests/cases/standalone/common/system/database.result +++ b/tests/cases/standalone/common/system/database.result @@ -10,6 +10,22 @@ select database(); | public | +------------+ +select schema(); + ++----------+ +| schema() | ++----------+ +| public | ++----------+ + +select user(), current_user(), system_user(); + ++----------+----------------+---------------+ +| user() | current_user() | system_user() | ++----------+----------------+---------------+ +| greptime | greptime | greptime | ++----------+----------------+---------------+ + use information_schema; Affected Rows: 0 @@ -22,6 +38,14 @@ select database(); | information_schema | +--------------------+ +select schema(); + ++--------------------+ +| schema() | ++--------------------+ +| information_schema | ++--------------------+ + use public; Affected Rows: 0 diff --git a/tests/cases/standalone/common/system/database.sql b/tests/cases/standalone/common/system/database.sql index fcad11ce88..6ea3447076 100644 --- a/tests/cases/standalone/common/system/database.sql +++ b/tests/cases/standalone/common/system/database.sql @@ -2,8 +2,14 @@ use public; select database(); +select schema(); + +select user(), current_user(), system_user(); + use information_schema; select database(); +select schema(); + use public; diff --git a/tests/cases/standalone/common/system/information_schema.result b/tests/cases/standalone/common/system/information_schema.result index 2546bb34d5..bb2c4413c3 100644 --- a/tests/cases/standalone/common/system/information_schema.result +++ b/tests/cases/standalone/common/system/information_schema.result @@ -34,8 +34,10 @@ order by table_schema, table_name; |greptime|information_schema|optimizer_trace|LOCALTEMPORARY|17|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|parameters|LOCALTEMPORARY|18|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|partitions|LOCALTEMPORARY|28|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| +|greptime|information_schema|plugins|LOCALTEMPORARY|47|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|procedure_info|LOCALTEMPORARY|34|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|process_list|LOCALTEMPORARY|36|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| +|greptime|information_schema|processlist|LOCALTEMPORARY|49|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|profiling|LOCALTEMPORARY|19|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|referential_constraints|LOCALTEMPORARY|20|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|region_info|LOCALTEMPORARY|41|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| @@ -53,6 +55,7 @@ order by table_schema, table_name; |greptime|information_schema|table_privileges|LOCALTEMPORARY|23|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|table_semantics|LOCALTEMPORARY|42|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|tables|LOCALTEMPORARY|3|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| +|greptime|information_schema|user_privileges|LOCALTEMPORARY|48|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|information_schema|views|LOCALTEMPORARY|32|0|0|0|0|0||11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| |greptime|public|numbers|LOCALTEMPORARY|2|0|0|0|0|0|test_engine|11|Fixed|0|0|0|DATETIME|DATETIME||utf8_bin|0|||Y| +++++++++++++++++++++++++ @@ -287,6 +290,17 @@ order by table_schema, table_name, column_name; | greptime | information_schema | partitions | table_schema | 2 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | | greptime | information_schema | partitions | tablespace_name | 25 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | YES | string | | | | greptime | information_schema | partitions | update_time | 20 | | | | | 0 | | | | | select,insert | | TimestampSecond | timestamp(0) | FIELD | | YES | timestamp(0) | | | +| greptime | information_schema | plugins | load_option | 11 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_author | 8 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_description | 9 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_library | 6 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_library_version | 7 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_license | 10 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_name | 1 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_status | 3 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_type | 4 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_type_version | 5 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | plugins | plugin_version | 2 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | | greptime | information_schema | procedure_info | end_time | 4 | | | | | 3 | | | | | select,insert | | TimestampMillisecond | timestamp(3) | FIELD | | YES | timestamp(3) | | | | greptime | information_schema | procedure_info | lock_keys | 6 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | YES | string | | | | greptime | information_schema | procedure_info | procedure_id | 1 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | @@ -301,6 +315,14 @@ order by table_schema, table_name, column_name; | greptime | information_schema | process_list | query | 4 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | | greptime | information_schema | process_list | schemas | 3 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | | greptime | information_schema | process_list | start_timestamp | 7 | | | | | 3 | | | | | select,insert | | TimestampMillisecond | timestamp(3) | FIELD | | NO | timestamp(3) | | | +| greptime | information_schema | processlist | command | 5 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | processlist | db | 4 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | processlist | host | 3 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | processlist | id | 1 | | | 19 | 0 | | | | | | select,insert | | Int64 | bigint | FIELD | | NO | bigint | | | +| greptime | information_schema | processlist | info | 8 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | processlist | state | 7 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | processlist | time | 6 | | | 19 | 0 | | | | | | select,insert | | Int64 | bigint | FIELD | | NO | bigint | | | +| greptime | information_schema | processlist | user | 2 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | | greptime | information_schema | profiling | block_ops_in | 9 | | | 19 | 0 | | | | | | select,insert | | Int64 | bigint | FIELD | | NO | bigint | | | | greptime | information_schema | profiling | block_ops_out | 10 | | | 19 | 0 | | | | | | select,insert | | Int64 | bigint | FIELD | | NO | bigint | | | | greptime | information_schema | profiling | context_involuntary | 8 | | | 19 | 0 | | | | | | select,insert | | Int64 | bigint | FIELD | | NO | bigint | | | @@ -526,6 +548,10 @@ order by table_schema, table_name, column_name; | greptime | information_schema | tables | temporary | 24 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | YES | string | | | | greptime | information_schema | tables | update_time | 18 | | | | | 0 | | | | | select,insert | | TimestampSecond | timestamp(0) | FIELD | | YES | timestamp(0) | | | | greptime | information_schema | tables | version | 12 | | | 20 | 0 | | | | | | select,insert | | UInt64 | bigint unsigned | FIELD | | YES | bigint unsigned | | | +| greptime | information_schema | user_privileges | grantee | 1 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | user_privileges | is_grantable | 4 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | user_privileges | privilege_type | 3 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | +| greptime | information_schema | user_privileges | table_catalog | 2 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | NO | string | | | | greptime | information_schema | views | character_set_client | 9 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | YES | string | | | | greptime | information_schema | views | check_option | 5 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | YES | string | | | | greptime | information_schema | views | collation_connection | 10 | 2147483647 | 2147483647 | | | | utf8 | utf8_bin | | | select,insert | | String | string | FIELD | | YES | string | | | diff --git a/tests/cases/standalone/common/view/create.result b/tests/cases/standalone/common/view/create.result index 76b5ed7893..c1e49fb3ae 100644 --- a/tests/cases/standalone/common/view/create.result +++ b/tests/cases/standalone/common/view/create.result @@ -116,8 +116,10 @@ ORDER BY TABLE_NAME, TABLE_TYPE; |greptime|information_schema|optimizer_trace|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|parameters|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|partitions|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| +|greptime|information_schema|plugins|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|procedure_info|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|process_list|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| +|greptime|information_schema|processlist|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|profiling|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|referential_constraints|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|region_info|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| @@ -138,6 +140,7 @@ ORDER BY TABLE_NAME, TABLE_TYPE; |greptime|public|test_table|BASETABLE|ID|ID|ID|ID|ID|ID|mito|ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||N| |greptime|public|test_view|VIEW|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||N| |greptime|public|test_view2|VIEW|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||N| +|greptime|information_schema|user_privileges|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| |greptime|information_schema|views|LOCALTEMPORARY|ID|ID|ID|ID|ID|ID||ID|Fixed|ID|ID|ID|DATETIME|DATETIME||utf8_bin|ID|||Y| +++++++++++++++++++++++++