1use hawdb_core::time::{SystemTime, UNIX_EPOCH};
22use hawdb_core::{
23 Catalog, ConstraintKind, ConstraintSubject, GraphStatistics, HawDBError, IndexKind,
24 IndexStatisticsSample, LabelId, PropertyType, RelTypeId, Result, RuntimeCapabilities,
25 SchemaObjectState, TableKind, Value,
26};
27use hawdb_executor::{binding::map_payload_bytes, QueryOutput, Row, VectorExecutionReport};
28use hawdb_plan_cache::PlanCacheStats;
29use hawdb_query::QueryIdentity;
30use hawdb_sql::{Expr, ExprKind};
31use hawdb_sql::{
32 SelectProjection, SelectStatement, SqlBound, SqlColumnRef, SqlComparisonOp, SqlOrderDirection,
33 SqlOrderItem, SqlPredicate, SqlStatement, SqlValue,
34};
35use hawdb_storage::{
36 append_table::{AppendOrderMode, AppendState, AppendStorageResidencyReport},
37 config::{RelationalIndexMode, StorageResidencyMode},
38 projection::{ProjectedGraphStatus, SearchProjectionChangefeedStatus},
39 relational::{
40 RelationalColumnDefault, RelationalScalarType, RelationalState, RelationalTableSchema,
41 RelationalValue,
42 },
43};
44use std::cmp::Ordering;
45use std::collections::{BTreeMap, VecDeque};
46
47#[doc(hidden)]
48pub const DEFAULT_SLOW_QUERY_LOG_CAPACITY: usize = 256;
49#[doc(hidden)]
50pub const DEFAULT_SLOW_QUERY_LOG_THRESHOLD_MICROS: u128 = 300_000;
51#[doc(hidden)]
52pub const DEFAULT_STATEMENT_SUMMARY_CAPACITY: usize = 256;
53pub const SLOW_QUERY_LOG_EVENT_PROTOCOL: &str = "hawdb-slow-query-log-event-v1";
54const MAX_SLOW_QUERY_TEXT_BYTES: usize = 4096;
55const MAX_STATEMENT_TEXT_BYTES: usize = 4096;
56const MAX_STATEMENT_ERROR_BYTES: usize = 1024;
57
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
59pub struct SlowQueryLogExportOptions {
60 pub include_query_text: bool,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct SlowQueryLogRecordSummary {
65 pub sequence: u64,
66 pub query_language: String,
67 pub statement_kind: String,
68 pub query_digest: String,
69 pub started_unix_micros: i64,
70 pub elapsed_micros: i64,
71 pub row_count: i64,
72 pub success: bool,
73 pub slow_log_candidate: bool,
74 pub access_control_policy_epoch: Option<u64>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78#[doc(hidden)]
79pub struct SlowQueryRecord {
80 pub sequence: u64,
81 pub query_language: String,
82 pub statement_kind: String,
83 pub query_digest: String,
84 pub query_text_hash: String,
85 pub query_text: String,
86 pub started_unix_micros: i64,
87 pub elapsed_micros: i64,
88 pub row_count: i64,
89 pub success: bool,
90 pub error: Option<String>,
91 pub slow_log_candidate: bool,
92 pub access_control_policy_epoch: Option<u64>,
93 pub vector_execution_reports: Vec<VectorExecutionReport>,
94}
95
96#[doc(hidden)]
97pub struct SlowQueryCompletion<'a> {
98 pub query_language: &'a str,
99 pub statement_kind: &'a str,
100 pub query_text: &'a str,
101 pub query_identity: &'a QueryIdentity,
102 pub elapsed_micros: u128,
103 pub row_count: usize,
104 pub success: bool,
105 pub error: Option<String>,
106 pub slow_log_candidate: bool,
107 pub access_control_policy_epoch: Option<u64>,
108 pub vector_execution_reports: Vec<VectorExecutionReport>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112#[doc(hidden)]
113pub struct SlowQueryLog {
114 capacity: usize,
115 next_sequence: u64,
116 records: VecDeque<SlowQueryRecord>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120#[doc(hidden)]
121pub struct StatementExecution {
122 pub query_language: String,
123 pub query_text: String,
124 pub statement_kind: String,
125 pub query_digest: String,
126 pub query_text_hash: String,
127 pub elapsed_micros: i64,
128 pub row_count: i64,
129 pub success: bool,
130 pub error: Option<String>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134#[doc(hidden)]
135pub struct StatementSummaryRecord {
136 pub digest: String,
137 pub query_language: String,
138 pub query_text: String,
139 pub sample_query_text_hash: String,
140 pub statement_kind: String,
141 pub execution_count: i64,
142 pub success_count: i64,
143 pub error_count: i64,
144 pub total_elapsed_micros: i64,
145 pub max_elapsed_micros: i64,
146 pub total_row_count: i64,
147 pub last_seen_unix_micros: i64,
148 pub last_elapsed_micros: i64,
149 pub last_row_count: i64,
150 pub last_success: bool,
151 pub last_error: Option<String>,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155#[doc(hidden)]
156pub struct StatementSummary {
157 capacity: usize,
158 records: BTreeMap<String, StatementSummaryRecord>,
159 insertion_order: VecDeque<String>,
160}
161
162#[doc(hidden)]
166pub trait SystemSqlStore {
167 fn commit_epoch(&self) -> u64;
168 fn append_storage_residency_report(&self) -> AppendStorageResidencyReport;
169 fn statistics(&self, catalog: &Catalog) -> GraphStatistics;
170 fn projected_graph_statuses(&self) -> Vec<ProjectedGraphStatus>;
171 fn search_projection_changefeed_status(&self) -> SearchProjectionChangefeedStatus;
172}
173
174#[doc(hidden)]
175pub struct SystemSqlContext<'a, Store: SystemSqlStore> {
176 pub catalog: &'a Catalog,
177 pub store: &'a Store,
178 pub relational_state: &'a RelationalState,
179 pub append_state: &'a AppendState,
180 pub runtime: SystemRuntimeSnapshot,
181 pub plan_cache_stats: &'a PlanCacheStats,
182 pub slow_queries: &'a [SlowQueryRecord],
183 pub statement_summaries: &'a [StatementSummaryRecord],
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187#[doc(hidden)]
188pub struct SystemRuntimeSnapshot {
189 read_only: bool,
190 max_read_result_rows: Option<usize>,
191 max_read_result_payload_bytes: Option<usize>,
192 storage_residency_mode: StorageResidencyMode,
193 relational_index_mode: RelationalIndexMode,
194 runtime_capabilities: RuntimeCapabilities,
195}
196
197impl SystemRuntimeSnapshot {
198 pub fn new(
199 read_only: bool,
200 max_read_result_rows: Option<usize>,
201 max_read_result_payload_bytes: Option<usize>,
202 storage_residency_mode: StorageResidencyMode,
203 relational_index_mode: RelationalIndexMode,
204 runtime_capabilities: RuntimeCapabilities,
205 ) -> Self {
206 Self {
207 read_only,
208 max_read_result_rows,
209 max_read_result_payload_bytes,
210 storage_residency_mode,
211 relational_index_mode,
212 runtime_capabilities,
213 }
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub(crate) enum SqlLogicalPlan {
219 SystemTableScan(SystemTableScan),
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub(crate) enum SqlPhysicalPlan {
224 SystemTableScanExec(SystemTableScan),
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub(crate) struct SystemTableScan {
229 table: SystemTable,
230 projection: Vec<SelectProjection>,
231 predicate: Option<SqlPredicate>,
232 order_by: Vec<SqlOrderItem>,
233 offset: Option<u64>,
234 limit: Option<u64>,
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238enum SystemTable {
239 Tables,
240 VectorColumns,
241 Properties,
242 Indexes,
243 Constraints,
244 AppendTables,
245 AppendStorage,
246 RuntimeStatus,
247 RuntimeCapabilities,
248 GraphStatistics,
249 ProjectedGraphs,
250 SearchProjectionChangefeed,
251 PlanCache,
252 SlowQueries,
253 StatementSummary,
254 InformationSchemaTables,
255 InformationSchemaColumns,
256 PgTables,
257 PgIndexes,
258}
259
260#[doc(hidden)]
261pub fn is_virtual_catalog_select(select: &SelectStatement) -> bool {
262 let Some(from) = &select.from else {
263 return false;
264 };
265 matches!(
266 (from.schema.as_deref(), from.name.as_str()),
267 (Some("system" | "information_schema" | "pg_catalog"), _)
268 | (None, "pg_tables" | "pg_indexes")
269 )
270}
271
272impl SlowQueryRecord {
273 pub fn completed(completion: SlowQueryCompletion<'_>) -> Self {
274 Self {
275 sequence: 0,
276 query_language: completion.query_language.to_string(),
277 statement_kind: completion.statement_kind.to_string(),
278 query_digest: completion.query_identity.query_digest().to_string(),
279 query_text_hash: completion.query_identity.query_text_hash().to_string(),
280 query_text: truncate_utf8(completion.query_text, MAX_SLOW_QUERY_TEXT_BYTES),
281 started_unix_micros: unix_now_micros(),
282 elapsed_micros: saturating_i64_from_u128(completion.elapsed_micros),
283 row_count: i64::try_from(completion.row_count).unwrap_or(i64::MAX),
284 success: completion.success,
285 error: completion.error,
286 slow_log_candidate: completion.slow_log_candidate,
287 access_control_policy_epoch: completion.access_control_policy_epoch,
288 vector_execution_reports: completion.vector_execution_reports,
289 }
290 }
291}
292
293impl SlowQueryLog {
294 pub fn new(capacity: usize) -> Self {
295 Self {
296 capacity,
297 next_sequence: 1,
298 records: VecDeque::with_capacity(capacity),
299 }
300 }
301
302 pub fn push(&mut self, mut record: SlowQueryRecord) {
303 if self.capacity == 0 {
304 return;
305 }
306 record.sequence = self.next_sequence;
307 self.next_sequence = self.next_sequence.saturating_add(1);
308 while self.records.len() >= self.capacity {
309 self.records.pop_front();
310 }
311 self.records.push_back(record);
312 }
313
314 pub fn snapshot(&self) -> Vec<SlowQueryRecord> {
315 self.records.iter().cloned().collect()
316 }
317}
318
319pub fn slow_query_log_jsonl(
320 records: &[SlowQueryRecord],
321 include_query_text: bool,
322) -> Result<String> {
323 let mut jsonl = String::new();
324 for record in records {
325 let line = serde_json::to_string(&slow_query_record_json(record, include_query_text))
326 .map_err(|error| {
327 HawDBError::Execution(format!("slow query log JSON error: {error}"))
328 })?;
329 jsonl.push_str(&line);
330 jsonl.push('\n');
331 }
332 Ok(jsonl)
333}
334
335pub fn slow_query_record_summary(record: &SlowQueryRecord) -> SlowQueryLogRecordSummary {
336 SlowQueryLogRecordSummary {
337 sequence: record.sequence,
338 query_language: record.query_language.clone(),
339 statement_kind: record.statement_kind.clone(),
340 query_digest: record.query_digest.clone(),
341 started_unix_micros: record.started_unix_micros,
342 elapsed_micros: record.elapsed_micros,
343 row_count: record.row_count,
344 success: record.success,
345 slow_log_candidate: record.slow_log_candidate,
346 access_control_policy_epoch: record.access_control_policy_epoch,
347 }
348}
349
350fn slow_query_record_json(record: &SlowQueryRecord, include_query_text: bool) -> serde_json::Value {
351 let mut object = serde_json::json!({
352 "protocol": SLOW_QUERY_LOG_EVENT_PROTOCOL,
353 "protocol_version": 1,
354 "sequence": record.sequence,
355 "query_language": record.query_language,
356 "statement_kind": record.statement_kind,
357 "query_digest": record.query_digest,
358 "started_unix_micros": record.started_unix_micros,
359 "elapsed_micros": record.elapsed_micros,
360 "row_count": record.row_count,
361 "success": record.success,
362 "slow_log_candidate": record.slow_log_candidate,
363 "access_control_policy_epoch": record.access_control_policy_epoch,
364 "redaction": {
365 "query_text_copied": include_query_text,
366 "parameters_copied": false,
367 "access_control_policy_inputs_copied": false
368 }
369 });
370 if include_query_text {
371 object["query_text"] = serde_json::Value::String(record.query_text.clone());
372 }
373 if let Some(error) = &record.error {
374 object["error"] =
375 serde_json::Value::String(truncate_utf8(error, MAX_STATEMENT_ERROR_BYTES));
376 }
377 object["vector_execution_report_count"] =
378 serde_json::json!(record.vector_execution_reports.len());
379 object["vector_execution_reports"] = serde_json::Value::Array(
380 record
381 .vector_execution_reports
382 .iter()
383 .map(vector_execution_report_json)
384 .collect(),
385 );
386 object
387}
388
389fn vector_execution_report_json(report: &VectorExecutionReport) -> serde_json::Value {
390 serde_json::json!({
391 "backend": report.backend.as_str(),
392 "compression_mode": report.compression_mode.as_str(),
393 "candidate_source": report.candidate_source.as_str(),
394 "backend_selection_reason": report.backend_selection_reason.map(|reason| reason.as_str()),
395 "estimated_raw_vector_bytes": report.estimated_raw_vector_bytes,
396 "filter_selectivity_per_million": report.filter_selectivity_per_million,
397 "candidate_score_source": report.candidate_score_source.as_str(),
398 "final_score_source": report.final_score_source.as_str(),
399 "generated_candidate_count": report.generated_candidate_count,
400 "descriptor_pruned_count": report.descriptor_pruned_count,
401 "scalar_filtered_count": report.scalar_filtered_count,
402 "residual_filtered_count": report.residual_filtered_count,
403 "candidate_scan_rounds": report.candidate_scan_rounds,
404 "reranked_candidate_count": report.reranked_candidate_count,
405 "returned_count": report.returned_count,
406 "raw_vector_bytes_read": report.raw_vector_bytes_read,
407 "candidate_scan": report.candidate_scan_metrics.as_ref().map(|metrics| serde_json::json!({
408 "kernel": metrics.kernel,
409 "worker_count": metrics.worker_count,
410 "segment_count": metrics.segment_count,
411 "scanned_segment_count": metrics.scanned_segment_count,
412 "scored_document_count": metrics.scored_document_count,
413 "filtered_document_count": metrics.filtered_document_count,
414 "scanned_block_count": metrics.scanned_block_count,
415 "skipped_block_count": metrics.skipped_block_count,
416 "payload_bytes_read": metrics.payload_bytes_read,
417 "admitted_working_bytes": metrics.admitted_working_bytes,
418 })),
419 "index_covered_document_count": report.index_covered_document_count,
420 "index_candidate_document_count": report.index_candidate_document_count,
421 "index_coverage_complete": report.index_coverage_complete,
422 "fallback_reason_codes": report.fallback_reason_codes.iter().map(|code| code.as_str()).collect::<Vec<_>>(),
423 })
424}
425
426impl StatementExecution {
427 pub fn completed(
428 query_language: &str,
429 query_text: &str,
430 statement_kind: &str,
431 query_identity: &QueryIdentity,
432 elapsed_micros: u128,
433 row_count: usize,
434 ) -> Self {
435 Self {
436 query_language: query_language.to_string(),
437 query_text: truncate_utf8(query_text, MAX_STATEMENT_TEXT_BYTES),
438 statement_kind: statement_kind.to_string(),
439 query_digest: query_identity.query_digest().to_string(),
440 query_text_hash: query_identity.query_text_hash().to_string(),
441 elapsed_micros: saturating_i64_from_u128(elapsed_micros),
442 row_count: i64::try_from(row_count).unwrap_or(i64::MAX),
443 success: true,
444 error: None,
445 }
446 }
447
448 pub fn failed(
449 query_language: &str,
450 query_text: &str,
451 statement_kind: &str,
452 query_identity: &QueryIdentity,
453 elapsed_micros: u128,
454 error: String,
455 ) -> Self {
456 Self {
457 query_language: query_language.to_string(),
458 query_text: truncate_utf8(query_text, MAX_STATEMENT_TEXT_BYTES),
459 statement_kind: statement_kind.to_string(),
460 query_digest: query_identity.query_digest().to_string(),
461 query_text_hash: query_identity.query_text_hash().to_string(),
462 elapsed_micros: saturating_i64_from_u128(elapsed_micros),
463 row_count: 0,
464 success: false,
465 error: Some(truncate_utf8(&error, MAX_STATEMENT_ERROR_BYTES)),
466 }
467 }
468}
469
470impl StatementSummary {
471 pub fn new(capacity: usize) -> Self {
472 Self {
473 capacity,
474 records: BTreeMap::new(),
475 insertion_order: VecDeque::with_capacity(capacity),
476 }
477 }
478
479 pub fn record(&mut self, execution: StatementExecution) {
480 if self.capacity == 0 {
481 return;
482 }
483
484 let digest = execution.query_digest.clone();
485 if let Some(record) = self.records.get_mut(&digest) {
486 record.apply(execution);
487 return;
488 }
489
490 while self.records.len() >= self.capacity {
491 let Some(evicted) = self.insertion_order.pop_front() else {
492 break;
493 };
494 self.records.remove(&evicted);
495 }
496
497 self.insertion_order.push_back(digest.clone());
498 self.records.insert(
499 digest.clone(),
500 StatementSummaryRecord::from_execution(digest, execution),
501 );
502 }
503
504 pub fn snapshot(&self) -> Vec<StatementSummaryRecord> {
505 self.insertion_order
506 .iter()
507 .filter_map(|digest| self.records.get(digest).cloned())
508 .collect()
509 }
510}
511
512impl StatementSummaryRecord {
513 fn from_execution(digest: String, execution: StatementExecution) -> Self {
514 let now = unix_now_micros();
515 let success_count = i64::from(execution.success);
516 let error_count = i64::from(!execution.success);
517 Self {
518 digest,
519 query_language: execution.query_language,
520 query_text: execution.query_text,
521 sample_query_text_hash: execution.query_text_hash,
522 statement_kind: execution.statement_kind,
523 execution_count: 1,
524 success_count,
525 error_count,
526 total_elapsed_micros: execution.elapsed_micros,
527 max_elapsed_micros: execution.elapsed_micros,
528 total_row_count: execution.row_count,
529 last_seen_unix_micros: now,
530 last_elapsed_micros: execution.elapsed_micros,
531 last_row_count: execution.row_count,
532 last_success: execution.success,
533 last_error: execution.error,
534 }
535 }
536
537 fn apply(&mut self, execution: StatementExecution) {
538 self.execution_count = self.execution_count.saturating_add(1);
539 if execution.success {
540 self.success_count = self.success_count.saturating_add(1);
541 } else {
542 self.error_count = self.error_count.saturating_add(1);
543 }
544 self.total_elapsed_micros = self
545 .total_elapsed_micros
546 .saturating_add(execution.elapsed_micros);
547 self.max_elapsed_micros = self.max_elapsed_micros.max(execution.elapsed_micros);
548 self.total_row_count = self.total_row_count.saturating_add(execution.row_count);
549 self.last_seen_unix_micros = unix_now_micros();
550 self.last_elapsed_micros = execution.elapsed_micros;
551 self.last_row_count = execution.row_count;
552 self.last_success = execution.success;
553 self.last_error = execution.error;
554 }
555}
556
557#[cfg(test)]
558pub fn query_sql<Store: SystemSqlStore>(
559 sql_text: &str,
560 max_rows: Option<usize>,
561 max_payload_bytes: Option<usize>,
562 context: &SystemSqlContext<'_, Store>,
563) -> Result<QueryOutput> {
564 query_sql_with_params(sql_text, &[], max_rows, max_payload_bytes, context)
565}
566
567pub fn query_sql_with_params<Store: SystemSqlStore>(
568 sql_text: &str,
569 parameters: &[Value],
570 max_rows: Option<usize>,
571 max_payload_bytes: Option<usize>,
572 context: &SystemSqlContext<'_, Store>,
573) -> Result<QueryOutput> {
574 let logical = plan_sql(sql_text, parameters)?;
575 let physical = optimize_sql(logical);
576 let rows = execute_sql(physical, context, max_rows)?;
577 let payload_bytes = rows.iter().fold(0usize, |total, row| {
578 total.saturating_add(map_payload_bytes(row))
579 });
580 if let Some(limit) = max_payload_bytes
581 && payload_bytes > limit
582 {
583 return Err(HawDBError::read_budget_exceeded(
584 hawdb_core::ReadBudgetResource::PayloadBytes,
585 limit,
586 format!(
587 "SQL query payload uses {payload_bytes} bytes, exceeding max_read_result_payload_bytes {limit}"
588 ),
589 ));
590 }
591 Ok(QueryOutput { rows: rows.into() })
592}
593
594fn plan_sql(sql_text: &str, parameters: &[Value]) -> Result<SqlLogicalPlan> {
595 let prepared = hawdb_sql::prepare_postgres_sql(sql_text)?;
596 if prepared.parameters.len() != parameters.len() {
597 return Err(HawDBError::Semantic(format!(
598 "PostgreSQL statement requires {} parameters, but {} parameters were supplied",
599 prepared.parameters.len(),
600 parameters.len()
601 )));
602 }
603 let SqlStatement::Select(select) = prepared.statement else {
604 return Err(HawDBError::Semantic(
605 "system SQL only supports SELECT statements".to_string(),
606 ));
607 };
608 validate_system_select_shape(&select)?;
609 let table = system_table(&select)?;
610 validate_projection(table, &select.projection)?;
611 validate_predicate_columns(table, select.selection.as_ref())?;
612 validate_order_columns(table, &select.order_by)?;
613 Ok(SqlLogicalPlan::SystemTableScan(SystemTableScan {
614 table,
615 projection: select.projection,
616 predicate: select
617 .selection
618 .map(|predicate| bind_predicate(predicate, parameters))
619 .transpose()?,
620 order_by: select.order_by,
621 offset: select
622 .offset
623 .map(|bound| bind_bound(bound, parameters, "OFFSET"))
624 .transpose()?,
625 limit: select
626 .limit
627 .map(|bound| bind_bound(bound, parameters, "LIMIT"))
628 .transpose()?,
629 }))
630}
631
632fn validate_system_select_shape(select: &SelectStatement) -> Result<()> {
633 if select.having.is_some() {
634 return Err(HawDBError::Semantic(
635 "system SQL does not support HAVING".into(),
636 ));
637 }
638 if select.distinct
639 || select.from_alias.is_some()
640 || !select.joins.is_empty()
641 || !select.group_by.is_empty()
642 || select.lock_strength.is_some()
643 {
644 return Err(HawDBError::Semantic(
645 "system SQL does not support DISTINCT, table aliases, joins, GROUP BY, or locking clauses"
646 .to_string(),
647 ));
648 }
649 if select
650 .order_by
651 .iter()
652 .any(|item| item.nulls != hawdb_sql::SqlNullOrder::DialectDefault)
653 {
654 return Err(HawDBError::Semantic(
655 "system SQL does not support explicit NULLS FIRST/LAST".to_string(),
656 ));
657 }
658 Ok(())
659}
660
661fn bind_predicate(mut predicate: SqlPredicate, parameters: &[Value]) -> Result<SqlPredicate> {
662 predicate.try_visit_mut(&mut |expression| {
663 if let ExprKind::Value(value) = &mut expression.kind {
664 let unbound = std::mem::replace(value, SqlValue::Literal(Value::Null));
665 *value = SqlValue::Literal(bind_value(unbound, parameters)?);
666 }
667 if let ExprKind::Like {
668 pattern,
669 escape,
670 case_insensitive,
671 ..
672 } = &expression.kind
673 && let SqlValue::Literal(Value::String(pattern)) = pattern.require_value()?
674 {
675 hawdb_sql::sql_like_matches("", pattern, *escape, *case_insensitive)?;
676 }
677 Ok::<_, HawDBError>(())
678 })?;
679 Ok(predicate)
680}
681
682fn bind_value(value: SqlValue, parameters: &[Value]) -> Result<Value> {
683 match value {
684 SqlValue::Literal(value) => Ok(value),
685 SqlValue::Parameter(position) => parameters.get(position - 1).cloned().ok_or_else(|| {
686 HawDBError::Semantic(format!("missing PostgreSQL parameter ${position}"))
687 }),
688 }
689}
690
691fn bind_bound(bound: SqlBound, parameters: &[Value], name: &str) -> Result<u64> {
692 match bound {
693 SqlBound::Literal(value) => Ok(value),
694 SqlBound::Parameter(position) => match parameters.get(position - 1) {
695 Some(Value::Int(value)) if *value >= 0 => Ok(*value as u64),
696 Some(_) => Err(HawDBError::Semantic(format!(
697 "PostgreSQL {name} parameter ${position} must be a non-negative integer"
698 ))),
699 None => Err(HawDBError::Semantic(format!(
700 "missing PostgreSQL parameter ${position}"
701 ))),
702 },
703 }
704}
705
706fn optimize_sql(logical: SqlLogicalPlan) -> SqlPhysicalPlan {
707 match logical {
708 SqlLogicalPlan::SystemTableScan(scan) => SqlPhysicalPlan::SystemTableScanExec(scan),
709 }
710}
711
712fn execute_sql<Store: SystemSqlStore>(
713 physical: SqlPhysicalPlan,
714 context: &SystemSqlContext<'_, Store>,
715 max_rows: Option<usize>,
716) -> Result<Vec<Row>> {
717 match physical {
718 SqlPhysicalPlan::SystemTableScanExec(scan) => {
719 execute_system_table_scan(scan, context, max_rows)
720 }
721 }
722}
723
724fn execute_system_table_scan<Store: SystemSqlStore>(
725 scan: SystemTableScan,
726 context: &SystemSqlContext<'_, Store>,
727 max_rows: Option<usize>,
728) -> Result<Vec<Row>> {
729 let mut rows = match scan.table {
730 SystemTable::Tables => table_rows(context.catalog),
731 SystemTable::VectorColumns => vector_column_rows(context.relational_state),
732 SystemTable::Properties => property_rows(context.catalog),
733 SystemTable::Indexes => index_rows(context.catalog),
734 SystemTable::Constraints => constraint_rows(context.catalog),
735 SystemTable::AppendTables => append_table_rows(context.append_state),
736 SystemTable::AppendStorage => append_storage_rows(context.store, context.append_state),
737 SystemTable::RuntimeStatus => runtime_status_rows(context),
738 SystemTable::RuntimeCapabilities => runtime_capability_rows(context.runtime),
739 SystemTable::GraphStatistics => {
740 graph_statistics_rows(context.catalog, &context.store.statistics(context.catalog))
741 }
742 SystemTable::ProjectedGraphs => {
743 projected_graph_rows(context.store.projected_graph_statuses())
744 }
745 SystemTable::SearchProjectionChangefeed => {
746 search_projection_changefeed_rows(context.store.search_projection_changefeed_status())
747 }
748 SystemTable::PlanCache => plan_cache_rows(context.plan_cache_stats),
749 SystemTable::SlowQueries => slow_query_rows(context.slow_queries),
750 SystemTable::StatementSummary => statement_summary_rows(context.statement_summaries),
751 SystemTable::InformationSchemaTables => {
752 information_schema_table_rows(context.relational_state)
753 }
754 SystemTable::InformationSchemaColumns => {
755 information_schema_column_rows(context.relational_state)
756 }
757 SystemTable::PgTables => pg_table_rows(context.relational_state),
758 SystemTable::PgIndexes => pg_index_rows(context.relational_state),
759 };
760
761 if let Some(predicate) = &scan.predicate {
762 rows.retain(|row| predicate_matches(predicate, row));
763 }
764
765 if !scan.order_by.is_empty() {
766 rows.sort_by(|left, right| compare_ordered_rows(left, right, &scan.order_by));
767 }
768
769 let offset = scan.offset.unwrap_or(0);
770 let limit = effective_limit(scan.limit, max_rows)?;
771 rows = rows
772 .into_iter()
773 .skip(usize::try_from(offset).unwrap_or(usize::MAX))
774 .take(limit.unwrap_or(usize::MAX))
775 .collect();
776
777 if let Some(max_rows) = max_rows
778 && rows.len() > max_rows
779 {
780 return Err(HawDBError::read_budget_exceeded(
781 hawdb_core::ReadBudgetResource::Rows,
782 max_rows,
783 format!(
784 "SQL query returned more than {max_rows} rows, exceeding max_read_result_rows {max_rows}"
785 ),
786 ));
787 }
788
789 project_rows(rows, &scan.projection)
790}
791
792fn effective_limit(query_limit: Option<u64>, max_rows: Option<usize>) -> Result<Option<usize>> {
793 let query_limit = query_limit
794 .map(|limit| {
795 usize::try_from(limit)
796 .map_err(|_| HawDBError::Semantic("SQL LIMIT is too large".to_string()))
797 })
798 .transpose()?;
799 Ok(match (query_limit, max_rows) {
800 (Some(query_limit), Some(max_rows)) => Some(query_limit.min(max_rows.saturating_add(1))),
801 (Some(query_limit), None) => Some(query_limit),
802 (None, Some(max_rows)) => Some(max_rows.saturating_add(1)),
803 (None, None) => None,
804 })
805}
806
807fn project_rows(rows: Vec<Row>, projection: &[SelectProjection]) -> Result<Vec<Row>> {
808 if projection
809 .iter()
810 .any(|projection| matches!(projection, SelectProjection::Wildcard))
811 {
812 return Ok(rows);
813 }
814 rows.into_iter()
815 .map(|row| {
816 projection
817 .iter()
818 .map(|projection| {
819 let SelectProjection::Expression {
820 expression:
821 Expr {
822 kind: ExprKind::Column(name),
823 ..
824 },
825 alias,
826 ..
827 } = projection
828 else {
829 unreachable!("wildcard handled above");
830 };
831 let value = row.get(&name.name).cloned().unwrap_or(Value::Null);
832 Ok((alias.clone().unwrap_or_else(|| name.name.clone()), value))
833 })
834 .collect()
835 })
836 .collect()
837}
838
839fn table_rows(catalog: &Catalog) -> Vec<Row> {
840 catalog
841 .table_descriptors()
842 .map(|table| {
843 BTreeMap::from([
844 ("table_id".to_string(), u32_value(table.id.0)),
845 ("table_name".to_string(), Value::String(table.name.clone())),
846 (
847 "table_kind".to_string(),
848 Value::String(table_kind_name(table.kind).to_string()),
849 ),
850 (
851 "state".to_string(),
852 Value::String(schema_object_state_name(table.state).to_string()),
853 ),
854 ])
855 })
856 .collect()
857}
858
859fn information_schema_table_rows(state: &RelationalState) -> Vec<Row> {
860 state
861 .table_schemas()
862 .map(|schema| {
863 BTreeMap::from([
864 (
865 "table_catalog".to_string(),
866 Value::String("hawdb".to_string()),
867 ),
868 (
869 "table_schema".to_string(),
870 Value::String("public".to_string()),
871 ),
872 ("table_name".to_string(), Value::String(schema.name.clone())),
873 (
874 "table_type".to_string(),
875 Value::String(
876 if schema.materialized_view.is_some() {
877 "MATERIALIZED VIEW"
878 } else {
879 "BASE TABLE"
880 }
881 .to_string(),
882 ),
883 ),
884 ("self_referencing_column_name".to_string(), Value::Null),
885 ("reference_generation".to_string(), Value::Null),
886 ("user_defined_type_catalog".to_string(), Value::Null),
887 ("user_defined_type_schema".to_string(), Value::Null),
888 ("user_defined_type_name".to_string(), Value::Null),
889 (
890 "is_insertable_into".to_string(),
891 Value::String(
892 if schema.materialized_view.is_some() {
893 "NO"
894 } else {
895 "YES"
896 }
897 .to_string(),
898 ),
899 ),
900 ("is_typed".to_string(), Value::String("NO".to_string())),
901 ("commit_action".to_string(), Value::Null),
902 ])
903 })
904 .collect()
905}
906
907fn vector_column_rows(state: &RelationalState) -> Vec<Row> {
908 state
909 .table_schemas()
910 .flat_map(|schema| {
911 schema.columns.iter().filter_map(move |column| {
912 let RelationalScalarType::Vector(dimension) = column.scalar_type else {
913 return None;
914 };
915 Some(BTreeMap::from([
916 ("table_schema".into(), Value::String("public".into())),
917 ("table_name".into(), Value::String(schema.name.clone())),
918 ("column_name".into(), Value::String(column.name.clone())),
919 ("dimensions".into(), Value::Int(i64::from(dimension.get()))),
920 ("element_type".into(), Value::String("float32".into())),
921 ("nullable".into(), Value::Bool(column.nullable)),
922 ]))
923 })
924 })
925 .collect()
926}
927
928fn information_schema_column_rows(state: &RelationalState) -> Vec<Row> {
929 state
930 .table_schemas()
931 .flat_map(|schema| {
932 schema
933 .visible_columns()
934 .iter()
935 .enumerate()
936 .map(move |(position, column)| {
937 let type_info = information_schema_type(column.scalar_type);
938 let ordinal_position = position.saturating_add(1);
939 BTreeMap::from([
940 (
941 "table_catalog".to_string(),
942 Value::String("hawdb".to_string()),
943 ),
944 (
945 "table_schema".to_string(),
946 Value::String("public".to_string()),
947 ),
948 ("table_name".to_string(), Value::String(schema.name.clone())),
949 (
950 "column_name".to_string(),
951 Value::String(column.name.clone()),
952 ),
953 (
954 "ordinal_position".to_string(),
955 usize_value(ordinal_position),
956 ),
957 (
958 "column_default".to_string(),
959 relational_default_value(column.default.as_ref()),
960 ),
961 (
962 "is_nullable".to_string(),
963 Value::String(if column.nullable { "YES" } else { "NO" }.to_string()),
964 ),
965 (
966 "data_type".to_string(),
967 Value::String(type_info.data_type.to_string()),
968 ),
969 (
970 "character_maximum_length".to_string(),
971 option_i64_value(type_info.character_maximum_length),
972 ),
973 (
974 "character_octet_length".to_string(),
975 option_i64_value(type_info.character_octet_length),
976 ),
977 (
978 "numeric_precision".to_string(),
979 option_i64_value(type_info.numeric_precision),
980 ),
981 (
982 "numeric_precision_radix".to_string(),
983 option_i64_value(type_info.numeric_precision_radix),
984 ),
985 (
986 "numeric_scale".to_string(),
987 option_i64_value(type_info.numeric_scale),
988 ),
989 ("datetime_precision".to_string(), Value::Null),
990 ("interval_type".to_string(), Value::Null),
991 ("interval_precision".to_string(), Value::Null),
992 ("character_set_catalog".to_string(), Value::Null),
993 ("character_set_schema".to_string(), Value::Null),
994 ("character_set_name".to_string(), Value::Null),
995 ("collation_catalog".to_string(), Value::Null),
996 ("collation_schema".to_string(), Value::Null),
997 ("collation_name".to_string(), Value::Null),
998 ("domain_catalog".to_string(), Value::Null),
999 ("domain_schema".to_string(), Value::Null),
1000 ("domain_name".to_string(), Value::Null),
1001 (
1002 "udt_catalog".to_string(),
1003 Value::String("hawdb".to_string()),
1004 ),
1005 (
1006 "udt_schema".to_string(),
1007 Value::String(
1008 if matches!(column.scalar_type, RelationalScalarType::Vector(_)) {
1009 "public"
1010 } else {
1011 "pg_catalog"
1012 }
1013 .to_string(),
1014 ),
1015 ),
1016 (
1017 "udt_name".to_string(),
1018 Value::String(type_info.udt_name.to_string()),
1019 ),
1020 ("scope_catalog".to_string(), Value::Null),
1021 ("scope_schema".to_string(), Value::Null),
1022 ("scope_name".to_string(), Value::Null),
1023 ("maximum_cardinality".to_string(), Value::Null),
1024 (
1025 "dtd_identifier".to_string(),
1026 Value::String(ordinal_position.to_string()),
1027 ),
1028 (
1029 "is_self_referencing".to_string(),
1030 Value::String("NO".to_string()),
1031 ),
1032 ("is_identity".to_string(), Value::String("NO".to_string())),
1033 ("identity_generation".to_string(), Value::Null),
1034 ("identity_start".to_string(), Value::Null),
1035 ("identity_increment".to_string(), Value::Null),
1036 ("identity_maximum".to_string(), Value::Null),
1037 ("identity_minimum".to_string(), Value::Null),
1038 ("identity_cycle".to_string(), Value::Null),
1039 (
1040 "is_generated".to_string(),
1041 Value::String("NEVER".to_string()),
1042 ),
1043 ("generation_expression".to_string(), Value::Null),
1044 ("is_updatable".to_string(), Value::String("YES".to_string())),
1045 ])
1046 })
1047 })
1048 .collect()
1049}
1050
1051#[derive(Debug, Clone, Copy)]
1052struct InformationSchemaType {
1053 data_type: &'static str,
1054 udt_name: &'static str,
1055 character_maximum_length: Option<i64>,
1056 character_octet_length: Option<i64>,
1057 numeric_precision: Option<i64>,
1058 numeric_precision_radix: Option<i64>,
1059 numeric_scale: Option<i64>,
1060}
1061
1062fn information_schema_type(scalar_type: RelationalScalarType) -> InformationSchemaType {
1063 match scalar_type {
1064 RelationalScalarType::Boolean => InformationSchemaType {
1065 data_type: "boolean",
1066 udt_name: "bool",
1067 character_maximum_length: None,
1068 character_octet_length: None,
1069 numeric_precision: None,
1070 numeric_precision_radix: None,
1071 numeric_scale: None,
1072 },
1073 RelationalScalarType::BigInt => InformationSchemaType {
1074 data_type: "bigint",
1075 udt_name: "int8",
1076 character_maximum_length: None,
1077 character_octet_length: None,
1078 numeric_precision: Some(64),
1079 numeric_precision_radix: Some(2),
1080 numeric_scale: Some(0),
1081 },
1082 RelationalScalarType::DoublePrecision => InformationSchemaType {
1083 data_type: "double precision",
1084 udt_name: "float8",
1085 character_maximum_length: None,
1086 character_octet_length: None,
1087 numeric_precision: Some(53),
1088 numeric_precision_radix: Some(2),
1089 numeric_scale: None,
1090 },
1091 RelationalScalarType::Text => InformationSchemaType {
1092 data_type: "text",
1093 udt_name: "text",
1094 character_maximum_length: None,
1095 character_octet_length: None,
1096 numeric_precision: None,
1097 numeric_precision_radix: None,
1098 numeric_scale: None,
1099 },
1100 RelationalScalarType::Bytea => InformationSchemaType {
1101 data_type: "bytea",
1102 udt_name: "bytea",
1103 character_maximum_length: None,
1104 character_octet_length: None,
1105 numeric_precision: None,
1106 numeric_precision_radix: None,
1107 numeric_scale: None,
1108 },
1109 RelationalScalarType::Uuid => InformationSchemaType {
1110 data_type: "uuid",
1111 udt_name: "uuid",
1112 character_maximum_length: None,
1113 character_octet_length: None,
1114 numeric_precision: None,
1115 numeric_precision_radix: None,
1116 numeric_scale: None,
1117 },
1118 RelationalScalarType::Vector(_) => InformationSchemaType {
1119 data_type: "USER-DEFINED",
1120 udt_name: "vector",
1121 character_maximum_length: None,
1122 character_octet_length: None,
1123 numeric_precision: None,
1124 numeric_precision_radix: None,
1125 numeric_scale: None,
1126 },
1127 }
1128}
1129
1130fn relational_default_value(default: Option<&RelationalColumnDefault>) -> Value {
1131 match default {
1132 None | Some(RelationalColumnDefault::Literal(RelationalValue::Overflow(_))) => Value::Null,
1133 Some(RelationalColumnDefault::Literal(RelationalValue::Null)) => {
1134 Value::String("NULL".to_string())
1135 }
1136 Some(RelationalColumnDefault::Literal(RelationalValue::Boolean(value))) => {
1137 Value::String(value.to_string())
1138 }
1139 Some(RelationalColumnDefault::Literal(RelationalValue::BigInt(value))) => {
1140 Value::String(value.to_string())
1141 }
1142 Some(RelationalColumnDefault::Literal(RelationalValue::DoublePrecision(value))) => {
1143 Value::String(value.to_string())
1144 }
1145 Some(RelationalColumnDefault::Literal(RelationalValue::Text(value))) => {
1146 Value::String(format!("'{}'::text", value.replace('\'', "''")))
1147 }
1148 Some(RelationalColumnDefault::Literal(RelationalValue::Bytea(value))) => {
1149 Value::String(format!("'\\x{}'::bytea", encode_hex(value)))
1150 }
1151 Some(RelationalColumnDefault::Literal(RelationalValue::Uuid(value))) => {
1152 Value::String(format!("'{value}'::uuid"))
1153 }
1154 Some(RelationalColumnDefault::Literal(RelationalValue::Vector(value))) => {
1155 Value::String(Value::Vector(value.clone()).to_string())
1156 }
1157 Some(RelationalColumnDefault::UuidV7) => Value::String("uuidv7()".to_string()),
1158 }
1159}
1160
1161fn encode_hex(bytes: &[u8]) -> String {
1162 const HEX: &[u8; 16] = b"0123456789abcdef";
1163 let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
1164 for byte in bytes {
1165 encoded.push(HEX[usize::from(byte >> 4)] as char);
1166 encoded.push(HEX[usize::from(byte & 0x0f)] as char);
1167 }
1168 encoded
1169}
1170
1171fn pg_table_rows(state: &RelationalState) -> Vec<Row> {
1172 state
1173 .table_schemas()
1174 .map(|schema| {
1175 BTreeMap::from([
1176 (
1177 "schemaname".to_string(),
1178 Value::String("public".to_string()),
1179 ),
1180 ("tablename".to_string(), Value::String(schema.name.clone())),
1181 ("tableowner".to_string(), Value::String("hawdb".to_string())),
1182 ("tablespace".to_string(), Value::Null),
1183 (
1184 "hasindexes".to_string(),
1185 Value::Bool(
1186 !schema.primary_key.is_empty()
1187 || !schema.unique_constraints.is_empty()
1188 || !schema.indexes.is_empty(),
1189 ),
1190 ),
1191 ("hasrules".to_string(), Value::Bool(false)),
1192 ("hastriggers".to_string(), Value::Bool(false)),
1193 ("rowsecurity".to_string(), Value::Bool(false)),
1194 ])
1195 })
1196 .collect()
1197}
1198
1199fn pg_index_rows(state: &RelationalState) -> Vec<Row> {
1200 state
1201 .table_schemas()
1202 .flat_map(relational_schema_index_rows)
1203 .collect()
1204}
1205
1206fn relational_schema_index_rows(schema: &RelationalTableSchema) -> Vec<Row> {
1207 let mut rows = Vec::with_capacity(
1208 1usize
1209 .saturating_add(schema.unique_constraints.len())
1210 .saturating_add(schema.indexes.len()),
1211 );
1212 if !schema.primary_key.is_empty() {
1213 let name = format!("{}_pkey", schema.name);
1214 rows.push(pg_index_row(schema, &name, &schema.primary_key, true));
1215 }
1216 rows.extend(schema.unique_constraints.iter().map(|columns| {
1217 let name = format!("{}_{}_key", schema.name, columns.join("_"));
1218 pg_index_row(schema, &name, columns, true)
1219 }));
1220 rows.extend(
1221 schema
1222 .indexes
1223 .iter()
1224 .map(|index| pg_index_row(schema, &index.name, &index.columns, index.unique)),
1225 );
1226 rows
1227}
1228
1229fn pg_index_row(
1230 schema: &RelationalTableSchema,
1231 index_name: &str,
1232 columns: &[String],
1233 unique: bool,
1234) -> Row {
1235 let unique = if unique { "UNIQUE " } else { "" };
1236 let columns = columns
1237 .iter()
1238 .map(|column| quote_postgres_identifier(column))
1239 .collect::<Vec<_>>()
1240 .join(", ");
1241 let index_definition = format!(
1242 "CREATE {unique}INDEX {} ON public.{} USING btree ({columns})",
1243 quote_postgres_identifier(index_name),
1244 quote_postgres_identifier(&schema.name),
1245 );
1246 BTreeMap::from([
1247 (
1248 "schemaname".to_string(),
1249 Value::String("public".to_string()),
1250 ),
1251 ("tablename".to_string(), Value::String(schema.name.clone())),
1252 (
1253 "indexname".to_string(),
1254 Value::String(index_name.to_string()),
1255 ),
1256 ("tablespace".to_string(), Value::Null),
1257 ("indexdef".to_string(), Value::String(index_definition)),
1258 ])
1259}
1260
1261fn quote_postgres_identifier(identifier: &str) -> String {
1262 format!("\"{}\"", identifier.replace('"', "\"\""))
1263}
1264
1265fn property_rows(catalog: &Catalog) -> Vec<Row> {
1266 catalog
1267 .property_descriptors()
1268 .map(|property| {
1269 let table = catalog.table_descriptor(property.table_id);
1270 BTreeMap::from([
1271 ("property_id".to_string(), u32_value(property.id.0)),
1272 ("table_id".to_string(), u32_value(property.table_id.0)),
1273 (
1274 "table_name".to_string(),
1275 table
1276 .map(|table| Value::String(table.name.clone()))
1277 .unwrap_or(Value::Null),
1278 ),
1279 (
1280 "table_kind".to_string(),
1281 table
1282 .map(|table| Value::String(table_kind_name(table.kind).to_string()))
1283 .unwrap_or(Value::Null),
1284 ),
1285 (
1286 "property_name".to_string(),
1287 Value::String(property.name.clone()),
1288 ),
1289 (
1290 "value_type".to_string(),
1291 Value::String(property_type_name(property.value_type).to_string()),
1292 ),
1293 ("nullable".to_string(), Value::Bool(property.nullable)),
1294 (
1295 "state".to_string(),
1296 Value::String(schema_object_state_name(property.state).to_string()),
1297 ),
1298 ])
1299 })
1300 .collect()
1301}
1302
1303fn index_rows(catalog: &Catalog) -> Vec<Row> {
1304 let mut rows = catalog
1305 .property_indexes()
1306 .map(|index| {
1307 BTreeMap::from([
1308 ("index_id".to_string(), u32_value(index.id.0)),
1309 (
1310 "subject_kind".to_string(),
1311 Value::String("node".to_string()),
1312 ),
1313 ("subject_id".to_string(), u32_value(index.label_id.0)),
1314 (
1315 "subject_name".to_string(),
1316 optional_string_value(catalog.label_name(index.label_id)),
1317 ),
1318 (
1319 "index_kind".to_string(),
1320 Value::String(index_kind_name(index.kind).to_string()),
1321 ),
1322 (
1323 "property_names".to_string(),
1324 Value::List(vec![Value::String(index.property.clone())]),
1325 ),
1326 ])
1327 })
1328 .chain(catalog.composite_property_indexes().map(|index| {
1329 BTreeMap::from([
1330 ("index_id".to_string(), u32_value(index.id.0)),
1331 (
1332 "subject_kind".to_string(),
1333 Value::String("node".to_string()),
1334 ),
1335 ("subject_id".to_string(), u32_value(index.label_id.0)),
1336 (
1337 "subject_name".to_string(),
1338 optional_string_value(catalog.label_name(index.label_id)),
1339 ),
1340 (
1341 "index_kind".to_string(),
1342 Value::String("composite_equality".to_string()),
1343 ),
1344 (
1345 "property_names".to_string(),
1346 Value::List(
1347 index
1348 .properties
1349 .iter()
1350 .cloned()
1351 .map(Value::String)
1352 .collect(),
1353 ),
1354 ),
1355 ])
1356 }))
1357 .collect::<Vec<_>>();
1358 rows.sort_by_key(|row| row.get("index_id").cloned());
1359 rows
1360}
1361
1362fn constraint_rows(catalog: &Catalog) -> Vec<Row> {
1363 let constraints = catalog
1364 .unique_constraints()
1365 .chain(catalog.node_property_exists_constraints())
1366 .chain(catalog.relationship_property_exists_constraints())
1367 .chain(catalog.relationship_unique_constraints());
1368 let mut rows = constraints
1369 .map(|constraint| {
1370 let (subject_kind, subject_id, subject_name) = match constraint.subject {
1371 ConstraintSubject::Node(label_id) => (
1372 "node",
1373 label_id.0,
1374 optional_string_value(catalog.label_name(label_id)),
1375 ),
1376 ConstraintSubject::Relationship(rel_type_id) => (
1377 "relationship",
1378 rel_type_id.0,
1379 optional_string_value(catalog.rel_type_name(rel_type_id)),
1380 ),
1381 };
1382 BTreeMap::from([
1383 ("constraint_id".to_string(), u32_value(constraint.id.0)),
1384 (
1385 "subject_kind".to_string(),
1386 Value::String(subject_kind.to_string()),
1387 ),
1388 ("subject_id".to_string(), u32_value(subject_id)),
1389 ("subject_name".to_string(), subject_name),
1390 (
1391 "property_name".to_string(),
1392 Value::String(constraint.property.clone()),
1393 ),
1394 (
1395 "constraint_kind".to_string(),
1396 Value::String(constraint_kind_name(constraint.kind).to_string()),
1397 ),
1398 ])
1399 })
1400 .collect::<Vec<_>>();
1401 rows.sort_by_key(|row| row.get("constraint_id").cloned());
1402 rows
1403}
1404
1405fn runtime_status_rows<Store: SystemSqlStore>(context: &SystemSqlContext<'_, Store>) -> Vec<Row> {
1406 vec![BTreeMap::from([
1407 (
1408 "commit_epoch".to_string(),
1409 u64_value(context.store.commit_epoch()),
1410 ),
1411 (
1412 "read_only".to_string(),
1413 Value::Bool(context.runtime.read_only),
1414 ),
1415 (
1416 "max_read_result_rows".to_string(),
1417 option_usize_value(context.runtime.max_read_result_rows),
1418 ),
1419 (
1420 "max_read_result_payload_bytes".to_string(),
1421 option_usize_value(context.runtime.max_read_result_payload_bytes),
1422 ),
1423 (
1424 "storage_residency_mode".to_string(),
1425 Value::String(
1426 storage_residency_mode_name(context.runtime.storage_residency_mode).to_string(),
1427 ),
1428 ),
1429 (
1430 "relational_index_mode".to_string(),
1431 Value::String(
1432 relational_index_mode_name(context.runtime.relational_index_mode).to_string(),
1433 ),
1434 ),
1435 ])]
1436}
1437
1438fn append_table_rows(state: &AppendState) -> Vec<Row> {
1439 state
1440 .schemas()
1441 .values()
1442 .map(|schema| {
1443 BTreeMap::from([
1444 ("table_name".to_string(), Value::String(schema.name.clone())),
1445 (
1446 "storage_mode".to_string(),
1447 Value::String("strict_append".to_string()),
1448 ),
1449 (
1450 "partition_key".to_string(),
1451 Value::List(
1452 schema
1453 .partition_key
1454 .iter()
1455 .cloned()
1456 .map(Value::String)
1457 .collect(),
1458 ),
1459 ),
1460 (
1461 "order_key".to_string(),
1462 Value::List(
1463 schema
1464 .order_key
1465 .iter()
1466 .cloned()
1467 .map(Value::String)
1468 .collect(),
1469 ),
1470 ),
1471 (
1472 "order_mode".to_string(),
1473 Value::String(
1474 match schema.order_mode {
1475 AppendOrderMode::CallerProvided => "caller_provided",
1476 AppendOrderMode::CommitSequence => "commit_sequence",
1477 }
1478 .to_string(),
1479 ),
1480 ),
1481 (
1482 "generated_order_watermark".to_string(),
1483 state
1484 .generated_order_watermark(&schema.name)
1485 .map_or(Value::Null, Value::Int),
1486 ),
1487 (
1488 "column_count".to_string(),
1489 Value::Int(i64::try_from(schema.columns.len()).unwrap_or(i64::MAX)),
1490 ),
1491 ])
1492 })
1493 .collect()
1494}
1495
1496fn append_storage_rows(store: &impl SystemSqlStore, state: &AppendState) -> Vec<Row> {
1497 let mut report = store.append_storage_residency_report();
1498 report.live_rows = state.live_rows();
1499 report.live_payload_bytes = state.live_payload_bytes();
1500 vec![BTreeMap::from([
1501 (
1502 "canonical_segment_count".to_string(),
1503 Value::Int(i64::try_from(report.canonical_segment_count).unwrap_or(i64::MAX)),
1504 ),
1505 (
1506 "canonical_segment_bytes".to_string(),
1507 Value::Int(i64::try_from(report.canonical_segment_bytes).unwrap_or(i64::MAX)),
1508 ),
1509 (
1510 "resident_segment_payload_bytes".to_string(),
1511 Value::Int(i64::try_from(report.resident_segment_payload_bytes).unwrap_or(i64::MAX)),
1512 ),
1513 (
1514 "resident_descriptor_count".to_string(),
1515 Value::Int(i64::try_from(report.resident_descriptor_count).unwrap_or(i64::MAX)),
1516 ),
1517 (
1518 "live_rows".to_string(),
1519 Value::Int(i64::try_from(report.live_rows).unwrap_or(i64::MAX)),
1520 ),
1521 (
1522 "live_payload_bytes".to_string(),
1523 Value::Int(i64::try_from(report.live_payload_bytes).unwrap_or(i64::MAX)),
1524 ),
1525 ])]
1526}
1527
1528fn runtime_capability_rows(runtime: SystemRuntimeSnapshot) -> Vec<Row> {
1529 [
1530 (
1531 "access_control",
1532 runtime.runtime_capabilities.access_control,
1533 ),
1534 (
1535 "full_text_search",
1536 runtime.runtime_capabilities.full_text_search,
1537 ),
1538 ("vector_search", runtime.runtime_capabilities.vector_search),
1539 (
1540 "graph_analytics",
1541 runtime.runtime_capabilities.graph_analytics,
1542 ),
1543 (
1544 "background_maintenance",
1545 runtime.runtime_capabilities.background_maintenance,
1546 ),
1547 ]
1548 .into_iter()
1549 .map(|(capability, enabled)| {
1550 BTreeMap::from([
1551 (
1552 "capability".to_string(),
1553 Value::String(capability.to_string()),
1554 ),
1555 ("enabled".to_string(), Value::Bool(enabled)),
1556 ])
1557 })
1558 .collect()
1559}
1560
1561fn graph_statistics_rows(catalog: &Catalog, statistics: &GraphStatistics) -> Vec<Row> {
1562 let mut rows = Vec::with_capacity(
1563 2usize
1564 .saturating_add(statistics.label_counts.len())
1565 .saturating_add(statistics.rel_type_counts.len())
1566 .saturating_add(statistics.rel_type_source_counts.len())
1567 .saturating_add(statistics.rel_type_target_counts.len())
1568 .saturating_add(statistics.path_counts.len())
1569 .saturating_add(statistics.path_source_distinct_counts.len())
1570 .saturating_add(statistics.path_target_distinct_counts.len())
1571 .saturating_add(statistics.bounded_path_counts.len())
1572 .saturating_add(statistics.bounded_path_source_distinct_counts.len())
1573 .saturating_add(statistics.bounded_path_target_distinct_counts.len())
1574 .saturating_add(statistics.index_samples.len())
1575 .saturating_add(statistics.property_distinct_counts.len())
1576 .saturating_add(statistics.rel_property_distinct_counts.len())
1577 .saturating_add(statistics.property_histograms.len())
1578 .saturating_add(statistics.rel_property_histograms.len()),
1579 );
1580 rows.push(graph_statistic_count_row(
1581 statistics,
1582 "node_count",
1583 statistics.node_count,
1584 ));
1585 rows.push(graph_statistic_count_row(
1586 statistics,
1587 "relationship_count",
1588 statistics.relationship_count,
1589 ));
1590
1591 for (label_id, count) in &statistics.label_counts {
1592 let mut row = graph_statistic_count_row(statistics, "label_count", *count);
1593 row.insert(
1594 "label_name".to_string(),
1595 optional_string_value(catalog.label_name(*label_id)),
1596 );
1597 rows.push(row);
1598 }
1599 for (rel_type_id, count) in &statistics.rel_type_counts {
1600 let mut row = graph_statistic_count_row(statistics, "relationship_type_count", *count);
1601 row.insert(
1602 "relationship_type_name".to_string(),
1603 optional_string_value(catalog.rel_type_name(*rel_type_id)),
1604 );
1605 rows.push(row);
1606 }
1607 for (kind, counts) in [
1608 (
1609 "relationship_source_count",
1610 &statistics.rel_type_source_counts,
1611 ),
1612 (
1613 "relationship_target_count",
1614 &statistics.rel_type_target_counts,
1615 ),
1616 ] {
1617 for (rel_type_id, count) in counts {
1618 let mut row = graph_statistic_count_row(statistics, kind, *count);
1619 row.insert(
1620 "relationship_type_name".to_string(),
1621 optional_string_value(catalog.rel_type_name(*rel_type_id)),
1622 );
1623 rows.push(row);
1624 }
1625 }
1626 for index in catalog.property_indexes() {
1627 let Some(sample) = statistics.index_samples.get(&index.id) else {
1628 continue;
1629 };
1630 let mut row = graph_statistic_base_row(statistics, "index_sample");
1631 row.insert("index_id".to_string(), u32_value(index.id.0));
1632 row.insert(
1633 "index_kind".to_string(),
1634 Value::String(index_kind_name(index.kind).to_string()),
1635 );
1636 row.insert(
1637 "label_name".to_string(),
1638 optional_string_value(catalog.label_name(index.label_id)),
1639 );
1640 row.insert(
1641 "property_name".to_string(),
1642 Value::String(index.property.clone()),
1643 );
1644 populate_index_sample(&mut row, *sample);
1645 rows.push(row);
1646 }
1647 for index in catalog.composite_property_indexes() {
1648 let Some(sample) = statistics.index_samples.get(&index.id) else {
1649 continue;
1650 };
1651 let mut row = graph_statistic_base_row(statistics, "index_sample");
1652 row.insert("index_id".to_string(), u32_value(index.id.0));
1653 row.insert(
1654 "index_kind".to_string(),
1655 Value::String("composite".to_string()),
1656 );
1657 row.insert(
1658 "label_name".to_string(),
1659 optional_string_value(catalog.label_name(index.label_id)),
1660 );
1661 row.insert(
1662 "index_properties".to_string(),
1663 Value::List(
1664 index
1665 .properties
1666 .iter()
1667 .cloned()
1668 .map(Value::String)
1669 .collect(),
1670 ),
1671 );
1672 populate_index_sample(&mut row, *sample);
1673 rows.push(row);
1674 }
1675 for (kind, counts) in [
1676 ("path_count", &statistics.path_counts),
1677 (
1678 "path_source_distinct_count",
1679 &statistics.path_source_distinct_counts,
1680 ),
1681 (
1682 "path_target_distinct_count",
1683 &statistics.path_target_distinct_counts,
1684 ),
1685 ] {
1686 for ((source_label_id, rel_type_id, target_label_id), count) in counts {
1687 let mut row = graph_statistic_count_row(statistics, kind, *count);
1688 populate_path_statistic_names(
1689 &mut row,
1690 catalog,
1691 *source_label_id,
1692 *rel_type_id,
1693 *target_label_id,
1694 );
1695 rows.push(row);
1696 }
1697 }
1698 for (kind, counts) in [
1699 ("bounded_path_count", &statistics.bounded_path_counts),
1700 (
1701 "bounded_path_source_distinct_count",
1702 &statistics.bounded_path_source_distinct_counts,
1703 ),
1704 (
1705 "bounded_path_target_distinct_count",
1706 &statistics.bounded_path_target_distinct_counts,
1707 ),
1708 ] {
1709 for ((source_label_id, rel_type_id, target_label_id, depth), count) in counts {
1710 let mut row = graph_statistic_count_row(statistics, kind, *count);
1711 populate_path_statistic_names(
1712 &mut row,
1713 catalog,
1714 *source_label_id,
1715 *rel_type_id,
1716 *target_label_id,
1717 );
1718 row.insert("depth".to_string(), usize_value(*depth));
1719 rows.push(row);
1720 }
1721 }
1722 for ((label_id, property_name), count) in &statistics.property_distinct_counts {
1723 let mut row = graph_statistic_count_row(statistics, "node_property_distinct_count", *count);
1724 row.insert(
1725 "label_name".to_string(),
1726 optional_string_value(catalog.label_name(*label_id)),
1727 );
1728 row.insert(
1729 "property_name".to_string(),
1730 Value::String(property_name.clone()),
1731 );
1732 rows.push(row);
1733 }
1734 for ((rel_type_id, property_name), count) in &statistics.rel_property_distinct_counts {
1735 let mut row =
1736 graph_statistic_count_row(statistics, "relationship_property_distinct_count", *count);
1737 row.insert(
1738 "relationship_type_name".to_string(),
1739 optional_string_value(catalog.rel_type_name(*rel_type_id)),
1740 );
1741 row.insert(
1742 "property_name".to_string(),
1743 Value::String(property_name.clone()),
1744 );
1745 rows.push(row);
1746 }
1747 for ((label_id, property_name), histogram) in &statistics.property_histograms {
1748 let mut row = graph_statistic_base_row(statistics, "node_property_histogram");
1749 row.insert(
1750 "label_name".to_string(),
1751 optional_string_value(catalog.label_name(*label_id)),
1752 );
1753 row.insert(
1754 "property_name".to_string(),
1755 Value::String(property_name.clone()),
1756 );
1757 row.insert("histogram".to_string(), Value::List(histogram.clone()));
1758 row.insert(
1759 "sampled".to_string(),
1760 Value::Bool(
1761 statistics
1762 .sampled_property_histograms
1763 .get(&(*label_id, property_name.clone()))
1764 .copied()
1765 .unwrap_or(false),
1766 ),
1767 );
1768 rows.push(row);
1769 }
1770 for ((rel_type_id, property_name), histogram) in &statistics.rel_property_histograms {
1771 let mut row = graph_statistic_base_row(statistics, "relationship_property_histogram");
1772 row.insert(
1773 "relationship_type_name".to_string(),
1774 optional_string_value(catalog.rel_type_name(*rel_type_id)),
1775 );
1776 row.insert(
1777 "property_name".to_string(),
1778 Value::String(property_name.clone()),
1779 );
1780 row.insert("histogram".to_string(), Value::List(histogram.clone()));
1781 row.insert(
1782 "sampled".to_string(),
1783 Value::Bool(
1784 statistics
1785 .sampled_rel_property_histograms
1786 .get(&(*rel_type_id, property_name.clone()))
1787 .copied()
1788 .unwrap_or(false),
1789 ),
1790 );
1791 rows.push(row);
1792 }
1793 rows
1794}
1795
1796fn graph_statistic_count_row(statistics: &GraphStatistics, kind: &str, count: u64) -> Row {
1797 let mut row = graph_statistic_base_row(statistics, kind);
1798 row.insert("count".to_string(), u64_value(count));
1799 row
1800}
1801
1802fn populate_index_sample(row: &mut Row, sample: IndexStatisticsSample) {
1803 row.insert("index_size".to_string(), u64_value(sample.index_size));
1804 row.insert("unique_values".to_string(), u64_value(sample.unique_values));
1805 row.insert("sample_size".to_string(), u64_value(sample.sample_size));
1806 row.insert(
1807 "updates_since_sample".to_string(),
1808 u64_value(sample.updates_since_sample),
1809 );
1810 row.insert("stale".to_string(), Value::Bool(sample.is_stale()));
1811}
1812
1813fn graph_statistic_base_row(statistics: &GraphStatistics, kind: &str) -> Row {
1814 BTreeMap::from([
1815 (
1816 "statistic_kind".to_string(),
1817 Value::String(kind.to_string()),
1818 ),
1819 (
1820 "computed_at_commit_epoch".to_string(),
1821 u64_value(statistics.computed_at_commit_epoch),
1822 ),
1823 (
1824 "advanced_statistics_complete".to_string(),
1825 Value::Bool(statistics.advanced_statistics_complete),
1826 ),
1827 (
1828 "histogram_sample_limit".to_string(),
1829 usize_value(statistics.histogram_sample_limit),
1830 ),
1831 ("label_name".to_string(), Value::Null),
1832 ("relationship_type_name".to_string(), Value::Null),
1833 ("source_label_name".to_string(), Value::Null),
1834 ("target_label_name".to_string(), Value::Null),
1835 ("depth".to_string(), Value::Null),
1836 ("property_name".to_string(), Value::Null),
1837 ("index_id".to_string(), Value::Null),
1838 ("index_kind".to_string(), Value::Null),
1839 ("index_properties".to_string(), Value::Null),
1840 ("index_size".to_string(), Value::Null),
1841 ("unique_values".to_string(), Value::Null),
1842 ("sample_size".to_string(), Value::Null),
1843 ("updates_since_sample".to_string(), Value::Null),
1844 ("stale".to_string(), Value::Null),
1845 ("count".to_string(), Value::Null),
1846 ("histogram".to_string(), Value::Null),
1847 ("sampled".to_string(), Value::Null),
1848 ])
1849}
1850
1851fn populate_path_statistic_names(
1852 row: &mut Row,
1853 catalog: &Catalog,
1854 source_label_id: LabelId,
1855 rel_type_id: RelTypeId,
1856 target_label_id: LabelId,
1857) {
1858 row.insert(
1859 "source_label_name".to_string(),
1860 optional_string_value(catalog.label_name(source_label_id)),
1861 );
1862 row.insert(
1863 "relationship_type_name".to_string(),
1864 optional_string_value(catalog.rel_type_name(rel_type_id)),
1865 );
1866 row.insert(
1867 "target_label_name".to_string(),
1868 optional_string_value(catalog.label_name(target_label_id)),
1869 );
1870}
1871
1872fn projected_graph_rows(statuses: Vec<ProjectedGraphStatus>) -> Vec<Row> {
1873 statuses
1874 .into_iter()
1875 .map(|status| {
1876 BTreeMap::from([
1877 ("name".to_string(), Value::String(status.name)),
1878 (
1879 "node_labels".to_string(),
1880 Value::List(status.node_labels.into_iter().map(Value::String).collect()),
1881 ),
1882 (
1883 "relationship_types".to_string(),
1884 Value::List(status.rel_types.into_iter().map(Value::String).collect()),
1885 ),
1886 (
1887 "projection_epoch".to_string(),
1888 option_u64_value(status.projection_epoch),
1889 ),
1890 (
1891 "commit_epoch".to_string(),
1892 option_u64_value(status.commit_epoch),
1893 ),
1894 (
1895 "node_count".to_string(),
1896 option_usize_value(status.node_count),
1897 ),
1898 (
1899 "edge_count".to_string(),
1900 option_usize_value(status.edge_count),
1901 ),
1902 ("reusable".to_string(), Value::Bool(status.reusable)),
1903 ])
1904 })
1905 .collect()
1906}
1907
1908fn search_projection_changefeed_rows(status: SearchProjectionChangefeedStatus) -> Vec<Row> {
1909 vec![BTreeMap::from([
1910 (
1911 "graph_commit_epoch".to_string(),
1912 u64_value(status.graph_commit_epoch),
1913 ),
1914 (
1915 "resume_floor_commit_epoch".to_string(),
1916 u64_value(status.resume_floor_commit_epoch),
1917 ),
1918 (
1919 "oldest_retained_mutation_id".to_string(),
1920 option_u64_value(
1921 status
1922 .oldest_retained_mutation_id
1923 .map(|id| id.commit_epoch()),
1924 ),
1925 ),
1926 (
1927 "newest_retained_mutation_id".to_string(),
1928 option_u64_value(
1929 status
1930 .newest_retained_mutation_id
1931 .map(|id| id.commit_epoch()),
1932 ),
1933 ),
1934 (
1935 "retained_mutation_count".to_string(),
1936 usize_value(status.retained_mutation_count),
1937 ),
1938 (
1939 "restart_recoverable".to_string(),
1940 Value::Bool(status.restart_recoverable),
1941 ),
1942 ])]
1943}
1944
1945fn optional_string_value(value: Option<&str>) -> Value {
1946 value
1947 .map(|value| Value::String(value.to_string()))
1948 .unwrap_or(Value::Null)
1949}
1950
1951const fn table_kind_name(kind: TableKind) -> &'static str {
1952 match kind {
1953 TableKind::Node => "node",
1954 TableKind::Relationship => "relationship",
1955 }
1956}
1957
1958const fn schema_object_state_name(state: SchemaObjectState) -> &'static str {
1959 match state {
1960 SchemaObjectState::DeleteOnly => "delete_only",
1961 SchemaObjectState::WriteOnly => "write_only",
1962 SchemaObjectState::Backfill => "backfill",
1963 SchemaObjectState::Validating => "validating",
1964 SchemaObjectState::Public => "public",
1965 SchemaObjectState::Gc => "gc",
1966 }
1967}
1968
1969fn property_type_name(value_type: PropertyType) -> std::borrow::Cow<'static, str> {
1970 match value_type {
1971 PropertyType::Any => "any",
1972 PropertyType::Bool => "bool",
1973 PropertyType::Int => "int",
1974 PropertyType::Float => "float",
1975 PropertyType::String => "string",
1976 PropertyType::Text => "text",
1977 PropertyType::List => "list",
1978 PropertyType::Vector(dimension) => return format!("vector({})", dimension.get()).into(),
1979 }
1980 .into()
1981}
1982
1983const fn index_kind_name(kind: IndexKind) -> &'static str {
1984 match kind {
1985 IndexKind::Equality => "equality",
1986 IndexKind::Range => "range",
1987 IndexKind::FullText => "full_text",
1988 }
1989}
1990
1991const fn constraint_kind_name(kind: ConstraintKind) -> &'static str {
1992 match kind {
1993 ConstraintKind::NodePropertyUnique => "node_property_unique",
1994 ConstraintKind::NodePropertyExists => "node_property_exists",
1995 ConstraintKind::RelationshipPropertyUnique => "relationship_property_unique",
1996 ConstraintKind::RelationshipPropertyExists => "relationship_property_exists",
1997 }
1998}
1999
2000fn plan_cache_rows(stats: &PlanCacheStats) -> Vec<Row> {
2001 [
2002 ("max_entries", option_usize_value(stats.max_entries)),
2003 ("entries", usize_value(stats.entries)),
2004 ("hits", u64_value(stats.hits)),
2005 ("misses", u64_value(stats.misses)),
2006 ("admissions", u64_value(stats.admissions)),
2007 ("disabled_misses", u64_value(stats.disabled_misses)),
2008 ("bypasses", u64_value(stats.bypasses)),
2009 ("evictions", u64_value(stats.evictions)),
2010 (
2011 "memory_pressure_events",
2012 u64_value(stats.memory_pressure_events),
2013 ),
2014 ]
2015 .into_iter()
2016 .map(|(metric, value)| {
2017 BTreeMap::from([
2018 ("metric".to_string(), Value::String(metric.to_string())),
2019 ("value".to_string(), value),
2020 ])
2021 })
2022 .collect()
2023}
2024
2025fn slow_query_rows(records: &[SlowQueryRecord]) -> Vec<Row> {
2026 records
2027 .iter()
2028 .map(|record| {
2029 BTreeMap::from([
2030 ("sequence".to_string(), u64_value(record.sequence)),
2031 (
2032 "query_language".to_string(),
2033 Value::String(record.query_language.clone()),
2034 ),
2035 (
2036 "statement_kind".to_string(),
2037 Value::String(record.statement_kind.clone()),
2038 ),
2039 (
2040 "query_digest".to_string(),
2041 Value::String(record.query_digest.clone()),
2042 ),
2043 (
2044 "query_text_hash".to_string(),
2045 Value::String(record.query_text_hash.clone()),
2046 ),
2047 (
2048 "query_text".to_string(),
2049 Value::String(record.query_text.clone()),
2050 ),
2051 (
2052 "started_unix_micros".to_string(),
2053 Value::Int(record.started_unix_micros),
2054 ),
2055 (
2056 "elapsed_micros".to_string(),
2057 Value::Int(record.elapsed_micros),
2058 ),
2059 ("row_count".to_string(), Value::Int(record.row_count)),
2060 ("success".to_string(), Value::Bool(record.success)),
2061 (
2062 "error".to_string(),
2063 record
2064 .error
2065 .as_ref()
2066 .map(|error| Value::String(error.clone()))
2067 .unwrap_or(Value::Null),
2068 ),
2069 (
2070 "slow_log_candidate".to_string(),
2071 Value::Bool(record.slow_log_candidate),
2072 ),
2073 (
2074 "access_control_policy_epoch".to_string(),
2075 record
2076 .access_control_policy_epoch
2077 .map(u64_value)
2078 .unwrap_or(Value::Null),
2079 ),
2080 ])
2081 })
2082 .collect()
2083}
2084
2085fn statement_summary_rows(records: &[StatementSummaryRecord]) -> Vec<Row> {
2086 records
2087 .iter()
2088 .map(|record| {
2089 BTreeMap::from([
2090 ("digest".to_string(), Value::String(record.digest.clone())),
2091 (
2092 "query_language".to_string(),
2093 Value::String(record.query_language.clone()),
2094 ),
2095 (
2096 "query_text".to_string(),
2097 Value::String(record.query_text.clone()),
2098 ),
2099 (
2100 "sample_query_text_hash".to_string(),
2101 Value::String(record.sample_query_text_hash.clone()),
2102 ),
2103 (
2104 "statement_kind".to_string(),
2105 Value::String(record.statement_kind.clone()),
2106 ),
2107 (
2108 "execution_count".to_string(),
2109 Value::Int(record.execution_count),
2110 ),
2111 (
2112 "success_count".to_string(),
2113 Value::Int(record.success_count),
2114 ),
2115 ("error_count".to_string(), Value::Int(record.error_count)),
2116 (
2117 "total_elapsed_micros".to_string(),
2118 Value::Int(record.total_elapsed_micros),
2119 ),
2120 (
2121 "max_elapsed_micros".to_string(),
2122 Value::Int(record.max_elapsed_micros),
2123 ),
2124 (
2125 "avg_elapsed_micros".to_string(),
2126 Value::Int(avg_i64(record.total_elapsed_micros, record.execution_count)),
2127 ),
2128 (
2129 "total_row_count".to_string(),
2130 Value::Int(record.total_row_count),
2131 ),
2132 (
2133 "last_seen_unix_micros".to_string(),
2134 Value::Int(record.last_seen_unix_micros),
2135 ),
2136 (
2137 "last_elapsed_micros".to_string(),
2138 Value::Int(record.last_elapsed_micros),
2139 ),
2140 (
2141 "last_row_count".to_string(),
2142 Value::Int(record.last_row_count),
2143 ),
2144 ("last_success".to_string(), Value::Bool(record.last_success)),
2145 (
2146 "last_error".to_string(),
2147 record
2148 .last_error
2149 .as_ref()
2150 .map(|error| Value::String(error.clone()))
2151 .unwrap_or(Value::Null),
2152 ),
2153 ])
2154 })
2155 .collect()
2156}
2157
2158fn predicate_matches(predicate: &SqlPredicate, row: &Row) -> bool {
2159 match &predicate.kind {
2160 ExprKind::And(left, right) => predicate_matches(left, row) && predicate_matches(right, row),
2161 ExprKind::Or(left, right) => predicate_matches(left, row) || predicate_matches(right, row),
2162 ExprKind::Not(inner) => !predicate_matches(inner, row),
2163 ExprKind::Compare { left, op, right } => system_expression_value(left, row)
2164 .zip(system_expression_value(right, row))
2165 .is_some_and(|(left, right)| compare_values(left, *op, right)),
2166 ExprKind::InList {
2167 left,
2168 values,
2169 negated,
2170 } => {
2171 let matched = system_expression_value(left, row).is_some_and(|left| {
2172 values.iter().any(|value| {
2173 system_expression_value(value, row).is_some_and(|value| left == value)
2174 })
2175 });
2176 matched != *negated
2177 }
2178 ExprKind::Like {
2179 left,
2180 pattern,
2181 case_insensitive,
2182 negated,
2183 escape,
2184 } => {
2185 let Some((Value::String(value), Value::String(pattern))) =
2186 system_expression_value(left, row).zip(system_expression_value(pattern, row))
2187 else {
2188 return false;
2189 };
2190 hawdb_sql::sql_like_matches(value, pattern, *escape, *case_insensitive)
2191 .is_ok_and(|matched| matched != *negated)
2192 }
2193 ExprKind::IsNull {
2194 expression,
2195 negated,
2196 } => {
2197 let matched = system_expression_value(expression, row)
2198 .is_none_or(|value| matches!(value, Value::Null));
2199 matched != *negated
2200 }
2201 _ => false,
2202 }
2203}
2204
2205fn system_expression_value<'a>(expression: &'a Expr, row: &'a Row) -> Option<&'a Value> {
2206 match &expression.kind {
2207 ExprKind::Column(column) => row.get(&column.name),
2208 ExprKind::Value(SqlValue::Literal(value)) => Some(value),
2209 _ => None,
2210 }
2211}
2212
2213fn compare_values(left: &Value, op: SqlComparisonOp, right: &Value) -> bool {
2214 if matches!(left, Value::Null) || matches!(right, Value::Null) {
2215 return false;
2216 }
2217 match op {
2218 SqlComparisonOp::Eq => left == right,
2219 SqlComparisonOp::NotEq => left != right,
2220 SqlComparisonOp::Lt => left < right,
2221 SqlComparisonOp::Lte => left <= right,
2222 SqlComparisonOp::Gt => left > right,
2223 SqlComparisonOp::Gte => left >= right,
2224 }
2225}
2226
2227fn compare_ordered_rows(left: &Row, right: &Row, order_by: &[SqlOrderItem]) -> Ordering {
2228 for item in order_by {
2229 let column = item
2230 .expression
2231 .as_column()
2232 .expect("system ORDER BY columns were validated");
2233 let ordering = left.get(&column.name).cmp(&right.get(&column.name));
2234 let ordering = match item.direction {
2235 SqlOrderDirection::Asc => ordering,
2236 SqlOrderDirection::Desc => ordering.reverse(),
2237 };
2238 if ordering != Ordering::Equal {
2239 return ordering;
2240 }
2241 }
2242 Ordering::Equal
2243}
2244
2245fn system_table(select: &SelectStatement) -> Result<SystemTable> {
2246 let from = select.from_table();
2247 match (from.schema.as_deref(), from.name.as_str()) {
2248 (Some("system"), "tables") => Ok(SystemTable::Tables),
2249 (Some("system"), "vector_columns") => Ok(SystemTable::VectorColumns),
2250 (Some("system"), "properties") => Ok(SystemTable::Properties),
2251 (Some("system"), "indexes") => Ok(SystemTable::Indexes),
2252 (Some("system"), "constraints") => Ok(SystemTable::Constraints),
2253 (Some("system"), "append_tables") => Ok(SystemTable::AppendTables),
2254 (Some("system"), "append_storage") => Ok(SystemTable::AppendStorage),
2255 (Some("system"), "runtime_status") => Ok(SystemTable::RuntimeStatus),
2256 (Some("system"), "runtime_capabilities") => Ok(SystemTable::RuntimeCapabilities),
2257 (Some("system"), "graph_statistics") => Ok(SystemTable::GraphStatistics),
2258 (Some("system"), "projected_graphs") => Ok(SystemTable::ProjectedGraphs),
2259 (Some("system"), "search_projection_changefeed") => {
2260 Ok(SystemTable::SearchProjectionChangefeed)
2261 }
2262 (Some("system"), "plan_cache") => Ok(SystemTable::PlanCache),
2263 (Some("system"), "slow_queries") => Ok(SystemTable::SlowQueries),
2264 (Some("system"), "statement_summary") => Ok(SystemTable::StatementSummary),
2265 (Some("information_schema"), "tables") => Ok(SystemTable::InformationSchemaTables),
2266 (Some("information_schema"), "columns") => Ok(SystemTable::InformationSchemaColumns),
2267 (Some("pg_catalog"), "pg_tables") => Ok(SystemTable::PgTables),
2268 (Some("pg_catalog"), "pg_indexes") => Ok(SystemTable::PgIndexes),
2269 (None, "pg_tables") => Ok(SystemTable::PgTables),
2270 (None, "pg_indexes") => Ok(SystemTable::PgIndexes),
2271 _ => Err(HawDBError::Semantic(format!(
2272 "unknown SQL virtual catalog table {}",
2273 format_table_name(select)
2274 ))),
2275 }
2276}
2277
2278fn validate_projection(table: SystemTable, projection: &[SelectProjection]) -> Result<()> {
2279 for projection in projection {
2280 match projection {
2281 SelectProjection::Wildcard => {}
2282 SelectProjection::Expression {
2283 expression:
2284 Expr {
2285 kind: ExprKind::Column(name),
2286 ..
2287 },
2288 ..
2289 } => validate_column(table, name)?,
2290 SelectProjection::Expression { .. } => {
2291 return Err(HawDBError::Semantic(
2292 "system SQL aggregate expressions are not supported".to_string(),
2293 ));
2294 }
2295 }
2296 }
2297 Ok(())
2298}
2299
2300fn validate_predicate_columns(table: SystemTable, predicate: Option<&SqlPredicate>) -> Result<()> {
2301 let mut result = Ok(());
2302 if let Some(predicate) = predicate {
2303 predicate.visit(&mut |expression| {
2304 if result.is_ok()
2305 && let Some(column) = expression.as_column()
2306 {
2307 result = validate_column(table, column);
2308 }
2309 });
2310 }
2311 result
2312}
2313
2314fn validate_order_columns(table: SystemTable, order_by: &[SqlOrderItem]) -> Result<()> {
2315 for item in order_by {
2316 validate_column(table, item.expression.require_column()?)?;
2317 }
2318 Ok(())
2319}
2320
2321fn validate_column(table: SystemTable, column: &SqlColumnRef) -> Result<()> {
2322 if let Some(qualifier) = &column.qualifier {
2323 let table_name = match table {
2324 SystemTable::Tables => "tables",
2325 SystemTable::VectorColumns => "vector_columns",
2326 SystemTable::Properties => "properties",
2327 SystemTable::Indexes => "indexes",
2328 SystemTable::Constraints => "constraints",
2329 SystemTable::AppendTables => "append_tables",
2330 SystemTable::AppendStorage => "append_storage",
2331 SystemTable::RuntimeStatus => "runtime_status",
2332 SystemTable::RuntimeCapabilities => "runtime_capabilities",
2333 SystemTable::GraphStatistics => "graph_statistics",
2334 SystemTable::ProjectedGraphs => "projected_graphs",
2335 SystemTable::SearchProjectionChangefeed => "search_projection_changefeed",
2336 SystemTable::PlanCache => "plan_cache",
2337 SystemTable::SlowQueries => "slow_queries",
2338 SystemTable::StatementSummary => "statement_summary",
2339 SystemTable::InformationSchemaTables => "tables",
2340 SystemTable::InformationSchemaColumns => "columns",
2341 SystemTable::PgTables => "pg_tables",
2342 SystemTable::PgIndexes => "pg_indexes",
2343 };
2344 if qualifier != table_name {
2345 return Err(HawDBError::Semantic(format!(
2346 "unknown SQL column qualifier {qualifier}"
2347 )));
2348 }
2349 }
2350 if table_columns(table).contains(&column.name.as_str()) {
2351 Ok(())
2352 } else {
2353 Err(HawDBError::Semantic(format!(
2354 "unknown SQL column {}",
2355 column.name
2356 )))
2357 }
2358}
2359
2360fn table_columns(table: SystemTable) -> &'static [&'static str] {
2361 match table {
2362 SystemTable::Tables => &["table_id", "table_name", "table_kind", "state"],
2363 SystemTable::VectorColumns => &[
2364 "table_schema",
2365 "table_name",
2366 "column_name",
2367 "dimensions",
2368 "element_type",
2369 "nullable",
2370 ],
2371 SystemTable::Properties => &[
2372 "property_id",
2373 "table_id",
2374 "table_name",
2375 "table_kind",
2376 "property_name",
2377 "value_type",
2378 "nullable",
2379 "state",
2380 ],
2381 SystemTable::Indexes => &[
2382 "index_id",
2383 "subject_kind",
2384 "subject_id",
2385 "subject_name",
2386 "index_kind",
2387 "property_names",
2388 ],
2389 SystemTable::Constraints => &[
2390 "constraint_id",
2391 "subject_kind",
2392 "subject_id",
2393 "subject_name",
2394 "property_name",
2395 "constraint_kind",
2396 ],
2397 SystemTable::AppendTables => &[
2398 "table_name",
2399 "storage_mode",
2400 "partition_key",
2401 "order_key",
2402 "order_mode",
2403 "generated_order_watermark",
2404 "column_count",
2405 ],
2406 SystemTable::AppendStorage => &[
2407 "canonical_segment_count",
2408 "canonical_segment_bytes",
2409 "resident_segment_payload_bytes",
2410 "resident_descriptor_count",
2411 "live_rows",
2412 "live_payload_bytes",
2413 ],
2414 SystemTable::RuntimeStatus => &[
2415 "commit_epoch",
2416 "read_only",
2417 "max_read_result_rows",
2418 "max_read_result_payload_bytes",
2419 "storage_residency_mode",
2420 "relational_index_mode",
2421 ],
2422 SystemTable::RuntimeCapabilities => &["capability", "enabled"],
2423 SystemTable::GraphStatistics => &[
2424 "statistic_kind",
2425 "computed_at_commit_epoch",
2426 "advanced_statistics_complete",
2427 "histogram_sample_limit",
2428 "label_name",
2429 "relationship_type_name",
2430 "source_label_name",
2431 "target_label_name",
2432 "depth",
2433 "property_name",
2434 "index_id",
2435 "index_kind",
2436 "index_properties",
2437 "index_size",
2438 "unique_values",
2439 "sample_size",
2440 "updates_since_sample",
2441 "stale",
2442 "count",
2443 "histogram",
2444 "sampled",
2445 ],
2446 SystemTable::ProjectedGraphs => &[
2447 "name",
2448 "node_labels",
2449 "relationship_types",
2450 "projection_epoch",
2451 "commit_epoch",
2452 "node_count",
2453 "edge_count",
2454 "reusable",
2455 ],
2456 SystemTable::SearchProjectionChangefeed => &[
2457 "graph_commit_epoch",
2458 "resume_floor_commit_epoch",
2459 "oldest_retained_mutation_id",
2460 "newest_retained_mutation_id",
2461 "retained_mutation_count",
2462 "restart_recoverable",
2463 ],
2464 SystemTable::PlanCache => &["metric", "value"],
2465 SystemTable::SlowQueries => &[
2466 "sequence",
2467 "query_language",
2468 "statement_kind",
2469 "query_digest",
2470 "query_text_hash",
2471 "query_text",
2472 "started_unix_micros",
2473 "elapsed_micros",
2474 "row_count",
2475 "success",
2476 "error",
2477 "slow_log_candidate",
2478 "access_control_policy_epoch",
2479 ],
2480 SystemTable::StatementSummary => &[
2481 "digest",
2482 "query_language",
2483 "query_text",
2484 "sample_query_text_hash",
2485 "statement_kind",
2486 "execution_count",
2487 "success_count",
2488 "error_count",
2489 "total_elapsed_micros",
2490 "max_elapsed_micros",
2491 "avg_elapsed_micros",
2492 "total_row_count",
2493 "last_seen_unix_micros",
2494 "last_elapsed_micros",
2495 "last_row_count",
2496 "last_success",
2497 "last_error",
2498 ],
2499 SystemTable::InformationSchemaTables => &[
2500 "table_catalog",
2501 "table_schema",
2502 "table_name",
2503 "table_type",
2504 "self_referencing_column_name",
2505 "reference_generation",
2506 "user_defined_type_catalog",
2507 "user_defined_type_schema",
2508 "user_defined_type_name",
2509 "is_insertable_into",
2510 "is_typed",
2511 "commit_action",
2512 ],
2513 SystemTable::InformationSchemaColumns => &[
2514 "table_catalog",
2515 "table_schema",
2516 "table_name",
2517 "column_name",
2518 "ordinal_position",
2519 "column_default",
2520 "is_nullable",
2521 "data_type",
2522 "character_maximum_length",
2523 "character_octet_length",
2524 "numeric_precision",
2525 "numeric_precision_radix",
2526 "numeric_scale",
2527 "datetime_precision",
2528 "interval_type",
2529 "interval_precision",
2530 "character_set_catalog",
2531 "character_set_schema",
2532 "character_set_name",
2533 "collation_catalog",
2534 "collation_schema",
2535 "collation_name",
2536 "domain_catalog",
2537 "domain_schema",
2538 "domain_name",
2539 "udt_catalog",
2540 "udt_schema",
2541 "udt_name",
2542 "scope_catalog",
2543 "scope_schema",
2544 "scope_name",
2545 "maximum_cardinality",
2546 "dtd_identifier",
2547 "is_self_referencing",
2548 "is_identity",
2549 "identity_generation",
2550 "identity_start",
2551 "identity_increment",
2552 "identity_maximum",
2553 "identity_minimum",
2554 "identity_cycle",
2555 "is_generated",
2556 "generation_expression",
2557 "is_updatable",
2558 ],
2559 SystemTable::PgTables => &[
2560 "schemaname",
2561 "tablename",
2562 "tableowner",
2563 "tablespace",
2564 "hasindexes",
2565 "hasrules",
2566 "hastriggers",
2567 "rowsecurity",
2568 ],
2569 SystemTable::PgIndexes => &[
2570 "schemaname",
2571 "tablename",
2572 "indexname",
2573 "tablespace",
2574 "indexdef",
2575 ],
2576 }
2577}
2578
2579fn format_table_name(select: &SelectStatement) -> String {
2580 let from = select.from_table();
2581 match &from.schema {
2582 Some(schema) => format!("{schema}.{}", from.name),
2583 None => from.name.clone(),
2584 }
2585}
2586
2587fn option_usize_value(value: Option<usize>) -> Value {
2588 value.map(usize_value).unwrap_or(Value::Null)
2589}
2590
2591fn option_i64_value(value: Option<i64>) -> Value {
2592 value.map(Value::Int).unwrap_or(Value::Null)
2593}
2594
2595fn option_u64_value(value: Option<u64>) -> Value {
2596 value.map(u64_value).unwrap_or(Value::Null)
2597}
2598
2599fn usize_value(value: usize) -> Value {
2600 Value::Int(i64::try_from(value).unwrap_or(i64::MAX))
2601}
2602
2603fn u32_value(value: u32) -> Value {
2604 Value::Int(i64::from(value))
2605}
2606
2607fn u64_value(value: u64) -> Value {
2608 Value::Int(i64::try_from(value).unwrap_or(i64::MAX))
2609}
2610
2611const fn storage_residency_mode_name(mode: StorageResidencyMode) -> &'static str {
2612 match mode {
2613 StorageResidencyMode::Auto => "auto",
2614 StorageResidencyMode::Materialized => "materialized",
2615 StorageResidencyMode::OutOfCore => "out_of_core",
2616 }
2617}
2618
2619const fn relational_index_mode_name(mode: RelationalIndexMode) -> &'static str {
2620 match mode {
2621 RelationalIndexMode::Materialized => "materialized",
2622 RelationalIndexMode::Shadow => "shadow",
2623 RelationalIndexMode::DemandPaged => "demand_paged",
2624 RelationalIndexMode::Authoritative => "authoritative",
2625 }
2626}
2627
2628fn avg_i64(total: i64, count: i64) -> i64 {
2629 if count <= 0 {
2630 0
2631 } else {
2632 total / count
2633 }
2634}
2635
2636fn saturating_i64_from_u128(value: u128) -> i64 {
2637 i64::try_from(value).unwrap_or(i64::MAX)
2638}
2639
2640fn unix_now_micros() -> i64 {
2641 let micros = SystemTime::now()
2642 .duration_since(UNIX_EPOCH)
2643 .map(|duration| duration.as_micros())
2644 .unwrap_or(0);
2645 saturating_i64_from_u128(micros)
2646}
2647
2648fn truncate_utf8(input: &str, max_bytes: usize) -> String {
2649 if input.len() <= max_bytes {
2650 return input.to_string();
2651 }
2652 let mut end = max_bytes;
2653 while !input.is_char_boundary(end) {
2654 end -= 1;
2655 }
2656 input[..end].to_string()
2657}
2658
2659mod graph_store_adapter;
2660
2661#[cfg(test)]
2662mod tests {
2663 use super::*;
2664
2665 #[derive(Debug, Default)]
2666 struct TestStore;
2667
2668 impl SystemSqlStore for TestStore {
2669 fn commit_epoch(&self) -> u64 {
2670 0
2671 }
2672
2673 fn append_storage_residency_report(&self) -> AppendStorageResidencyReport {
2674 AppendStorageResidencyReport::default()
2675 }
2676
2677 fn statistics(&self, _catalog: &Catalog) -> GraphStatistics {
2678 GraphStatistics::default()
2679 }
2680
2681 fn projected_graph_statuses(&self) -> Vec<ProjectedGraphStatus> {
2682 Vec::new()
2683 }
2684
2685 fn search_projection_changefeed_status(&self) -> SearchProjectionChangefeedStatus {
2686 SearchProjectionChangefeedStatus {
2687 graph_commit_epoch: 0,
2688 resume_floor_commit_epoch: 0,
2689 oldest_retained_mutation_id: None,
2690 newest_retained_mutation_id: None,
2691 first_rebuild_required_mutation_id: None,
2692 retained_mutation_count: 0,
2693 retained_bytes: 0,
2694 max_retained_bytes: None,
2695 restart_recoverable: true,
2696 }
2697 }
2698 }
2699
2700 fn default_runtime() -> SystemRuntimeSnapshot {
2701 SystemRuntimeSnapshot::new(
2702 false,
2703 Some(512),
2704 Some(4 * 1024 * 1024),
2705 StorageResidencyMode::Auto,
2706 RelationalIndexMode::default(),
2707 RuntimeCapabilities::default(),
2708 )
2709 }
2710
2711 #[test]
2712 fn predicate_binding_preserves_source_spans_and_system_null_behavior() {
2713 let SqlStatement::Select(select) = hawdb_sql::prepare_postgres_sql(
2714 "SELECT value FROM system.plan_cache WHERE \
2715 (value IN ($1, $2) OR NOT value = $3) AND \
2716 (metric LIKE $4 OR value IS NULL)",
2717 )
2718 .unwrap()
2719 .statement
2720 else {
2721 panic!("expected SELECT");
2722 };
2723 let predicate = select.selection.unwrap();
2724 let mut before = Vec::new();
2725 predicate.visit(&mut |node| before.push(node.span));
2726 let bound = bind_predicate(
2727 predicate.clone(),
2728 &[
2729 Value::Null,
2730 Value::Int(5),
2731 Value::Null,
2732 Value::String("hit%".to_owned()),
2733 ],
2734 )
2735 .unwrap();
2736 let mut after = Vec::new();
2737 let mut parameters = Vec::new();
2738 bound.visit(&mut |node| {
2739 after.push(node.span);
2740 if let ExprKind::Value(SqlValue::Parameter(position)) = &node.kind {
2741 parameters.push(*position);
2742 }
2743 });
2744 assert_eq!(after, before);
2745 assert!(parameters.is_empty());
2746 assert!(predicate_matches(
2747 &bound,
2748 &BTreeMap::from([
2749 ("value".to_owned(), Value::Null),
2750 ("metric".to_owned(), Value::String("misses".to_owned())),
2751 ])
2752 ));
2753 assert!(bind_predicate(
2754 predicate,
2755 &[
2756 Value::Null,
2757 Value::Int(5),
2758 Value::Null,
2759 Value::String("dangling\\".to_owned()),
2760 ]
2761 )
2762 .unwrap_err()
2763 .to_string()
2764 .contains("ends with its escape"));
2765 }
2766
2767 #[test]
2768 fn query_plan_cache_virtual_table_with_predicate_and_projection() {
2769 let catalog = Catalog::default();
2770 let store = TestStore;
2771 let relational_state = RelationalState::default();
2772 let append_state = AppendState::default();
2773 let stats = PlanCacheStats {
2774 max_entries: Some(128),
2775 entries: 3,
2776 hits: 5,
2777 misses: 7,
2778 admissions: 4,
2779 disabled_misses: 0,
2780 bypasses: 2,
2781 evictions: 1,
2782 memory_pressure_events: 1,
2783 };
2784
2785 let context = SystemSqlContext {
2786 catalog: &catalog,
2787 store: &store,
2788 relational_state: &relational_state,
2789 append_state: &append_state,
2790 runtime: default_runtime(),
2791 plan_cache_stats: &stats,
2792 slow_queries: &[],
2793 statement_summaries: &[],
2794 };
2795 let sql = "SELECT value FROM system.plan_cache WHERE metric = 'hits'";
2796 let output = query_sql(sql, None, None, &context).expect("system plan cache query");
2797
2798 assert_eq!(
2799 output.rows,
2800 vec![BTreeMap::from([("value".to_string(), Value::Int(5))])]
2801 );
2802 let payload_bytes = output.payload_bytes();
2803 let exact = query_sql(sql, Some(1), Some(payload_bytes), &context).unwrap();
2804 assert_eq!(exact.rows, output.rows);
2805 for (rows, payload, resource, limit) in [
2806 (Some(0), Some(0), hawdb_core::ReadBudgetResource::Rows, 0),
2807 (
2808 Some(1),
2809 Some(payload_bytes - 1),
2810 hawdb_core::ReadBudgetResource::PayloadBytes,
2811 payload_bytes - 1,
2812 ),
2813 ] {
2814 let error = query_sql(sql, rows, payload, &context).unwrap_err();
2815 assert!(
2816 matches!(&error, HawDBError::ReadBudgetExceeded(cause)
2817 if cause.resource == resource && cause.limit == limit),
2818 "system SQL lost its result budget cause: {error}"
2819 );
2820 }
2821 }
2822
2823 #[test]
2824 fn query_system_table_with_postgres_parameters() {
2825 let catalog = Catalog::default();
2826 let store = TestStore;
2827 let relational_state = RelationalState::default();
2828 let append_state = AppendState::default();
2829 let stats = PlanCacheStats {
2830 max_entries: Some(128),
2831 entries: 3,
2832 hits: 5,
2833 misses: 7,
2834 admissions: 4,
2835 disabled_misses: 0,
2836 bypasses: 2,
2837 evictions: 1,
2838 memory_pressure_events: 1,
2839 };
2840 let context = SystemSqlContext {
2841 catalog: &catalog,
2842 store: &store,
2843 relational_state: &relational_state,
2844 append_state: &append_state,
2845 runtime: default_runtime(),
2846 plan_cache_stats: &stats,
2847 slow_queries: &[],
2848 statement_summaries: &[],
2849 };
2850
2851 let output = query_sql_with_params(
2852 "SELECT metric, value FROM system.plan_cache \
2853 WHERE metric IN ($1, $2) ORDER BY metric ASC LIMIT $3 OFFSET $4",
2854 &[
2855 Value::String("hits".to_string()),
2856 Value::String("misses".to_string()),
2857 Value::Int(1),
2858 Value::Int(1),
2859 ],
2860 None,
2861 None,
2862 &context,
2863 )
2864 .expect("parameterized system table query");
2865
2866 assert_eq!(
2867 output.rows,
2868 vec![BTreeMap::from([
2869 ("metric".to_string(), Value::String("misses".to_string())),
2870 ("value".to_string(), Value::Int(7)),
2871 ])]
2872 );
2873 }
2874
2875 #[test]
2876 fn query_system_table_rejects_parameter_contract_mismatch() {
2877 let catalog = Catalog::default();
2878 let store = TestStore;
2879 let relational_state = RelationalState::default();
2880 let append_state = AppendState::default();
2881 let stats = PlanCacheStats {
2882 max_entries: None,
2883 entries: 0,
2884 hits: 0,
2885 misses: 0,
2886 admissions: 0,
2887 disabled_misses: 0,
2888 bypasses: 0,
2889 evictions: 0,
2890 memory_pressure_events: 0,
2891 };
2892 let context = SystemSqlContext {
2893 catalog: &catalog,
2894 store: &store,
2895 relational_state: &relational_state,
2896 append_state: &append_state,
2897 runtime: default_runtime(),
2898 plan_cache_stats: &stats,
2899 slow_queries: &[],
2900 statement_summaries: &[],
2901 };
2902
2903 let missing = query_sql_with_params(
2904 "SELECT * FROM system.plan_cache WHERE metric = $1",
2905 &[],
2906 None,
2907 None,
2908 &context,
2909 )
2910 .expect_err("missing parameter must fail");
2911 assert!(missing.to_string().contains("requires 1 parameters"));
2912
2913 let invalid_bound = query_sql_with_params(
2914 "SELECT * FROM system.plan_cache LIMIT $1",
2915 &[Value::String("one".to_string())],
2916 None,
2917 None,
2918 &context,
2919 )
2920 .expect_err("non-integer LIMIT must fail");
2921 assert!(invalid_bound
2922 .to_string()
2923 .contains("must be a non-negative integer"));
2924 }
2925
2926 #[test]
2927 fn query_slow_queries_pushes_filter_order_and_limit_into_scan() {
2928 let catalog = Catalog::default();
2929 let store = TestStore;
2930 let relational_state = RelationalState::default();
2931 let append_state = AppendState::default();
2932 let stats = PlanCacheStats {
2933 max_entries: None,
2934 entries: 0,
2935 hits: 0,
2936 misses: 0,
2937 admissions: 0,
2938 disabled_misses: 0,
2939 bypasses: 0,
2940 evictions: 0,
2941 memory_pressure_events: 0,
2942 };
2943 let records = vec![
2944 SlowQueryRecord {
2945 sequence: 1,
2946 query_language: "cypher".to_string(),
2947 statement_kind: "match_return".to_string(),
2948 query_digest: "q1:first".to_string(),
2949 query_text_hash: "t1:first".to_string(),
2950 query_text: "MATCH (m:Memory) RETURN m".to_string(),
2951 started_unix_micros: 10,
2952 elapsed_micros: 200,
2953 row_count: 1,
2954 success: true,
2955 error: None,
2956 slow_log_candidate: false,
2957 access_control_policy_epoch: None,
2958 vector_execution_reports: Vec::new(),
2959 },
2960 SlowQueryRecord {
2961 sequence: 2,
2962 query_language: "cypher".to_string(),
2963 statement_kind: "match_return".to_string(),
2964 query_digest: "q1:second".to_string(),
2965 query_text_hash: "t1:second".to_string(),
2966 query_text: "MATCH (m:Memory) RETURN m ORDER BY m.id".to_string(),
2967 started_unix_micros: 20,
2968 elapsed_micros: 500,
2969 row_count: 2,
2970 success: true,
2971 error: None,
2972 slow_log_candidate: true,
2973 access_control_policy_epoch: None,
2974 vector_execution_reports: Vec::new(),
2975 },
2976 SlowQueryRecord {
2977 sequence: 3,
2978 query_language: "cypher".to_string(),
2979 statement_kind: "match_return".to_string(),
2980 query_digest: "q1:third".to_string(),
2981 query_text_hash: "t1:third".to_string(),
2982 query_text: "MATCH (m:Memory {id: 'x'}) RETURN m".to_string(),
2983 started_unix_micros: 30,
2984 elapsed_micros: 300,
2985 row_count: 1,
2986 success: true,
2987 error: None,
2988 slow_log_candidate: true,
2989 access_control_policy_epoch: None,
2990 vector_execution_reports: Vec::new(),
2991 },
2992 ];
2993
2994 let output = query_sql(
2995 "SELECT sequence, elapsed_micros FROM system.slow_queries \
2996 WHERE slow_log_candidate = true \
2997 ORDER BY elapsed_micros DESC LIMIT 1",
2998 None,
2999 None,
3000 &SystemSqlContext {
3001 catalog: &catalog,
3002 store: &store,
3003 relational_state: &relational_state,
3004 append_state: &append_state,
3005 runtime: default_runtime(),
3006 plan_cache_stats: &stats,
3007 slow_queries: &records,
3008 statement_summaries: &[],
3009 },
3010 )
3011 .expect("system slow query scan");
3012
3013 assert_eq!(
3014 output.rows,
3015 vec![BTreeMap::from([
3016 ("elapsed_micros".to_string(), Value::Int(500)),
3017 ("sequence".to_string(), Value::Int(2)),
3018 ])]
3019 );
3020 }
3021
3022 #[test]
3023 fn query_catalog_virtual_tables_expose_schema_without_typed_getters() {
3024 let mut catalog = Catalog::default();
3025 let store = TestStore;
3026 let relational_state = RelationalState::default();
3027 let append_state = AppendState::default();
3028 let memory_label = catalog.get_or_create_label("Memory");
3029 let memory_table = catalog.get_or_create_table(TableKind::Node, "Memory");
3030 catalog.get_or_create_property(memory_table, "id", PropertyType::String, false);
3031 catalog.get_or_create_property_index_with_kind(memory_label, "id", IndexKind::Equality);
3032 catalog.get_or_create_unique_constraint(memory_label, "id");
3033 let stats = PlanCacheStats {
3034 max_entries: None,
3035 entries: 0,
3036 hits: 0,
3037 misses: 0,
3038 admissions: 0,
3039 disabled_misses: 0,
3040 bypasses: 0,
3041 evictions: 0,
3042 memory_pressure_events: 0,
3043 };
3044 let context = SystemSqlContext {
3045 catalog: &catalog,
3046 store: &store,
3047 relational_state: &relational_state,
3048 append_state: &append_state,
3049 runtime: default_runtime(),
3050 plan_cache_stats: &stats,
3051 slow_queries: &[],
3052 statement_summaries: &[],
3053 };
3054
3055 let tables = query_sql(
3056 "SELECT table_id, table_name, table_kind, state FROM system.tables",
3057 None,
3058 None,
3059 &context,
3060 )
3061 .expect("system tables query");
3062 assert_eq!(
3063 tables.rows,
3064 vec![BTreeMap::from([
3065 ("state".to_string(), Value::String("public".to_string())),
3066 ("table_id".to_string(), Value::Int(0)),
3067 ("table_kind".to_string(), Value::String("node".to_string()),),
3068 (
3069 "table_name".to_string(),
3070 Value::String("Memory".to_string()),
3071 ),
3072 ])]
3073 );
3074
3075 let properties = query_sql(
3076 "SELECT table_name, property_name, value_type, nullable \
3077 FROM system.properties WHERE table_name = 'Memory'",
3078 None,
3079 None,
3080 &context,
3081 )
3082 .expect("system properties query");
3083 assert_eq!(
3084 properties.rows,
3085 vec![BTreeMap::from([
3086 ("nullable".to_string(), Value::Bool(false)),
3087 ("property_name".to_string(), Value::String("id".to_string()),),
3088 (
3089 "table_name".to_string(),
3090 Value::String("Memory".to_string()),
3091 ),
3092 (
3093 "value_type".to_string(),
3094 Value::String("string".to_string()),
3095 ),
3096 ])]
3097 );
3098
3099 let indexes = query_sql(
3100 "SELECT subject_name, index_kind, property_names FROM system.indexes",
3101 None,
3102 None,
3103 &context,
3104 )
3105 .expect("system indexes query");
3106 assert_eq!(
3107 indexes.rows,
3108 vec![BTreeMap::from([
3109 (
3110 "index_kind".to_string(),
3111 Value::String("equality".to_string()),
3112 ),
3113 (
3114 "property_names".to_string(),
3115 Value::List(vec![Value::String("id".to_string())]),
3116 ),
3117 (
3118 "subject_name".to_string(),
3119 Value::String("Memory".to_string()),
3120 ),
3121 ])]
3122 );
3123
3124 let constraints = query_sql(
3125 "SELECT subject_name, property_name, constraint_kind FROM system.constraints",
3126 None,
3127 None,
3128 &context,
3129 )
3130 .expect("system constraints query");
3131 assert_eq!(
3132 constraints.rows,
3133 vec![BTreeMap::from([
3134 (
3135 "constraint_kind".to_string(),
3136 Value::String("node_property_unique".to_string()),
3137 ),
3138 ("property_name".to_string(), Value::String("id".to_string()),),
3139 (
3140 "subject_name".to_string(),
3141 Value::String("Memory".to_string()),
3142 ),
3143 ])]
3144 );
3145 }
3146}
3147
3148#[cfg(all(test, feature = "loom-tests"))]
3149pub(crate) mod loom_tests {
3150 use super::{SlowQueryCompletion, SlowQueryLog, SlowQueryRecord};
3151 use hawdb_query::QueryIdentity;
3152 use loom::sync::{Arc, Mutex};
3153 use loom::thread;
3154
3155 #[test]
3156 fn slow_query_ring_preserves_bounds_under_modeled_concurrent_access() {
3157 loom::model(|| {
3158 let log = Arc::new(Mutex::new(SlowQueryLog::new(2)));
3159
3160 let first_writer = spawn_slow_query_writer(Arc::clone(&log), "first");
3161 let second_writer = spawn_slow_query_writer(Arc::clone(&log), "second");
3162 let snapshotter = {
3163 let log = Arc::clone(&log);
3164 thread::spawn(move || {
3165 let snapshot = log.lock().unwrap().snapshot();
3166 assert_snapshot_invariants(&snapshot, 2);
3167 })
3168 };
3169
3170 first_writer.join().unwrap();
3171 second_writer.join().unwrap();
3172 snapshotter.join().unwrap();
3173
3174 let snapshot = log.lock().unwrap().snapshot();
3175 assert_snapshot_invariants(&snapshot, 2);
3176 assert_eq!(snapshot.len(), 2);
3177 assert_eq!(snapshot.last().map(|record| record.sequence), Some(2));
3178 });
3179 }
3180
3181 fn spawn_slow_query_writer(
3182 log: Arc<Mutex<SlowQueryLog>>,
3183 query_text: &'static str,
3184 ) -> thread::JoinHandle<()> {
3185 thread::spawn(move || {
3186 let query_identity = QueryIdentity::new("cypher", query_text);
3187 log.lock()
3188 .unwrap()
3189 .push(SlowQueryRecord::completed(SlowQueryCompletion {
3190 query_language: "cypher",
3191 statement_kind: "match_return",
3192 query_text,
3193 query_identity: &query_identity,
3194 elapsed_micros: 1,
3195 row_count: 1,
3196 success: true,
3197 error: None,
3198 slow_log_candidate: true,
3199 access_control_policy_epoch: None,
3200 vector_execution_reports: Vec::new(),
3201 }));
3202 })
3203 }
3204
3205 fn assert_snapshot_invariants(snapshot: &[SlowQueryRecord], capacity: usize) {
3206 assert!(snapshot.len() <= capacity);
3207 for pair in snapshot.windows(2) {
3208 assert!(pair[0].sequence < pair[1].sequence);
3209 }
3210 }
3211}