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