Skip to main content

micromegas/servers/
flight_sql_service_impl.rs

1use super::http_utils::get_client_ip;
2use super::query_audit::{QueryAuditRecord, ScanMetrics, aggregate_scan_metrics};
3use super::sqlinfo::{
4    SQL_INFO_DATE_TIME_FUNCTIONS, SQL_INFO_NUMERIC_FUNCTIONS, SQL_INFO_SQL_KEYWORDS,
5    SQL_INFO_STRING_FUNCTIONS, SQL_INFO_SYSTEM_FUNCTIONS,
6};
7use anyhow::Result;
8use arrow_flight::decode::FlightRecordBatchStream;
9use arrow_flight::encode::{DictionaryHandling, FlightDataEncoderBuilder};
10use arrow_flight::error::FlightError;
11use arrow_flight::sql::DoPutPreparedStatementResult;
12use arrow_flight::sql::metadata::{SqlInfoData, SqlInfoDataBuilder};
13use arrow_flight::sql::server::PeekableFlightDataStream;
14use arrow_flight::sql::{
15    ActionBeginSavepointRequest, ActionBeginSavepointResult, ActionBeginTransactionRequest,
16    ActionBeginTransactionResult, ActionCancelQueryRequest, ActionCancelQueryResult,
17    ActionClosePreparedStatementRequest, ActionCreatePreparedStatementRequest,
18    ActionCreatePreparedStatementResult, ActionCreatePreparedSubstraitPlanRequest,
19    ActionEndSavepointRequest, ActionEndTransactionRequest, Any, CommandGetCatalogs,
20    CommandGetCrossReference, CommandGetDbSchemas, CommandGetExportedKeys, CommandGetImportedKeys,
21    CommandGetPrimaryKeys, CommandGetSqlInfo, CommandGetTableTypes, CommandGetTables,
22    CommandGetXdbcTypeInfo, CommandPreparedStatementQuery, CommandPreparedStatementUpdate,
23    CommandStatementIngest, CommandStatementQuery, CommandStatementSubstraitPlan,
24    CommandStatementUpdate, ProstMessageExt, SqlInfo, TicketStatementQuery,
25    server::FlightSqlService,
26};
27use arrow_flight::{
28    Action, FlightDescriptor, FlightEndpoint, FlightInfo, HandshakeRequest, HandshakeResponse,
29    Ticket, flight_service_server::FlightService,
30};
31use core::str;
32use datafusion::arrow::array::{RecordBatch, StringArray};
33use datafusion::arrow::datatypes::{DataType, Field, Schema};
34use datafusion::arrow::ipc::writer::StreamWriter;
35use datafusion::error::DataFusionError;
36use datafusion::physical_plan::{ExecutionPlan, execute_stream};
37use futures::StreamExt;
38use futures::{Stream, TryStreamExt};
39use micromegas_analytics::lakehouse::lakehouse_context::LakehouseContext;
40use micromegas_analytics::lakehouse::partition_cache::QueryPartitionProvider;
41use micromegas_analytics::lakehouse::query::make_session_context;
42use micromegas_analytics::lakehouse::query_deny_list::{
43    QueryAttribution, fingerprint_of, skip_for_admin_recovery,
44};
45use micromegas_analytics::lakehouse::read_scope::{CallerContext, IsolationConfig, ReadScope};
46use micromegas_analytics::lakehouse::runtime::scoped_runtime;
47use micromegas_analytics::lakehouse::scoped_memory_pool::ScopedMemoryPool;
48use micromegas_analytics::lakehouse::session_configurator::SessionConfigurator;
49use micromegas_analytics::lakehouse::view_definition::{
50    build_sql_batch_view, validate_view_definition,
51};
52use micromegas_analytics::lakehouse::view_definition_store::{
53    delete_tx, list_tx, partition_insert_range, upsert_tx,
54};
55use micromegas_analytics::lakehouse::view_registry::{BuildPurpose, ViewRegistry};
56use micromegas_analytics::lakehouse::write_partition::{RetireMatch, retire_partitions};
57use micromegas_analytics::replication::bulk_ingest;
58use micromegas_analytics::response_writer::ResponseWriter;
59use micromegas_analytics::time::TimeRange;
60use micromegas_auth::policy::{ReadPolicy, caller_selectors};
61use micromegas_auth::types::{AuthContext, ProviderUnavailable};
62use micromegas_auth::user_attribution::{is_admin, validate_and_resolve_user_attribution_grpc};
63use micromegas_ingestion::remote_data_lake::acquire_lock;
64use micromegas_tracing::prelude::*;
65use once_cell::sync::Lazy;
66use prost::Message;
67use std::pin::Pin;
68use std::str::FromStr;
69use std::sync::Arc;
70use std::task::{Context, Poll};
71use std::time::Instant;
72use tonic::metadata::MetadataMap;
73use tonic::{Request, Response, Status, Streaming};
74use uuid::Uuid;
75
76use super::view_ddl::{ViewDdl, authorize_view_ddl, parse_view_ddl};
77
78/// `pg_advisory_xact_lock` key serializing every view-definition mutation cluster-wide: `0` is
79/// `migrate_db`'s, `1` is `migrate_lakehouse`'s, so this DDL executor gets its own, `2`, rather
80/// than reusing either and serializing unrelated migrations against it.
81const VIEW_DDL_ADVISORY_LOCK_KEY: i64 = 2;
82
83type FlightDataStream =
84    Pin<Box<dyn Stream<Item = Result<arrow_flight::FlightData, Status>> + Send>>;
85
86macro_rules! status {
87    ($desc:expr, $err:expr) => {
88        Status::internal(format!("{}: {} at {}:{}", $desc, $err, file!(), line!()))
89    };
90}
91
92macro_rules! api_entry_not_implemented {
93    () => {{
94        let function_name = micromegas_tracing::__function_name!();
95        error!("not implemented: {function_name}");
96        Err(Status::unimplemented(format!(
97            "{}:{} not implemented: {function_name}",
98            file!(),
99            line!()
100        )))
101    }};
102}
103
104/// For gRPC-metadata parses that are unambiguously caller-supplied input
105/// (range-header/limit-header values the caller set directly) with no
106/// `DataFusionError` involved -- `Status::invalid_argument` directly, no
107/// file:line/path suffix (nothing here ever went through `status!`'s own
108/// `file!()/line!()`).
109macro_rules! client_input_error {
110    ($desc:expr, $err:expr) => {
111        Status::invalid_argument(format!("{}: {}", $desc, $err))
112    };
113}
114
115/// Classifies the root cause of a `DataFusionError` into the gRPC status code
116/// that best distinguishes "the caller's SQL/input was bad" from "the server
117/// broke": `Execution`/`Configuration` land under `InvalidArgument` since they
118/// signal bad input, while `ArrowError` stays `Internal` since it reflects a
119/// real server-side decode failure.
120pub fn classify_datafusion_error(err: &DataFusionError) -> tonic::Code {
121    use datafusion::error::DataFusionError as DFE;
122    match err.find_root() {
123        DFE::SQL(..)
124        | DFE::Plan(_)
125        | DFE::SchemaError(..)
126        | DFE::Execution(_)
127        | DFE::Configuration(_) => tonic::Code::InvalidArgument,
128        DFE::ResourcesExhausted(_) => tonic::Code::ResourceExhausted,
129        DFE::NotImplemented(_) => tonic::Code::Unimplemented,
130        _ => tonic::Code::Internal,
131    }
132}
133
134/// Maps a gRPC status code to the `QueryAuditRecord.error_class` bucket used
135/// both by the audit log and to gate the `query_failed`/
136/// `query_duration_with_error` metrics. This is the *derived* mapping used for
137/// every `DataFusionError`-rooted failure; the query-deny-list check stamps a fourth value,
138/// `"denied"`, directly via `QueryAuditState::fail_with_class` rather than through this
139/// function, since a denial's `ResourceExhausted` code would otherwise land in the
140/// `"resource"` bucket alongside a genuine resource-budget failure.
141pub fn error_class(code: tonic::Code) -> &'static str {
142    match code {
143        tonic::Code::InvalidArgument | tonic::Code::Unimplemented => "user",
144        tonic::Code::ResourceExhausted => "resource",
145        _ => "internal",
146    }
147}
148
149/// Cap on the physical-plan text appended to the server-side log line. The
150/// plan can render arbitrarily long `file_groups=` sections for wide
151/// scans, and this text is server-log-only, never returned to the client.
152pub const MAX_PLAN_CHARS: usize = 2000;
153
154/// Truncate `text` at a char boundary once it exceeds `MAX_PLAN_CHARS`, tagging
155/// the cut with a trailing marker. Driven by `char_indices()` (not byte
156/// length) so a multibyte plan text (e.g. a non-ASCII string literal from a
157/// query predicate, rendered via `ScalarValue`'s `Display` impl) can't panic
158/// on a byte-offset slice landing mid-character.
159fn truncate_plan_text(text: &str) -> String {
160    match text.char_indices().nth(MAX_PLAN_CHARS) {
161        None => text.to_string(),
162        Some((i, _)) => format!("{}... (truncated)", &text[..i]),
163    }
164}
165
166/// Builds the full server-side log line for a classified `DataFusionError`:
167/// `desc`, the error's own `Display` (its outer `Context` chain intact, with
168/// backtrace if enabled -- unlike `find_root()`, which is for classification
169/// only), the `query_id`, and -- when there's no diagnostic span to show the
170/// caller instead -- the truncated physical plan text. `pub` so
171/// tests can assert its content, including the truncation marker,
172/// without capturing log output.
173pub fn build_log_line(
174    desc: &str,
175    err: &DataFusionError,
176    query_id: &str,
177    plan: Option<&Arc<dyn ExecutionPlan>>,
178) -> String {
179    let mut full = format!("{desc}: {err} (query_id={query_id})");
180    if let Some(plan) = plan {
181        let plan_text = format!(
182            "{}",
183            datafusion::physical_plan::displayable(plan.as_ref()).indent(true)
184        );
185        full.push_str(&format!(
186            "\nphysical plan:\n{}",
187            truncate_plan_text(&plan_text)
188        ));
189    }
190    full
191}
192
193/// Builds the `warn!` line for a query-deny-list denial: the rule id, its
194/// reason, the query's normalized fingerprint, the caller's identity/client attribution, and
195/// the `query_id`. `pub` so a test can assert its content directly, the same way
196/// `build_log_line`'s content is asserted, rather than by capturing log output.
197#[allow(clippy::too_many_arguments)]
198pub fn build_denial_warn_line(
199    rule_id: &str,
200    reason: &str,
201    sql_hash: &str,
202    user: &str,
203    email: &str,
204    client: &str,
205    entrypoint: &str,
206    client_ip: &str,
207    query_id: &str,
208) -> String {
209    format!(
210        "query denied rule_id={rule_id} reason={reason:?} sql_hash={sql_hash} user={user} \
211         email={email} client={client} entrypoint={entrypoint} client_ip={client_ip} \
212         query_id={query_id}"
213    )
214}
215
216/// Logs `build_log_line`'s output at `error!` for `error_class == "internal"`,
217/// `warn!` otherwise -- the single log point for every `DataFusionError`
218/// reaching `client_error`/`classify_flight_error`.
219fn error_or_warn_log(
220    code: tonic::Code,
221    desc: &str,
222    err: &DataFusionError,
223    query_id: &str,
224    plan: Option<&Arc<dyn ExecutionPlan>>,
225) {
226    let full = build_log_line(desc, err, query_id, plan);
227    match error_class(code) {
228        "internal" => error!("{full}"),
229        _ => warn!("{full}"),
230    }
231}
232
233/// Classifies `err`, builds the client-facing `Status` -- `desc` + root-error
234/// text (backtrace stripped) + an optional diagnostic span/notes/helps +
235/// `query_id` -- and logs the full error server-side. The physical
236/// plan, when present and there's no diagnostic span to show instead, only
237/// ever reaches the server log, never the returned `Status`: `displayable`
238/// can render object-store partition paths from Micromegas view scans, which
239/// would leak internal lakehouse details to every caller hitting an
240/// execution-time error.
241pub fn client_error(
242    desc: &str,
243    err: DataFusionError,
244    query_id: &str,
245    plan: Option<&Arc<dyn ExecutionPlan>>,
246) -> Status {
247    let code = classify_datafusion_error(&err);
248    let mut msg = format!("{desc}: {}", err.find_root().strip_backtrace());
249    let mut has_span = false;
250    if let Some(diag) = err.diagnostic() {
251        if let Some(span) = diag.span {
252            has_span = true;
253            msg.push_str(&format!(
254                " (at line {}, column {})",
255                span.start.line, span.start.column
256            ));
257        }
258        for note in &diag.notes {
259            msg.push_str(&format!("\nnote: {}", note.message));
260        }
261        for help in &diag.helps {
262            msg.push_str(&format!("\nhelp: {}", help.message));
263        }
264    }
265    msg.push_str(&format!(" (query_id={query_id})"));
266    error_or_warn_log(
267        code,
268        desc,
269        &err,
270        query_id,
271        if has_span { None } else { plan },
272    );
273    Status::new(code, msg)
274}
275
276/// Recovers the `DataFusionError` from the per-batch `FlightError` re-wrap
277/// (`FlightError::ExternalError(Box::new(e))`, applied once the raw
278/// `DataFusionError` stream is wrapped for Arrow Flight) and classifies it via
279/// `client_error`. Every branch logs exactly once before returning, so this is
280/// a single, unconditional log point for the per-batch stream-error site
281/// regardless of which branch is taken. `pub` so step 9's external
282/// integration tests can exercise this recovery path directly.
283pub fn classify_flight_error(
284    err: FlightError,
285    query_id: &str,
286    plan: Option<&Arc<dyn ExecutionPlan>>,
287) -> Status {
288    match err {
289        FlightError::ExternalError(inner) => match inner.downcast::<DataFusionError>() {
290            Ok(df_err) => client_error("error building data stream", *df_err, query_id, plan),
291            Err(inner) => {
292                error!("error building data stream: {inner} (query_id={query_id})");
293                Status::internal(format!(
294                    "error building data stream: {inner} (query_id={query_id})"
295                ))
296            }
297        },
298        other => {
299            error!("error building data stream: {other} (query_id={query_id})");
300            Status::internal(format!(
301                "error building data stream: {other} (query_id={query_id})"
302            ))
303        }
304    }
305}
306
307/// Attribution, per-stage timing, and (once created) the physical plan for
308/// one query. Built as soon as attribution is resolved, updated as each
309/// setup stage completes, and emitted exactly once as a [`QueryAuditRecord`]
310/// under the `flightsql_query_audit` log target — either on an early setup
311/// failure, at stream completion/error, or (if the stream is dropped mid-drain)
312/// from `CompletionTrackedStream`'s `Drop` impl.
313struct QueryAuditState {
314    /// Minted as the very first statement of `execute_query`, before any
315    /// fallible step -- included in every client-facing `Status` built by
316    /// `client_error`/`classify_flight_error` and in this query's audit
317    /// record, so a failure's server-log line and its audit record can be
318    /// correlated by grepping this id.
319    query_id: String,
320    client_ip: String,
321    client: String,
322    agent: String,
323    entrypoint: String,
324    session: Option<String>,
325    notebook: Option<String>,
326    cell: Option<String>,
327    user: String,
328    email: String,
329    name: Option<String>,
330    service_account: bool,
331    service_account_name: Option<String>,
332    sql: String,
333    /// Normalized SQL fingerprint, computed once per query
334    /// regardless of whether the deny list is in active use.
335    sql_hash: String,
336    range_begin: Option<String>,
337    range_end: Option<String>,
338    limit: Option<u64>,
339    context_init_ms: f64,
340    planning_ms: f64,
341    execution_ms: f64,
342    setup_ms: f64,
343    request_start: Instant,
344    /// `None` until the physical plan is created; set as soon as it is, and
345    /// still `None` for a record emitted on a setup failure that happens
346    /// before that point.
347    plan: Option<Arc<dyn ExecutionPlan>>,
348    /// This query's per-query memory-pool wrapper; owned from construction so
349    /// `emit()` can read its peak on every terminal path, including setup
350    /// failures.
351    pool: Arc<ScopedMemoryPool>,
352}
353
354impl QueryAuditState {
355    /// Aggregate the plan's DataFusion metrics (if a physical plan was
356    /// created before the failure/completion), assemble the audit record,
357    /// and emit it as a single JSON log line.
358    ///
359    /// Takes `&self` (rather than consuming) so it can be called from a
360    /// setup-error `map_err` closure while leaving the state available for
361    /// further updates/completion on the success path, and so `Drop` can
362    /// call it on an abandoned/cancelled stream without needing to
363    /// reconstruct anything.
364    fn emit(&self, status: &'static str, error: Option<String>, error_class: Option<&'static str>) {
365        let scan = match &self.plan {
366            Some(plan) => aggregate_scan_metrics(plan.as_ref()),
367            None => ScanMetrics {
368                output_rows: None,
369                bytes_scanned: 0,
370                spilled_bytes: 0,
371                spill_count: 0,
372            },
373        };
374        let peak_memory_bytes = self.pool.peak() as u64;
375        imetric!("query_peak_memory_bytes", "bytes", peak_memory_bytes);
376        let total_ms = self.request_start.elapsed().as_secs_f64() * 1000.0;
377        let record = QueryAuditRecord {
378            query_id: self.query_id.clone(),
379            client_ip: self.client_ip.clone(),
380            client: self.client.clone(),
381            agent: self.agent.clone(),
382            entrypoint: self.entrypoint.clone(),
383            session: self.session.clone(),
384            notebook: self.notebook.clone(),
385            cell: self.cell.clone(),
386            user: self.user.clone(),
387            email: self.email.clone(),
388            name: self.name.clone(),
389            service_account: self.service_account,
390            service_account_name: self.service_account_name.clone(),
391            sql: self.sql.clone(),
392            sql_hash: self.sql_hash.clone(),
393            range_begin: self.range_begin.clone(),
394            range_end: self.range_end.clone(),
395            limit: self.limit,
396            context_init_ms: self.context_init_ms,
397            planning_ms: self.planning_ms,
398            execution_ms: self.execution_ms,
399            setup_ms: self.setup_ms,
400            total_ms,
401            status,
402            error,
403            error_class,
404            output_rows: scan.output_rows,
405            bytes_scanned: scan.bytes_scanned,
406            peak_memory_bytes,
407            spilled_bytes: scan.spilled_bytes,
408            spill_count: scan.spill_count,
409        };
410        match serde_json::to_string(&record) {
411            Ok(json) => info!(target: "flightsql_query_audit", "{json}"),
412            Err(e) => warn!("failed to serialize query audit record: {e}"),
413        }
414    }
415
416    /// Emits an "error" audit record for an already-built client-facing `status`, with an
417    /// explicit `error_class` rather than one derived from the gRPC code -- used by the
418    /// query-deny-list check, whose `error_class` is `"denied"`, a value
419    /// `error_class(status.code())` (`ResourceExhausted` -> `"resource"`) would get wrong.
420    /// Returns `status` unchanged.
421    fn fail_with_class(&self, status: Status, class: &'static str) -> Status {
422        self.emit("error", Some(status.message().to_string()), Some(class));
423        status
424    }
425
426    /// Emits an "error" audit record for an already-built client-facing
427    /// `status` (message + `error_class` derived from its code) and returns it
428    /// unchanged -- the shared tail of every setup-phase `map_err` in
429    /// `execute_query`. A thin wrapper over [`Self::fail_with_class`] that derives the class
430    /// from the code.
431    fn fail(&self, status: Status) -> Status {
432        let class = error_class(status.code());
433        self.fail_with_class(status, class)
434    }
435}
436
437/// Stream wrapper that tracks when the stream is fully consumed
438struct CompletionTrackedStream<S> {
439    inner: S,
440    start_time: i64,
441    completed: bool,
442    audit: Option<QueryAuditState>,
443}
444
445impl<S> CompletionTrackedStream<S> {
446    fn new(inner: S, start_time: i64, audit: QueryAuditState) -> Self {
447        Self {
448            inner,
449            start_time,
450            completed: false,
451            audit: Some(audit),
452        }
453    }
454}
455
456impl<S> Drop for CompletionTrackedStream<S> {
457    /// If the stream is dropped before yielding `None` or an `Err` (client
458    /// disconnect/cancel mid-drain), `poll_next` never ran its completion
459    /// arms, so `self.audit` is still `Some(...)`. Emit it here with a
460    /// terminal "incomplete" status so cancelled/abandoned queries are still
461    /// audited instead of silently vanishing. A no-op for streams that
462    /// completed or errored normally, since those arms already `take()` the
463    /// audit state.
464    fn drop(&mut self) {
465        if let Some(state) = self.audit.take() {
466            state.emit("incomplete", None, None);
467        }
468    }
469}
470
471impl<S> Stream for CompletionTrackedStream<S>
472where
473    S: Stream<Item = Result<arrow_flight::FlightData, Status>> + Unpin + Send,
474{
475    type Item = Result<arrow_flight::FlightData, Status>;
476
477    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
478        match Pin::new(&mut self.inner).poll_next(cx) {
479            Poll::Ready(Some(result)) => {
480                // Every `Status` reaching this stream was already logged once by
481                // `classify_flight_error` (the sole `map_err` feeding it) when it
482                // built that `Status` -- this arm only reads the code to derive
483                // the audit/metric class, it never logs again.
484                if let Err(ref err) = result
485                    && !self.completed
486                {
487                    let class = error_class(err.code());
488                    let total_duration = now() - self.start_time;
489                    match class {
490                        "internal" => {
491                            imetric!("query_duration_with_error", "ticks", total_duration as u64);
492                            imetric!("query_failed", "count", 1);
493                        }
494                        "resource" => {
495                            imetric!("query_failed_resource", "count", 1);
496                        }
497                        _ => {
498                            imetric!("query_failed_user", "count", 1);
499                        }
500                    }
501                    self.completed = true;
502                    if let Some(state) = self.audit.take() {
503                        state.emit("error", Some(err.message().to_string()), Some(class));
504                    }
505                }
506                Poll::Ready(Some(result))
507            }
508            Poll::Ready(None) => {
509                // Stream completed successfully
510                if !self.completed {
511                    let total_duration = now() - self.start_time;
512                    imetric!("query_duration_total", "ticks", total_duration as u64);
513                    imetric!("query_completed_successfully", "count", 1);
514                    self.completed = true;
515                    if let Some(state) = self.audit.take() {
516                        state.emit("ok", None, None);
517                    }
518                }
519                Poll::Ready(None)
520            }
521            Poll::Pending => Poll::Pending,
522        }
523    }
524}
525
526static INSTANCE_SQL_DATA: Lazy<SqlInfoData> = Lazy::new(|| {
527    let mut builder = SqlInfoDataBuilder::new();
528    // Server information
529    builder.append(SqlInfo::FlightSqlServerName, "Micromegas Flight SQL Server");
530    builder.append(SqlInfo::FlightSqlServerVersion, "1");
531    // 1.3 comes from https://github.com/apache/arrow/blob/f9324b79bf4fc1ec7e97b32e3cce16e75ef0f5e3/format/Schema.fbs#L24
532    builder.append(SqlInfo::FlightSqlServerArrowVersion, "1.3");
533    builder.append(SqlInfo::SqlKeywords, SQL_INFO_SQL_KEYWORDS);
534    builder.append(SqlInfo::SqlNumericFunctions, SQL_INFO_NUMERIC_FUNCTIONS);
535    builder.append(SqlInfo::SqlStringFunctions, SQL_INFO_STRING_FUNCTIONS);
536    builder.append(SqlInfo::SqlSystemFunctions, SQL_INFO_SYSTEM_FUNCTIONS);
537    builder.append(SqlInfo::SqlDatetimeFunctions, SQL_INFO_DATE_TIME_FUNCTIONS);
538    builder.build().unwrap()
539});
540
541/// Implementation of the Flight SQL service.
542#[derive(Clone)]
543pub struct FlightSqlServiceImpl {
544    lakehouse: Arc<LakehouseContext>,
545    part_provider: Arc<dyn QueryPartitionProvider>,
546    view_registry: Arc<ViewRegistry>,
547    session_configurator: Arc<dyn SessionConfigurator>,
548    read_policy: Arc<dyn ReadPolicy>,
549    isolation_config: Arc<IsolationConfig>,
550}
551
552impl FlightSqlServiceImpl {
553    pub fn new(
554        lakehouse: Arc<LakehouseContext>,
555        part_provider: Arc<dyn QueryPartitionProvider>,
556        view_registry: Arc<ViewRegistry>,
557        session_configurator: Arc<dyn SessionConfigurator>,
558        read_policy: Arc<dyn ReadPolicy>,
559        isolation_config: Arc<IsolationConfig>,
560    ) -> Self {
561        Self {
562            lakehouse,
563            part_provider,
564            view_registry,
565            session_configurator,
566            read_policy,
567            isolation_config,
568        }
569    }
570
571    /// Resolves the [`CallerContext`] a request should plan under -- the seam's fail-closed
572    /// resolver.
573    ///
574    /// **Absent-extension convention.** When no `AuthContext` extension is present at all (no
575    /// auth provider configured, e.g. `--disable-auth`), the resolved scope is
576    /// [`ReadScope::All`]. This is the *only* permissive branch here: `AuthService::call`
577    /// (`rust/auth/src/tower.rs`) rejects the request before this inner service ever runs
578    /// whenever a provider *is* configured, so the extension is always present in that case --
579    /// the same safety argument `is_admin`'s absent-header-⇒-trusted convention already relies
580    /// on (`user_attribution.rs`).
581    ///
582    /// **Failure convention.** A [`ReadPolicy::resolve`] that returns `Err` is a hard failure and
583    /// must never become a scope -- not `ReadScope::Audiences(Arc::from([]))` (which would read
584    /// as a legitimate, audited fail-closed decision by the isolation-config layer) and not
585    /// `ReadScope::All` (a silent fail-open bypass). Mirrors `tower.rs`'s own discriminator:
586    /// `Status::unavailable` when the error downcasts to [`ProviderUnavailable`] (a
587    /// store/provider outage), `Status::permission_denied` otherwise.
588    ///
589    /// `is_admin` is read from `md` unchanged (`is_admin(md)`), equivalent to reading
590    /// `AuthContext.is_admin` off `ext` when the extension is present, since the header is
591    /// derived from the same `AuthContext` with client-supplied copies stripped
592    /// (`tower.rs:107-139`) -- and it is what preserves today's `--disable-auth`
593    /// absent-header-⇒-trusted convention when `ext` has none.
594    async fn caller_context(
595        &self,
596        ext: &http::Extensions,
597        md: &MetadataMap,
598    ) -> Result<CallerContext, Status> {
599        // `deny_queries` requires `Some` -- resolved from the
600        // same attribution `execute_query` already validates, so a query already carries this by
601        // the time it could call `deny_queries`. This is simply "the caller's resolved user id"
602        // (`user_attribution::validate_and_resolve_user_attribution_grpc`).
603        let identity = validate_and_resolve_user_attribution_grpc(md)
604            .ok()
605            .map(|attr| attr.user_id);
606        // Read once, up front, so both `read_scope`'s resolution below and `grant_selectors`
607        // can consult the same `AuthContext` -- the `Some(auth_ctx)` arm's
608        // binding otherwise goes out of scope before the `CallerContext` literal is built.
609        let auth_ctx = ext.get::<AuthContext>();
610        let read_scope = match auth_ctx {
611            Some(auth_ctx) => match self.read_policy.resolve(auth_ctx).await {
612                Ok(audiences) => ReadScope::Audiences(audiences.into_inner()),
613                Err(e) => {
614                    return Err(if e.downcast_ref::<ProviderUnavailable>().is_some() {
615                        Status::unavailable(format!("read policy unavailable: {e:#}"))
616                    } else {
617                        Status::permission_denied(format!("read scope denied: {e:#}"))
618                    });
619                }
620            },
621            None => ReadScope::All,
622        };
623        Ok(CallerContext {
624            read_scope,
625            is_admin: is_admin(md),
626            // Not permission-sensitive the way `read_scope` is (it's deployment config, not
627            // derived from the caller's identity), so it is copied verbatim on both branches
628            // above rather than participating in the absent-extension/`Err` distinction.
629            isolation_config: self.isolation_config.clone(),
630            identity,
631            // Empty when there's no `AuthContext` extension at all (no auth provider configured,
632            // e.g. `--disable-auth`) -- the same absent-extension convention `read_scope` follows
633            // above.
634            grant_selectors: auth_ctx.map(caller_selectors).unwrap_or_default().into(),
635        })
636    }
637
638    fn should_preserve_dictionary(metadata: &MetadataMap) -> bool {
639        metadata
640            .get("preserve_dictionary")
641            .and_then(|v| v.to_str().ok())
642            .map(|s| s.eq_ignore_ascii_case("true"))
643            .unwrap_or(false)
644    }
645
646    /// Extracts an optional client-attribution header, treating an empty string as absent.
647    fn optional_metadata(metadata: &MetadataMap, key: &str) -> Option<String> {
648        metadata
649            .get(key)
650            .and_then(|v| v.to_str().ok())
651            .filter(|s| !s.is_empty())
652            .map(|s| s.to_string())
653    }
654
655    #[span_fn]
656    async fn execute_query(
657        &self,
658        ticket_stmt: TicketStatementQuery,
659        metadata: &MetadataMap,
660        extensions: &http::Extensions,
661        client_ip: &str,
662    ) -> Result<Response<FlightDataStream>, Status> {
663        // Minted first, before any fallible step, so it's Always available --
664        // for every client-facing `Status` built by `client_error`/
665        // `classify_flight_error` and for this query's audit record.
666        let query_id = Uuid::new_v4().to_string();
667        let begin_request = now();
668        let request_start = Instant::now();
669        let sql = std::str::from_utf8(&ticket_stmt.statement_handle)
670            .map_err(|e| status!("Unable to parse query", e))?;
671
672        let mut begin = metadata.get("query_range_begin");
673        if let Some(s) = &begin
674            && s.is_empty()
675        {
676            begin = None;
677        }
678        let mut end = metadata.get("query_range_end");
679        if let Some(s) = &end
680            && s.is_empty()
681        {
682            end = None;
683        }
684        let query_range = if begin.is_some() && end.is_some() {
685            let begin_datetime =
686                chrono::DateTime::parse_from_rfc3339(begin.unwrap().to_str().map_err(|e| {
687                    client_input_error!("Unable to convert query_range_begin to string", e)
688                })?)
689                .map_err(|e| {
690                    client_input_error!(
691                        "Unable to parse query_range_begin as a rfc3339 datetime",
692                        e
693                    )
694                })?;
695            let end_datetime =
696                chrono::DateTime::parse_from_rfc3339(end.unwrap().to_str().map_err(|e| {
697                    client_input_error!("Unable to convert query_range_end to string", e)
698                })?)
699                .map_err(|e| {
700                    client_input_error!("Unable to parse query_range_end as a rfc3339 datetime", e)
701                })?;
702            Some(TimeRange::new(begin_datetime.into(), end_datetime.into()))
703        } else {
704            None
705        };
706
707        // Validate and resolve user attribution
708        let attr = validate_and_resolve_user_attribution_grpc(metadata).map_err(|e| *e)?;
709
710        let client_type = metadata
711            .get("x-client-type")
712            .and_then(|v| v.to_str().ok())
713            .unwrap_or("unknown");
714        let client_agent = metadata
715            .get("x-client-agent")
716            .and_then(|v| v.to_str().ok())
717            .unwrap_or("unknown");
718        let client_entrypoint = metadata
719            .get("x-client-entrypoint")
720            .and_then(|v| v.to_str().ok())
721            .unwrap_or("unknown");
722        let client_session = Self::optional_metadata(metadata, "x-client-session");
723        let client_notebook = Self::optional_metadata(metadata, "x-client-notebook");
724        let client_cell = Self::optional_metadata(metadata, "x-client-cell");
725
726        let user_name_display = attr.user_name.as_deref().unwrap_or("");
727
728        // Log query with full attribution
729        if let Some(service_account_name) = &attr.service_account {
730            info!(
731                "execute_query range={query_range:?} sql={sql:?} limit={:?} user={} email={} name={user_name_display:?} service_account={service_account_name} client={client_type} agent={client_agent} entrypoint={client_entrypoint} notebook={client_notebook:?} cell={client_cell:?} client_ip={client_ip}",
732                metadata.get("limit"),
733                attr.user_id,
734                attr.user_email
735            );
736        } else {
737            info!(
738                "execute_query range={query_range:?} sql={sql:?} limit={:?} user={} email={} name={user_name_display:?} client={client_type} agent={client_agent} entrypoint={client_entrypoint} notebook={client_notebook:?} cell={client_cell:?} client_ip={client_ip}",
739                metadata.get("limit"),
740                attr.user_id,
741                attr.user_email
742            );
743        }
744
745        // The per-query memory-pool wrapper is infallible to construct, so it's built
746        // before `audit_state` and owned by it from the start -- every subsequent
747        // setup failure (session context, planning, limit, physical plan, stream
748        // construction) can then still emit an "error" audit record carrying whatever
749        // peak the pool had accrued by that point, instead of silently disappearing on
750        // an early `?` return.
751        let scoped_pool = Arc::new(ScopedMemoryPool::new(
752            self.lakehouse.runtime().memory_pool.clone(),
753        ));
754
755        // Attribution is resolved from here on, so build the audit state now
756        // (durations/limit/plan filled in as they become known) instead of
757        // only after the physical plan exists.
758        // Computed once per query, regardless of whether the deny list is in active use --
759        // it's what an operator pastes into `deny_queries` after finding an offender in the
760        // audit log.
761        let sql_hash = fingerprint_of(sql);
762
763        let mut audit_state = QueryAuditState {
764            query_id: query_id.clone(),
765            client_ip: client_ip.to_string(),
766            client: client_type.to_string(),
767            agent: client_agent.to_string(),
768            entrypoint: client_entrypoint.to_string(),
769            session: client_session,
770            notebook: client_notebook,
771            cell: client_cell,
772            user: attr.user_id.clone(),
773            email: attr.user_email.clone(),
774            name: attr.user_name.clone(),
775            service_account: attr.service_account.is_some(),
776            service_account_name: attr.service_account.clone(),
777            sql: sql.to_string(),
778            sql_hash: sql_hash.clone(),
779            range_begin: query_range.as_ref().map(|r| r.begin.to_rfc3339()),
780            range_end: query_range.as_ref().map(|r| r.end.to_rfc3339()),
781            limit: None,
782            context_init_ms: 0.0,
783            planning_ms: 0.0,
784            execution_ms: 0.0,
785            setup_ms: 0.0,
786            request_start,
787            plan: None,
788            pool: scoped_pool.clone(),
789        };
790
791        // The deny-list check, in front of everything expensive -- before the scoped
792        // runtime/session context/planning below. `check` returns `None` immediately on an
793        // empty snapshot, so this costs nothing while the deny list stands empty.
794        if let Some(rule) = self.lakehouse.query_denials().check(&QueryAttribution {
795            user_id: &attr.user_id,
796            email: &attr.user_email,
797            service_account: attr.service_account.as_deref(),
798            client: client_type,
799            agent: client_agent,
800            entrypoint: client_entrypoint,
801            session: audit_state.session.as_deref(),
802            notebook: audit_state.notebook.as_deref(),
803            cell: audit_state.cell.as_deref(),
804            client_ip,
805            sql,
806            sql_hash: &sql_hash,
807        }) && !skip_for_admin_recovery(sql, is_admin(metadata))
808        {
809            let status = Status::resource_exhausted(format!(
810                "query denied by rule {} (reason: {}); ask an admin to lift it with \
811                 remove_query_denial('{}'); query_id={query_id}",
812                rule.row.rule_id, rule.row.reason, rule.row.rule_id
813            ));
814            warn!(
815                "{}",
816                build_denial_warn_line(
817                    &rule.row.rule_id.to_string(),
818                    &rule.row.reason,
819                    &sql_hash,
820                    &attr.user_id,
821                    &attr.user_email,
822                    client_type,
823                    client_entrypoint,
824                    client_ip,
825                    &query_id,
826                )
827            );
828            imetric!("query_denied", "count", rule_id = %rule.row.rule_id, 1_u64);
829            rule.record_hit();
830            return Err(audit_state.fail_with_class(status, "denied"));
831        }
832
833        // Caller is resolved here, ahead of the scoped runtime, because the view-DDL intercept
834        // below needs it and returns before either the scoped runtime or a session context is
835        // ever built for a `CREATE`/`DROP MATERIALIZED VIEW` statement.
836        //
837        // `context_init_ms` is measured starting here (not at `make_session_context`) because
838        // caller resolution can itself hit the read-policy store (e.g. a DB round trip to
839        // resolve audience grants), and that cost belongs in the same phase.
840        let session_begin = now();
841        let session_begin_instant = Instant::now();
842        let caller = self
843            .caller_context(extensions, metadata)
844            .await
845            .map_err(|status| audit_state.fail(status))?;
846
847        // View DDL is intercepted here, ahead of the scoped runtime/`make_session_context`: a
848        // `CREATE`/`DROP MATERIALIZED VIEW` statement is never planned as an ordinary query.
849        match parse_view_ddl(sql) {
850            Ok(Some(ddl)) => {
851                return self
852                    .execute_view_ddl(ddl, &caller, begin_request, audit_state)
853                    .await;
854            }
855            Ok(None) => {}
856            Err(e) => {
857                return Err(audit_state.fail(client_input_error!("error parsing view DDL", e)));
858            }
859        }
860
861        // Build a `RuntimeEnv`/`LakehouseContext` scoped to this query's memory-pool
862        // wrapper, so every session context created from it (including nested ones,
863        // e.g. Perfetto trace queries and JIT materialization) attributes its memory
864        // to this query alone instead of the process-shared pool.
865        let scoped_env = scoped_runtime(self.lakehouse.runtime(), scoped_pool.clone())
866            .map_err(|e| audit_state.fail(status!("error building scoped runtime", e)))?;
867        let lakehouse = self.lakehouse.with_runtime(scoped_env);
868
869        // Session context creation phase
870        let ctx = make_session_context(
871            lakehouse,
872            self.part_provider.clone(),
873            query_range,
874            self.view_registry.current(),
875            self.session_configurator.clone(),
876            caller,
877        )
878        .await
879        .map_err(|e| audit_state.fail(status!("error in make_session_context", e)))?;
880        let context_init_duration = now() - session_begin;
881        audit_state.context_init_ms = session_begin_instant.elapsed().as_secs_f64() * 1000.0;
882
883        // Query planning phase
884        let planning_begin = now();
885        let planning_begin_instant = Instant::now();
886        let mut df = ctx.sql(sql).await.map_err(|e| {
887            audit_state.fail(client_error("error building dataframe", e, &query_id, None))
888        })?;
889        let planning_duration = now() - planning_begin;
890        audit_state.planning_ms = planning_begin_instant.elapsed().as_secs_f64() * 1000.0;
891
892        if let Some(limit_str) = metadata.get("limit") {
893            let parsed_limit: usize = usize::from_str(limit_str.to_str().map_err(|e| {
894                audit_state.fail(client_input_error!("error converting limit to str", e))
895            })?)
896            .map_err(|e| audit_state.fail(client_input_error!("error parsing limit", e)))?;
897            audit_state.limit = Some(parsed_limit as u64);
898            df = df.limit(0, Some(parsed_limit)).map_err(|e| {
899                audit_state.fail(client_error(
900                    "error building dataframe with limit",
901                    e,
902                    &query_id,
903                    None,
904                ))
905            })?;
906        }
907
908        // Query execution phase: build the physical plan (kept for post-drain metrics)
909        // and run it via the free-function `execute_stream`, which is what
910        // `DataFrame::execute_stream` does internally, minus dropping the plan.
911        let execution_begin = now();
912        let execution_begin_instant = Instant::now();
913        let schema = Arc::new(df.schema().as_arrow().clone());
914        let task_ctx = Arc::new(df.task_ctx());
915        let plan = df.create_physical_plan().await.map_err(|e| {
916            audit_state.fail(client_error(
917                "error creating physical plan",
918                e,
919                &query_id,
920                None,
921            ))
922        })?;
923        audit_state.plan = Some(plan.clone());
924        // `plan`/`query_id` are cloned here because both are moved shortly:
925        // `plan` into `execute_stream` below, `audit_state` (which owns
926        // `query_id`'s sibling on the struct) into `CompletionTrackedStream::new`
927        // further down. The per-batch closure below outlives this function call
928        // (it's polled from the returned stream), so it needs its own owned
929        // clones rather than borrowing `plan`/`query_id` directly.
930        let plan_for_errors = plan.clone();
931        let query_id_for_stream = query_id.clone();
932        let stream = execute_stream(plan, task_ctx)
933            .map_err(|e| {
934                audit_state.fail(client_error(
935                    "error executing plan",
936                    e,
937                    &query_id_for_stream,
938                    Some(&plan_for_errors),
939                ))
940            })?
941            .map_err(|e| FlightError::ExternalError(Box::new(e)));
942        let builder = if Self::should_preserve_dictionary(metadata) {
943            FlightDataEncoderBuilder::new()
944                .with_schema(schema.clone())
945                .with_dictionary_handling(DictionaryHandling::Resend)
946        } else {
947            FlightDataEncoderBuilder::new().with_schema(schema.clone())
948        };
949        let flight_data_stream = builder.build(stream);
950        let execution_duration = now() - execution_begin;
951        audit_state.execution_ms = execution_begin_instant.elapsed().as_secs_f64() * 1000.0;
952
953        // Calculate total setup time and record detailed metrics
954        let total_setup_duration = now() - begin_request;
955        audit_state.setup_ms = request_start.elapsed().as_secs_f64() * 1000.0;
956
957        // Record detailed timing metrics
958        imetric!(
959            "context_init_duration",
960            "ticks",
961            context_init_duration as u64
962        );
963        imetric!("query_planning_duration", "ticks", planning_duration as u64);
964        imetric!(
965            "query_execution_duration",
966            "ticks",
967            execution_duration as u64
968        );
969        imetric!("query_setup_duration", "ticks", total_setup_duration as u64);
970
971        // Create instrumented stream that tracks completion. This closure owns
972        // `plan_for_errors`/`query_id_for_stream` (moved in) since it's polled
973        // from the returned stream, long after this function returns -- see the
974        // comment above `plan_for_errors`'s definition.
975        let instrumented_stream = flight_data_stream.map_err(move |e| {
976            classify_flight_error(e, &query_id_for_stream, Some(&plan_for_errors))
977        });
978        let completion_tracked_stream =
979            CompletionTrackedStream::new(instrumented_stream.boxed(), begin_request, audit_state);
980        Ok(Response::new(
981            Box::pin(completion_tracked_stream) as FlightDataStream
982        ))
983    }
984
985    /// Executes a parsed `CREATE [OR REPLACE] MATERIALIZED VIEW` / `DROP MATERIALIZED VIEW`
986    /// statement: admin-gates it, validates/upserts or deletes its row inside one transaction
987    /// (serialized cluster-wide by an advisory lock), refuses a mutation that would break a
988    /// dependent, retires partitions on `DROP`, reloads this replica's registry inline after
989    /// commit, and returns a one-row `(view_set_name, status)` result through the same
990    /// `CompletionTrackedStream` every other query uses.
991    async fn execute_view_ddl(
992        &self,
993        ddl: ViewDdl,
994        caller: &CallerContext,
995        begin_request: i64,
996        audit_state: QueryAuditState,
997    ) -> Result<Response<FlightDataStream>, Status> {
998        authorize_view_ddl(caller).map_err(|status| audit_state.fail(status))?;
999
1000        let pool = self.lakehouse.lake().db_pool.clone();
1001        let mut tx = pool
1002            .begin()
1003            .await
1004            .map_err(|e| audit_state.fail(status!("error starting view DDL transaction", e)))?;
1005        acquire_lock(&mut tx, VIEW_DDL_ADVISORY_LOCK_KEY)
1006            .await
1007            .map_err(|e| audit_state.fail(status!("error acquiring view DDL lock", e)))?;
1008        let pre_rows = list_tx(&mut tx)
1009            .await
1010            .map_err(|e| audit_state.fail(status!("error reading view definitions", e)))?;
1011
1012        let (view_set_name, status_text) = match ddl {
1013            ViewDdl::Create {
1014                name,
1015                or_replace,
1016                definition,
1017                sql,
1018            } => {
1019                let existing = pre_rows.iter().any(|r| r.view_set_name == name);
1020                if existing && !or_replace {
1021                    return Err(audit_state.fail(Status::already_exists(format!(
1022                        "materialized view '{name}' already exists; use CREATE OR REPLACE to \
1023                         redefine it"
1024                    ))));
1025                }
1026                // Validated against the factory the loader would build for this row: the
1027                // pre-mutation rows, this row's own name excluded (identical to the
1028                // post-mutation set for this single-row mutation) -- so a definition reading
1029                // another DDL view validates against the real thing, and a `CREATE OR REPLACE`
1030                // that raises its own update_group cannot validate against a since-superseded
1031                // copy of itself.
1032                let validation_rows: Vec<_> = pre_rows
1033                    .iter()
1034                    .filter(|r| r.view_set_name != name)
1035                    .cloned()
1036                    .collect();
1037                // A `Probe`: a row in `validation_rows` that no longer builds is a pre-existing
1038                // breakage the live reload already reports on every tick -- re-logging it here
1039                // would pin it on whoever happened to run the next DDL statement.
1040                let (factory, _failed) = self
1041                    .view_registry
1042                    .build_from_rows(&validation_rows, BuildPurpose::Probe)
1043                    .await
1044                    .map_err(|e| {
1045                        audit_state.fail(status!("error building validation factory", e))
1046                    })?;
1047                let view = build_sql_batch_view(
1048                    &definition,
1049                    self.lakehouse.runtime().clone(),
1050                    self.lakehouse.lake().clone(),
1051                    factory.clone(),
1052                    self.session_configurator.clone(),
1053                )
1054                .await
1055                .map_err(|e| {
1056                    audit_state.fail(client_input_error!("error building materialized view", e))
1057                })?;
1058                validate_view_definition(
1059                    &definition,
1060                    &view,
1061                    &factory,
1062                    self.lakehouse.runtime().clone(),
1063                    self.lakehouse.lake().clone(),
1064                    self.session_configurator.clone(),
1065                )
1066                .await
1067                .map_err(|e| {
1068                    audit_state.fail(client_input_error!("view definition failed validation", e))
1069                })?;
1070                let updated_by = caller.identity.as_deref().unwrap_or("unknown");
1071                upsert_tx(&mut tx, &definition, &sql, updated_by)
1072                    .await
1073                    .map_err(|e| audit_state.fail(status!("error upserting view definition", e)))?;
1074                (name, if existing { "replaced" } else { "created" })
1075            }
1076            ViewDdl::Drop { name, if_exists } => {
1077                let existing = pre_rows.iter().any(|r| r.view_set_name == name);
1078                if !existing {
1079                    if if_exists {
1080                        (name, "not_found")
1081                    } else {
1082                        return Err(audit_state.fail(Status::not_found(format!(
1083                            "materialized view '{name}' does not exist"
1084                        ))));
1085                    }
1086                } else {
1087                    let range = partition_insert_range(&mut tx, &name).await.map_err(|e| {
1088                        audit_state.fail(status!("error reading partition insert range", e))
1089                    })?;
1090                    delete_tx(&mut tx, &name).await.map_err(|e| {
1091                        audit_state.fail(status!("error deleting view definition", e))
1092                    })?;
1093                    if let Some((begin, end)) = range {
1094                        let null_logger = Arc::new(ResponseWriter::new(None));
1095                        retire_partitions(
1096                            &mut tx,
1097                            &name,
1098                            "global",
1099                            begin,
1100                            end,
1101                            RetireMatch::Containment,
1102                            &[],
1103                            null_logger,
1104                        )
1105                        .await
1106                        .map_err(|e| audit_state.fail(status!("error retiring partitions", e)))?;
1107                    }
1108                    (name, "dropped")
1109                }
1110            }
1111        };
1112
1113        // `DROP ... IF EXISTS` on an absent view writes nothing, so `post_rows` would be
1114        // byte-identical to `pre_rows`; skip the probe rather than replanning every stored
1115        // definition twice over for a statement that changed nothing, all while holding the
1116        // advisory lock and this open transaction.
1117        if status_text != "not_found" {
1118            let post_rows = list_tx(&mut tx)
1119                .await
1120                .map_err(|e| audit_state.fail(status!("error reading view definitions", e)))?;
1121            self.view_registry
1122                .check_dependents_survive(&pre_rows, &post_rows)
1123                .await
1124                .map_err(|e| {
1125                    // The inner error already says what would break and why; this prefix only has
1126                    // to mark where the refusal came from.
1127                    audit_state.fail(client_input_error!("view DDL refused", e))
1128                })?;
1129        }
1130
1131        tx.commit()
1132            .await
1133            .map_err(|e| audit_state.fail(status!("error committing view DDL transaction", e)))?;
1134
1135        // Run after the commit: `reload()` starts with a second, uncommitted-write-blind
1136        // connection, so calling it any earlier would have it read the pre-mutation rows and
1137        // swap in the old factory. `Err` is `warn!`-logged and discarded -- the row is already
1138        // committed, and the next periodic refresh will pick it up regardless.
1139        if let Err(e) = self.view_registry.reload().await {
1140            warn!("view_ddl: inline reload after DDL failed: {e:#}");
1141        }
1142
1143        let schema = Arc::new(Schema::new(vec![
1144            Field::new("view_set_name", DataType::Utf8, false),
1145            Field::new("status", DataType::Utf8, false),
1146        ]));
1147        let batch = RecordBatch::try_new(
1148            schema.clone(),
1149            vec![
1150                Arc::new(StringArray::from(vec![view_set_name])),
1151                Arc::new(StringArray::from(vec![status_text.to_string()])),
1152            ],
1153        )
1154        .map_err(|e| audit_state.fail(status!("error building view DDL result batch", e)))?;
1155        let stream = FlightDataEncoderBuilder::new()
1156            .with_schema(schema)
1157            .build(futures::stream::once(async { Ok(batch) }))
1158            .map_err(Status::from);
1159        let completion_tracked_stream =
1160            CompletionTrackedStream::new(stream.boxed(), begin_request, audit_state);
1161        Ok(Response::new(
1162            Box::pin(completion_tracked_stream) as FlightDataStream
1163        ))
1164    }
1165}
1166
1167#[tonic::async_trait]
1168impl FlightSqlService for FlightSqlServiceImpl {
1169    type FlightService = FlightSqlServiceImpl;
1170
1171    async fn do_handshake(
1172        &self,
1173        _request: Request<Streaming<HandshakeRequest>>,
1174    ) -> Result<
1175        Response<Pin<Box<dyn Stream<Item = Result<HandshakeResponse, Status>> + Send>>>,
1176        Status,
1177    > {
1178        api_entry_not_implemented!()
1179    }
1180
1181    #[span_fn]
1182    async fn do_get_fallback(
1183        &self,
1184        request: Request<Ticket>,
1185        _message: Any,
1186    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1187        let client_ip = get_client_ip(request.metadata().as_ref(), request.extensions());
1188        let ticket_stmt = TicketStatementQuery::decode(request.get_ref().ticket.clone())
1189            .map_err(|e| status!("Could not read ticket", e))?;
1190        self.execute_query(
1191            ticket_stmt,
1192            request.metadata(),
1193            request.extensions(),
1194            &client_ip,
1195        )
1196        .await
1197    }
1198
1199    #[span_fn]
1200    async fn get_flight_info_statement(
1201        &self,
1202        query: CommandStatementQuery,
1203        _request: Request<FlightDescriptor>,
1204    ) -> Result<Response<FlightInfo>, Status> {
1205        let begin_request = now();
1206        info!("get_flight_info_statement {query:?} ");
1207        let CommandStatementQuery { query, .. } = query;
1208        let schema = Schema::empty();
1209        let ticket = TicketStatementQuery {
1210            statement_handle: query.into(),
1211        };
1212        let mut bytes: Vec<u8> = Vec::new();
1213        if ticket.encode(&mut bytes).is_ok() {
1214            let info = FlightInfo::new()
1215                .try_with_schema(&schema)
1216                .unwrap()
1217                .with_endpoint(FlightEndpoint::new().with_ticket(Ticket::new(bytes)));
1218            let duration = now() - begin_request;
1219            imetric!("request_duration", "ticks", duration as u64);
1220            Ok(Response::new(info))
1221        } else {
1222            error!("Error encoding ticket");
1223            Err(Status::internal("Error encoding ticket"))
1224        }
1225    }
1226
1227    async fn get_flight_info_substrait_plan(
1228        &self,
1229        _query: CommandStatementSubstraitPlan,
1230        _request: Request<FlightDescriptor>,
1231    ) -> Result<Response<FlightInfo>, Status> {
1232        api_entry_not_implemented!()
1233    }
1234
1235    async fn get_flight_info_prepared_statement(
1236        &self,
1237        _cmd: CommandPreparedStatementQuery,
1238        _request: Request<FlightDescriptor>,
1239    ) -> Result<Response<FlightInfo>, Status> {
1240        api_entry_not_implemented!()
1241    }
1242
1243    async fn get_flight_info_catalogs(
1244        &self,
1245        _query: CommandGetCatalogs,
1246        _request: Request<FlightDescriptor>,
1247    ) -> Result<Response<FlightInfo>, Status> {
1248        api_entry_not_implemented!()
1249    }
1250
1251    async fn get_flight_info_schemas(
1252        &self,
1253        _query: CommandGetDbSchemas,
1254        _request: Request<FlightDescriptor>,
1255    ) -> Result<Response<FlightInfo>, Status> {
1256        api_entry_not_implemented!()
1257    }
1258
1259    #[span_fn]
1260    async fn get_flight_info_tables(
1261        &self,
1262        query: CommandGetTables,
1263        request: Request<FlightDescriptor>,
1264    ) -> Result<Response<FlightInfo>, Status> {
1265        let begin_request = now();
1266        info!("get_flight_info_tables");
1267        let flight_descriptor = request.into_inner();
1268        let ticket = Ticket {
1269            ticket: query.as_any().encode_to_vec().into(),
1270        };
1271        let endpoint = FlightEndpoint::new().with_ticket(ticket);
1272        let flight_info = FlightInfo::new()
1273            .try_with_schema(&query.into_builder().schema())
1274            .map_err(|e| status!("Unable to encode schema", e))?
1275            .with_endpoint(endpoint)
1276            .with_descriptor(flight_descriptor);
1277        let duration = now() - begin_request;
1278        imetric!("request_duration", "ticks", duration as u64);
1279        Ok(tonic::Response::new(flight_info))
1280    }
1281
1282    async fn get_flight_info_table_types(
1283        &self,
1284        _query: CommandGetTableTypes,
1285        _request: Request<FlightDescriptor>,
1286    ) -> Result<Response<FlightInfo>, Status> {
1287        api_entry_not_implemented!()
1288    }
1289
1290    #[span_fn]
1291    async fn get_flight_info_sql_info(
1292        &self,
1293        query: CommandGetSqlInfo,
1294        request: Request<FlightDescriptor>,
1295    ) -> Result<Response<FlightInfo>, Status> {
1296        let begin_request = now();
1297        info!("get_flight_info_sql_info");
1298        let flight_descriptor = request.into_inner();
1299        let ticket = Ticket::new(query.as_any().encode_to_vec());
1300        let endpoint = FlightEndpoint::new().with_ticket(ticket);
1301        let flight_info = FlightInfo::new()
1302            .try_with_schema(query.into_builder(&INSTANCE_SQL_DATA).schema().as_ref())
1303            .map_err(|e| status!("Unable to encode schema", e))?
1304            .with_endpoint(endpoint)
1305            .with_descriptor(flight_descriptor);
1306        let duration = now() - begin_request;
1307        imetric!("request_duration", "ticks", duration as u64);
1308        Ok(tonic::Response::new(flight_info))
1309    }
1310
1311    async fn get_flight_info_primary_keys(
1312        &self,
1313        _query: CommandGetPrimaryKeys,
1314        _request: Request<FlightDescriptor>,
1315    ) -> Result<Response<FlightInfo>, Status> {
1316        api_entry_not_implemented!()
1317    }
1318
1319    async fn get_flight_info_exported_keys(
1320        &self,
1321        _query: CommandGetExportedKeys,
1322        _request: Request<FlightDescriptor>,
1323    ) -> Result<Response<FlightInfo>, Status> {
1324        api_entry_not_implemented!()
1325    }
1326
1327    async fn get_flight_info_imported_keys(
1328        &self,
1329        _query: CommandGetImportedKeys,
1330        _request: Request<FlightDescriptor>,
1331    ) -> Result<Response<FlightInfo>, Status> {
1332        api_entry_not_implemented!()
1333    }
1334
1335    async fn get_flight_info_cross_reference(
1336        &self,
1337        _query: CommandGetCrossReference,
1338        _request: Request<FlightDescriptor>,
1339    ) -> Result<Response<FlightInfo>, Status> {
1340        api_entry_not_implemented!()
1341    }
1342
1343    async fn get_flight_info_xdbc_type_info(
1344        &self,
1345        _query: CommandGetXdbcTypeInfo,
1346        _request: Request<FlightDescriptor>,
1347    ) -> Result<Response<FlightInfo>, Status> {
1348        api_entry_not_implemented!()
1349    }
1350
1351    #[span_fn]
1352    async fn do_get_statement(
1353        &self,
1354        ticket: TicketStatementQuery,
1355        request: Request<Ticket>,
1356    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1357        let client_ip = get_client_ip(request.metadata().as_ref(), request.extensions());
1358        self.execute_query(ticket, request.metadata(), request.extensions(), &client_ip)
1359            .await
1360    }
1361
1362    async fn do_get_prepared_statement(
1363        &self,
1364        _query: CommandPreparedStatementQuery,
1365        _request: Request<Ticket>,
1366    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1367        api_entry_not_implemented!()
1368    }
1369
1370    async fn do_get_catalogs(
1371        &self,
1372        _query: CommandGetCatalogs,
1373        _request: Request<Ticket>,
1374    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1375        api_entry_not_implemented!()
1376    }
1377
1378    async fn do_get_schemas(
1379        &self,
1380        _query: CommandGetDbSchemas,
1381        _request: Request<Ticket>,
1382    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1383        api_entry_not_implemented!()
1384    }
1385
1386    #[span_fn]
1387    async fn do_get_tables(
1388        &self,
1389        query: CommandGetTables,
1390        _request: Request<Ticket>,
1391    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1392        let begin_request = now();
1393        info!("do_get_tables {query:?}");
1394        let mut builder = query.into_builder();
1395        let view_factory = self.view_registry.current();
1396        for view in view_factory.get_global_views() {
1397            let catalog_name = "";
1398            let schema_name = "";
1399            builder
1400                .append(
1401                    catalog_name,
1402                    schema_name,
1403                    &*view.get_view_set_name(),
1404                    "table",
1405                    &view.get_file_schema(),
1406                )
1407                .map_err(Status::from)?;
1408        }
1409        let schema = builder.schema();
1410        let batch = builder.build();
1411        let stream = FlightDataEncoderBuilder::new()
1412            .with_schema(schema)
1413            .build(futures::stream::once(async { batch }))
1414            .map_err(Status::from);
1415        let duration = now() - begin_request;
1416        imetric!("request_duration", "ticks", duration as u64);
1417        Ok(Response::new(Box::pin(stream)))
1418    }
1419
1420    async fn do_get_table_types(
1421        &self,
1422        _query: CommandGetTableTypes,
1423        _request: Request<Ticket>,
1424    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1425        api_entry_not_implemented!()
1426    }
1427
1428    #[span_fn]
1429    async fn do_get_sql_info(
1430        &self,
1431        query: CommandGetSqlInfo,
1432        _request: Request<Ticket>,
1433    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1434        info!("do_get_sql_info");
1435        let builder = query.into_builder(&INSTANCE_SQL_DATA);
1436        let schema = builder.schema();
1437        let batch = builder.build();
1438        let stream = FlightDataEncoderBuilder::new()
1439            .with_schema(schema)
1440            .build(futures::stream::once(async { batch }))
1441            .map_err(Status::from);
1442        Ok(Response::new(Box::pin(stream)))
1443    }
1444
1445    async fn do_get_primary_keys(
1446        &self,
1447        _query: CommandGetPrimaryKeys,
1448        _request: Request<Ticket>,
1449    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1450        api_entry_not_implemented!()
1451    }
1452
1453    async fn do_get_exported_keys(
1454        &self,
1455        _query: CommandGetExportedKeys,
1456        _request: Request<Ticket>,
1457    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1458        api_entry_not_implemented!()
1459    }
1460
1461    async fn do_get_imported_keys(
1462        &self,
1463        _query: CommandGetImportedKeys,
1464        _request: Request<Ticket>,
1465    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1466        api_entry_not_implemented!()
1467    }
1468
1469    async fn do_get_cross_reference(
1470        &self,
1471        _query: CommandGetCrossReference,
1472        _request: Request<Ticket>,
1473    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1474        api_entry_not_implemented!()
1475    }
1476
1477    async fn do_get_xdbc_type_info(
1478        &self,
1479        _query: CommandGetXdbcTypeInfo,
1480        _request: Request<Ticket>,
1481    ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
1482        api_entry_not_implemented!()
1483    }
1484
1485    async fn do_put_statement_update(
1486        &self,
1487        _ticket: CommandStatementUpdate,
1488        _request: Request<PeekableFlightDataStream>,
1489    ) -> Result<i64, Status> {
1490        api_entry_not_implemented!()
1491    }
1492
1493    /// `bulk_ingest` (routed here from `CommandStatementIngest`) is a replication/administrative
1494    /// API, not an ordinary analytics write path: it writes the authoritative `audience` column
1495    /// on `processes`/`streams`/`blocks` rows -- and every other column, including properties --
1496    /// verbatim, with none of the server-side stamping or reserved-namespace stripping the HTTP
1497    /// ingestion paths apply; a missing `audience` column is a hard error rather than a silent
1498    /// default. Gating this RPC on `is_admin` is what keeps that verbatim write safe: an
1499    /// admin-run replication tool re-ingesting rows already stamped at their origin lake is
1500    /// exactly the case the docs describe (`mkdocs/docs/query-guide/python-api.md`), while an
1501    /// ordinary authenticated analytics client must not be able to set the `audience` column
1502    /// directly.
1503    #[span_fn]
1504    async fn do_put_statement_ingest(
1505        &self,
1506        command: CommandStatementIngest,
1507        request: Request<PeekableFlightDataStream>,
1508    ) -> Result<i64, Status> {
1509        let table_name = command.table;
1510        info!("do_put_statement_ingest table_name={table_name}");
1511        if !is_admin(request.metadata()) {
1512            return Err(Status::permission_denied(
1513                "bulk_ingest requires admin privileges",
1514            ));
1515        }
1516        let stream = FlightRecordBatchStream::new_from_flight_data(
1517            request.into_inner().map_err(|e| e.into()),
1518        );
1519        bulk_ingest(self.lakehouse.lake().clone(), &table_name, stream)
1520            .await
1521            .map_err(|e| {
1522                let msg = format!("error ingesting into {table_name}: {e:?}");
1523                error!("{msg}");
1524                status!(msg, e)
1525            })
1526    }
1527
1528    async fn do_put_substrait_plan(
1529        &self,
1530        _ticket: CommandStatementSubstraitPlan,
1531        _request: Request<PeekableFlightDataStream>,
1532    ) -> Result<i64, Status> {
1533        api_entry_not_implemented!()
1534    }
1535
1536    async fn do_put_prepared_statement_query(
1537        &self,
1538        _query: CommandPreparedStatementQuery,
1539        _request: Request<PeekableFlightDataStream>,
1540    ) -> Result<DoPutPreparedStatementResult, Status> {
1541        api_entry_not_implemented!()
1542    }
1543
1544    async fn do_put_prepared_statement_update(
1545        &self,
1546        _query: CommandPreparedStatementUpdate,
1547        _request: Request<PeekableFlightDataStream>,
1548    ) -> Result<i64, Status> {
1549        api_entry_not_implemented!()
1550    }
1551
1552    #[span_fn]
1553    async fn do_action_create_prepared_statement(
1554        &self,
1555        query: ActionCreatePreparedStatementRequest,
1556        request: Request<Action>,
1557    ) -> Result<ActionCreatePreparedStatementResult, Status> {
1558        info!("do_action_create_prepared_statement query={}", &query.query);
1559
1560        // DDL (`CREATE`/`DROP MATERIALIZED VIEW`) is never prepared: nothing in the repo
1561        // prepares DDL, and planning it here via `ctx.sql` would fail with an opaque DataFusion
1562        // error instead of this clear message.
1563        if parse_view_ddl(&query.query)
1564            .map_err(|e| client_input_error!("error parsing view DDL", e))?
1565            .is_some()
1566        {
1567            return Err(Status::invalid_argument(
1568                "CREATE/DROP MATERIALIZED VIEW must be executed directly, not prepared",
1569            ));
1570        }
1571
1572        // Prepared statements resolve the same CallerContext as the do_get execute path.
1573        let caller = self
1574            .caller_context(request.extensions(), request.metadata())
1575            .await?;
1576        let ctx = make_session_context(
1577            self.lakehouse.clone(),
1578            self.part_provider.clone(),
1579            None,
1580            self.view_registry.current(),
1581            self.session_configurator.clone(),
1582            caller,
1583        )
1584        .await
1585        .map_err(|e| status!("error in make_session_context", e))?;
1586
1587        // No `QueryAuditState`/audit record exists on this RPC (it only plans the
1588        // query, never executes it) -- this id exists solely so the client-facing
1589        // message and a server log line can be correlated with each other.
1590        let local_query_id = Uuid::new_v4().to_string();
1591        let df = ctx
1592            .sql(&query.query)
1593            .await
1594            .map_err(|e| client_error("error building dataframe", e, &local_query_id, None))?;
1595        let schema = df.schema().as_arrow();
1596        let mut schema_buffer = Vec::new();
1597        let mut writer = StreamWriter::try_new(&mut schema_buffer, schema)
1598            .map_err(|e| status!("error writing schema to in-memory buffer", e))?;
1599        writer
1600            .finish()
1601            .map_err(|e| status!("error closing arrow ipc stream writer", e))?;
1602        // here we could serialize the logical plan and return that as the prepared statement, but we would
1603        // need to register LogicalExtensionCodec for user-defined functions
1604        // instead, we are sending back the sql as we received it
1605        let result = ActionCreatePreparedStatementResult {
1606            prepared_statement_handle: query.query.into(),
1607            dataset_schema: schema_buffer.into(),
1608            parameter_schema: "".into(),
1609        };
1610        Ok(result)
1611    }
1612
1613    async fn do_action_close_prepared_statement(
1614        &self,
1615        _query: ActionClosePreparedStatementRequest,
1616        _request: Request<Action>,
1617    ) -> Result<(), Status> {
1618        info!("do_action_close_prepared_statement");
1619        Ok(())
1620    }
1621
1622    async fn do_action_create_prepared_substrait_plan(
1623        &self,
1624        _query: ActionCreatePreparedSubstraitPlanRequest,
1625        _request: Request<Action>,
1626    ) -> Result<ActionCreatePreparedStatementResult, Status> {
1627        api_entry_not_implemented!()
1628    }
1629
1630    async fn do_action_begin_transaction(
1631        &self,
1632        _query: ActionBeginTransactionRequest,
1633        _request: Request<Action>,
1634    ) -> Result<ActionBeginTransactionResult, Status> {
1635        api_entry_not_implemented!()
1636    }
1637
1638    async fn do_action_end_transaction(
1639        &self,
1640        _query: ActionEndTransactionRequest,
1641        _request: Request<Action>,
1642    ) -> Result<(), Status> {
1643        api_entry_not_implemented!()
1644    }
1645
1646    async fn do_action_begin_savepoint(
1647        &self,
1648        _query: ActionBeginSavepointRequest,
1649        _request: Request<Action>,
1650    ) -> Result<ActionBeginSavepointResult, Status> {
1651        api_entry_not_implemented!()
1652    }
1653
1654    async fn do_action_end_savepoint(
1655        &self,
1656        _query: ActionEndSavepointRequest,
1657        _request: Request<Action>,
1658    ) -> Result<(), Status> {
1659        api_entry_not_implemented!()
1660    }
1661
1662    async fn do_action_cancel_query(
1663        &self,
1664        _query: ActionCancelQueryRequest,
1665        _request: Request<Action>,
1666    ) -> Result<ActionCancelQueryResult, Status> {
1667        api_entry_not_implemented!()
1668    }
1669
1670    async fn register_sql_info(&self, _id: i32, _result: &SqlInfo) {
1671        info!("register_sql_info");
1672    }
1673}