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

* fix(mysql): strip leading comments before the federated statement filter

JDBC clients prefix every statement with a comment. DataGrip sends
`/* ApplicationName=DataGrip <version> */` 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 <killme2008@gmail.com>

* 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 <killme2008@gmail.com>

---------

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
This commit is contained in:
dennis zhuang
2026-09-15 08:59:30 +00:00
committed by GitHub
parent 7c85798ad0
commit 7cbd20a053
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;
@@ -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 | | |
@@ -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|
+++++++++++++++++++++++++