fix(mysql): strip leading comments before federated statement filter (#9156)

Backport upstream 7cbd20a053 for DataGrip/JDBC introspection compatibility.

Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com>
This commit is contained in:
discord9
2026-09-16 11:17:27 +08:00
parent 3e3943226c
commit 179ff8e509
13 changed files with 745 additions and 53 deletions
@@ -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(),
@@ -383,6 +383,47 @@ pub(super) fn get_schema_columns(table_name: &str) -> (SchemaRef, Vec<VectorRef>
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),
};
@@ -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";
+6
View File
@@ -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 -----
+6 -1
View File
@@ -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());
+70 -18
View File
@@ -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<DataType> {
Ok(DataType::Utf8View)
}
fn return_type(&self, _: &[DataType]) -> datafusion_common::Result<DataType> {
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<ColumnarValue> {
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<ColumnarValue> {
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<DataType> {
Ok(DataType::Utf8View)
}
fn signature(&self) -> &Signature {
&self.signature
}
fn invoke_with_args(
&self,
args: ScalarFunctionArgs,
) -> datafusion_common::Result<ColumnarValue> {
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
+499 -34
View File
@@ -31,23 +31,47 @@ use regex::bytes::RegexSet;
use session::SessionRef;
use session::context::QueryContextRef;
static SELECT_VAR_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new("(?i)^(SELECT @@(.*))").unwrap());
static MYSQL_CONN_JAVA_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new("(?i)^(/\\* mysql-connector-j(.*))").unwrap());
static SHOW_LOWER_CASE_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new("(?i)^(SHOW VARIABLES LIKE 'lower_case_table_names'(.*))").unwrap());
static SHOW_VARIABLES_LIKE_PATTERN: Lazy<Regex> =
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<Regex> =
Lazy::new(|| Regex::new("(?i)^(SELECT\\s+@@(.*))").unwrap());
static SHOW_LOWER_CASE_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(&format!(
"(?i)^(SHOW {VARIABLES_SCOPE}VARIABLES LIKE 'lower_case_table_names'(.*))"
))
.unwrap()
});
static SHOW_VARIABLES_LIKE_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(&format!(
"(?i)^(SHOW {VARIABLES_SCOPE}VARIABLES( LIKE (.*))?)"
))
.unwrap()
});
static SHOW_WARNINGS_PATTERN: Lazy<Regex> =
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<Regex> = 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<Regex> =
Lazy::new(|| Regex::new("(?i)^(SELECT TIMEDIFF\\(NOW\\(\\), UTC_TIMESTAMP\\(\\)\\))").unwrap());
// sqlalchemy < 1.4.30
static SHOW_SQL_MODE_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new("(?i)^(SHOW VARIABLES LIKE 'sql_mode'(.*))").unwrap());
static SHOW_SQL_MODE_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(&format!(
"(?i)^(SHOW {VARIABLES_SCOPE}VARIABLES LIKE 'sql_mode'(.*))"
))
.unwrap()
});
static OTHER_NOT_SUPPORTED_STMT: Lazy<RegexSet> = Lazy::new(|| {
RegexSet::new([
@@ -55,6 +79,7 @@ static OTHER_NOT_SUPPORTED_STMT: Lazy<RegexSet> = Lazy::new(|| {
"(?i)^(ROLLBACK(.*))",
"(?i)^(COMMIT(.*))",
"(?i)^(START(.*))",
"(?i)^(BEGIN(.*))",
// Set.
"(?i)^(SET NAMES(.*))",
@@ -88,13 +113,9 @@ static OTHER_NOT_SUPPORTED_STMT: Lazy<RegexSet> = 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<HashMap<&str, &str>> = 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<Output
}
fn check_select_variable(query: &str, query_context: QueryContextRef) -> Option<Output> {
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<Output> {
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<Output> {
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<Output> {
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<Output> {
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(&quote) {
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<Output> {
// 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::<usize>(), 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::<usize>(), 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}");
}
}
}
+44
View File
@@ -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<Statement> {
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());
@@ -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|||
+++++++++++++++++++
@@ -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
@@ -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;
@@ -33,8 +33,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|
@@ -52,6 +54,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|
+++++++++++++++++++++++++
@@ -283,6 +286,17 @@ select * from information_schema.columns order by table_schema, table_name, colu
| 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 | | |
@@ -297,6 +311,14 @@ select * from information_schema.columns order by table_schema, table_name, colu
| 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 | | |
@@ -519,6 +541,10 @@ select * from information_schema.columns order by table_schema, table_name, colu
| 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 | | |
@@ -113,8 +113,10 @@ SELECT * FROM INFORMATION_SCHEMA.TABLES 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|
@@ -135,6 +137,7 @@ SELECT * FROM INFORMATION_SCHEMA.TABLES 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|
+++++++++++++++++++++++++