1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
// SPDX-License-Identifier: BUSL-1.1
//! String-recognized introspection DDL arms: admin inspection & audit,
//! impersonation, session management, observability, permission/scope explain,
//! usage metering, organizations, scopes, and collection introspection.
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::state::SharedState;
use crate::types::DatabaseId;
use super::super::super::result::{DdlError, DdlResult};
use super::super::collection;
use super::super::explain_ddl;
use super::super::impersonation;
use super::super::inspect;
use super::super::inspect_audit;
use super::super::metering_ddl;
use super::super::observability;
use super::super::org_ddl;
use super::super::scope_ddl;
use super::super::scope_query_ddl;
use super::super::session_admin;
pub(super) async fn try_string(
state: &SharedState,
identity: &AuthenticatedIdentity,
sql: &str,
upper: &str,
database_id: DatabaseId,
) -> Option<Result<Vec<DdlResult>, DdlError>> {
// Administrative introspection & audit: SHOW USERS, SHOW TENANTS, SHOW
// ROLES, SHOW SESSION, EXPORT AUDIT, SHOW AUDIT IN DATABASE / WHERE / LOG,
// SHOW GRANTS. `SHOW USERS`, `SHOW GRANTS`, and `SHOW AUDIT…` parse into
// typed AST variants (`AuthStmt::ShowUsers` / `ShowGrants`,
// `MiscStmt::ShowAuditLog`) and bare `SHOW TENANTS` into
// `DatabaseStmt::ShowTenants`, but the pgwire typed-AST path has no arm for
// any of them — they fell through to the admin/observability string router,
// which dispatched them by prefix from the raw token slice. `SHOW ROLES`,
// `SHOW SESSION`, and `EXPORT AUDIT` parse into no typed DDL variant.
// Replicate the string dispatch exactly here, before the parse gate, so the
// prefix recognition and the `parts`-based extraction stay byte-identical.
// `SHOW SESSIONS` is excluded here (see the `session_admin::show_sessions`
// arm above, which is now checked first) so the two never race.
//
// Audit-log truncation is rejected: entries are pruned only by the retention
// policy. Recognized by string prefix before the parse gate so the message
// stays byte-identical regardless of how the tail parses.
if upper.starts_with("TRUNCATE AUDIT")
|| upper.starts_with("DELETE AUDIT")
|| upper.starts_with("CLEAR AUDIT")
{
return Some(Err(DdlError {
sqlstate: "42501".to_string(),
message: "audit log cannot be manually truncated. Entries are pruned automatically by the retention policy (audit_retention_days in config).".to_string(),
}));
}
if upper.starts_with("SHOW USERS") {
return Some(inspect::show_users(state, identity));
}
// Exact-match only. Filtered forms (`SHOW TENANTS WITH NAME <name>`,
// `SHOW TENANT <ident>`) are parsed into typed variants and routed through
// the typed match below; a prefix match here would silently drop the filter
// and list every tenant.
if upper == "SHOW TENANTS" {
return Some(inspect::show_tenants(state, identity));
}
if upper == "SHOW ROLES" || upper.starts_with("SHOW ROLES ") {
return Some(inspect::show_roles(state, identity));
}
if upper.starts_with("SHOW SESSION") && !upper.starts_with("SHOW SESSIONS") {
return Some(inspect::show_session(identity));
}
if upper.starts_with("EXPORT AUDIT") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(inspect_audit::export_audit_log(state, identity, &parts));
}
if upper.starts_with("SHOW AUDIT IN DATABASE") {
// SHOW AUDIT IN DATABASE <name> [LIMIT <n>]
// parts: ["SHOW", "AUDIT", "IN", "DATABASE", "<name>", ...]
let parts: Vec<&str> = sql.split_whitespace().collect();
let db_name = if parts.len() >= 5 {
parts[4]
} else {
return Some(Err(DdlError {
sqlstate: "42601".to_string(),
message: "syntax: SHOW AUDIT IN DATABASE <name> [LIMIT <n>]".to_string(),
}));
};
let limit = if parts.len() >= 7 && parts[5].eq_ignore_ascii_case("LIMIT") {
parts[6].parse::<usize>().unwrap_or(100)
} else {
100
};
return Some(inspect_audit::show_audit_in_database(
state, identity, db_name, limit,
));
}
if upper.starts_with("SHOW AUDIT WHERE") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(inspect_audit::show_audit_where(state, identity, &parts));
}
if upper.starts_with("SHOW AUDIT LOG") || upper.starts_with("SHOW AUDIT_LOG") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(inspect_audit::show_audit_log(state, identity, &parts));
}
if upper.starts_with("SHOW GRANTS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(inspect::show_grants(state, identity, &parts));
}
// Impersonation & delegation: IMPERSONATE AUTH USER, STOP IMPERSONATION,
// DELEGATE AUTH USER, REVOKE DELEGATION, SHOW DELEGATIONS. None of these
// parse into any typed AST variant — the pgwire admin router dispatched
// all five by string prefix from the raw token slice. Replicate that
// exactly here, before the parse gate, so the prefix recognition and the
// `parts`-based extraction / syntax messages stay byte-identical.
if upper.starts_with("IMPERSONATE AUTH USER ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(impersonation::impersonate(state, identity, &parts));
}
if upper.starts_with("STOP IMPERSONATION") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(impersonation::stop_impersonation(state, identity, &parts));
}
if upper.starts_with("DELEGATE AUTH USER ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(impersonation::delegate(state, identity, &parts));
}
if upper.starts_with("REVOKE DELEGATION ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(impersonation::revoke_delegation(state, identity, &parts));
}
if upper.starts_with("SHOW DELEGATIONS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(impersonation::show_delegations(state, identity, &parts));
}
// Session management: SHOW SESSIONS, KILL SESSION, KILL USER SESSIONS,
// VERIFY AUDIT CHAIN. None of these parse into any typed AST variant —
// the pgwire admin router dispatched all four by string prefix from the
// raw token slice. Replicate that exactly here, before the parse gate, so
// the prefix recognition and the `parts`-based extraction / syntax
// messages stay byte-identical. `SHOW SESSIONS` is matched here (before
// the observability `SHOW SESSION` prefix below), mirroring the pgwire
// admin router's precedence over the pgwire observability router; the
// `SHOW SESSION` guard below already excludes `SHOW SESSIONS` explicitly,
// so the two never race regardless of which is checked first.
if upper.starts_with("SHOW SESSIONS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(session_admin::show_sessions(state, identity, &parts));
}
if upper.starts_with("KILL SESSION ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(session_admin::kill_session(state, identity, &parts));
}
if upper.starts_with("KILL USER SESSIONS ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(session_admin::kill_user_sessions(state, identity, &parts));
}
if upper.starts_with("VERIFY AUDIT CHAIN") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(session_admin::verify_audit_chain(state, identity, &parts));
}
// Administrative observability: SHOW SERVER STATS / SHOW STATS / SHOW
// METRICS / SHOW MEMORY. None of these parse into a typed DDL AST variant —
// the pgwire admin observability router recognized all four by the
// exact-or-trailing-space prefix from the raw SQL. Replicate that exactly
// here, before the parse gate, so the recognition (and the `SHOW SERVER
// STATS` / `SHOW STATS` shared handler) stays byte-identical. `SHOW SERVER
// STATS` is checked before `SHOW STATS` exactly as the pgwire router did.
if upper == "SHOW SERVER STATS" || upper.starts_with("SHOW SERVER STATS ") {
return Some(observability::show_server_stats(state, identity));
}
if upper == "SHOW STATS" || upper.starts_with("SHOW STATS ") {
return Some(observability::show_server_stats(state, identity));
}
if upper == "SHOW METRICS" || upper.starts_with("SHOW METRICS ") {
return Some(observability::show_metrics(state, identity));
}
if upper == "SHOW MEMORY" || upper.starts_with("SHOW MEMORY ") {
return Some(observability::show_memory(state, identity));
}
// Permission / scope introspection: EXPLAIN PERMISSION / EXPLAIN SCOPE.
// Neither parses into a typed DDL AST variant — the pgwire admin router
// recognized both by string prefix from the raw token slice. Replicate that
// exactly here, before the parse gate, so the prefix recognition and the
// `parts`-based extraction / syntax messages stay byte-identical. The
// pgwire wire path reaches these full-`EXPLAIN …` statements through the
// DDL dispatch (native / http always; pgwire only for the non-`EXPLAIN `
// full-SQL dispatch), so recognizing them here preserves behavior; the
// `EXPLAIN <query>` handler strips the leading `EXPLAIN ` and never yields a
// `PERMISSION …` / `SCOPE …` prefix, so it is unaffected.
if upper.starts_with("EXPLAIN PERMISSION ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(explain_ddl::explain_permission(state, identity, &parts));
}
if upper.starts_with("EXPLAIN SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(explain_ddl::explain_scope(state, identity, &parts));
}
// Usage metering: DEFINE METERING DIMENSION, SHOW USAGE FOR TENANT, EXPORT
// USAGE, SHOW USAGE, SHOW QUOTA. None of these parse into a typed DDL AST
// variant — the pgwire admin router recognized all five by string prefix
// from the raw token slice. Replicate that exactly here, before the parse
// gate, so the prefix recognition and the `parts`-based extraction / syntax
// messages stay byte-identical. Guard ordering (SHOW USAGE FOR TENANT and
// EXPORT USAGE before the broader SHOW USAGE) mirrors the pgwire router.
if upper.starts_with("DEFINE METERING DIMENSION ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(metering_ddl::define_dimension(state, identity, &parts));
}
if upper.starts_with("SHOW USAGE FOR TENANT ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(metering_ddl::show_usage_for_tenant(state, identity, &parts));
}
if upper.starts_with("EXPORT USAGE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(metering_ddl::export_usage(state, identity, &parts));
}
if upper.starts_with("SHOW USAGE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(metering_ddl::show_usage(state, identity, &parts));
}
if upper.starts_with("SHOW QUOTA ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(metering_ddl::show_quota(state, identity, &parts));
}
// Organization management. None of `CREATE ORG`, `ALTER ORG`, `DROP ORG`,
// `SHOW ORGS`, or `SHOW MEMBERS OF ORG` parse into any typed AST variant —
// the pgwire admin router dispatched all of them by string prefix from the
// raw token slice. Replicate that exactly here, before the parse gate, so
// the prefix recognition and the `parts`-based extraction / syntax messages
// stay byte-identical.
if upper.starts_with("CREATE ORG ")
|| upper.starts_with("ALTER ORG ")
|| upper.starts_with("DROP ORG ")
{
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(org_ddl::handle_org(state, identity, &parts));
}
if upper.starts_with("SHOW ORGS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(org_ddl::show_orgs(state, identity, &parts));
}
if upper.starts_with("SHOW MEMBERS OF ORG") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(org_ddl::show_members(state, identity, &parts));
}
// Scope management: DEFINE / DROP / GRANT / REVOKE / ALTER / RENEW SCOPE,
// SHOW MY SCOPES, SHOW SCOPES FOR, SHOW SCOPE GRANTS, SHOW SCOPE(S). None of
// these parse into any typed AST variant — `GRANT SCOPE` / `REVOKE SCOPE`
// are explicitly excluded from the typed grant parser (returning `None`),
// and the rest have no grammar at all — so the pgwire admin router
// dispatched all of them by string prefix from the raw token slice.
// Replicate that exactly here, before the parse gate, so the prefix
// recognition and the `parts`-based extraction / syntax messages stay
// byte-identical. Guard ordering mirrors the pgwire admin router: `SHOW MY
// SCOPES` and `SHOW SCOPES FOR ` are matched before the broader `SHOW SCOPE
// GRANTS` / `SHOW SCOPE` pair (nothing between them in the pgwire router
// claimed a scope input, so grouping them here is behavior-preserving), and
// `SHOW SCOPE GRANTS` is checked before the `SHOW SCOPE` catch-all.
if upper.starts_with("DEFINE SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::define_scope(state, identity, &parts));
}
if upper.starts_with("DROP SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::drop_scope(state, identity, &parts));
}
if upper.starts_with("GRANT SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::grant_scope(state, identity, &parts));
}
if upper.starts_with("REVOKE SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::revoke_scope(state, identity, &parts));
}
if upper.starts_with("ALTER SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_query_ddl::alter_scope(state, identity, &parts));
}
if upper.starts_with("SHOW MY SCOPES") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_query_ddl::show_my_scopes(state, identity, &parts));
}
if upper.starts_with("SHOW SCOPES FOR ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_query_ddl::show_scopes_for(state, identity, &parts));
}
if upper.starts_with("RENEW SCOPE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::renew_scope(state, identity, &parts));
}
if upper.starts_with("SHOW SCOPE GRANTS") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::show_scope_grants(state, identity, &parts));
}
if upper.starts_with("SHOW SCOPE") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(scope_ddl::show_scopes(state, identity, &parts));
}
// Collection introspection: DESCRIBE <collection> / `\D <collection>`,
// UNDROP COLLECTION|TABLE, SHOW COLLECTIONS, SHOW INDEXES|INDEX. All four
// parse into typed `CollectionStmt` variants, but the pgwire schema string
// router dispatched them by string prefix from the raw token slice, using
// `parts`-based name / filter extraction and the `\D` alias that the typed
// parser does not reproduce (`\D <coll>` never parses into
// `DescribeCollection`; bare `\D` parses into `ShowCollections`; the
// `SHOW INDEXES` typed `collection` field is `parts[2]`, not the handler's
// `parts[3]` filter). Replicate the string dispatch exactly here, before the
// parse gate, so the prefix recognition, `parts` extraction, and syntax
// messages stay byte-identical. `DESCRIBE SEQUENCE` is excluded so it falls
// through to the typed `DescribeSequence` arm (claimed by the sequence
// family), exactly as it was before this block existed.
if (upper.starts_with("DESCRIBE ") && !upper.starts_with("DESCRIBE SEQUENCE"))
|| upper.starts_with("\\D ")
{
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(collection::describe_collection(
state,
identity,
&parts,
database_id,
));
}
if upper.starts_with("UNDROP COLLECTION ") || upper.starts_with("UNDROP TABLE ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(collection::undrop_collection(
state,
identity,
&parts,
database_id,
));
}
if upper == "SHOW COLLECTIONS" || upper.starts_with("SHOW COLLECTIONS") {
return Some(collection::show_collections(state, identity, database_id));
}
if upper.starts_with("SHOW INDEXES") || upper.starts_with("SHOW INDEX") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(collection::show_indexes(state, identity, &parts));
}
// DROP INDEX <name>. Parses into a typed `CollectionStmt::DropIndex`, but
// the pgwire schema string router dispatched it by string prefix from the
// raw token slice (the pgwire typed guards / sync / async arms all returned
// `None` for it), reading `parts[2]` for the name and handling IF EXISTS
// inside `drop_index`. Replicate that exactly here, before the parse gate,
// so the prefix recognition and `parts`-based name extraction stay
// byte-identical.
if upper.starts_with("DROP INDEX ") {
let parts: Vec<&str> = sql.split_whitespace().collect();
return Some(collection::drop_index(state, identity, &parts, database_id).await);
}
None
}