1#![forbid(unsafe_code)]
2pub mod synthetic;
3
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::fs;
6use std::path::PathBuf;
7
8use asupersync::Cx;
9use chrono::{DateTime, Utc};
10use plsql_core::{
11 AnalysisProfile, ColumnName, EditionName, MemberName, ObjectName, OracleVersion, RoleName,
12 SchemaName, SymbolId, SymbolInterner, UserName,
13};
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use tracing::instrument;
17
18use plsql_output::SchemaVersion;
19
20macro_rules! catalog_name {
21 ($name:ident) => {
22 #[derive(
23 Clone,
24 Copy,
25 Debug,
26 Default,
27 Eq,
28 PartialEq,
29 Ord,
30 PartialOrd,
31 Hash,
32 Serialize,
33 Deserialize,
34 )]
35 #[serde(transparent)]
36 pub struct $name(SymbolId);
37
38 impl $name {
39 #[must_use]
40 #[instrument(level = "trace")]
41 pub fn new(symbol: SymbolId) -> Self {
42 Self(symbol)
43 }
44
45 #[must_use]
46 #[instrument(level = "trace", skip(self))]
47 pub fn symbol(self) -> SymbolId {
48 self.0
49 }
50 }
51
52 impl From<SymbolId> for $name {
53 fn from(value: SymbolId) -> Self {
54 Self::new(value)
55 }
56 }
57 };
58}
59
60catalog_name!(SynonymName);
61catalog_name!(IndexName);
62catalog_name!(ConstraintName);
63catalog_name!(TriggerName);
64
65#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
66#[serde(transparent)]
67pub struct Hash(String);
68
69impl Hash {
70 #[must_use]
71 #[instrument(level = "trace", skip(value))]
72 pub fn new(value: impl Into<String>) -> Self {
73 Self(value.into())
74 }
75
76 #[must_use]
77 #[instrument(level = "trace", skip(self))]
78 pub fn as_str(&self) -> &str {
79 self.0.as_str()
80 }
81}
82
83#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
84pub struct DbmsMetadataDdl {
85 pub ddl_text: String,
86 pub normalized_ddl: Option<String>,
87 pub xml_text: Option<String>,
88}
89
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
91pub enum CatalogSourceKind {
92 #[default]
93 JsonSnapshot,
94 LiveConnection,
95 DbmsMetadataFiles,
96 SyntheticTestCatalog,
97}
98
99#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
100pub struct CatalogSource {
101 pub kind: CatalogSourceKind,
102 pub path: Option<PathBuf>,
103 pub description: Option<String>,
104}
105
106#[derive(
107 Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
108)]
109pub enum ObjectType {
110 Table,
111 View,
112 MaterializedView,
113 Sequence,
114 Type,
115 Package,
116 Procedure,
117 Function,
118 Trigger,
119 SchedulerJob,
120 EditioningView,
121 Synonym,
122 Index,
123 Constraint,
124 #[default]
125 Unknown,
126}
127
128#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
129pub enum ObjectStatus {
130 Valid,
131 Invalid,
132 #[default]
133 NotApplicable,
134}
135
136#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
137pub struct ObjectCommon {
138 pub owner: SchemaName,
139 pub name: ObjectName,
140 pub object_type: ObjectType,
141 pub status: ObjectStatus,
142 pub edition_name: Option<EditionName>,
143 pub editionable: Option<bool>,
144 pub last_ddl_time: Option<DateTime<Utc>>,
145 pub source_hash: Option<Hash>,
146 pub ddl: Option<DbmsMetadataDdl>,
147}
148
149#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
150pub struct CatalogCapabilities {
151 pub can_query_dba_views: bool,
152 pub can_query_all_views: bool,
153 pub can_use_dbms_metadata: bool,
154 pub can_read_source: bool,
155 pub plscope_enabled: bool,
156 pub can_query_scheduler: bool,
157 pub can_query_roles_and_grants: bool,
158 pub warnings: Vec<CapabilityWarning>,
159}
160
161#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
162pub struct CapabilityWarning {
163 pub code: String,
164 pub message: String,
165 pub remediation: Option<String>,
166}
167
168#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
169pub struct CatalogSnapshot {
170 pub schemas: HashMap<SchemaName, SchemaCatalog>,
171 pub profile: AnalysisProfile,
172 pub capabilities: CatalogCapabilities,
173 pub generated_at: DateTime<Utc>,
174 pub source: CatalogSource,
175 pub interner: SymbolInterner,
176 #[serde(default)]
179 pub editions: Vec<Edition>,
180 #[serde(default, skip)]
196 pub known_users: Option<HashSet<UserName>>,
197}
198
199impl CatalogSnapshot {
200 #[must_use]
201 #[instrument(level = "trace", skip(profile, capabilities, source))]
202 pub fn new(
203 profile: AnalysisProfile,
204 capabilities: CatalogCapabilities,
205 source: CatalogSource,
206 generated_at: DateTime<Utc>,
207 ) -> Self {
208 Self {
209 schemas: HashMap::new(),
210 profile,
211 capabilities,
212 generated_at,
213 source,
214 interner: SymbolInterner::new(),
215 editions: Vec::new(),
216 known_users: None,
217 }
218 }
219
220 #[must_use]
224 #[instrument(level = "trace", skip(self, text))]
225 pub fn intern_user_name(&mut self, text: impl Into<String>) -> Option<UserName> {
226 self.interner.intern_user_name(text)
227 }
228
229 #[must_use]
231 #[instrument(level = "trace", skip(self, text))]
232 pub fn intern_role_name(&mut self, text: impl Into<String>) -> Option<RoleName> {
233 self.interner.intern_role_name(text)
234 }
235
236 #[must_use]
237 #[instrument(level = "trace", skip(self, text))]
238 pub fn intern_schema_name(&mut self, text: impl Into<String>) -> Option<SchemaName> {
239 self.interner.intern_schema_name(text)
240 }
241
242 #[must_use]
243 #[instrument(level = "trace", skip(self, text))]
244 pub fn intern_object_name(&mut self, text: impl Into<String>) -> Option<ObjectName> {
245 self.interner.intern(text).map(ObjectName::from)
246 }
247
248 #[must_use]
249 #[instrument(level = "trace", skip(self, text))]
250 pub fn intern_column_name(&mut self, text: impl Into<String>) -> Option<ColumnName> {
251 self.interner.intern(text).map(ColumnName::from)
252 }
253
254 #[must_use]
255 #[instrument(level = "trace", skip(self, text))]
256 pub fn intern_member_name(&mut self, text: impl Into<String>) -> Option<MemberName> {
257 self.interner.intern(text).map(MemberName::from)
258 }
259
260 #[must_use]
261 #[instrument(level = "trace", skip(self, text))]
262 pub fn intern_synonym_name(&mut self, text: impl Into<String>) -> Option<SynonymName> {
263 self.interner.intern(text).map(SynonymName::from)
264 }
265
266 #[must_use]
267 #[instrument(level = "trace", skip(self, text))]
268 pub fn intern_index_name(&mut self, text: impl Into<String>) -> Option<IndexName> {
269 self.interner.intern(text).map(IndexName::from)
270 }
271
272 #[must_use]
273 #[instrument(level = "trace", skip(self, text))]
274 pub fn intern_constraint_name(&mut self, text: impl Into<String>) -> Option<ConstraintName> {
275 self.interner.intern(text).map(ConstraintName::from)
276 }
277
278 #[must_use]
279 #[instrument(level = "trace", skip(self, text))]
280 pub fn intern_trigger_name(&mut self, text: impl Into<String>) -> Option<TriggerName> {
281 self.interner.intern(text).map(TriggerName::from)
282 }
283}
284
285pub const CATALOG_SNAPSHOT_SCHEMA_ID: &str = "plsql.catalog.snapshot";
286pub const CATALOG_SNAPSHOT_SCHEMA_VERSION: SchemaVersion = SchemaVersion::new(1, 1, 0);
287
288pub const CATALOG_DOCTOR_SCHEMA_ID: &str = "plsql.catalog.doctor";
289pub const CATALOG_DOCTOR_SCHEMA_VERSION: SchemaVersion = SchemaVersion::new(1, 0, 0);
290
291#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
293pub struct DoctorObjectCount {
294 pub object_type: ObjectType,
295 pub total: usize,
296 pub valid: usize,
297 pub invalid: usize,
298 pub other: usize,
299}
300
301#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
304pub struct DoctorExtractionTotals {
305 pub schemas_observed: usize,
306 pub objects_total: usize,
307 pub columns_total: usize,
308 pub indexes_total: usize,
309 pub constraints_total: usize,
310 pub triggers_total: usize,
311 pub synonyms_total: usize,
312 pub grants_total: usize,
313 pub dependencies_total: usize,
314}
315
316#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
319pub struct MissingPermissionReport {
320 pub view_name: String,
321 pub required_for: Vec<String>,
322 pub suggested_grant: String,
323}
324
325#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
332pub struct CatalogDoctorReport {
333 pub source_description: String,
336 pub source_kind: CatalogSourceKind,
337 pub generated_at: Option<DateTime<Utc>>,
338 pub totals: DoctorExtractionTotals,
339 pub object_counts: Vec<DoctorObjectCount>,
340 pub capability_warnings: Vec<CapabilityWarning>,
341 pub missing_permissions: Vec<MissingPermissionReport>,
342 pub plscope_availability_per_schema: Vec<PlScopeAvailabilityRow>,
345 pub can_query_dba_views: bool,
348 pub can_query_all_views: bool,
349 pub can_use_dbms_metadata: bool,
350 pub can_read_source: bool,
351 pub plscope_enabled: bool,
352 pub can_query_scheduler: bool,
353 pub can_query_roles_and_grants: bool,
354}
355
356#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
360pub struct PlScopeAvailabilityRow {
361 pub schema_name: String,
362 pub availability: PlScopeAvailability,
363}
364
365impl CatalogSnapshot {
366 #[must_use]
374 #[instrument(level = "trace", skip(self))]
375 pub fn doctor_report(&self) -> CatalogDoctorReport {
376 let mut counts: BTreeMap<ObjectType, DoctorObjectCount> = BTreeMap::new();
377 let mut columns_total = 0usize;
378 let mut indexes_total = 0usize;
379 let mut constraints_total = 0usize;
380 let mut triggers_total = 0usize;
381 let mut synonyms_total = 0usize;
382 let mut grants_total = 0usize;
383 let mut dependencies_total = 0usize;
384 let mut objects_total = 0usize;
385
386 for schema_catalog in self.schemas.values() {
387 for object in schema_catalog.objects.values() {
388 let common = catalog_object_common(object);
389 let tile = counts
390 .entry(common.object_type)
391 .or_insert(DoctorObjectCount {
392 object_type: common.object_type,
393 ..DoctorObjectCount::default()
394 });
395 tile.total = tile.total.saturating_add(1);
396 match common.status {
397 ObjectStatus::Valid => {
398 tile.valid = tile.valid.saturating_add(1);
399 }
400 ObjectStatus::Invalid => {
401 tile.invalid = tile.invalid.saturating_add(1);
402 }
403 ObjectStatus::NotApplicable => {
404 tile.other = tile.other.saturating_add(1);
405 }
406 }
407 objects_total = objects_total.saturating_add(1);
408
409 columns_total = columns_total.saturating_add(catalog_object_column_count(object));
410 }
411 indexes_total = indexes_total.saturating_add(schema_catalog.indexes.len());
412 constraints_total = constraints_total.saturating_add(schema_catalog.constraints.len());
413 triggers_total = triggers_total.saturating_add(schema_catalog.triggers.len());
414 synonyms_total = synonyms_total.saturating_add(schema_catalog.synonyms.len());
415 grants_total = grants_total.saturating_add(schema_catalog.grants.len());
416 dependencies_total =
417 dependencies_total.saturating_add(schema_catalog.dependencies.len());
418 }
419
420 let totals = DoctorExtractionTotals {
421 schemas_observed: self.schemas.len(),
422 objects_total,
423 columns_total,
424 indexes_total,
425 constraints_total,
426 triggers_total,
427 synonyms_total,
428 grants_total,
429 dependencies_total,
430 };
431
432 let mut object_counts: Vec<DoctorObjectCount> = counts.into_values().collect();
433 object_counts.sort_by_key(|tile| std::cmp::Reverse(tile.total));
434
435 let missing_permissions =
436 derive_missing_permission_reports(&self.capabilities, &self.source);
437
438 let mut plscope_availability_per_schema: Vec<PlScopeAvailabilityRow> = self
439 .schemas
440 .iter()
441 .filter_map(|(owner, schema_catalog)| {
442 let availability = schema_catalog.plscope.as_ref()?.availability;
443 let schema_name = self.interner.resolve(owner.symbol())?.to_string();
444 Some(PlScopeAvailabilityRow {
445 schema_name,
446 availability,
447 })
448 })
449 .collect();
450 plscope_availability_per_schema.sort_by(|a, b| a.schema_name.cmp(&b.schema_name));
451
452 CatalogDoctorReport {
453 source_description: self.source.description.clone().unwrap_or_default(),
454 source_kind: self.source.kind,
455 generated_at: Some(self.generated_at),
456 totals,
457 object_counts,
458 capability_warnings: self.capabilities.warnings.clone(),
459 missing_permissions,
460 plscope_availability_per_schema,
461 can_query_dba_views: self.capabilities.can_query_dba_views,
462 can_query_all_views: self.capabilities.can_query_all_views,
463 can_use_dbms_metadata: self.capabilities.can_use_dbms_metadata,
464 can_read_source: self.capabilities.can_read_source,
465 plscope_enabled: self.capabilities.plscope_enabled,
466 can_query_scheduler: self.capabilities.can_query_scheduler,
467 can_query_roles_and_grants: self.capabilities.can_query_roles_and_grants,
468 }
469 }
470}
471
472fn catalog_object_common(object: &CatalogObject) -> &ObjectCommon {
473 match object {
474 CatalogObject::Table(metadata) => &metadata.common,
475 CatalogObject::View(metadata) => &metadata.common,
476 CatalogObject::MaterializedView(metadata) => &metadata.common,
477 CatalogObject::Sequence(metadata) => &metadata.common,
478 CatalogObject::Type(metadata) => &metadata.common,
479 CatalogObject::Package(metadata) => &metadata.common,
480 CatalogObject::Procedure(metadata) => &metadata.common,
481 CatalogObject::Function(metadata) => &metadata.common,
482 CatalogObject::Trigger(metadata) => &metadata.common,
483 CatalogObject::SchedulerJob(metadata) => &metadata.common,
484 CatalogObject::EditioningView(metadata) => &metadata.common,
485 }
486}
487
488fn catalog_object_column_count(object: &CatalogObject) -> usize {
489 match object {
490 CatalogObject::Table(metadata) => metadata.columns.len(),
491 CatalogObject::View(metadata) => metadata.columns.len(),
492 CatalogObject::MaterializedView(metadata) => metadata.columns.len(),
493 CatalogObject::EditioningView(metadata) => metadata.columns.len(),
494 CatalogObject::Sequence(_)
495 | CatalogObject::Type(_)
496 | CatalogObject::Package(_)
497 | CatalogObject::Procedure(_)
498 | CatalogObject::Function(_)
499 | CatalogObject::Trigger(_)
500 | CatalogObject::SchedulerJob(_) => 0,
501 }
502}
503
504fn derive_missing_permission_reports(
505 capabilities: &CatalogCapabilities,
506 source: &CatalogSource,
507) -> Vec<MissingPermissionReport> {
508 if !matches!(source.kind, CatalogSourceKind::LiveConnection) {
513 return Vec::new();
514 }
515
516 let mut reports = Vec::new();
517 if !capabilities.can_query_dba_views {
518 reports.push(MissingPermissionReport {
519 view_name: String::from("DBA_OBJECTS / DBA_TAB_COLUMNS / DBA_DEPENDENCIES"),
520 required_for: vec![
521 String::from("cross-schema extraction beyond ALL_*"),
522 String::from("PLSQL-CAT-014 dependency reachability over schemas"),
523 ],
524 suggested_grant: String::from(
525 "grant select_catalog_role to <user>; -- or individual grants on DBA_* views",
526 ),
527 });
528 }
529 if !capabilities.can_use_dbms_metadata {
530 reports.push(MissingPermissionReport {
531 view_name: String::from("DBMS_METADATA"),
532 required_for: vec![
533 String::from("PLSQL-CAT-015 DBMS_METADATA.GET_DDL extraction"),
534 String::from("normalized DDL hashes for `what-breaks`"),
535 ],
536 suggested_grant: String::from("grant execute on DBMS_METADATA to <user>;"),
537 });
538 }
539 if !capabilities.can_read_source {
540 reports.push(MissingPermissionReport {
541 view_name: String::from("ALL_SOURCE / DBA_SOURCE"),
542 required_for: vec![
543 String::from("packaged routine body inspection"),
544 String::from("get_object_source MCP tool"),
545 ],
546 suggested_grant: String::from(
547 "grant select on ALL_SOURCE to <user>; -- ALL_SOURCE itself is normally readable; ensure no DROP/REVOKE narrowed it",
548 ),
549 });
550 }
551 if !capabilities.plscope_enabled {
552 reports.push(MissingPermissionReport {
553 view_name: String::from("PLSCOPE_SETTINGS / ALL_IDENTIFIERS"),
554 required_for: vec![
555 String::from("PLSQL-CAT-010 PL/Scope availability detection"),
556 String::from("PLSQL-CAT-011 identifier extraction"),
557 ],
558 suggested_grant: String::from(
559 "alter session set plscope_settings = 'identifiers:all'; -- and recompile target objects",
560 ),
561 });
562 }
563 if !capabilities.can_query_scheduler {
564 reports.push(MissingPermissionReport {
565 view_name: String::from("ALL_SCHEDULER_JOBS / ALL_SCHEDULER_PROGRAMS"),
566 required_for: vec![String::from("scheduler job lineage edges")],
567 suggested_grant: String::from(
568 "grant select on ALL_SCHEDULER_JOBS to <user>; grant select on ALL_SCHEDULER_PROGRAMS to <user>;",
569 ),
570 });
571 }
572 if !capabilities.can_query_roles_and_grants {
573 reports.push(MissingPermissionReport {
574 view_name: String::from("DBA_ROLE_PRIVS / DBA_SYS_PRIVS / DBA_TAB_PRIVS"),
575 required_for: vec![
576 String::from("definer-rights privilege chain analysis"),
577 String::from("role-mediated execution evidence (PRIVILEGES-* beads)"),
578 ],
579 suggested_grant: String::from(
580 "grant select_catalog_role to <user>; -- enables DBA_*_PRIVS reads",
581 ),
582 });
583 }
584 reports
585}
586
587#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
588pub struct CatalogSnapshotDocument {
589 pub schema_id: String,
590 pub schema_version: SchemaVersion,
591 pub snapshot: CatalogSnapshot,
592}
593
594impl CatalogSnapshotDocument {
595 #[must_use]
596 #[instrument(level = "trace", skip(snapshot))]
597 pub fn new(snapshot: CatalogSnapshot) -> Self {
598 Self {
599 schema_id: String::from(CATALOG_SNAPSHOT_SCHEMA_ID),
600 schema_version: CATALOG_SNAPSHOT_SCHEMA_VERSION,
601 snapshot,
602 }
603 }
604}
605
606#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
607pub struct CatalogLoadRequest {
608 pub schema_filters: Vec<CatalogSchemaFilter>,
609}
610
611impl CatalogLoadRequest {
612 #[must_use]
613 #[instrument(level = "trace")]
614 pub fn for_current_schema() -> Self {
615 Self {
616 schema_filters: vec![CatalogSchemaFilter::CurrentSchema],
617 }
618 }
619
620 #[must_use]
621 #[instrument(level = "trace", skip(schema_names))]
622 pub fn for_named_schemas<I, S>(schema_names: I) -> Self
623 where
624 I: IntoIterator<Item = S>,
625 S: Into<String>,
626 {
627 Self {
628 schema_filters: schema_names
629 .into_iter()
630 .map(CatalogSchemaFilter::named)
631 .collect(),
632 }
633 }
634}
635
636impl Default for CatalogLoadRequest {
637 fn default() -> Self {
638 Self::for_current_schema()
639 }
640}
641
642#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
643pub enum CatalogSchemaFilter {
644 CurrentSchema,
645 Named(String),
646}
647
648impl CatalogSchemaFilter {
649 #[must_use]
650 #[instrument(level = "trace")]
651 pub fn current_schema() -> Self {
652 Self::CurrentSchema
653 }
654
655 #[must_use]
656 #[instrument(level = "trace", skip(schema_name))]
657 pub fn named(schema_name: impl Into<String>) -> Self {
658 Self::Named(schema_name.into())
659 }
660}
661
662#[derive(Debug, Error)]
663pub enum CatalogError {
664 #[error("i/o error: {0}")]
665 Io(#[from] std::io::Error),
666 #[error("json error: {0}")]
667 Json(#[from] serde_json::Error),
668 #[error("oracle backend `{backend}` is unavailable in this build; use `{feature}`")]
669 OracleBackendNotCompiled {
670 backend: OracleBackend,
671 feature: &'static str,
672 },
673 #[error("oracle backend `{backend}` error: {message}")]
674 OracleBackendError {
675 backend: OracleBackend,
676 message: String,
677 },
678 #[error("expected {expected} row(s) but received {actual}")]
679 UnexpectedRowCount { expected: String, actual: usize },
680 #[error("required column `{column}` was missing from the query result")]
681 MissingColumn { column: String },
682 #[error("column `{column}` was null")]
683 NullColumnValue { column: String },
684 #[error("column `{column}` could not be parsed as {expected}: `{value}`")]
685 InvalidColumnValue {
686 column: String,
687 expected: &'static str,
688 value: String,
689 },
690 #[error("unsupported catalog snapshot schema {found} for {schema_id}; expected {expected}")]
691 UnsupportedSchemaVersion {
692 schema_id: String,
693 found: SchemaVersion,
694 expected: SchemaVersion,
695 },
696 #[error("unexpected catalog snapshot schema id `{0}`")]
697 UnexpectedSchemaId(String),
698 #[error("catalog load request could not resolve the current schema from the Oracle connection")]
699 CurrentSchemaUnavailable,
700 #[error("schema filter `{schema_name}` is invalid: schema names must not be blank")]
701 InvalidSchemaFilter { schema_name: String },
702}
703
704#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
705pub enum OracleBackend {
706 RustOracle,
707 OracleRs,
708}
709
710impl OracleBackend {
711 #[must_use]
712 #[instrument(level = "trace", skip(self))]
713 pub fn as_str(self) -> &'static str {
714 match self {
715 Self::RustOracle => "oracle",
716 Self::OracleRs => "oracle-rs",
717 }
718 }
719}
720
721impl std::fmt::Display for OracleBackend {
722 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723 f.write_str(self.as_str())
724 }
725}
726
727#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
728pub struct OracleConnectOptions {
729 pub username: String,
730 pub password: String,
731 pub connect_string: String,
732 pub current_schema: Option<String>,
733 pub module: Option<String>,
734 pub action: Option<String>,
735 pub client_info: Option<String>,
736 pub client_identifier: Option<String>,
737}
738
739impl OracleConnectOptions {
740 #[must_use]
741 pub fn new(
742 username: impl Into<String>,
743 password: impl Into<String>,
744 connect_string: impl Into<String>,
745 ) -> Self {
746 Self {
747 username: username.into(),
748 password: password.into(),
749 connect_string: connect_string.into(),
750 current_schema: None,
751 module: None,
752 action: None,
753 client_info: None,
754 client_identifier: None,
755 }
756 }
757
758 #[must_use]
759 pub fn with_current_schema(mut self, current_schema: impl Into<String>) -> Self {
760 self.current_schema = Some(current_schema.into());
761 self
762 }
763
764 #[must_use]
765 pub fn with_module(mut self, module: impl Into<String>) -> Self {
766 self.module = Some(module.into());
767 self
768 }
769
770 #[must_use]
771 pub fn with_action(mut self, action: impl Into<String>) -> Self {
772 self.action = Some(action.into());
773 self
774 }
775
776 #[must_use]
777 pub fn with_client_info(mut self, client_info: impl Into<String>) -> Self {
778 self.client_info = Some(client_info.into());
779 self
780 }
781
782 #[must_use]
783 pub fn with_client_identifier(mut self, client_identifier: impl Into<String>) -> Self {
784 self.client_identifier = Some(client_identifier.into());
785 self
786 }
787}
788
789#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
790pub enum OracleBind {
791 String(String),
792 I64(i64),
793 U64(u64),
794 Bool(bool),
795}
796
797impl From<&str> for OracleBind {
798 fn from(value: &str) -> Self {
799 Self::String(String::from(value))
800 }
801}
802
803impl From<String> for OracleBind {
804 fn from(value: String) -> Self {
805 Self::String(value)
806 }
807}
808
809impl From<i32> for OracleBind {
810 fn from(value: i32) -> Self {
811 Self::I64(i64::from(value))
812 }
813}
814
815impl From<i64> for OracleBind {
816 fn from(value: i64) -> Self {
817 Self::I64(value)
818 }
819}
820
821impl From<u32> for OracleBind {
822 fn from(value: u32) -> Self {
823 Self::U64(u64::from(value))
824 }
825}
826
827impl From<u64> for OracleBind {
828 fn from(value: u64) -> Self {
829 Self::U64(value)
830 }
831}
832
833impl From<bool> for OracleBind {
834 fn from(value: bool) -> Self {
835 Self::Bool(value)
836 }
837}
838
839#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
840pub struct OracleCell {
841 pub oracle_type: String,
842 pub value: Option<String>,
843}
844
845impl OracleCell {
846 #[must_use]
847 #[instrument(level = "trace", skip(oracle_type, value))]
848 pub fn new(oracle_type: impl Into<String>, value: Option<String>) -> Self {
849 Self {
850 oracle_type: oracle_type.into(),
851 value,
852 }
853 }
854}
855
856#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
857pub struct OracleRow {
858 pub columns: BTreeMap<String, OracleCell>,
859}
860
861impl OracleRow {
862 pub fn insert(
863 &mut self,
864 name: impl Into<String>,
865 oracle_type: impl Into<String>,
866 value: Option<String>,
867 ) {
868 self.columns.insert(
869 name.into().to_ascii_uppercase(),
870 OracleCell::new(oracle_type, value),
871 );
872 }
873
874 #[must_use]
875 #[instrument(level = "trace", skip(self))]
876 pub fn cell(&self, name: &str) -> Option<&OracleCell> {
877 self.columns.get(&name.to_ascii_uppercase())
878 }
879
880 #[must_use]
881 #[instrument(level = "trace", skip(self))]
882 pub fn text(&self, name: &str) -> Option<&str> {
883 self.cell(name).and_then(|cell| cell.value.as_deref())
884 }
885
886 #[instrument(level = "trace", skip(self))]
887 pub fn require_text(&self, name: &str) -> Result<&str, CatalogError> {
888 let Some(cell) = self.cell(name) else {
889 return Err(CatalogError::MissingColumn {
890 column: name.to_ascii_uppercase(),
891 });
892 };
893 cell.value
894 .as_deref()
895 .ok_or_else(|| CatalogError::NullColumnValue {
896 column: name.to_ascii_uppercase(),
897 })
898 }
899
900 #[instrument(level = "trace", skip(self))]
901 pub fn parse_i64(&self, name: &str) -> Result<i64, CatalogError> {
902 let text = self.require_text(name)?;
903 text.parse::<i64>()
904 .map_err(|_| CatalogError::InvalidColumnValue {
905 column: name.to_ascii_uppercase(),
906 expected: "i64",
907 value: String::from(text),
908 })
909 }
910
911 #[instrument(level = "trace", skip(self))]
912 pub fn parse_u64(&self, name: &str) -> Result<u64, CatalogError> {
913 let text = self.require_text(name)?;
914 text.parse::<u64>()
915 .map_err(|_| CatalogError::InvalidColumnValue {
916 column: name.to_ascii_uppercase(),
917 expected: "u64",
918 value: String::from(text),
919 })
920 }
921
922 #[instrument(level = "trace", skip(self))]
923 pub fn parse_bool(&self, name: &str) -> Result<bool, CatalogError> {
924 let text = self.require_text(name)?;
925 let normalized = text.trim().to_ascii_uppercase();
926 match normalized.as_str() {
927 "Y" | "YES" | "TRUE" | "1" => Ok(true),
928 "N" | "NO" | "FALSE" | "0" => Ok(false),
929 _ => Err(CatalogError::InvalidColumnValue {
930 column: name.to_ascii_uppercase(),
931 expected: "bool",
932 value: String::from(text),
933 }),
934 }
935 }
936}
937
938#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
939pub struct OracleConnectionInfo {
940 pub backend: OracleBackend,
941 pub connect_string: String,
942 pub current_schema: Option<String>,
943 pub server_version: String,
944 pub db_name: String,
945 pub db_domain: String,
946 pub service_name: String,
947 pub instance_name: String,
948 pub server_type: String,
949 pub max_identifier_length: u32,
950 pub max_open_cursors: u32,
951}
952
953#[async_trait::async_trait(?Send)]
954pub trait OracleConnection: Send + Sync {
955 fn backend(&self) -> OracleBackend;
956 async fn ping(&self, cx: &Cx) -> Result<(), CatalogError>;
957 async fn describe(&self, cx: &Cx) -> Result<OracleConnectionInfo, CatalogError>;
958 async fn query_rows(
959 &self,
960 cx: &Cx,
961 sql: &str,
962 params: &[OracleBind],
963 ) -> Result<Vec<OracleRow>, CatalogError>;
964 async fn execute(&self, cx: &Cx, sql: &str, params: &[OracleBind])
965 -> Result<u64, CatalogError>;
966
967 #[instrument(level = "trace", skip(self, sql, params))]
968 async fn query_optional_row(
969 &self,
970 cx: &Cx,
971 sql: &str,
972 params: &[OracleBind],
973 ) -> Result<Option<OracleRow>, CatalogError> {
974 let mut rows = self.query_rows(cx, sql, params).await?;
975 match rows.len() {
976 0 => Ok(None),
977 1 => Ok(rows.pop()),
978 actual => Err(CatalogError::UnexpectedRowCount {
979 expected: String::from("0 or 1"),
980 actual,
981 }),
982 }
983 }
984
985 #[instrument(level = "trace", skip(self, sql, params))]
986 async fn query_one_row(
987 &self,
988 cx: &Cx,
989 sql: &str,
990 params: &[OracleBind],
991 ) -> Result<OracleRow, CatalogError> {
992 let mut rows = self.query_rows(cx, sql, params).await?;
993 match rows.len() {
994 1 => rows.pop().ok_or(CatalogError::UnexpectedRowCount {
995 expected: String::from("exactly 1"),
996 actual: 0,
997 }),
998 actual => Err(CatalogError::UnexpectedRowCount {
999 expected: String::from("exactly 1"),
1000 actual,
1001 }),
1002 }
1003 }
1004}
1005
1006#[cfg(feature = "oraclemcp-db")]
1013pub struct OraclemcpDbConnection {
1014 inner: oraclemcp_db::RustOracleConnection,
1015 connect_string: String,
1016}
1017
1018#[cfg(feature = "oraclemcp-db")]
1019impl OraclemcpDbConnection {
1020 pub async fn connect(
1022 cx: &Cx,
1023 options: oraclemcp_db::OracleConnectOptions,
1024 ) -> Result<Self, CatalogError> {
1025 let connect_string = options.connect_string.clone();
1026 let inner = oraclemcp_db::RustOracleConnection::connect(cx, options)
1027 .await
1028 .map_err(map_oraclemcp_db_error)?;
1029 Ok(Self {
1030 inner,
1031 connect_string,
1032 })
1033 }
1034
1035 pub async fn connect_with_password(
1038 cx: &Cx,
1039 username: impl Into<String>,
1040 password: impl Into<String>,
1041 connect_string: impl Into<String>,
1042 module: impl Into<String>,
1043 action: impl Into<String>,
1044 ) -> Result<Self, CatalogError> {
1045 let options = oraclemcp_db::OracleConnectOptions {
1046 connect_string: connect_string.into(),
1047 username: Some(username.into()),
1048 password: Some(password.into()),
1049 session_identity: Some(oraclemcp_db::OracleSessionIdentity {
1050 module: Some(module.into()),
1051 action: Some(action.into()),
1052 ..oraclemcp_db::OracleSessionIdentity::default()
1053 }),
1054 ..oraclemcp_db::OracleConnectOptions::default()
1055 };
1056 Self::connect(cx, options).await
1057 }
1058
1059 #[must_use]
1061 pub fn inner(&self) -> &oraclemcp_db::RustOracleConnection {
1062 &self.inner
1063 }
1064}
1065
1066#[cfg(feature = "oraclemcp-db")]
1067#[async_trait::async_trait(?Send)]
1068impl OracleConnection for OraclemcpDbConnection {
1069 #[instrument(level = "trace", skip(self))]
1070 fn backend(&self) -> OracleBackend {
1071 OracleBackend::OracleRs
1072 }
1073
1074 #[instrument(level = "trace", skip(self))]
1075 async fn ping(&self, cx: &Cx) -> Result<(), CatalogError> {
1076 oraclemcp_db::OracleConnection::ping(&self.inner, cx)
1077 .await
1078 .map_err(map_oraclemcp_db_error)
1079 }
1080
1081 #[instrument(level = "trace", skip(self))]
1082 async fn describe(&self, cx: &Cx) -> Result<OracleConnectionInfo, CatalogError> {
1083 oraclemcp_db::OracleConnection::describe(&self.inner, cx)
1084 .await
1085 .map(|info| map_oraclemcp_connection_info(info, &self.connect_string))
1086 .map_err(map_oraclemcp_db_error)
1087 }
1088
1089 #[instrument(level = "trace", skip(self, sql, params))]
1090 async fn query_rows(
1091 &self,
1092 cx: &Cx,
1093 sql: &str,
1094 params: &[OracleBind],
1095 ) -> Result<Vec<OracleRow>, CatalogError> {
1096 let binds = map_oraclemcp_binds(params)?;
1097 oraclemcp_db::OracleConnection::query_rows(&self.inner, cx, sql, &binds)
1098 .await
1099 .map(map_oraclemcp_rows)
1100 .map_err(map_oraclemcp_db_error)
1101 }
1102
1103 #[instrument(level = "trace", skip(self, sql, params))]
1104 async fn execute(
1105 &self,
1106 cx: &Cx,
1107 sql: &str,
1108 params: &[OracleBind],
1109 ) -> Result<u64, CatalogError> {
1110 let binds = map_oraclemcp_binds(params)?;
1111 oraclemcp_db::OracleConnection::execute(&self.inner, cx, sql, &binds)
1112 .await
1113 .map_err(map_oraclemcp_db_error)
1114 }
1115}
1116
1117#[cfg(feature = "oraclemcp-db")]
1118fn map_oraclemcp_connection_info(
1119 info: oraclemcp_db::OracleConnectionInfo,
1120 connect_string: &str,
1121) -> OracleConnectionInfo {
1122 OracleConnectionInfo {
1123 backend: OracleBackend::OracleRs,
1124 connect_string: connect_string.to_owned(),
1125 current_schema: info.current_schema,
1126 server_version: info.server_version.unwrap_or_default(),
1127 db_name: String::new(),
1128 db_domain: String::new(),
1129 service_name: String::new(),
1130 instance_name: String::new(),
1131 server_type: info.database_role.unwrap_or_default(),
1132 max_identifier_length: 128,
1133 max_open_cursors: 0,
1134 }
1135}
1136
1137#[cfg(feature = "oraclemcp-db")]
1138fn map_oraclemcp_binds(
1139 params: &[OracleBind],
1140) -> Result<Vec<oraclemcp_db::OracleBind>, CatalogError> {
1141 params
1142 .iter()
1143 .map(|param| match param {
1144 OracleBind::String(value) => Ok(oraclemcp_db::OracleBind::String(value.clone())),
1145 OracleBind::I64(value) => Ok(oraclemcp_db::OracleBind::I64(*value)),
1146 OracleBind::U64(value) => {
1147 let signed =
1148 i64::try_from(*value).map_err(|_| CatalogError::InvalidColumnValue {
1149 column: String::from("bind"),
1150 expected: "u64 <= i64::MAX for oraclemcp-db positional bind",
1151 value: value.to_string(),
1152 })?;
1153 Ok(oraclemcp_db::OracleBind::I64(signed))
1154 }
1155 OracleBind::Bool(value) => Ok(oraclemcp_db::OracleBind::Bool(*value)),
1156 })
1157 .collect()
1158}
1159
1160#[cfg(feature = "oraclemcp-db")]
1161fn map_oraclemcp_rows(rows: Vec<oraclemcp_db::OracleRow>) -> Vec<OracleRow> {
1162 rows.into_iter().map(map_oraclemcp_row).collect()
1163}
1164
1165#[cfg(feature = "oraclemcp-db")]
1166fn map_oraclemcp_row(row: oraclemcp_db::OracleRow) -> OracleRow {
1167 let mut mapped = OracleRow::default();
1168 for (name, cell) in row.columns {
1169 mapped.columns.insert(
1170 name.to_ascii_uppercase(),
1171 OracleCell::new(cell.oracle_type, cell.value),
1172 );
1173 }
1174 mapped
1175}
1176
1177#[cfg(feature = "oraclemcp-db")]
1178fn map_oraclemcp_db_error(err: oraclemcp_db::DbError) -> CatalogError {
1179 CatalogError::OracleBackendError {
1180 backend: OracleBackend::OracleRs,
1181 message: err.to_string(),
1182 }
1183}
1184
1185#[instrument(level = "trace")]
1186pub fn load_snapshot_from_json(path: &std::path::Path) -> Result<CatalogSnapshot, CatalogError> {
1187 let raw = fs::read_to_string(path)?;
1188 let document: CatalogSnapshotDocument = serde_json::from_str(&raw)?;
1189
1190 if !document.schema_id.as_str().eq(CATALOG_SNAPSHOT_SCHEMA_ID) {
1191 return Err(CatalogError::UnexpectedSchemaId(document.schema_id));
1192 }
1193
1194 if !matches!(
1195 document
1196 .schema_version
1197 .cmp(&CATALOG_SNAPSHOT_SCHEMA_VERSION),
1198 std::cmp::Ordering::Equal
1199 ) {
1200 return Err(CatalogError::UnsupportedSchemaVersion {
1201 schema_id: String::from(CATALOG_SNAPSHOT_SCHEMA_ID),
1202 found: document.schema_version,
1203 expected: CATALOG_SNAPSHOT_SCHEMA_VERSION,
1204 });
1205 }
1206
1207 Ok(document.snapshot)
1208}
1209
1210#[instrument(level = "trace", skip(snapshot))]
1211pub fn export_snapshot_to_json(
1212 snapshot: &CatalogSnapshot,
1213 path: &std::path::Path,
1214) -> Result<(), CatalogError> {
1215 let document = CatalogSnapshotDocument::new(snapshot.clone());
1216 let rendered = serde_json::to_string_pretty(&document)?;
1217 fs::write(path, rendered)?;
1218 Ok(())
1219}
1220
1221#[instrument(level = "info", skip_all, fields(dir = %dir.display()))]
1253pub fn load_from_dbms_metadata_dir(dir: &std::path::Path) -> Result<CatalogSnapshot, CatalogError> {
1254 if !dir.is_dir() {
1255 return Err(CatalogError::Io(std::io::Error::new(
1256 std::io::ErrorKind::NotFound,
1257 format!("not a directory: {}", dir.display()),
1258 )));
1259 }
1260
1261 let mut interner = SymbolInterner::default();
1262 let mut schemas: HashMap<SchemaName, SchemaCatalog> = HashMap::new();
1263 let mut file_count = 0usize;
1264 let mut classified_count = 0usize;
1265
1266 let mut paths: Vec<std::path::PathBuf> = fs::read_dir(dir)?
1270 .filter_map(|e| e.ok().map(|e| e.path()))
1271 .filter(|p| {
1272 p.extension()
1273 .and_then(|e| e.to_str())
1274 .is_some_and(|ext| ext.eq("sql"))
1275 })
1276 .collect();
1277 paths.sort();
1278
1279 for path in paths {
1280 file_count += 1;
1281 let ddl_text = match fs::read_to_string(&path) {
1282 Ok(text) => text,
1283 Err(_) => continue,
1284 };
1285
1286 if let Some((schema, obj_name, obj)) = classify_dbms_metadata_ddl(&ddl_text, &mut interner)
1287 {
1288 let schema_catalog = schemas.entry(schema).or_default();
1289 schema_catalog.objects.insert(obj_name, obj);
1290 classified_count += 1;
1291 }
1292 }
1293
1294 tracing::info!(
1295 files = file_count,
1296 classified = classified_count,
1297 "loaded DBMS_METADATA directory"
1298 );
1299
1300 Ok(CatalogSnapshot {
1301 schemas,
1302 profile: AnalysisProfile::default(),
1303 capabilities: CatalogCapabilities {
1304 can_query_all_views: false,
1305 can_query_dba_views: false,
1306 can_use_dbms_metadata: true,
1307 can_read_source: true,
1308 plscope_enabled: false,
1309 can_query_scheduler: false,
1310 can_query_roles_and_grants: false,
1311 warnings: vec![],
1312 },
1313 generated_at: Utc::now(),
1314 source: CatalogSource {
1315 kind: CatalogSourceKind::DbmsMetadataFiles,
1316 description: Some(format!("loaded from {}", dir.display())),
1317 ..CatalogSource::default()
1318 },
1319 interner,
1320 editions: Vec::new(),
1321 known_users: None,
1324 })
1325}
1326
1327const UNQUALIFIED_DDL_SCHEMA: &str = "PUBLIC";
1332
1333fn classify_dbms_metadata_ddl(
1341 ddl_text: &str,
1342 interner: &mut SymbolInterner,
1343) -> Option<(SchemaName, ObjectName, CatalogObject)> {
1344 let header = parse_create_header(ddl_text)?;
1348
1349 if matches!(
1352 header.kind,
1353 DdlKind::PackageBody | DdlKind::TypeBody | DdlKind::Unknown
1354 ) {
1355 return None;
1356 }
1357
1358 let (owner_text, object_text) = extract_owner_and_name(&header.after_kind)?;
1359
1360 let owner_text = owner_text.unwrap_or_else(|| UNQUALIFIED_DDL_SCHEMA.to_string());
1361 let owner = interner.intern_schema_name(owner_text)?;
1362 let name_sid = interner.intern(&object_text)?;
1363 let obj_name = ObjectName::new(name_sid);
1364
1365 let ddl = DbmsMetadataDdl {
1366 ddl_text: ddl_text.to_string(),
1367 normalized_ddl: Some(normalize_dbms_metadata_ddl(ddl_text)),
1368 xml_text: None,
1369 };
1370
1371 let common = ObjectCommon {
1372 owner,
1373 name: obj_name,
1374 object_type: header.kind.object_type(),
1375 ddl: Some(ddl),
1376 ..ObjectCommon::default()
1377 };
1378
1379 let object = match header.kind {
1380 DdlKind::Table => CatalogObject::Table(TableMetadata {
1381 common,
1382 ..TableMetadata::default()
1383 }),
1384 DdlKind::View => CatalogObject::View(ViewMetadata {
1385 common,
1386 ..ViewMetadata::default()
1387 }),
1388 DdlKind::MaterializedView => CatalogObject::MaterializedView(MViewMetadata {
1389 common,
1390 ..MViewMetadata::default()
1391 }),
1392 DdlKind::Package => CatalogObject::Package(PackageMetadata {
1393 common,
1394 ..PackageMetadata::default()
1395 }),
1396 DdlKind::Procedure => CatalogObject::Procedure(ProcedureMetadata {
1397 common,
1398 signature: RoutineSignature {
1399 routine_name: obj_name,
1400 ..RoutineSignature::default()
1401 },
1402 }),
1403 DdlKind::Function => CatalogObject::Function(FunctionMetadata {
1404 common,
1405 signature: RoutineSignature {
1406 routine_name: obj_name,
1407 ..RoutineSignature::default()
1408 },
1409 ..FunctionMetadata::default()
1410 }),
1411 DdlKind::Sequence => CatalogObject::Sequence(SequenceMetadata {
1412 common,
1413 ..SequenceMetadata::default()
1414 }),
1415 DdlKind::Trigger => CatalogObject::Trigger(TriggerMetadata {
1416 common,
1417 ..TriggerMetadata::default()
1418 }),
1419 DdlKind::Type => CatalogObject::Type(TypeMetadata {
1420 common,
1421 ..TypeMetadata::default()
1422 }),
1423 DdlKind::PackageBody | DdlKind::TypeBody | DdlKind::Unknown => return None,
1426 };
1427
1428 Some((owner, obj_name, object))
1429}
1430
1431#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1437enum DdlKind {
1438 Table,
1439 View,
1440 MaterializedView,
1441 Package,
1442 PackageBody,
1443 Procedure,
1444 Function,
1445 Sequence,
1446 Trigger,
1447 Type,
1448 TypeBody,
1449 Unknown,
1450}
1451
1452impl DdlKind {
1453 fn object_type(self) -> ObjectType {
1454 match self {
1455 DdlKind::Table => ObjectType::Table,
1456 DdlKind::View => ObjectType::View,
1457 DdlKind::MaterializedView => ObjectType::MaterializedView,
1458 DdlKind::Package | DdlKind::PackageBody => ObjectType::Package,
1459 DdlKind::Procedure => ObjectType::Procedure,
1460 DdlKind::Function => ObjectType::Function,
1461 DdlKind::Sequence => ObjectType::Sequence,
1462 DdlKind::Trigger => ObjectType::Trigger,
1463 DdlKind::Type | DdlKind::TypeBody => ObjectType::Type,
1464 DdlKind::Unknown => ObjectType::Unknown,
1465 }
1466 }
1467}
1468
1469#[derive(Clone, Debug)]
1476struct ParsedCreateHeader {
1477 kind: DdlKind,
1478 after_kind: String,
1479}
1480
1481fn parse_create_header(ddl: &str) -> Option<ParsedCreateHeader> {
1492 let mut cursor = Cursor::new(ddl);
1493 cursor.skip_ws_and_comments();
1494
1495 if !cursor.consume_keyword("CREATE") {
1497 return None;
1498 }
1499 cursor.skip_ws_and_comments();
1500
1501 if cursor.consume_keyword("OR") {
1503 cursor.skip_ws_and_comments();
1504 let _ = cursor.consume_keyword("REPLACE");
1508 cursor.skip_ws_and_comments();
1509 }
1510
1511 loop {
1514 if cursor.consume_keyword("FORCE")
1515 || cursor.consume_keyword("NONEDITIONABLE")
1516 || cursor.consume_keyword("EDITIONABLE")
1517 || cursor.consume_keyword("NO")
1518 {
1519 cursor.skip_ws_and_comments();
1520 continue;
1521 }
1522 break;
1523 }
1524
1525 let first = match cursor.consume_identifier() {
1528 Some(tok) => tok,
1529 None => {
1530 return Some(ParsedCreateHeader {
1531 kind: DdlKind::Unknown,
1532 after_kind: cursor.upper_remainder(),
1533 });
1534 }
1535 };
1536 cursor.skip_ws_and_comments();
1537
1538 let kind = match first.as_str() {
1541 "MATERIALIZED" => {
1542 if cursor.peek_keyword("VIEW") {
1543 cursor.consume_keyword("VIEW");
1544 cursor.skip_ws_and_comments();
1545 DdlKind::MaterializedView
1546 } else {
1547 DdlKind::Unknown
1548 }
1549 }
1550 "PACKAGE" => {
1551 if cursor.peek_keyword("BODY") {
1552 cursor.consume_keyword("BODY");
1553 cursor.skip_ws_and_comments();
1554 DdlKind::PackageBody
1555 } else {
1556 DdlKind::Package
1557 }
1558 }
1559 "TYPE" => {
1560 if cursor.peek_keyword("BODY") {
1561 cursor.consume_keyword("BODY");
1562 cursor.skip_ws_and_comments();
1563 DdlKind::TypeBody
1564 } else {
1565 DdlKind::Type
1566 }
1567 }
1568 "TABLE" => DdlKind::Table,
1569 "VIEW" => DdlKind::View,
1570 "PROCEDURE" => DdlKind::Procedure,
1571 "FUNCTION" => DdlKind::Function,
1572 "SEQUENCE" => DdlKind::Sequence,
1573 "TRIGGER" => DdlKind::Trigger,
1574 _ => DdlKind::Unknown,
1575 };
1576
1577 Some(ParsedCreateHeader {
1578 kind,
1579 after_kind: cursor.upper_remainder(),
1580 })
1581}
1582
1583struct Cursor<'a> {
1592 bytes: &'a [u8],
1593 pos: usize,
1594}
1595
1596impl<'a> Cursor<'a> {
1597 fn new(text: &'a str) -> Self {
1598 Self {
1599 bytes: text.as_bytes(),
1600 pos: 0,
1601 }
1602 }
1603
1604 fn skip_ws_and_comments(&mut self) {
1605 loop {
1606 while self.pos < self.bytes.len() && self.bytes[self.pos].is_ascii_whitespace() {
1608 self.pos += 1;
1609 }
1610 if self.pos + 1 < self.bytes.len()
1612 && self.bytes[self.pos].eq(&b'-')
1613 && self.bytes[self.pos + 1].eq(&b'-')
1614 {
1615 self.pos += 2;
1616 while self.pos < self.bytes.len() && self.bytes[self.pos].ne(&b'\n') {
1617 self.pos += 1;
1618 }
1619 continue;
1620 }
1621 if self.pos + 1 < self.bytes.len()
1623 && self.bytes[self.pos].eq(&b'/')
1624 && self.bytes[self.pos + 1].eq(&b'*')
1625 {
1626 self.pos += 2;
1627 while self.pos + 1 < self.bytes.len()
1628 && !(self.bytes[self.pos].eq(&b'*') && self.bytes[self.pos + 1].eq(&b'/'))
1629 {
1630 self.pos += 1;
1631 }
1632 if self.pos + 1 < self.bytes.len() {
1633 self.pos += 2; } else {
1635 self.pos = self.bytes.len(); }
1637 continue;
1638 }
1639 break;
1640 }
1641 }
1642
1643 fn peek_keyword(&self, kw: &str) -> bool {
1647 let end = self.pos + kw.len();
1648 if end > self.bytes.len() {
1649 return false;
1650 }
1651 if !self.bytes[self.pos..end].eq_ignore_ascii_case(kw.as_bytes()) {
1652 return false;
1653 }
1654 if end < self.bytes.len() {
1656 let next = self.bytes[end];
1657 if next.eq(&b'_') || next.is_ascii_alphanumeric() {
1658 return false;
1659 }
1660 }
1661 true
1662 }
1663
1664 fn consume_keyword(&mut self, kw: &str) -> bool {
1665 if self.peek_keyword(kw) {
1666 self.pos += kw.len();
1667 true
1668 } else {
1669 false
1670 }
1671 }
1672
1673 fn consume_identifier(&mut self) -> Option<String> {
1680 if self.pos >= self.bytes.len() {
1681 return None;
1682 }
1683 let first = self.bytes[self.pos];
1684 if !first.is_ascii_alphabetic() {
1685 return None;
1686 }
1687 let start = self.pos;
1688 while self.pos < self.bytes.len() {
1689 let b = self.bytes[self.pos];
1690 if b.is_ascii_alphanumeric() || b.eq(&b'_') {
1691 self.pos += 1;
1692 } else {
1693 break;
1694 }
1695 }
1696 let raw = std::str::from_utf8(&self.bytes[start..self.pos]).ok()?;
1697 Some(raw.to_ascii_uppercase())
1698 }
1699
1700 fn upper_remainder(&self) -> String {
1703 std::str::from_utf8(&self.bytes[self.pos..])
1704 .unwrap_or("")
1705 .to_ascii_uppercase()
1706 }
1707}
1708
1709fn extract_owner_and_name(after_kind: &str) -> Option<(Option<String>, String)> {
1716 let after = after_kind.trim_start();
1717
1718 let mut segments: Vec<Segment> = Vec::new();
1724 let bytes = after.as_bytes();
1725 let mut i = 0usize;
1726 'scan: while i < bytes.len() {
1727 if bytes[i].eq(&b'"') {
1728 let content_start = i + 1;
1730 let mut j = content_start;
1731 while j < bytes.len() && bytes[j].ne(&b'"') {
1732 j += 1;
1733 }
1734 if j >= bytes.len() {
1736 return None;
1737 }
1738 segments.push(Segment {
1739 text: after[content_start..j].to_string(),
1740 quoted: true,
1741 });
1742 i = j + 1; } else {
1744 let start = i;
1748 while i < bytes.len() {
1749 let c = bytes[i] as char;
1750 if c.is_ascii_alphanumeric() || c.eq(&'_') {
1751 i += 1;
1752 } else {
1753 break;
1754 }
1755 }
1756 if i.eq(&start) {
1759 break 'scan;
1760 }
1761 segments.push(Segment {
1762 text: after[start..i].to_string(),
1763 quoted: false,
1764 });
1765 }
1766
1767 if i < bytes.len() && bytes[i].eq(&b'.') {
1770 i += 1;
1771 } else {
1772 break 'scan;
1773 }
1774 }
1775
1776 let valid = |seg: &Segment| -> bool {
1779 if seg.text.is_empty() {
1780 return false;
1781 }
1782 seg.quoted || seg.text.chars().all(|c| c.is_alphanumeric() || c.eq(&'_'))
1783 };
1784
1785 match segments.as_slice() {
1786 [name] if valid(name) => Some((None, name.text.clone())),
1787 [owner, name] if valid(owner) && valid(name) => {
1788 Some((Some(owner.text.clone()), name.text.clone()))
1789 }
1790 _ => None,
1791 }
1792}
1793
1794struct Segment {
1798 text: String,
1799 quoted: bool,
1800}
1801#[instrument(level = "trace", skip(cx, conn, request))]
1802pub async fn load_snapshot_from_connection<C: OracleConnection>(
1803 cx: &Cx,
1804 conn: &C,
1805 request: &CatalogLoadRequest,
1806) -> Result<CatalogSnapshot, CatalogError> {
1807 let connection_info = conn.describe(cx).await?;
1808 let resolved_schemas = resolve_schema_filters(&connection_info, request)?;
1809 let (oracle_version, version_warning) =
1810 oracle_version_from_server_version(&connection_info.server_version);
1811
1812 let mut capabilities = negotiate_capabilities(cx, conn).await;
1813 if let Some(warning) = version_warning {
1814 capabilities.warnings.push(warning);
1815 }
1816
1817 let mut snapshot = CatalogSnapshot::new(
1818 AnalysisProfile::for_version(oracle_version),
1819 capabilities,
1820 CatalogSource {
1821 kind: CatalogSourceKind::LiveConnection,
1822 path: None,
1823 description: Some(format!(
1824 "live extraction via {} from {}",
1825 connection_info.backend, connection_info.connect_string
1826 )),
1827 },
1828 Utc::now(),
1829 );
1830
1831 if let Some(current_schema) = connection_info.current_schema.as_deref() {
1832 snapshot.profile.current_schema = snapshot.intern_schema_name(current_schema);
1833 }
1834
1835 load_catalog_objects(cx, conn, &mut snapshot, &resolved_schemas).await?;
1836 load_catalog_columns(cx, conn, &mut snapshot, &resolved_schemas).await?;
1837 load_catalog_constraints(cx, conn, &mut snapshot, &resolved_schemas).await?;
1838 load_catalog_indexes(cx, conn, &mut snapshot, &resolved_schemas).await?;
1839 load_catalog_triggers(cx, conn, &mut snapshot, &resolved_schemas).await?;
1840 load_catalog_synonyms(cx, conn, &mut snapshot, &resolved_schemas).await?;
1841 load_catalog_routines(cx, conn, &mut snapshot, &resolved_schemas).await?;
1842 load_catalog_views(cx, conn, &mut snapshot, &resolved_schemas).await?;
1843 load_catalog_mviews(cx, conn, &mut snapshot, &resolved_schemas).await?;
1844 load_catalog_sequences(cx, conn, &mut snapshot, &resolved_schemas).await?;
1845 load_catalog_type_attrs(cx, conn, &mut snapshot, &resolved_schemas).await?;
1846 load_catalog_users(cx, conn, &mut snapshot).await?;
1849 load_catalog_grants(cx, conn, &mut snapshot, &resolved_schemas).await?;
1850 load_catalog_db_links(cx, conn, &mut snapshot, &resolved_schemas).await?;
1851 load_catalog_table_comments(cx, conn, &mut snapshot, &resolved_schemas).await?;
1852 load_catalog_column_comments(cx, conn, &mut snapshot, &resolved_schemas).await?;
1853 load_catalog_editions(cx, conn, &mut snapshot).await?;
1854 load_catalog_editioning_views(cx, conn, &mut snapshot, &resolved_schemas).await?;
1855 load_catalog_vpd_policies(cx, conn, &mut snapshot, &resolved_schemas).await?;
1856 load_catalog_dependencies(cx, conn, &mut snapshot, &resolved_schemas).await?;
1857 if snapshot.capabilities.plscope_enabled {
1858 load_catalog_plscope_availability(cx, conn, &mut snapshot, &resolved_schemas).await?;
1859 load_catalog_plscope_identifiers(cx, conn, &mut snapshot, &resolved_schemas).await?;
1860 }
1861
1862 Ok(snapshot)
1863}
1864
1865#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1866enum RoutineKind {
1867 Procedure,
1868 Function,
1869}
1870
1871#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
1872struct RoutineLocator {
1873 owner: SchemaName,
1874 package_name: Option<ObjectName>,
1875 routine_name: ObjectName,
1876 subprogram_id: Option<u32>,
1877 overload: Option<u32>,
1878}
1879
1880#[derive(Clone, Debug, Default)]
1881struct RoutineAccumulator {
1882 signature: Option<RoutineSignature>,
1883 kind_hint: Option<RoutineKind>,
1884 deterministic: bool,
1885 pipelined: bool,
1886}
1887
1888#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1889pub struct SchemaCatalog {
1890 pub objects: HashMap<ObjectName, CatalogObject>,
1891 pub synonyms: HashMap<SynonymName, SynonymTarget>,
1892 pub grants: Vec<Grant>,
1893 pub indexes: HashMap<IndexName, IndexMetadata>,
1894 pub constraints: HashMap<ConstraintName, ConstraintMetadata>,
1895 pub triggers: HashMap<TriggerName, TriggerMetadata>,
1896 pub dependencies: Vec<CatalogDependency>,
1897 pub plscope: Option<PlScopeSnapshot>,
1898 #[serde(default)]
1901 pub db_links: Vec<DatabaseLink>,
1902 #[serde(default)]
1905 pub table_comments: Vec<TableComment>,
1906 #[serde(default)]
1909 pub column_comments: Vec<ColumnComment>,
1910 #[serde(default)]
1914 pub editioning_views: Vec<EditioningView>,
1915 #[serde(default)]
1918 pub vpd_policies: Vec<VpdPolicy>,
1919}
1920
1921fn resolve_schema_filters(
1922 connection_info: &OracleConnectionInfo,
1923 request: &CatalogLoadRequest,
1924) -> Result<Vec<String>, CatalogError> {
1925 let mut resolved = Vec::<String>::new();
1926
1927 for filter in &request.schema_filters {
1928 let schema_name = match filter {
1929 CatalogSchemaFilter::CurrentSchema => connection_info
1930 .current_schema
1931 .clone()
1932 .ok_or(CatalogError::CurrentSchemaUnavailable)?,
1933 CatalogSchemaFilter::Named(schema_name) => {
1934 let trimmed = schema_name.trim();
1935 if trimmed.is_empty() {
1936 return Err(CatalogError::InvalidSchemaFilter {
1937 schema_name: schema_name.clone(),
1938 });
1939 }
1940 String::from(trimmed)
1941 }
1942 };
1943
1944 if !resolved.iter().any(|candidate| candidate.eq(&schema_name)) {
1945 resolved.push(schema_name);
1946 }
1947 }
1948
1949 if resolved.is_empty() {
1950 return Err(CatalogError::CurrentSchemaUnavailable);
1951 }
1952
1953 Ok(resolved)
1954}
1955
1956#[instrument(level = "trace", skip(cx, conn))]
1965pub async fn fetch_dbms_metadata_ddl<C: OracleConnection>(
1966 cx: &Cx,
1967 conn: &C,
1968 object_type: ObjectType,
1969 name: &str,
1970 owner: &str,
1971) -> Result<Option<DbmsMetadataDdl>, CatalogError> {
1972 let Some(dbms_type) = object_type_to_dbms_metadata_value(object_type) else {
1973 return Ok(None);
1974 };
1975 let sql = "select dbms_metadata.get_ddl(:1, :2, :3) as ddl_text, \
1976 dbms_metadata.get_xml(:1, :2, :3) as xml_text from dual";
1977 let params = vec![
1978 OracleBind::from(dbms_type.to_string()),
1979 OracleBind::from(name.to_string()),
1980 OracleBind::from(owner.to_string()),
1981 ];
1982 let rows = conn.query_rows(cx, sql, ¶ms).await?;
1983 let Some(row) = rows.into_iter().next() else {
1984 return Ok(None);
1985 };
1986
1987 let ddl_text = match optional_nonblank_text(&row, "DDL_TEXT") {
1988 Some(value) => value.to_string(),
1989 None => return Ok(None),
1990 };
1991 let xml_text = optional_nonblank_text(&row, "XML_TEXT").map(String::from);
1992 let normalized_ddl = Some(normalize_dbms_metadata_ddl(&ddl_text));
1993
1994 Ok(Some(DbmsMetadataDdl {
1995 ddl_text,
1996 normalized_ddl,
1997 xml_text,
1998 }))
1999}
2000
2001#[instrument(level = "trace", skip(cx, conn, snapshot))]
2007pub async fn populate_dbms_metadata_ddl<C: OracleConnection>(
2008 cx: &Cx,
2009 conn: &C,
2010 snapshot: &mut CatalogSnapshot,
2011) -> Result<(), CatalogError> {
2012 if !snapshot.capabilities.can_use_dbms_metadata {
2013 return Ok(());
2014 }
2015
2016 let mut targets: Vec<(SchemaName, ObjectName, ObjectType, String, String)> = Vec::new();
2017 for (owner, schema) in &snapshot.schemas {
2018 let owner_name = snapshot
2019 .interner
2020 .resolve(owner.symbol())
2021 .unwrap_or("")
2022 .to_string();
2023 for (name, object) in &schema.objects {
2024 let common = catalog_object_common(object);
2025 let object_name = snapshot
2026 .interner
2027 .resolve(name.symbol())
2028 .unwrap_or("")
2029 .to_string();
2030 targets.push((
2031 *owner,
2032 *name,
2033 common.object_type,
2034 owner_name.clone(),
2035 object_name,
2036 ));
2037 }
2038 }
2039
2040 let mut warnings: Vec<CapabilityWarning> = Vec::new();
2041 let mut writes: Vec<(SchemaName, ObjectName, DbmsMetadataDdl)> = Vec::new();
2042 for (owner_symbol, name_symbol, object_type, owner_text, name_text) in targets {
2043 if owner_text.is_empty() || name_text.is_empty() {
2044 continue;
2045 }
2046 match fetch_dbms_metadata_ddl(cx, conn, object_type, &name_text, &owner_text).await {
2047 Ok(Some(ddl)) => writes.push((owner_symbol, name_symbol, ddl)),
2048 Ok(None) => {}
2049 Err(error) => warnings.push(CapabilityWarning {
2050 code: String::from("dbms-metadata-fetch-failed"),
2051 message: format!("DBMS_METADATA.GET_DDL({owner_text}.{name_text}) failed: {error}"),
2052 remediation: Some(String::from(
2053 "Ensure DBMS_METADATA execute privilege is granted; the object may be wrapped or in an inaccessible edition.",
2054 )),
2055 }),
2056 }
2057 }
2058
2059 for (owner_symbol, name_symbol, ddl) in writes {
2060 if let Some(catalog_object) = snapshot
2061 .schemas
2062 .get_mut(&owner_symbol)
2063 .and_then(|schema| schema.objects.get_mut(&name_symbol))
2064 {
2065 set_catalog_object_ddl(catalog_object, ddl);
2066 }
2067 }
2068
2069 snapshot.capabilities.warnings.extend(warnings);
2070 Ok(())
2071}
2072
2073fn set_catalog_object_ddl(object: &mut CatalogObject, ddl: DbmsMetadataDdl) {
2074 match object {
2075 CatalogObject::Table(metadata) => metadata.common.ddl = Some(ddl),
2076 CatalogObject::View(metadata) => metadata.common.ddl = Some(ddl),
2077 CatalogObject::MaterializedView(metadata) => metadata.common.ddl = Some(ddl),
2078 CatalogObject::Sequence(metadata) => metadata.common.ddl = Some(ddl),
2079 CatalogObject::Type(metadata) => metadata.common.ddl = Some(ddl),
2080 CatalogObject::Package(metadata) => metadata.common.ddl = Some(ddl),
2081 CatalogObject::Procedure(metadata) => metadata.common.ddl = Some(ddl),
2082 CatalogObject::Function(metadata) => metadata.common.ddl = Some(ddl),
2083 CatalogObject::Trigger(metadata) => metadata.common.ddl = Some(ddl),
2084 CatalogObject::SchedulerJob(metadata) => metadata.common.ddl = Some(ddl),
2085 CatalogObject::EditioningView(metadata) => metadata.common.ddl = Some(ddl),
2086 }
2087}
2088
2089#[must_use]
2097pub fn normalize_dbms_metadata_ddl(text: &str) -> String {
2098 let trimmed = text.trim();
2099 let trimmed = trimmed.strip_suffix('/').unwrap_or(trimmed).trim_end();
2100 let mut normalized = String::with_capacity(trimmed.len());
2101 let mut prev_space = false;
2102 for c in trimmed.chars() {
2103 if c.eq(&' ') || c.eq(&'\t') {
2104 if !prev_space {
2105 normalized.push(' ');
2106 prev_space = true;
2107 }
2108 } else {
2109 normalized.push(c);
2110 prev_space = false;
2111 }
2112 }
2113 normalized
2114}
2115
2116#[must_use]
2121pub fn object_type_to_dbms_metadata_value(object_type: ObjectType) -> Option<&'static str> {
2122 match object_type {
2123 ObjectType::Table => Some("TABLE"),
2124 ObjectType::View => Some("VIEW"),
2125 ObjectType::MaterializedView => Some("MATERIALIZED_VIEW"),
2126 ObjectType::Sequence => Some("SEQUENCE"),
2127 ObjectType::Type => Some("TYPE"),
2128 ObjectType::Package => Some("PACKAGE"),
2129 ObjectType::Procedure => Some("PROCEDURE"),
2130 ObjectType::Function => Some("FUNCTION"),
2131 ObjectType::Trigger => Some("TRIGGER"),
2132 ObjectType::EditioningView => Some("VIEW"),
2133 ObjectType::SchedulerJob => Some("PROCOBJ"),
2134 ObjectType::Synonym => Some("SYNONYM"),
2135 ObjectType::Index => Some("INDEX"),
2136 ObjectType::Constraint | ObjectType::Unknown => None,
2137 }
2138}
2139
2140#[must_use]
2150#[instrument(level = "trace", skip(cx, conn))]
2151pub async fn negotiate_capabilities<C: OracleConnection>(cx: &Cx, conn: &C) -> CatalogCapabilities {
2152 let mut capabilities = CatalogCapabilities {
2153 can_query_all_views: false,
2154 ..CatalogCapabilities::default()
2155 };
2156
2157 type CapabilitySetter = fn(&mut CatalogCapabilities);
2158 let probes: &[(&str, &str, &str, CapabilitySetter)] = &[
2159 (
2160 "select 1 from all_objects where rownum = 0",
2161 "all-views-probe",
2162 "ALL_OBJECTS unreachable; ensure the user has SELECT privilege on the standard ALL_* views.",
2163 |c| c.can_query_all_views = true,
2164 ),
2165 (
2166 "select 1 from dba_objects where rownum = 0",
2167 "dba-views-probe",
2168 "DBA_OBJECTS unreachable; grant SELECT_CATALOG_ROLE or specific DBA_* privileges to widen cross-schema coverage.",
2169 |c| c.can_query_dba_views = true,
2170 ),
2171 (
2172 "select 1 from all_source where rownum = 0",
2173 "all-source-probe",
2174 "ALL_SOURCE unreachable; ensure the user can read package/procedure bodies for source extraction.",
2175 |c| c.can_read_source = true,
2176 ),
2177 (
2178 "select 1 from all_scheduler_jobs where rownum = 0",
2179 "scheduler-probe",
2180 "ALL_SCHEDULER_JOBS unreachable; grant SELECT on the scheduler dictionary views to enable scheduler lineage.",
2181 |c| c.can_query_scheduler = true,
2182 ),
2183 (
2184 "select 1 from all_tab_privs where rownum = 0",
2185 "roles-and-grants-probe",
2186 "ALL_TAB_PRIVS unreachable; grant SELECT_CATALOG_ROLE to enable privilege chain analysis.",
2187 |c| c.can_query_roles_and_grants = true,
2188 ),
2189 (
2190 "select 1 from all_plsql_object_settings where rownum = 0",
2191 "plscope-probe",
2192 "ALL_PLSQL_OBJECT_SETTINGS unreachable; PL/Scope identifier extraction (PLSQL-CAT-010/011) will be unavailable.",
2193 |c| c.plscope_enabled = true,
2194 ),
2195 ];
2196
2197 for (sql, probe_code, remediation, setter) in probes {
2198 match conn.query_rows(cx, sql, &[]).await {
2199 Ok(_) => setter(&mut capabilities),
2200 Err(error) => capabilities.warnings.push(CapabilityWarning {
2201 code: String::from(*probe_code),
2202 message: format!("probe `{sql}` failed: {error}"),
2203 remediation: Some(String::from(*remediation)),
2204 }),
2205 }
2206 }
2207
2208 let dbms_metadata_probe =
2213 "begin if dbms_metadata.get_ddl('TABLE', 'DUAL', 'SYS') is null then null; end if; end;";
2214 match conn.execute(cx, dbms_metadata_probe, &[]).await {
2215 Ok(_) => {
2216 capabilities.can_use_dbms_metadata = true;
2217 }
2218 Err(error) => {
2219 capabilities.warnings.push(CapabilityWarning {
2220 code: String::from("dbms-metadata-probe"),
2221 message: format!("DBMS_METADATA probe failed: {error}"),
2222 remediation: Some(String::from(
2223 "grant execute on DBMS_METADATA to <user> to enable PLSQL-CAT-015 DDL extraction.",
2224 )),
2225 });
2226 }
2227 }
2228
2229 capabilities
2230}
2231
2232fn oracle_version_from_server_version(
2233 server_version: &str,
2234) -> (OracleVersion, Option<CapabilityWarning>) {
2235 let major_component = server_version
2236 .split('.')
2237 .next()
2238 .unwrap_or_default()
2239 .trim()
2240 .parse::<u32>()
2241 .ok();
2242
2243 match major_component {
2244 Some(11) => (OracleVersion::Oracle11g, None),
2245 Some(12) => (OracleVersion::Oracle12c, None),
2246 Some(19) => (OracleVersion::Oracle19c, None),
2247 Some(21) => (OracleVersion::Oracle21c, None),
2248 Some(23) => (OracleVersion::Oracle23ai, None),
2249 Some(26) => (OracleVersion::Oracle26ai, None),
2250 _ => (
2251 OracleVersion::Oracle19c,
2252 Some(CapabilityWarning {
2253 code: String::from("catalog-version-parse-fallback"),
2254 message: format!(
2255 "server version `{server_version}` did not map cleanly to a supported OracleVersion; defaulted AnalysisProfile.oracle_version to Oracle19c"
2256 ),
2257 remediation: Some(String::from(
2258 "Set the workspace AnalysisProfile explicitly if this estate targets a newer or older Oracle release.",
2259 )),
2260 }),
2261 ),
2262 }
2263}
2264
2265async fn load_catalog_objects<C: OracleConnection>(
2266 cx: &Cx,
2267 conn: &C,
2268 snapshot: &mut CatalogSnapshot,
2269 schema_names: &[String],
2270) -> Result<(), CatalogError> {
2271 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2272 let sql = format!(
2273 "
2274select
2275 owner,
2276 object_name,
2277 object_type,
2278 status,
2279 to_char(last_ddl_time, 'YYYY-MM-DD\"T\"HH24:MI:SS') as last_ddl_time_iso,
2280 editionable,
2281 edition_name
2282from all_objects
2283where owner in ({owner_clause})
2284 and object_type in (
2285 'TABLE',
2286 'VIEW',
2287 'MATERIALIZED VIEW',
2288 'SEQUENCE',
2289 'TYPE',
2290 'PACKAGE',
2291 'PROCEDURE',
2292 'FUNCTION',
2293 'TRIGGER',
2294 'EDITIONING VIEW'
2295 )
2296order by owner, object_type, object_name
2297"
2298 );
2299 let params = schema_filter_params(schema_names);
2300
2301 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2302 apply_object_row(snapshot, &row)?;
2303 }
2304
2305 Ok(())
2306}
2307
2308async fn load_catalog_plscope_identifiers<C: OracleConnection>(
2309 cx: &Cx,
2310 conn: &C,
2311 snapshot: &mut CatalogSnapshot,
2312 schema_names: &[String],
2313) -> Result<(), CatalogError> {
2314 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2315 let sql = format!(
2316 "
2317select
2318 owner,
2319 name,
2320 type,
2321 usage,
2322 line,
2323 col,
2324 object_name
2325from all_identifiers
2326where owner in ({owner_clause})
2327order by owner, object_name, line, col
2328"
2329 );
2330 let params = schema_filter_params(schema_names);
2331 let rows = match conn.query_rows(cx, &sql, ¶ms).await {
2332 Ok(rows) => rows,
2333 Err(error) => {
2334 snapshot.capabilities.warnings.push(CapabilityWarning {
2335 code: String::from("plscope-identifiers-failed"),
2336 message: format!("ALL_IDENTIFIERS query failed: {error}"),
2337 remediation: Some(String::from(
2338 "Ensure the user can read ALL_IDENTIFIERS, or recompile target objects with `alter session set plscope_settings = 'identifiers:all'`.",
2339 )),
2340 });
2341 return Ok(());
2342 }
2343 };
2344
2345 for row in &rows {
2346 let Some(owner_text) = optional_nonblank_text(row, "OWNER") else {
2347 continue;
2348 };
2349 let Some(object_name_text) = optional_nonblank_text(row, "OBJECT_NAME") else {
2350 continue;
2351 };
2352 let Some(identifier_name_text) = optional_nonblank_text(row, "NAME") else {
2353 continue;
2354 };
2355 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
2356 continue;
2357 };
2358 let Some(object_name) = snapshot.intern_object_name(object_name_text) else {
2359 continue;
2360 };
2361 let Some(identifier_name) = snapshot.intern_member_name(identifier_name_text) else {
2362 continue;
2363 };
2364 let identifier_type = optional_nonblank_text(row, "TYPE")
2365 .map(String::from)
2366 .unwrap_or_default();
2367 let usage = optional_nonblank_text(row, "USAGE")
2368 .map(String::from)
2369 .unwrap_or_default();
2370 let line = optional_u32(row, "LINE")?.unwrap_or(0);
2371 let column = optional_u32(row, "COL")?.unwrap_or(0);
2372
2373 let identifier = CompilerIdentifier {
2374 owner,
2375 object_name,
2376 identifier_name,
2377 identifier_type,
2378 usage,
2379 line,
2380 column,
2381 };
2382
2383 let plscope = snapshot
2384 .schemas
2385 .entry(owner)
2386 .or_default()
2387 .plscope
2388 .get_or_insert_with(|| PlScopeSnapshot {
2389 availability: PlScopeAvailability::IdentifiersOnly,
2390 collected_at: Some(snapshot.generated_at),
2391 ..PlScopeSnapshot::default()
2392 });
2393 plscope.identifiers.push(identifier);
2394 }
2395
2396 Ok(())
2397}
2398
2399async fn load_catalog_plscope_availability<C: OracleConnection>(
2400 cx: &Cx,
2401 conn: &C,
2402 snapshot: &mut CatalogSnapshot,
2403 schema_names: &[String],
2404) -> Result<(), CatalogError> {
2405 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2406 let sql = format!(
2407 "
2408select
2409 owner,
2410 plscope_settings
2411from all_plsql_object_settings
2412where owner in ({owner_clause})
2413"
2414 );
2415 let params = schema_filter_params(schema_names);
2416 let rows = match conn.query_rows(cx, &sql, ¶ms).await {
2417 Ok(rows) => rows,
2418 Err(error) => {
2419 snapshot.capabilities.warnings.push(CapabilityWarning {
2422 code: String::from("plscope-detect-failed"),
2423 message: format!("ALL_PLSQL_OBJECT_SETTINGS query failed: {error}"),
2424 remediation: Some(String::from(
2425 "Grant SELECT on ALL_PLSQL_OBJECT_SETTINGS, or accept that PL/Scope detection is unavailable.",
2426 )),
2427 });
2428 return Ok(());
2429 }
2430 };
2431
2432 let mut per_schema: HashMap<SchemaName, PlScopeTally> = HashMap::new();
2436 for row in &rows {
2437 let owner_text = match row.text("OWNER") {
2438 Some(value) if !value.trim().is_empty() => value,
2439 _ => continue,
2440 };
2441 let settings = row
2442 .text("PLSCOPE_SETTINGS")
2443 .unwrap_or("")
2444 .to_ascii_uppercase();
2445 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
2446 continue;
2447 };
2448 let tally = per_schema.entry(owner).or_default();
2449 tally.total = tally.total.saturating_add(1);
2450 if settings.contains("STATEMENTS:") && !settings.contains("STATEMENTS:NONE") {
2451 tally.with_statements = tally.with_statements.saturating_add(1);
2452 }
2453 if settings.contains("IDENTIFIERS:") && !settings.contains("IDENTIFIERS:NONE") {
2454 tally.with_identifiers = tally.with_identifiers.saturating_add(1);
2455 }
2456 }
2457
2458 for (owner, tally) in per_schema {
2459 let availability = if tally.with_statements > 0 {
2460 PlScopeAvailability::IdentifiersAndStatements
2461 } else if tally.with_identifiers > 0 {
2462 PlScopeAvailability::IdentifiersOnly
2463 } else if tally.total > 0 {
2464 PlScopeAvailability::AvailableButStale
2466 } else {
2467 PlScopeAvailability::NotAvailable
2468 };
2469 let schema_catalog = snapshot.schemas.entry(owner).or_default();
2470 schema_catalog.plscope = Some(PlScopeSnapshot {
2471 availability,
2472 collected_at: Some(snapshot.generated_at),
2473 ..PlScopeSnapshot::default()
2474 });
2475 }
2476
2477 Ok(())
2478}
2479
2480#[derive(Default)]
2481struct PlScopeTally {
2482 total: usize,
2483 with_identifiers: usize,
2484 with_statements: usize,
2485}
2486
2487async fn load_catalog_dependencies<C: OracleConnection>(
2488 cx: &Cx,
2489 conn: &C,
2490 snapshot: &mut CatalogSnapshot,
2491 schema_names: &[String],
2492) -> Result<(), CatalogError> {
2493 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2494 let sql = format!(
2495 "
2496select
2497 owner,
2498 name,
2499 type,
2500 referenced_owner,
2501 referenced_name,
2502 referenced_type,
2503 dependency_type
2504from all_dependencies
2505where owner in ({owner_clause})
2506order by owner, name, referenced_owner, referenced_name
2507"
2508 );
2509 let params = schema_filter_params(schema_names);
2510
2511 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2512 apply_dependency_row(snapshot, &row)?;
2513 }
2514
2515 Ok(())
2516}
2517
2518async fn load_catalog_columns<C: OracleConnection>(
2519 cx: &Cx,
2520 conn: &C,
2521 snapshot: &mut CatalogSnapshot,
2522 schema_names: &[String],
2523) -> Result<(), CatalogError> {
2524 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2525 let sql = format!(
2526 "
2527select
2528 owner,
2529 table_name,
2530 column_name,
2531 nvl(column_id, internal_column_id) as column_position,
2532 data_type_owner,
2533 data_type,
2534 data_length,
2535 data_precision,
2536 data_scale,
2537 char_used,
2538 nullable,
2539 data_default_vc,
2540 virtual_column,
2541 hidden_column
2542from all_tab_cols
2543where owner in ({owner_clause})
2544order by owner, table_name, nvl(column_id, internal_column_id)
2545"
2546 );
2547 let params = schema_filter_params(schema_names);
2548
2549 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2550 apply_column_row(snapshot, &row)?;
2551 }
2552
2553 Ok(())
2554}
2555
2556async fn load_catalog_constraints<C: OracleConnection>(
2557 cx: &Cx,
2558 conn: &C,
2559 snapshot: &mut CatalogSnapshot,
2560 schema_names: &[String],
2561) -> Result<(), CatalogError> {
2562 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2563 let sql = format!(
2564 "
2565select
2566 c.owner,
2567 c.constraint_name,
2568 c.table_name,
2569 c.constraint_type,
2570 c.r_owner as referenced_table_owner,
2571 p.table_name as referenced_table_name,
2572 c.search_condition_vc,
2573 case when c.deferrable = 'DEFERRABLE' then 'Y' else 'N' end as is_deferrable,
2574 case when c.deferred = 'DEFERRED' then 'Y' else 'N' end as is_deferred,
2575 child.column_name,
2576 child.position as column_position,
2577 parent.column_name as referenced_column_name
2578from all_constraints c
2579left join all_constraints p
2580 on p.owner = c.r_owner
2581 and p.constraint_name = c.r_constraint_name
2582left join all_cons_columns child
2583 on child.owner = c.owner
2584 and child.constraint_name = c.constraint_name
2585left join all_cons_columns parent
2586 on parent.owner = p.owner
2587 and parent.constraint_name = p.constraint_name
2588 and parent.position = child.position
2589where c.owner in ({owner_clause})
2590 and c.constraint_type in ('P', 'R', 'U', 'C', 'F')
2591order by c.owner, c.constraint_name, child.position
2592"
2593 );
2594 let params = schema_filter_params(schema_names);
2595
2596 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2597 apply_constraint_row(snapshot, &row)?;
2598 }
2599
2600 Ok(())
2601}
2602
2603async fn load_catalog_indexes<C: OracleConnection>(
2604 cx: &Cx,
2605 conn: &C,
2606 snapshot: &mut CatalogSnapshot,
2607 schema_names: &[String],
2608) -> Result<(), CatalogError> {
2609 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2610 let sql = format!(
2611 "
2612select
2613 i.owner,
2614 i.index_name,
2615 i.table_owner,
2616 i.table_name,
2617 case when i.uniqueness = 'UNIQUE' then 'Y' else 'N' end as is_unique,
2618 i.index_type,
2619 i.status,
2620 c.column_name,
2621 c.column_position
2622from all_indexes i
2623left join all_ind_columns c
2624 on c.index_owner = i.owner
2625 and c.index_name = i.index_name
2626 and c.table_owner = i.table_owner
2627 and c.table_name = i.table_name
2628where i.owner in ({owner_clause})
2629order by i.owner, i.index_name, c.column_position
2630"
2631 );
2632 let params = schema_filter_params(schema_names);
2633
2634 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2635 apply_index_row(snapshot, &row)?;
2636 }
2637
2638 Ok(())
2639}
2640
2641async fn load_catalog_triggers<C: OracleConnection>(
2642 cx: &Cx,
2643 conn: &C,
2644 snapshot: &mut CatalogSnapshot,
2645 schema_names: &[String],
2646) -> Result<(), CatalogError> {
2647 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2648 let sql = format!(
2649 "
2650select
2651 owner,
2652 trigger_name,
2653 table_owner,
2654 table_name,
2655 trigger_type,
2656 triggering_event,
2657 when_clause
2658from all_triggers
2659where owner in ({owner_clause})
2660 and base_object_type in ('TABLE', 'VIEW')
2661order by owner, trigger_name
2662"
2663 );
2664 let params = schema_filter_params(schema_names);
2665
2666 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2667 apply_trigger_row(snapshot, &row)?;
2668 }
2669
2670 Ok(())
2671}
2672
2673async fn load_catalog_synonyms<C: OracleConnection>(
2674 cx: &Cx,
2675 conn: &C,
2676 snapshot: &mut CatalogSnapshot,
2677 schema_names: &[String],
2678) -> Result<(), CatalogError> {
2679 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2680 let sql = format!(
2681 "
2682select
2683 owner,
2684 synonym_name,
2685 table_owner,
2686 table_name,
2687 db_link
2688from all_synonyms
2689where owner = 'PUBLIC'
2690 or owner in ({owner_clause})
2691order by owner, synonym_name
2692"
2693 );
2694 let params = schema_filter_params(schema_names);
2695
2696 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2697 apply_synonym_row(snapshot, &row)?;
2698 }
2699
2700 Ok(())
2701}
2702
2703async fn load_catalog_routines<C: OracleConnection>(
2704 cx: &Cx,
2705 conn: &C,
2706 snapshot: &mut CatalogSnapshot,
2707 schema_names: &[String],
2708) -> Result<(), CatalogError> {
2709 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2710 let procedure_sql = format!(
2711 "
2712select
2713 owner,
2714 object_name,
2715 procedure_name,
2716 subprogram_id,
2717 overload,
2718 object_type,
2719 deterministic,
2720 pipelined
2721from all_procedures
2722where owner in ({owner_clause})
2723 and (procedure_name is not null or object_type in ('FUNCTION', 'PROCEDURE'))
2724order by owner, object_name, procedure_name, subprogram_id
2725"
2726 );
2727 let argument_sql = format!(
2728 "
2729select
2730 owner,
2731 package_name,
2732 object_name,
2733 subprogram_id,
2734 overload,
2735 argument_name,
2736 position,
2737 sequence,
2738 data_type,
2739 type_owner,
2740 type_name,
2741 data_length,
2742 data_precision,
2743 data_scale,
2744 in_out,
2745 defaulted
2746from all_arguments
2747where owner in ({owner_clause})
2748 and data_level = 0
2749order by owner, package_name, object_name, subprogram_id, sequence
2750"
2751 );
2752 let params = schema_filter_params(schema_names);
2753 let procedure_rows = conn.query_rows(cx, &procedure_sql, ¶ms).await?;
2754 let argument_rows = conn.query_rows(cx, &argument_sql, ¶ms).await?;
2755 let mut routines = HashMap::<RoutineLocator, RoutineAccumulator>::new();
2756
2757 for row in &procedure_rows {
2758 apply_routine_row(snapshot, row, &mut routines)?;
2759 }
2760 for row in &argument_rows {
2761 apply_argument_row(snapshot, row, &mut routines)?;
2762 }
2763
2764 finalize_routines(snapshot, routines)
2765}
2766
2767async fn load_catalog_views<C: OracleConnection>(
2768 cx: &Cx,
2769 conn: &C,
2770 snapshot: &mut CatalogSnapshot,
2771 schema_names: &[String],
2772) -> Result<(), CatalogError> {
2773 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2774 let sql = format!(
2775 "
2776select
2777 owner,
2778 view_name,
2779 text_vc,
2780 read_only
2781from all_views
2782where owner in ({owner_clause})
2783order by owner, view_name
2784"
2785 );
2786 let params = schema_filter_params(schema_names);
2787
2788 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2789 apply_view_row(snapshot, &row)?;
2790 }
2791
2792 Ok(())
2793}
2794
2795async fn load_catalog_mviews<C: OracleConnection>(
2796 cx: &Cx,
2797 conn: &C,
2798 snapshot: &mut CatalogSnapshot,
2799 schema_names: &[String],
2800) -> Result<(), CatalogError> {
2801 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2802 let sql = format!(
2803 "
2804select
2805 owner,
2806 mview_name,
2807 refresh_mode,
2808 refresh_method,
2809 query
2810from all_mviews
2811where owner in ({owner_clause})
2812order by owner, mview_name
2813"
2814 );
2815 let params = schema_filter_params(schema_names);
2816
2817 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2818 apply_mview_row(snapshot, &row)?;
2819 }
2820
2821 Ok(())
2822}
2823
2824async fn load_catalog_sequences<C: OracleConnection>(
2825 cx: &Cx,
2826 conn: &C,
2827 snapshot: &mut CatalogSnapshot,
2828 schema_names: &[String],
2829) -> Result<(), CatalogError> {
2830 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2831 let sql = format!(
2832 "
2833select
2834 sequence_owner,
2835 sequence_name,
2836 min_value,
2837 max_value,
2838 increment_by,
2839 cycle_flag,
2840 order_flag,
2841 cache_size
2842from all_sequences
2843where sequence_owner in ({owner_clause})
2844order by sequence_owner, sequence_name
2845"
2846 );
2847 let params = schema_filter_params(schema_names);
2848
2849 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2850 apply_sequence_row(snapshot, &row)?;
2851 }
2852
2853 Ok(())
2854}
2855
2856async fn load_catalog_type_attrs<C: OracleConnection>(
2857 cx: &Cx,
2858 conn: &C,
2859 snapshot: &mut CatalogSnapshot,
2860 schema_names: &[String],
2861) -> Result<(), CatalogError> {
2862 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2863 let sql = format!(
2864 "
2865select
2866 owner,
2867 type_name,
2868 attr_name,
2869 attr_no,
2870 attr_type_owner,
2871 attr_type_name,
2872 length,
2873 precision,
2874 scale
2875from all_type_attrs
2876where owner in ({owner_clause})
2877order by owner, type_name, attr_no
2878"
2879 );
2880 let params = schema_filter_params(schema_names);
2881
2882 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2883 apply_type_attr_row(snapshot, &row)?;
2884 }
2885
2886 Ok(())
2887}
2888
2889async fn load_catalog_db_links<C: OracleConnection>(
2897 cx: &Cx,
2898 conn: &C,
2899 snapshot: &mut CatalogSnapshot,
2900 schema_names: &[String],
2901) -> Result<(), CatalogError> {
2902 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2903 let sql = format!(
2904 "
2905select
2906 owner,
2907 db_link,
2908 host
2909from all_db_links
2910where owner = 'PUBLIC'
2911 or owner in ({owner_clause})
2912order by owner, db_link
2913"
2914 );
2915 let params = schema_filter_params(schema_names);
2916
2917 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2918 apply_db_link_row(snapshot, &row)?;
2919 }
2920
2921 Ok(())
2922}
2923
2924async fn load_catalog_vpd_policies<C: OracleConnection>(
2929 cx: &Cx,
2930 conn: &C,
2931 snapshot: &mut CatalogSnapshot,
2932 schema_names: &[String],
2933) -> Result<(), CatalogError> {
2934 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2935 let sql = format!(
2936 "
2937select
2938 object_owner,
2939 object_name,
2940 policy_group,
2941 policy_name,
2942 pf_owner,
2943 package,
2944 function,
2945 sel,
2946 ins,
2947 upd,
2948 del,
2949 enable
2950from all_policies
2951where object_owner in ({owner_clause})
2952order by object_owner, object_name, policy_group, policy_name
2953"
2954 );
2955 let params = schema_filter_params(schema_names);
2956
2957 for row in conn.query_rows(cx, &sql, ¶ms).await? {
2958 apply_vpd_policy_row(snapshot, &row)?;
2959 }
2960 Ok(())
2961}
2962
2963async fn load_catalog_editions<C: OracleConnection>(
2967 cx: &Cx,
2968 conn: &C,
2969 snapshot: &mut CatalogSnapshot,
2970) -> Result<(), CatalogError> {
2971 let sql = "
2972select
2973 edition_name,
2974 parent_edition_name,
2975 usable
2976from all_editions
2977order by edition_name
2978";
2979 for row in conn.query_rows(cx, sql, &[]).await? {
2980 apply_edition_row(snapshot, &row)?;
2981 }
2982 Ok(())
2983}
2984
2985async fn load_catalog_editioning_views<C: OracleConnection>(
2988 cx: &Cx,
2989 conn: &C,
2990 snapshot: &mut CatalogSnapshot,
2991 schema_names: &[String],
2992) -> Result<(), CatalogError> {
2993 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
2994 let sql = format!(
2995 "
2996select
2997 owner,
2998 view_name,
2999 table_name
3000from all_editioning_views
3001where owner in ({owner_clause})
3002order by owner, view_name
3003"
3004 );
3005 let params = schema_filter_params(schema_names);
3006
3007 for row in conn.query_rows(cx, &sql, ¶ms).await? {
3008 apply_editioning_view_row(snapshot, &row)?;
3009 }
3010 Ok(())
3011}
3012
3013async fn load_catalog_table_comments<C: OracleConnection>(
3016 cx: &Cx,
3017 conn: &C,
3018 snapshot: &mut CatalogSnapshot,
3019 schema_names: &[String],
3020) -> Result<(), CatalogError> {
3021 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
3022 let sql = format!(
3023 "
3024select
3025 owner,
3026 table_name,
3027 table_type,
3028 comments
3029from all_tab_comments
3030where owner in ({owner_clause})
3031 and comments is not null
3032order by owner, table_name
3033"
3034 );
3035 let params = schema_filter_params(schema_names);
3036
3037 for row in conn.query_rows(cx, &sql, ¶ms).await? {
3038 apply_table_comment_row(snapshot, &row)?;
3039 }
3040
3041 Ok(())
3042}
3043
3044async fn load_catalog_column_comments<C: OracleConnection>(
3047 cx: &Cx,
3048 conn: &C,
3049 snapshot: &mut CatalogSnapshot,
3050 schema_names: &[String],
3051) -> Result<(), CatalogError> {
3052 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
3053 let sql = format!(
3054 "
3055select
3056 owner,
3057 table_name,
3058 column_name,
3059 comments
3060from all_col_comments
3061where owner in ({owner_clause})
3062 and comments is not null
3063order by owner, table_name, column_name
3064"
3065 );
3066 let params = schema_filter_params(schema_names);
3067
3068 for row in conn.query_rows(cx, &sql, ¶ms).await? {
3069 apply_column_comment_row(snapshot, &row)?;
3070 }
3071
3072 Ok(())
3073}
3074
3075async fn load_catalog_users<C: OracleConnection>(
3088 cx: &Cx,
3089 conn: &C,
3090 snapshot: &mut CatalogSnapshot,
3091) -> Result<(), CatalogError> {
3092 let sql = "select username from all_users order by username";
3093 match conn.query_rows(cx, sql, &[]).await {
3094 Ok(rows) => {
3095 let mut users = HashSet::with_capacity(rows.len());
3096 for row in &rows {
3097 let username = row.require_text("USERNAME")?;
3098 let Some(user) = snapshot.intern_user_name(username) else {
3099 return Err(CatalogError::InvalidColumnValue {
3100 column: String::from("USERNAME"),
3101 expected: "interned user name",
3102 value: String::from(username),
3103 });
3104 };
3105 users.insert(user);
3106 }
3107 snapshot.known_users = Some(users);
3108 }
3109 Err(error) => {
3110 snapshot.known_users = None;
3114 snapshot.capabilities.warnings.push(CapabilityWarning {
3115 code: String::from("all-users-probe"),
3116 message: format!("ALL_USERS read failed: {error}"),
3117 remediation: Some(String::from(
3118 "ensure the analysis user can SELECT ALL_USERS so object grants to roles are not misclassified as direct user grants.",
3119 )),
3120 });
3121 }
3122 }
3123 Ok(())
3124}
3125
3126async fn load_catalog_grants<C: OracleConnection>(
3127 cx: &Cx,
3128 conn: &C,
3129 snapshot: &mut CatalogSnapshot,
3130 schema_names: &[String],
3131) -> Result<(), CatalogError> {
3132 let owner_clause = oracle_bind_placeholders(schema_names.len(), 1);
3133 let sql = format!(
3134 "
3135select
3136 table_schema,
3137 table_name,
3138 grantee,
3139 privilege,
3140 grantable,
3141 hierarchy
3142from all_tab_privs
3143where table_schema in ({owner_clause})
3144order by table_schema, table_name, grantee, privilege
3145"
3146 );
3147 let params = schema_filter_params(schema_names);
3148
3149 for row in conn.query_rows(cx, &sql, ¶ms).await? {
3150 apply_grant_row(snapshot, &row)?;
3151 }
3152
3153 Ok(())
3154}
3155
3156fn oracle_bind_placeholders(count: usize, start_index: usize) -> String {
3157 (0..count)
3158 .map(|offset| format!(":{}", start_index + offset))
3159 .collect::<Vec<_>>()
3160 .join(", ")
3161}
3162
3163fn hash_text(text: &str) -> Hash {
3164 use sha2::{Digest as _, Sha256};
3165 let mut hasher = Sha256::new();
3166 hasher.update(text.as_bytes());
3167 let digest = hasher.finalize();
3172 let mut rendered = String::with_capacity(7 + digest.len() * 2);
3173 rendered.push_str("sha256:");
3174 for byte in digest {
3175 rendered.push_str(&format!("{byte:02x}"));
3176 }
3177 Hash::new(rendered)
3178}
3179
3180fn schema_filter_params(schema_names: &[String]) -> Vec<OracleBind> {
3181 schema_names
3182 .iter()
3183 .cloned()
3184 .map(OracleBind::from)
3185 .collect::<Vec<_>>()
3186}
3187
3188fn apply_object_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3189 let owner_text = row.require_text("OWNER")?;
3190 let object_name_text = row.require_text("OBJECT_NAME")?;
3191 let object_type_text = row.require_text("OBJECT_TYPE")?;
3192 let Some(object_type) = object_type_from_dictionary_value(object_type_text) else {
3193 return Ok(());
3194 };
3195
3196 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3197 return Err(CatalogError::InvalidColumnValue {
3198 column: String::from("OWNER"),
3199 expected: "interned schema name",
3200 value: String::from(owner_text),
3201 });
3202 };
3203 let Some(object_name) = snapshot.intern_object_name(object_name_text) else {
3204 return Err(CatalogError::InvalidColumnValue {
3205 column: String::from("OBJECT_NAME"),
3206 expected: "interned object name",
3207 value: String::from(object_name_text),
3208 });
3209 };
3210
3211 let last_ddl_time =
3212 optional_nonblank_text(row, "LAST_DDL_TIME_ISO").and_then(parse_dictionary_timestamp);
3213 let editionable = optional_bool(row, "EDITIONABLE")?;
3214 let edition_name = optional_nonblank_text(row, "EDITION_NAME")
3215 .map(|value| {
3216 snapshot
3217 .interner
3218 .intern(value)
3219 .map(EditionName::from)
3220 .ok_or(CatalogError::InvalidColumnValue {
3221 column: String::from("EDITION_NAME"),
3222 expected: "interned edition name",
3223 value: String::from(value),
3224 })
3225 })
3226 .transpose()?;
3227
3228 let common = ObjectCommon {
3229 owner,
3230 name: object_name,
3231 object_type,
3232 status: row
3233 .text("STATUS")
3234 .map(object_status_from_dictionary_value)
3235 .unwrap_or_default(),
3236 edition_name,
3237 editionable,
3238 last_ddl_time,
3239 ..ObjectCommon::default()
3240 };
3241
3242 let Some(catalog_object) = blank_catalog_object(common) else {
3243 return Ok(());
3244 };
3245
3246 snapshot
3247 .schemas
3248 .entry(owner)
3249 .or_default()
3250 .objects
3251 .insert(object_name, catalog_object);
3252
3253 Ok(())
3254}
3255
3256fn apply_dependency_row(
3257 snapshot: &mut CatalogSnapshot,
3258 row: &OracleRow,
3259) -> Result<(), CatalogError> {
3260 let owner_text = row.require_text("OWNER")?;
3261 let name_text = row.require_text("NAME")?;
3262 let referenced_owner_text = row.require_text("REFERENCED_OWNER")?;
3263 let referenced_name_text = row.require_text("REFERENCED_NAME")?;
3264
3265 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3266 return Err(CatalogError::InvalidColumnValue {
3267 column: String::from("OWNER"),
3268 expected: "interned schema name",
3269 value: String::from(owner_text),
3270 });
3271 };
3272 let Some(object_name) = snapshot.intern_object_name(name_text) else {
3273 return Err(CatalogError::InvalidColumnValue {
3274 column: String::from("NAME"),
3275 expected: "interned object name",
3276 value: String::from(name_text),
3277 });
3278 };
3279 let Some(referenced_owner) = snapshot.intern_schema_name(referenced_owner_text) else {
3280 return Err(CatalogError::InvalidColumnValue {
3281 column: String::from("REFERENCED_OWNER"),
3282 expected: "interned schema name",
3283 value: String::from(referenced_owner_text),
3284 });
3285 };
3286 let Some(referenced_name) = snapshot.intern_object_name(referenced_name_text) else {
3287 return Err(CatalogError::InvalidColumnValue {
3288 column: String::from("REFERENCED_NAME"),
3289 expected: "interned object name",
3290 value: String::from(referenced_name_text),
3291 });
3292 };
3293
3294 let object_type = optional_nonblank_text(row, "TYPE")
3295 .and_then(object_type_from_dictionary_value)
3296 .unwrap_or_default();
3297 let referenced_type =
3298 optional_nonblank_text(row, "REFERENCED_TYPE").and_then(object_type_from_dictionary_value);
3299
3300 let dependency = CatalogDependency {
3301 owner,
3302 name: object_name,
3303 object_type,
3304 referenced_owner: Some(referenced_owner),
3305 referenced_name,
3306 referenced_type,
3307 dependency_kind: optional_nonblank_text(row, "DEPENDENCY_TYPE")
3308 .map(catalog_dependency_kind_from_dictionary_value)
3309 .unwrap_or_default(),
3310 via_db_link: None,
3311 };
3312
3313 snapshot
3314 .schemas
3315 .entry(owner)
3316 .or_default()
3317 .dependencies
3318 .push(dependency);
3319
3320 Ok(())
3321}
3322
3323fn parse_dictionary_timestamp(text: &str) -> Option<DateTime<Utc>> {
3324 chrono::NaiveDateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S")
3326 .ok()
3327 .map(|naive| DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
3328}
3329
3330fn catalog_dependency_kind_from_dictionary_value(text: &str) -> CatalogDependencyKind {
3331 match text.to_ascii_uppercase().as_str() {
3332 "HARD" => CatalogDependencyKind::Hard,
3333 "REF" => CatalogDependencyKind::Reference,
3334 "EXTENDED" => CatalogDependencyKind::Extended,
3335 _ => CatalogDependencyKind::default(),
3336 }
3337}
3338
3339fn apply_column_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3340 let owner_text = row.require_text("OWNER")?;
3341 let table_name_text = row.require_text("TABLE_NAME")?;
3342 let column_name_text = row.require_text("COLUMN_NAME")?;
3343
3344 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3345 return Err(CatalogError::InvalidColumnValue {
3346 column: String::from("OWNER"),
3347 expected: "interned schema name",
3348 value: String::from(owner_text),
3349 });
3350 };
3351 let Some(table_name) = snapshot.intern_object_name(table_name_text) else {
3352 return Err(CatalogError::InvalidColumnValue {
3353 column: String::from("TABLE_NAME"),
3354 expected: "interned object name",
3355 value: String::from(table_name_text),
3356 });
3357 };
3358 let Some(column_name) = snapshot.intern_column_name(column_name_text) else {
3359 return Err(CatalogError::InvalidColumnValue {
3360 column: String::from("COLUMN_NAME"),
3361 expected: "interned column name",
3362 value: String::from(column_name_text),
3363 });
3364 };
3365 let data_type = data_type_ref_from_row(snapshot, row)?;
3366
3367 let Some(schema_catalog) = snapshot.schemas.get_mut(&owner) else {
3368 return Ok(());
3369 };
3370 let Some(catalog_object) = schema_catalog.objects.get_mut(&table_name) else {
3371 return Ok(());
3372 };
3373
3374 let default_expression = row
3375 .text("DATA_DEFAULT_VC")
3376 .map(String::from)
3377 .filter(|value| !value.trim().is_empty());
3378 let virtual_column = optional_bool(row, "VIRTUAL_COLUMN")?.unwrap_or(false);
3379 let column = ColumnMetadata {
3380 name: column_name,
3381 position: required_u32(row, "COLUMN_POSITION")?,
3382 data_type,
3383 nullable: optional_bool(row, "NULLABLE")?.unwrap_or(false),
3384 default_expression: if virtual_column {
3385 None
3386 } else {
3387 default_expression.clone()
3388 },
3389 generated_expression: if virtual_column {
3390 default_expression
3391 } else {
3392 None
3393 },
3394 hidden: optional_bool(row, "HIDDEN_COLUMN")?.unwrap_or(false),
3395 };
3396
3397 match catalog_object {
3398 CatalogObject::Table(metadata) => {
3399 metadata.columns.insert(column.name, column);
3400 }
3401 CatalogObject::View(metadata) => {
3402 metadata.columns.insert(column.name, column);
3403 }
3404 CatalogObject::MaterializedView(metadata) => {
3405 metadata.columns.insert(column.name, column);
3406 }
3407 CatalogObject::EditioningView(metadata) => {
3408 metadata.columns.insert(column.name, column);
3409 }
3410 CatalogObject::Sequence(_)
3411 | CatalogObject::Type(_)
3412 | CatalogObject::Package(_)
3413 | CatalogObject::Procedure(_)
3414 | CatalogObject::Function(_)
3415 | CatalogObject::Trigger(_)
3416 | CatalogObject::SchedulerJob(_) => {}
3417 }
3418
3419 Ok(())
3420}
3421
3422fn apply_constraint_row(
3423 snapshot: &mut CatalogSnapshot,
3424 row: &OracleRow,
3425) -> Result<(), CatalogError> {
3426 let owner_text = row.require_text("OWNER")?;
3427 let constraint_name_text = row.require_text("CONSTRAINT_NAME")?;
3428 let table_name_text = row.require_text("TABLE_NAME")?;
3429 let search_condition = optional_nonblank_text(row, "SEARCH_CONDITION_VC").map(String::from);
3430
3431 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3432 return Err(CatalogError::InvalidColumnValue {
3433 column: String::from("OWNER"),
3434 expected: "interned schema name",
3435 value: String::from(owner_text),
3436 });
3437 };
3438 let Some(constraint_name) = snapshot.intern_constraint_name(constraint_name_text) else {
3439 return Err(CatalogError::InvalidColumnValue {
3440 column: String::from("CONSTRAINT_NAME"),
3441 expected: "interned constraint name",
3442 value: String::from(constraint_name_text),
3443 });
3444 };
3445 let Some(table_name) = snapshot.intern_object_name(table_name_text) else {
3446 return Err(CatalogError::InvalidColumnValue {
3447 column: String::from("TABLE_NAME"),
3448 expected: "interned object name",
3449 value: String::from(table_name_text),
3450 });
3451 };
3452 let referenced_table_owner = optional_nonblank_text(row, "REFERENCED_TABLE_OWNER")
3453 .map(|value| {
3454 snapshot
3455 .intern_schema_name(value)
3456 .ok_or(CatalogError::InvalidColumnValue {
3457 column: String::from("REFERENCED_TABLE_OWNER"),
3458 expected: "interned schema name",
3459 value: String::from(value),
3460 })
3461 })
3462 .transpose()?;
3463 let referenced_table_name = optional_nonblank_text(row, "REFERENCED_TABLE_NAME")
3464 .map(|value| {
3465 snapshot
3466 .intern_object_name(value)
3467 .ok_or(CatalogError::InvalidColumnValue {
3468 column: String::from("REFERENCED_TABLE_NAME"),
3469 expected: "interned object name",
3470 value: String::from(value),
3471 })
3472 })
3473 .transpose()?;
3474 let child_column = optional_nonblank_text(row, "COLUMN_NAME")
3475 .map(|value| {
3476 snapshot
3477 .intern_column_name(value)
3478 .ok_or(CatalogError::InvalidColumnValue {
3479 column: String::from("COLUMN_NAME"),
3480 expected: "interned column name",
3481 value: String::from(value),
3482 })
3483 })
3484 .transpose()?;
3485 let referenced_column = optional_nonblank_text(row, "REFERENCED_COLUMN_NAME")
3486 .map(|value| {
3487 snapshot
3488 .intern_column_name(value)
3489 .ok_or(CatalogError::InvalidColumnValue {
3490 column: String::from("REFERENCED_COLUMN_NAME"),
3491 expected: "interned column name",
3492 value: String::from(value),
3493 })
3494 })
3495 .transpose()?;
3496
3497 let constraint_type = constraint_type_from_dictionary_value(
3498 row.require_text("CONSTRAINT_TYPE")?,
3499 search_condition.as_deref(),
3500 child_column.is_some(),
3501 );
3502
3503 let metadata = snapshot
3504 .schemas
3505 .entry(owner)
3506 .or_default()
3507 .constraints
3508 .entry(constraint_name)
3509 .or_insert_with(|| ConstraintMetadata {
3510 name: constraint_name,
3511 table_owner: owner,
3512 table_name,
3513 constraint_type,
3514 columns: Vec::new(),
3515 referenced_table_owner,
3516 referenced_table_name,
3517 referenced_columns: Vec::new(),
3518 search_condition: search_condition.clone(),
3519 deferrable: optional_bool(row, "IS_DEFERRABLE").ok().flatten(),
3520 initially_deferred: optional_bool(row, "IS_DEFERRED").ok().flatten(),
3521 });
3522
3523 metadata.table_name = table_name;
3524 metadata.constraint_type = constraint_type;
3525 metadata.referenced_table_owner = referenced_table_owner;
3526 metadata.referenced_table_name = referenced_table_name;
3527 metadata.search_condition = search_condition;
3528 metadata.deferrable = optional_bool(row, "IS_DEFERRABLE")?;
3529 metadata.initially_deferred = optional_bool(row, "IS_DEFERRED")?;
3530
3531 if let Some(column_name) = child_column {
3532 push_unique_column(&mut metadata.columns, column_name);
3533 }
3534 if let Some(column_name) = referenced_column {
3535 push_unique_column(&mut metadata.referenced_columns, column_name);
3536 }
3537
3538 Ok(())
3539}
3540
3541fn apply_index_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3542 let owner_text = row.require_text("OWNER")?;
3543 let index_name_text = row.require_text("INDEX_NAME")?;
3544 let table_owner_text = row.require_text("TABLE_OWNER")?;
3545 let table_name_text = row.require_text("TABLE_NAME")?;
3546
3547 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3548 return Err(CatalogError::InvalidColumnValue {
3549 column: String::from("OWNER"),
3550 expected: "interned schema name",
3551 value: String::from(owner_text),
3552 });
3553 };
3554 let Some(index_name) = snapshot.intern_index_name(index_name_text) else {
3555 return Err(CatalogError::InvalidColumnValue {
3556 column: String::from("INDEX_NAME"),
3557 expected: "interned index name",
3558 value: String::from(index_name_text),
3559 });
3560 };
3561 let Some(table_owner) = snapshot.intern_schema_name(table_owner_text) else {
3562 return Err(CatalogError::InvalidColumnValue {
3563 column: String::from("TABLE_OWNER"),
3564 expected: "interned schema name",
3565 value: String::from(table_owner_text),
3566 });
3567 };
3568 let Some(table_name) = snapshot.intern_object_name(table_name_text) else {
3569 return Err(CatalogError::InvalidColumnValue {
3570 column: String::from("TABLE_NAME"),
3571 expected: "interned object name",
3572 value: String::from(table_name_text),
3573 });
3574 };
3575 let index_column = optional_nonblank_text(row, "COLUMN_NAME")
3576 .map(|value| {
3577 snapshot
3578 .intern_column_name(value)
3579 .ok_or(CatalogError::InvalidColumnValue {
3580 column: String::from("COLUMN_NAME"),
3581 expected: "interned column name",
3582 value: String::from(value),
3583 })
3584 })
3585 .transpose()?;
3586
3587 let metadata = snapshot
3588 .schemas
3589 .entry(owner)
3590 .or_default()
3591 .indexes
3592 .entry(index_name)
3593 .or_insert_with(|| IndexMetadata {
3594 name: index_name,
3595 table_owner,
3596 table_name,
3597 unique: optional_bool(row, "IS_UNIQUE")
3598 .ok()
3599 .flatten()
3600 .unwrap_or(false),
3601 columns: Vec::new(),
3602 index_type: String::from(row.text("INDEX_TYPE").unwrap_or_default()),
3603 status: row
3604 .text("STATUS")
3605 .map(object_status_from_dictionary_value)
3606 .unwrap_or_default(),
3607 });
3608
3609 metadata.table_owner = table_owner;
3610 metadata.table_name = table_name;
3611 metadata.unique = optional_bool(row, "IS_UNIQUE")?.unwrap_or(false);
3612 metadata.index_type = String::from(row.text("INDEX_TYPE").unwrap_or_default());
3613 metadata.status = row
3614 .text("STATUS")
3615 .map(object_status_from_dictionary_value)
3616 .unwrap_or_default();
3617
3618 if let Some(column_name) = index_column {
3619 push_unique_column(&mut metadata.columns, column_name);
3620 }
3621
3622 Ok(())
3623}
3624
3625fn apply_trigger_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3626 let owner_text = row.require_text("OWNER")?;
3627 let trigger_name_text = row.require_text("TRIGGER_NAME")?;
3628 let table_owner_text = row.require_text("TABLE_OWNER")?;
3629 let table_name_text = row.require_text("TABLE_NAME")?;
3630
3631 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3632 return Err(CatalogError::InvalidColumnValue {
3633 column: String::from("OWNER"),
3634 expected: "interned schema name",
3635 value: String::from(owner_text),
3636 });
3637 };
3638 let Some(trigger_name) = snapshot.intern_trigger_name(trigger_name_text) else {
3639 return Err(CatalogError::InvalidColumnValue {
3640 column: String::from("TRIGGER_NAME"),
3641 expected: "interned trigger name",
3642 value: String::from(trigger_name_text),
3643 });
3644 };
3645 let Some(object_name) = snapshot.intern_object_name(trigger_name_text) else {
3646 return Err(CatalogError::InvalidColumnValue {
3647 column: String::from("TRIGGER_NAME"),
3648 expected: "interned object name",
3649 value: String::from(trigger_name_text),
3650 });
3651 };
3652 let Some(target_owner) = snapshot.intern_schema_name(table_owner_text) else {
3653 return Err(CatalogError::InvalidColumnValue {
3654 column: String::from("TABLE_OWNER"),
3655 expected: "interned schema name",
3656 value: String::from(table_owner_text),
3657 });
3658 };
3659 let Some(target_name) = snapshot.intern_object_name(table_name_text) else {
3660 return Err(CatalogError::InvalidColumnValue {
3661 column: String::from("TABLE_NAME"),
3662 expected: "interned object name",
3663 value: String::from(table_name_text),
3664 });
3665 };
3666
3667 let schema_catalog = snapshot.schemas.entry(owner).or_default();
3668 let common = schema_catalog
3669 .objects
3670 .get(&object_name)
3671 .and_then(|object| {
3672 if let CatalogObject::Trigger(metadata) = object {
3673 Some(metadata.common.clone())
3674 } else {
3675 None
3676 }
3677 })
3678 .unwrap_or_else(|| ObjectCommon {
3679 owner,
3680 name: object_name,
3681 object_type: ObjectType::Trigger,
3682 ..ObjectCommon::default()
3683 });
3684
3685 let metadata = TriggerMetadata {
3686 common,
3687 target_owner,
3688 target_name,
3689 timing: trigger_timing_from_dictionary_value(row.text("TRIGGER_TYPE").unwrap_or_default()),
3690 level: trigger_level_from_dictionary_value(row.text("TRIGGER_TYPE").unwrap_or_default()),
3691 events: trigger_events_from_dictionary_value(
3692 row.text("TRIGGERING_EVENT").unwrap_or_default(),
3693 ),
3694 when_clause: optional_nonblank_text(row, "WHEN_CLAUSE").map(String::from),
3695 body_hash: None,
3696 };
3697
3698 schema_catalog
3699 .triggers
3700 .insert(trigger_name, metadata.clone());
3701 schema_catalog
3702 .objects
3703 .insert(object_name, CatalogObject::Trigger(metadata));
3704
3705 Ok(())
3706}
3707
3708fn apply_synonym_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3709 let owner_text = row.require_text("OWNER")?;
3710 let synonym_name_text = row.require_text("SYNONYM_NAME")?;
3711 let target_name_text = row.require_text("TABLE_NAME")?;
3712
3713 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3714 return Err(CatalogError::InvalidColumnValue {
3715 column: String::from("OWNER"),
3716 expected: "interned schema name",
3717 value: String::from(owner_text),
3718 });
3719 };
3720 let Some(synonym_name) = snapshot.intern_synonym_name(synonym_name_text) else {
3721 return Err(CatalogError::InvalidColumnValue {
3722 column: String::from("SYNONYM_NAME"),
3723 expected: "interned synonym name",
3724 value: String::from(synonym_name_text),
3725 });
3726 };
3727 let Some(target_name) = snapshot.intern_object_name(target_name_text) else {
3728 return Err(CatalogError::InvalidColumnValue {
3729 column: String::from("TABLE_NAME"),
3730 expected: "interned object name",
3731 value: String::from(target_name_text),
3732 });
3733 };
3734 let target_owner = optional_nonblank_text(row, "TABLE_OWNER")
3735 .map(|value| {
3736 snapshot
3737 .intern_schema_name(value)
3738 .ok_or(CatalogError::InvalidColumnValue {
3739 column: String::from("TABLE_OWNER"),
3740 expected: "interned schema name",
3741 value: String::from(value),
3742 })
3743 })
3744 .transpose()?;
3745
3746 snapshot.schemas.entry(owner).or_default().synonyms.insert(
3747 synonym_name,
3748 SynonymTarget {
3749 target_owner,
3750 target_name,
3751 target_type: None,
3752 db_link: optional_nonblank_text(row, "DB_LINK").map(String::from),
3753 public_synonym: owner_text.eq_ignore_ascii_case("PUBLIC"),
3754 },
3755 );
3756
3757 Ok(())
3758}
3759
3760fn apply_view_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3761 let owner_text = row.require_text("OWNER")?;
3762 let view_name_text = row.require_text("VIEW_NAME")?;
3763
3764 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3765 return Err(CatalogError::InvalidColumnValue {
3766 column: String::from("OWNER"),
3767 expected: "interned schema name",
3768 value: String::from(owner_text),
3769 });
3770 };
3771 let Some(view_name) = snapshot.intern_object_name(view_name_text) else {
3772 return Err(CatalogError::InvalidColumnValue {
3773 column: String::from("VIEW_NAME"),
3774 expected: "interned object name",
3775 value: String::from(view_name_text),
3776 });
3777 };
3778
3779 let query_hash = optional_nonblank_text(row, "TEXT_VC").map(hash_text);
3780 let read_only = optional_bool(row, "READ_ONLY")?;
3781
3782 let Some(schema_catalog) = snapshot.schemas.get_mut(&owner) else {
3783 return Ok(());
3784 };
3785 let Some(catalog_object) = schema_catalog.objects.get_mut(&view_name) else {
3786 return Ok(());
3787 };
3788
3789 if let CatalogObject::View(metadata) = catalog_object {
3790 metadata.query_hash = query_hash;
3791 metadata.read_only = read_only;
3792 }
3793
3794 Ok(())
3795}
3796
3797fn apply_mview_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3798 let owner_text = row.require_text("OWNER")?;
3799 let mview_name_text = row.require_text("MVIEW_NAME")?;
3800
3801 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3802 return Err(CatalogError::InvalidColumnValue {
3803 column: String::from("OWNER"),
3804 expected: "interned schema name",
3805 value: String::from(owner_text),
3806 });
3807 };
3808 let Some(mview_name) = snapshot.intern_object_name(mview_name_text) else {
3809 return Err(CatalogError::InvalidColumnValue {
3810 column: String::from("MVIEW_NAME"),
3811 expected: "interned object name",
3812 value: String::from(mview_name_text),
3813 });
3814 };
3815
3816 let refresh_mode = optional_nonblank_text(row, "REFRESH_MODE").map(String::from);
3817 let refresh_method = optional_nonblank_text(row, "REFRESH_METHOD").map(String::from);
3818 let query_hash = optional_nonblank_text(row, "QUERY").map(hash_text);
3819
3820 let Some(schema_catalog) = snapshot.schemas.get_mut(&owner) else {
3821 return Ok(());
3822 };
3823 let Some(catalog_object) = schema_catalog.objects.get_mut(&mview_name) else {
3824 return Ok(());
3825 };
3826
3827 if let CatalogObject::MaterializedView(metadata) = catalog_object {
3828 metadata.refresh_mode = refresh_mode;
3829 metadata.refresh_method = refresh_method;
3830 metadata.query_hash = query_hash;
3831 }
3832
3833 Ok(())
3834}
3835
3836fn apply_sequence_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3837 let owner_text = row.require_text("SEQUENCE_OWNER")?;
3838 let sequence_name_text = row.require_text("SEQUENCE_NAME")?;
3839
3840 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3841 return Err(CatalogError::InvalidColumnValue {
3842 column: String::from("SEQUENCE_OWNER"),
3843 expected: "interned schema name",
3844 value: String::from(owner_text),
3845 });
3846 };
3847 let Some(sequence_name) = snapshot.intern_object_name(sequence_name_text) else {
3848 return Err(CatalogError::InvalidColumnValue {
3849 column: String::from("SEQUENCE_NAME"),
3850 expected: "interned object name",
3851 value: String::from(sequence_name_text),
3852 });
3853 };
3854
3855 let increment_by = row.parse_i64("INCREMENT_BY").unwrap_or(1);
3856 let min_value = row.parse_i64("MIN_VALUE").ok();
3857 let max_value = row.parse_i64("MAX_VALUE").ok();
3858 let cycle = row
3859 .text("CYCLE_FLAG")
3860 .map(|value| value.eq_ignore_ascii_case("Y"))
3861 .unwrap_or(false);
3862 let ordered = row
3863 .text("ORDER_FLAG")
3864 .map(|value| value.eq_ignore_ascii_case("Y"))
3865 .unwrap_or(false);
3866 let cache_size = row.parse_u64("CACHE_SIZE").ok();
3867
3868 let Some(schema_catalog) = snapshot.schemas.get_mut(&owner) else {
3869 return Ok(());
3870 };
3871 let Some(catalog_object) = schema_catalog.objects.get_mut(&sequence_name) else {
3872 return Ok(());
3873 };
3874
3875 if let CatalogObject::Sequence(metadata) = catalog_object {
3876 metadata.increment_by = increment_by;
3877 metadata.min_value = min_value;
3878 metadata.max_value = max_value;
3879 metadata.cycle = cycle;
3880 metadata.ordered = ordered;
3881 metadata.cache_size = cache_size;
3882 }
3883
3884 Ok(())
3885}
3886
3887fn apply_type_attr_row(
3888 snapshot: &mut CatalogSnapshot,
3889 row: &OracleRow,
3890) -> Result<(), CatalogError> {
3891 let owner_text = row.require_text("OWNER")?;
3892 let type_name_text = row.require_text("TYPE_NAME")?;
3893 let attr_name_text = row.require_text("ATTR_NAME")?;
3894
3895 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3896 return Err(CatalogError::InvalidColumnValue {
3897 column: String::from("OWNER"),
3898 expected: "interned schema name",
3899 value: String::from(owner_text),
3900 });
3901 };
3902 let Some(type_name) = snapshot.intern_object_name(type_name_text) else {
3903 return Err(CatalogError::InvalidColumnValue {
3904 column: String::from("TYPE_NAME"),
3905 expected: "interned object name",
3906 value: String::from(type_name_text),
3907 });
3908 };
3909 let Some(attr_name) = snapshot.intern_member_name(attr_name_text) else {
3910 return Err(CatalogError::InvalidColumnValue {
3911 column: String::from("ATTR_NAME"),
3912 expected: "interned member name",
3913 value: String::from(attr_name_text),
3914 });
3915 };
3916
3917 let attr_type_owner = optional_nonblank_text(row, "ATTR_TYPE_OWNER")
3918 .map(|value| {
3919 snapshot
3920 .intern_schema_name(value)
3921 .ok_or(CatalogError::InvalidColumnValue {
3922 column: String::from("ATTR_TYPE_OWNER"),
3923 expected: "interned schema name",
3924 value: String::from(value),
3925 })
3926 })
3927 .transpose()?;
3928 let attr_type_name = row
3929 .text("ATTR_TYPE_NAME")
3930 .map(String::from)
3931 .unwrap_or_default();
3932
3933 let attribute = TypeAttribute {
3934 name: attr_name,
3935 position: required_u32(row, "ATTR_NO")?,
3936 data_type: DataTypeRef {
3937 owner: attr_type_owner,
3938 name: attr_type_name,
3939 length: optional_u32(row, "LENGTH")?,
3940 precision: optional_u32(row, "PRECISION")?,
3941 scale: optional_i32(row, "SCALE")?,
3942 char_semantics: None,
3943 },
3944 };
3945
3946 let Some(schema_catalog) = snapshot.schemas.get_mut(&owner) else {
3947 return Ok(());
3948 };
3949 let Some(catalog_object) = schema_catalog.objects.get_mut(&type_name) else {
3950 return Ok(());
3951 };
3952
3953 if let CatalogObject::Type(metadata) = catalog_object {
3954 match metadata
3955 .attributes
3956 .iter()
3957 .position(|existing| existing.position.eq(&attribute.position))
3958 {
3959 Some(index) => metadata.attributes[index] = attribute,
3960 None => metadata.attributes.push(attribute),
3961 }
3962 metadata
3963 .attributes
3964 .sort_by_key(|attribute| attribute.position);
3965 }
3966
3967 Ok(())
3968}
3969
3970fn apply_db_link_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
3975 let owner_text = row.require_text("OWNER")?;
3976 let link_name_text = row.require_text("DB_LINK")?;
3977
3978 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
3979 return Err(CatalogError::InvalidColumnValue {
3980 column: String::from("OWNER"),
3981 expected: "interned schema name",
3982 value: String::from(owner_text),
3983 });
3984 };
3985
3986 let host = optional_nonblank_text(row, "HOST").map(String::from);
3987 let public_link = owner_text.eq_ignore_ascii_case("PUBLIC");
3988
3989 let schema_catalog = snapshot.schemas.entry(owner).or_default();
3990 schema_catalog.db_links.push(DatabaseLink {
3991 owner,
3992 name: String::from(link_name_text),
3993 host,
3994 public_link,
3995 });
3996
3997 Ok(())
3998}
3999
4000fn apply_vpd_policy_row(
4002 snapshot: &mut CatalogSnapshot,
4003 row: &OracleRow,
4004) -> Result<(), CatalogError> {
4005 let object_owner_text = row.require_text("OBJECT_OWNER")?;
4006 let object_name_text = row.require_text("OBJECT_NAME")?;
4007 let policy_name = row.require_text("POLICY_NAME")?.to_string();
4008 let function_owner_text = row.require_text("PF_OWNER")?;
4009 let function_name = row.require_text("FUNCTION")?.to_string();
4010
4011 let Some(object_owner) = snapshot.intern_schema_name(object_owner_text) else {
4012 return Err(CatalogError::InvalidColumnValue {
4013 column: String::from("OBJECT_OWNER"),
4014 expected: "interned schema name",
4015 value: String::from(object_owner_text),
4016 });
4017 };
4018 let Some(object_name) = snapshot.intern_object_name(object_name_text) else {
4019 return Err(CatalogError::InvalidColumnValue {
4020 column: String::from("OBJECT_NAME"),
4021 expected: "interned object name",
4022 value: String::from(object_name_text),
4023 });
4024 };
4025 let Some(function_owner) = snapshot.intern_schema_name(function_owner_text) else {
4026 return Err(CatalogError::InvalidColumnValue {
4027 column: String::from("PF_OWNER"),
4028 expected: "interned schema name",
4029 value: String::from(function_owner_text),
4030 });
4031 };
4032
4033 let policy_group = optional_nonblank_text(row, "POLICY_GROUP").map(String::from);
4034 let function_package = optional_nonblank_text(row, "PACKAGE").map(String::from);
4035
4036 let yn = |col: &str| {
4037 row.text(col)
4038 .map(|v| v.eq_ignore_ascii_case("Y") || v.eq_ignore_ascii_case("YES"))
4039 .unwrap_or(false)
4040 };
4041
4042 let schema_catalog = snapshot.schemas.entry(object_owner).or_default();
4043 schema_catalog.vpd_policies.push(VpdPolicy {
4044 object_owner,
4045 object_name,
4046 policy_group,
4047 policy_name,
4048 function_owner,
4049 function_package,
4050 function_name,
4051 on_select: yn("SEL"),
4052 on_insert: yn("INS"),
4053 on_update: yn("UPD"),
4054 on_delete: yn("DEL"),
4055 enabled: yn("ENABLE"),
4056 });
4057 Ok(())
4058}
4059
4060fn apply_edition_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
4062 let edition_name = row.require_text("EDITION_NAME")?.to_string();
4063 let parent_edition_name = optional_nonblank_text(row, "PARENT_EDITION_NAME").map(String::from);
4064 let usable = row
4065 .text("USABLE")
4066 .map(|v| v.eq_ignore_ascii_case("Y"))
4067 .unwrap_or(true);
4068 snapshot.editions.push(Edition {
4069 edition_name,
4070 parent_edition_name,
4071 usable,
4072 });
4073 Ok(())
4074}
4075
4076fn apply_editioning_view_row(
4078 snapshot: &mut CatalogSnapshot,
4079 row: &OracleRow,
4080) -> Result<(), CatalogError> {
4081 let owner_text = row.require_text("OWNER")?;
4082 let view_name_text = row.require_text("VIEW_NAME")?;
4083 let table_name_text = row.require_text("TABLE_NAME")?;
4084
4085 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
4086 return Err(CatalogError::InvalidColumnValue {
4087 column: String::from("OWNER"),
4088 expected: "interned schema name",
4089 value: String::from(owner_text),
4090 });
4091 };
4092 let Some(view_name) = snapshot.intern_object_name(view_name_text) else {
4093 return Err(CatalogError::InvalidColumnValue {
4094 column: String::from("VIEW_NAME"),
4095 expected: "interned object name",
4096 value: String::from(view_name_text),
4097 });
4098 };
4099 let Some(table_name) = snapshot.intern_object_name(table_name_text) else {
4100 return Err(CatalogError::InvalidColumnValue {
4101 column: String::from("TABLE_NAME"),
4102 expected: "interned object name",
4103 value: String::from(table_name_text),
4104 });
4105 };
4106
4107 let schema_catalog = snapshot.schemas.entry(owner).or_default();
4108 schema_catalog.editioning_views.push(EditioningView {
4109 owner,
4110 view_name,
4111 table_name,
4112 });
4113 Ok(())
4114}
4115
4116fn apply_table_comment_row(
4118 snapshot: &mut CatalogSnapshot,
4119 row: &OracleRow,
4120) -> Result<(), CatalogError> {
4121 let owner_text = row.require_text("OWNER")?;
4122 let table_name_text = row.require_text("TABLE_NAME")?;
4123 let table_type = row.text("TABLE_TYPE").map(String::from).unwrap_or_default();
4124 let comments = row.require_text("COMMENTS")?.to_string();
4125
4126 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
4127 return Err(CatalogError::InvalidColumnValue {
4128 column: String::from("OWNER"),
4129 expected: "interned schema name",
4130 value: String::from(owner_text),
4131 });
4132 };
4133 let Some(table_name) = snapshot.intern_object_name(table_name_text) else {
4134 return Err(CatalogError::InvalidColumnValue {
4135 column: String::from("TABLE_NAME"),
4136 expected: "interned object name",
4137 value: String::from(table_name_text),
4138 });
4139 };
4140
4141 let schema_catalog = snapshot.schemas.entry(owner).or_default();
4142 schema_catalog.table_comments.push(TableComment {
4143 owner,
4144 table_name,
4145 table_type,
4146 comments,
4147 });
4148 Ok(())
4149}
4150
4151fn apply_column_comment_row(
4153 snapshot: &mut CatalogSnapshot,
4154 row: &OracleRow,
4155) -> Result<(), CatalogError> {
4156 let owner_text = row.require_text("OWNER")?;
4157 let table_name_text = row.require_text("TABLE_NAME")?;
4158 let column_name_text = row.require_text("COLUMN_NAME")?;
4159 let comments = row.require_text("COMMENTS")?.to_string();
4160
4161 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
4162 return Err(CatalogError::InvalidColumnValue {
4163 column: String::from("OWNER"),
4164 expected: "interned schema name",
4165 value: String::from(owner_text),
4166 });
4167 };
4168 let Some(table_name) = snapshot.intern_object_name(table_name_text) else {
4169 return Err(CatalogError::InvalidColumnValue {
4170 column: String::from("TABLE_NAME"),
4171 expected: "interned object name",
4172 value: String::from(table_name_text),
4173 });
4174 };
4175 let Some(column_name) = snapshot.intern_column_name(column_name_text) else {
4176 return Err(CatalogError::InvalidColumnValue {
4177 column: String::from("COLUMN_NAME"),
4178 expected: "interned column name",
4179 value: String::from(column_name_text),
4180 });
4181 };
4182
4183 let schema_catalog = snapshot.schemas.entry(owner).or_default();
4184 schema_catalog.column_comments.push(ColumnComment {
4185 owner,
4186 table_name,
4187 column_name,
4188 comments,
4189 });
4190 Ok(())
4191}
4192
4193fn apply_grant_row(snapshot: &mut CatalogSnapshot, row: &OracleRow) -> Result<(), CatalogError> {
4194 let owner_text = row.require_text("TABLE_SCHEMA")?;
4195 let object_name_text = row.require_text("TABLE_NAME")?;
4196 let grantee_text = row.require_text("GRANTEE")?;
4197 let privilege_text = row.require_text("PRIVILEGE")?;
4198
4199 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
4200 return Err(CatalogError::InvalidColumnValue {
4201 column: String::from("TABLE_SCHEMA"),
4202 expected: "interned schema name",
4203 value: String::from(owner_text),
4204 });
4205 };
4206 let Some(object_name) = snapshot.intern_object_name(object_name_text) else {
4207 return Err(CatalogError::InvalidColumnValue {
4208 column: String::from("TABLE_NAME"),
4209 expected: "interned object name",
4210 value: String::from(object_name_text),
4211 });
4212 };
4213
4214 let grantee = grantee_from_dictionary_value(snapshot, grantee_text)?;
4215 let privilege = grant_privilege_from_dictionary_value(privilege_text);
4216 let grantable = row
4217 .text("GRANTABLE")
4218 .map(|value| value.eq_ignore_ascii_case("YES"))
4219 .unwrap_or(false);
4220 let with_hierarchy = row
4221 .text("HIERARCHY")
4222 .map(|value| value.eq_ignore_ascii_case("YES"))
4223 .unwrap_or(false);
4224
4225 let grant = Grant {
4226 object_owner: owner,
4227 object_name,
4228 privilege,
4229 grantee,
4230 grantable,
4231 via_role: None,
4232 with_hierarchy,
4233 };
4234
4235 let schema_catalog = snapshot.schemas.entry(owner).or_default();
4236 if !schema_catalog
4237 .grants
4238 .iter()
4239 .any(|existing| existing.eq(&grant))
4240 {
4241 schema_catalog.grants.push(grant);
4242 }
4243
4244 Ok(())
4245}
4246
4247fn grantee_from_dictionary_value(
4266 snapshot: &mut CatalogSnapshot,
4267 text: &str,
4268) -> Result<Grantee, CatalogError> {
4269 if text.eq_ignore_ascii_case("PUBLIC") {
4270 return Ok(Grantee::Public);
4271 }
4272 let Some(symbol) = snapshot.interner.intern(text) else {
4273 return Err(CatalogError::InvalidColumnValue {
4274 column: String::from("GRANTEE"),
4275 expected: "interned grantee name",
4276 value: String::from(text),
4277 });
4278 };
4279 let is_known_user = snapshot
4280 .known_users
4281 .as_ref()
4282 .is_some_and(|users| users.contains(&UserName::from(symbol)));
4283 if is_known_user {
4284 Ok(Grantee::User(UserName::from(symbol)))
4285 } else {
4286 Ok(Grantee::Role(RoleName::from(symbol)))
4287 }
4288}
4289
4290fn grant_privilege_from_dictionary_value(text: &str) -> GrantPrivilege {
4291 match text.to_ascii_uppercase().as_str() {
4292 "SELECT" => GrantPrivilege::Select,
4293 "INSERT" => GrantPrivilege::Insert,
4294 "UPDATE" => GrantPrivilege::Update,
4295 "DELETE" => GrantPrivilege::Delete,
4296 "EXECUTE" => GrantPrivilege::Execute,
4297 "ALTER" => GrantPrivilege::Alter,
4298 "INDEX" => GrantPrivilege::Index,
4299 "REFERENCES" => GrantPrivilege::References,
4300 "DEBUG" => GrantPrivilege::Debug,
4301 _ => GrantPrivilege::Other,
4302 }
4303}
4304
4305fn apply_routine_row(
4306 snapshot: &mut CatalogSnapshot,
4307 row: &OracleRow,
4308 routines: &mut HashMap<RoutineLocator, RoutineAccumulator>,
4309) -> Result<(), CatalogError> {
4310 let locator = routine_locator_from_procedure_row(snapshot, row)?;
4311 let deterministic = optional_bool(row, "DETERMINISTIC")?.unwrap_or(false);
4312 let pipelined = optional_bool(row, "PIPELINED")?.unwrap_or(false);
4313 let kind_hint = routine_kind_from_dictionary_value(optional_nonblank_text(row, "OBJECT_TYPE"));
4314
4315 let accumulator = routines.entry(locator).or_default();
4316 accumulator
4317 .signature
4318 .get_or_insert_with(|| RoutineSignature {
4319 routine_name: locator.routine_name,
4320 overload: locator.overload,
4321 ..RoutineSignature::default()
4322 });
4323 accumulator.kind_hint = kind_hint.or(accumulator.kind_hint);
4324 accumulator.deterministic = deterministic;
4325 accumulator.pipelined = pipelined;
4326
4327 Ok(())
4328}
4329
4330fn apply_argument_row(
4331 snapshot: &mut CatalogSnapshot,
4332 row: &OracleRow,
4333 routines: &mut HashMap<RoutineLocator, RoutineAccumulator>,
4334) -> Result<(), CatalogError> {
4335 let locator = routine_locator_from_argument_row(snapshot, row)?;
4336 let data_type = data_type_ref_from_argument_row(snapshot, row)?;
4337 let accumulator = routines.entry(locator).or_default();
4338 let signature = accumulator
4339 .signature
4340 .get_or_insert_with(|| RoutineSignature {
4341 routine_name: locator.routine_name,
4342 overload: locator.overload,
4343 ..RoutineSignature::default()
4344 });
4345 let position = required_u32(row, "POSITION")?;
4346
4347 if position.eq(&0) {
4348 signature.return_type = Some(data_type);
4349 accumulator.kind_hint = Some(RoutineKind::Function);
4350 return Ok(());
4351 }
4352
4353 signature.arguments.push(ArgumentMetadata {
4354 position,
4355 name: optional_nonblank_text(row, "ARGUMENT_NAME")
4356 .map(|value| {
4357 snapshot
4358 .intern_member_name(value)
4359 .ok_or(CatalogError::InvalidColumnValue {
4360 column: String::from("ARGUMENT_NAME"),
4361 expected: "interned member name",
4362 value: String::from(value),
4363 })
4364 })
4365 .transpose()?,
4366 mode: parameter_mode_from_dictionary_value(row.text("IN_OUT")),
4367 data_type,
4368 defaulted: optional_bool(row, "DEFAULTED")?.unwrap_or(false),
4369 });
4370
4371 Ok(())
4372}
4373
4374fn finalize_routines(
4375 snapshot: &mut CatalogSnapshot,
4376 routines: HashMap<RoutineLocator, RoutineAccumulator>,
4377) -> Result<(), CatalogError> {
4378 for (locator, accumulator) in routines {
4379 let Some(signature) = accumulator.signature else {
4380 continue;
4381 };
4382 let kind = accumulator
4383 .kind_hint
4384 .or_else(|| {
4385 if signature.return_type.is_some() {
4386 Some(RoutineKind::Function)
4387 } else {
4388 Some(RoutineKind::Procedure)
4389 }
4390 })
4391 .unwrap_or(RoutineKind::Procedure);
4392
4393 if let Some(package_name) = locator.package_name {
4394 upsert_packaged_routine(snapshot, locator.owner, package_name, kind, signature)?;
4395 } else {
4396 upsert_top_level_routine(
4397 snapshot,
4398 locator.owner,
4399 locator.routine_name,
4400 kind,
4401 signature,
4402 accumulator.deterministic,
4403 accumulator.pipelined,
4404 )?;
4405 }
4406 }
4407
4408 Ok(())
4409}
4410
4411fn object_type_from_dictionary_value(text: &str) -> Option<ObjectType> {
4412 match text.trim().to_ascii_uppercase().as_str() {
4413 "TABLE" => Some(ObjectType::Table),
4414 "VIEW" => Some(ObjectType::View),
4415 "MATERIALIZED VIEW" => Some(ObjectType::MaterializedView),
4416 "SEQUENCE" => Some(ObjectType::Sequence),
4417 "TYPE" => Some(ObjectType::Type),
4418 "PACKAGE" => Some(ObjectType::Package),
4419 "PROCEDURE" => Some(ObjectType::Procedure),
4420 "FUNCTION" => Some(ObjectType::Function),
4421 "TRIGGER" => Some(ObjectType::Trigger),
4422 "EDITIONING VIEW" => Some(ObjectType::EditioningView),
4423 _ => None,
4424 }
4425}
4426
4427fn object_status_from_dictionary_value(text: &str) -> ObjectStatus {
4428 match text.trim().to_ascii_uppercase().as_str() {
4429 "VALID" => ObjectStatus::Valid,
4430 "ENABLED" => ObjectStatus::Valid,
4431 "INVALID" => ObjectStatus::Invalid,
4432 "UNUSABLE" | "DISABLED" => ObjectStatus::Invalid,
4433 _ => ObjectStatus::NotApplicable,
4434 }
4435}
4436
4437fn routine_kind_from_dictionary_value(text: Option<&str>) -> Option<RoutineKind> {
4438 match text.map(|value| value.trim().to_ascii_uppercase()) {
4439 Some(value) if value.eq("FUNCTION") => Some(RoutineKind::Function),
4440 Some(value) if value.eq("PROCEDURE") => Some(RoutineKind::Procedure),
4441 _ => None,
4442 }
4443}
4444
4445fn constraint_type_from_dictionary_value(
4446 text: &str,
4447 search_condition: Option<&str>,
4448 has_columns: bool,
4449) -> ConstraintType {
4450 match text.trim().to_ascii_uppercase().as_str() {
4451 "P" => ConstraintType::PrimaryKey,
4452 "R" => ConstraintType::ForeignKey,
4453 "U" => ConstraintType::Unique,
4454 "F" => ConstraintType::Ref,
4455 "C" => {
4456 if has_columns
4457 && search_condition
4458 .map(|condition| {
4459 condition
4460 .trim()
4461 .to_ascii_uppercase()
4462 .contains("IS NOT NULL")
4463 })
4464 .unwrap_or(false)
4465 {
4466 ConstraintType::NotNull
4467 } else {
4468 ConstraintType::Check
4469 }
4470 }
4471 _ => ConstraintType::Other,
4472 }
4473}
4474
4475fn trigger_timing_from_dictionary_value(text: &str) -> TriggerTiming {
4476 let normalized = text.trim().to_ascii_uppercase();
4477 if normalized.contains("INSTEAD OF") {
4478 TriggerTiming::InsteadOf
4479 } else if normalized.contains("BEFORE") {
4480 TriggerTiming::Before
4481 } else if normalized.contains("AFTER") {
4482 TriggerTiming::After
4483 } else {
4484 TriggerTiming::Unknown
4485 }
4486}
4487
4488fn trigger_level_from_dictionary_value(text: &str) -> TriggerLevel {
4489 let normalized = text.trim().to_ascii_uppercase();
4490 if normalized.contains("EACH ROW") {
4491 TriggerLevel::Row
4492 } else if normalized.contains("STATEMENT") {
4493 TriggerLevel::Statement
4494 } else {
4495 TriggerLevel::Unknown
4496 }
4497}
4498
4499fn trigger_events_from_dictionary_value(text: &str) -> Vec<TriggerEvent> {
4500 let normalized = text.trim().to_ascii_uppercase();
4501 let mut events = Vec::<TriggerEvent>::new();
4502
4503 if normalized.contains("INSERT") {
4504 events.push(TriggerEvent::Insert);
4505 }
4506 if normalized.contains("UPDATE") {
4507 events.push(TriggerEvent::Update);
4508 }
4509 if normalized.contains("DELETE") {
4510 events.push(TriggerEvent::Delete);
4511 }
4512 if normalized.contains("LOGON") {
4513 events.push(TriggerEvent::Logon);
4514 }
4515 if normalized.contains("LOGOFF") {
4516 events.push(TriggerEvent::Logoff);
4517 }
4518 if normalized.contains("DDL") {
4519 events.push(TriggerEvent::Ddl);
4520 }
4521
4522 if events.is_empty() {
4523 events.push(TriggerEvent::Other);
4524 }
4525
4526 events
4527}
4528
4529fn push_unique_column(columns: &mut Vec<ColumnName>, column_name: ColumnName) {
4530 if !columns.contains(&column_name) {
4531 columns.push(column_name);
4532 }
4533}
4534
4535fn routine_locator_from_procedure_row(
4536 snapshot: &mut CatalogSnapshot,
4537 row: &OracleRow,
4538) -> Result<RoutineLocator, CatalogError> {
4539 let owner_text = row.require_text("OWNER")?;
4540 let container_name_text = row.require_text("OBJECT_NAME")?;
4541 let routine_name_text = row
4542 .text("PROCEDURE_NAME")
4543 .unwrap_or(container_name_text)
4544 .trim();
4545
4546 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
4547 return Err(CatalogError::InvalidColumnValue {
4548 column: String::from("OWNER"),
4549 expected: "interned schema name",
4550 value: String::from(owner_text),
4551 });
4552 };
4553 let Some(container_name) = snapshot.intern_object_name(container_name_text) else {
4554 return Err(CatalogError::InvalidColumnValue {
4555 column: String::from("OBJECT_NAME"),
4556 expected: "interned object name",
4557 value: String::from(container_name_text),
4558 });
4559 };
4560 let Some(routine_name) = snapshot.intern_object_name(routine_name_text) else {
4561 return Err(CatalogError::InvalidColumnValue {
4562 column: String::from("PROCEDURE_NAME"),
4563 expected: "interned object name",
4564 value: String::from(routine_name_text),
4565 });
4566 };
4567
4568 Ok(RoutineLocator {
4569 owner,
4570 package_name: if optional_nonblank_text(row, "PROCEDURE_NAME").is_some() {
4571 Some(container_name)
4572 } else {
4573 None
4574 },
4575 routine_name,
4576 subprogram_id: optional_u32(row, "SUBPROGRAM_ID")?,
4577 overload: optional_u32(row, "OVERLOAD")?,
4578 })
4579}
4580
4581fn routine_locator_from_argument_row(
4582 snapshot: &mut CatalogSnapshot,
4583 row: &OracleRow,
4584) -> Result<RoutineLocator, CatalogError> {
4585 let owner_text = row.require_text("OWNER")?;
4586 let routine_name_text = row.require_text("OBJECT_NAME")?;
4587
4588 let Some(owner) = snapshot.intern_schema_name(owner_text) else {
4589 return Err(CatalogError::InvalidColumnValue {
4590 column: String::from("OWNER"),
4591 expected: "interned schema name",
4592 value: String::from(owner_text),
4593 });
4594 };
4595 let package_name = optional_nonblank_text(row, "PACKAGE_NAME")
4596 .map(|value| {
4597 snapshot
4598 .intern_object_name(value)
4599 .ok_or(CatalogError::InvalidColumnValue {
4600 column: String::from("PACKAGE_NAME"),
4601 expected: "interned object name",
4602 value: String::from(value),
4603 })
4604 })
4605 .transpose()?;
4606 let Some(routine_name) = snapshot.intern_object_name(routine_name_text) else {
4607 return Err(CatalogError::InvalidColumnValue {
4608 column: String::from("OBJECT_NAME"),
4609 expected: "interned object name",
4610 value: String::from(routine_name_text),
4611 });
4612 };
4613
4614 Ok(RoutineLocator {
4615 owner,
4616 package_name,
4617 routine_name,
4618 subprogram_id: optional_u32(row, "SUBPROGRAM_ID")?,
4619 overload: optional_u32(row, "OVERLOAD")?,
4620 })
4621}
4622
4623fn upsert_packaged_routine(
4624 snapshot: &mut CatalogSnapshot,
4625 owner: SchemaName,
4626 package_name: ObjectName,
4627 kind: RoutineKind,
4628 signature: RoutineSignature,
4629) -> Result<(), CatalogError> {
4630 let schema_catalog = snapshot.schemas.entry(owner).or_default();
4631
4632 schema_catalog
4633 .objects
4634 .entry(package_name)
4635 .or_insert_with(|| {
4636 CatalogObject::Package(PackageMetadata {
4637 common: ObjectCommon {
4638 owner,
4639 name: package_name,
4640 object_type: ObjectType::Package,
4641 ..ObjectCommon::default()
4642 },
4643 ..PackageMetadata::default()
4644 })
4645 });
4646
4647 let Some(CatalogObject::Package(metadata)) = schema_catalog.objects.get_mut(&package_name)
4648 else {
4649 return Ok(());
4650 };
4651
4652 match kind {
4653 RoutineKind::Procedure => upsert_signature(&mut metadata.procedures, signature),
4654 RoutineKind::Function => upsert_signature(&mut metadata.functions, signature),
4655 }
4656
4657 Ok(())
4658}
4659
4660fn upsert_top_level_routine(
4661 snapshot: &mut CatalogSnapshot,
4662 owner: SchemaName,
4663 routine_name: ObjectName,
4664 kind: RoutineKind,
4665 signature: RoutineSignature,
4666 deterministic: bool,
4667 pipelined: bool,
4668) -> Result<(), CatalogError> {
4669 let schema_catalog = snapshot.schemas.entry(owner).or_default();
4670 let common = schema_catalog
4671 .objects
4672 .get(&routine_name)
4673 .and_then(|object| match object {
4674 CatalogObject::Procedure(metadata) => Some(metadata.common.clone()),
4675 CatalogObject::Function(metadata) => Some(metadata.common.clone()),
4676 _ => None,
4677 })
4678 .unwrap_or_else(|| ObjectCommon {
4679 owner,
4680 name: routine_name,
4681 object_type: match kind {
4682 RoutineKind::Procedure => ObjectType::Procedure,
4683 RoutineKind::Function => ObjectType::Function,
4684 },
4685 ..ObjectCommon::default()
4686 });
4687
4688 let catalog_object = match kind {
4689 RoutineKind::Procedure => CatalogObject::Procedure(ProcedureMetadata { common, signature }),
4690 RoutineKind::Function => CatalogObject::Function(FunctionMetadata {
4691 common,
4692 signature,
4693 deterministic,
4694 pipelined,
4695 }),
4696 };
4697 schema_catalog.objects.insert(routine_name, catalog_object);
4698
4699 Ok(())
4700}
4701
4702fn upsert_signature(signatures: &mut Vec<RoutineSignature>, signature: RoutineSignature) {
4703 if let Some(existing) = signatures.iter_mut().find(|candidate| {
4704 candidate.routine_name.eq(&signature.routine_name)
4705 && candidate.overload.eq(&signature.overload)
4706 }) {
4707 *existing = signature;
4708 } else {
4709 signatures.push(signature);
4710 }
4711}
4712
4713fn data_type_ref_from_argument_row(
4714 snapshot: &mut CatalogSnapshot,
4715 row: &OracleRow,
4716) -> Result<DataTypeRef, CatalogError> {
4717 let owner = optional_nonblank_text(row, "TYPE_OWNER")
4718 .map(|value| {
4719 snapshot
4720 .intern_schema_name(value)
4721 .ok_or(CatalogError::InvalidColumnValue {
4722 column: String::from("TYPE_OWNER"),
4723 expected: "interned schema name",
4724 value: String::from(value),
4725 })
4726 })
4727 .transpose()?;
4728 let type_name = optional_nonblank_text(row, "TYPE_NAME")
4729 .or_else(|| optional_nonblank_text(row, "DATA_TYPE"))
4730 .unwrap_or_default();
4731
4732 Ok(DataTypeRef {
4733 owner,
4734 name: String::from(type_name),
4735 length: optional_u32(row, "DATA_LENGTH")?,
4736 precision: optional_u32(row, "DATA_PRECISION")?,
4737 scale: optional_i32(row, "DATA_SCALE")?,
4738 char_semantics: None,
4739 })
4740}
4741
4742fn parameter_mode_from_dictionary_value(text: Option<&str>) -> ParameterMode {
4743 match text.map(|value| value.trim().to_ascii_uppercase()) {
4744 Some(value) if value.eq("OUT") => ParameterMode::Out,
4745 Some(value) if value.eq("IN/OUT") => ParameterMode::InOut,
4746 _ => ParameterMode::In,
4747 }
4748}
4749
4750fn blank_catalog_object(common: ObjectCommon) -> Option<CatalogObject> {
4751 match common.object_type {
4752 ObjectType::Table => Some(CatalogObject::Table(TableMetadata {
4753 common,
4754 ..TableMetadata::default()
4755 })),
4756 ObjectType::View => Some(CatalogObject::View(ViewMetadata {
4757 common,
4758 ..ViewMetadata::default()
4759 })),
4760 ObjectType::MaterializedView => Some(CatalogObject::MaterializedView(MViewMetadata {
4761 common,
4762 ..MViewMetadata::default()
4763 })),
4764 ObjectType::Sequence => Some(CatalogObject::Sequence(SequenceMetadata {
4765 common,
4766 ..SequenceMetadata::default()
4767 })),
4768 ObjectType::Type => Some(CatalogObject::Type(TypeMetadata {
4769 common,
4770 ..TypeMetadata::default()
4771 })),
4772 ObjectType::Package => Some(CatalogObject::Package(PackageMetadata {
4773 common,
4774 ..PackageMetadata::default()
4775 })),
4776 ObjectType::Procedure => Some(CatalogObject::Procedure(ProcedureMetadata {
4777 common,
4778 ..ProcedureMetadata::default()
4779 })),
4780 ObjectType::Function => Some(CatalogObject::Function(FunctionMetadata {
4781 common,
4782 ..FunctionMetadata::default()
4783 })),
4784 ObjectType::Trigger => Some(CatalogObject::Trigger(TriggerMetadata {
4785 common,
4786 ..TriggerMetadata::default()
4787 })),
4788 ObjectType::SchedulerJob => Some(CatalogObject::SchedulerJob(SchedulerJobMetadata {
4789 common,
4790 ..SchedulerJobMetadata::default()
4791 })),
4792 ObjectType::EditioningView => Some(CatalogObject::EditioningView(EditioningViewMetadata {
4793 common,
4794 ..EditioningViewMetadata::default()
4795 })),
4796 ObjectType::Synonym | ObjectType::Index | ObjectType::Constraint | ObjectType::Unknown => {
4797 None
4798 }
4799 }
4800}
4801
4802fn data_type_ref_from_row(
4803 snapshot: &mut CatalogSnapshot,
4804 row: &OracleRow,
4805) -> Result<DataTypeRef, CatalogError> {
4806 let owner = row
4807 .text("DATA_TYPE_OWNER")
4808 .map(str::trim)
4809 .filter(|value| !value.is_empty())
4810 .map(|value| {
4811 snapshot
4812 .intern_schema_name(value)
4813 .ok_or(CatalogError::InvalidColumnValue {
4814 column: String::from("DATA_TYPE_OWNER"),
4815 expected: "interned schema name",
4816 value: String::from(value),
4817 })
4818 })
4819 .transpose()?;
4820
4821 Ok(DataTypeRef {
4822 owner,
4823 name: String::from(row.require_text("DATA_TYPE")?),
4824 length: optional_u32(row, "DATA_LENGTH")?,
4825 precision: optional_u32(row, "DATA_PRECISION")?,
4826 scale: optional_i32(row, "DATA_SCALE")?,
4827 char_semantics: row.text("CHAR_USED").map(String::from),
4828 })
4829}
4830
4831fn optional_bool(row: &OracleRow, column: &str) -> Result<Option<bool>, CatalogError> {
4832 match row.text(column) {
4833 Some(_) => row.parse_bool(column).map(Some),
4834 None => Ok(None),
4835 }
4836}
4837
4838fn optional_nonblank_text<'a>(row: &'a OracleRow, column: &str) -> Option<&'a str> {
4839 row.text(column)
4840 .map(str::trim)
4841 .filter(|value| !value.is_empty())
4842}
4843
4844fn optional_u32(row: &OracleRow, column: &str) -> Result<Option<u32>, CatalogError> {
4845 match row.text(column) {
4846 Some(_) => {
4847 let parsed = row.parse_u64(column)?;
4848 u32::try_from(parsed)
4849 .map(Some)
4850 .map_err(|_| CatalogError::InvalidColumnValue {
4851 column: column.to_ascii_uppercase(),
4852 expected: "u32",
4853 value: parsed.to_string(),
4854 })
4855 }
4856 None => Ok(None),
4857 }
4858}
4859
4860fn required_u32(row: &OracleRow, column: &str) -> Result<u32, CatalogError> {
4861 let parsed = row.parse_u64(column)?;
4862 u32::try_from(parsed).map_err(|_| CatalogError::InvalidColumnValue {
4863 column: column.to_ascii_uppercase(),
4864 expected: "u32",
4865 value: parsed.to_string(),
4866 })
4867}
4868
4869fn optional_i32(row: &OracleRow, column: &str) -> Result<Option<i32>, CatalogError> {
4870 match row.text(column) {
4871 Some(_) => {
4872 let parsed = row.parse_i64(column)?;
4873 i32::try_from(parsed)
4874 .map(Some)
4875 .map_err(|_| CatalogError::InvalidColumnValue {
4876 column: column.to_ascii_uppercase(),
4877 expected: "i32",
4878 value: parsed.to_string(),
4879 })
4880 }
4881 None => Ok(None),
4882 }
4883}
4884
4885#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
4886pub struct SynonymTarget {
4887 pub target_owner: Option<SchemaName>,
4888 pub target_name: ObjectName,
4889 pub target_type: Option<ObjectType>,
4890 pub db_link: Option<String>,
4891 pub public_synonym: bool,
4892}
4893
4894#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
4904pub struct VpdPolicy {
4905 pub object_owner: SchemaName,
4907 pub object_name: ObjectName,
4909 pub policy_group: Option<String>,
4911 pub policy_name: String,
4913 pub function_owner: SchemaName,
4915 pub function_package: Option<String>,
4917 pub function_name: String,
4919 pub on_select: bool,
4921 pub on_insert: bool,
4922 pub on_update: bool,
4923 pub on_delete: bool,
4924 pub enabled: bool,
4926}
4927
4928#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
4932pub struct Edition {
4933 pub edition_name: String,
4936 pub parent_edition_name: Option<String>,
4939 pub usable: bool,
4941}
4942
4943#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
4947pub struct EditioningView {
4948 pub owner: SchemaName,
4950 pub view_name: ObjectName,
4952 pub table_name: ObjectName,
4954}
4955
4956#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
4963pub struct TableComment {
4964 pub owner: SchemaName,
4966 pub table_name: ObjectName,
4968 pub table_type: String,
4972 pub comments: String,
4975}
4976
4977#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
4981pub struct ColumnComment {
4982 pub owner: SchemaName,
4984 pub table_name: ObjectName,
4986 pub column_name: ColumnName,
4988 pub comments: String,
4990}
4991
4992#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5005pub struct DatabaseLink {
5006 pub owner: SchemaName,
5008 pub name: String,
5010 pub host: Option<String>,
5013 pub public_link: bool,
5016}
5017
5018#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5019pub enum GrantPrivilege {
5020 Select,
5021 Insert,
5022 Update,
5023 Delete,
5024 Execute,
5025 Alter,
5026 Index,
5027 References,
5028 Debug,
5029 #[default]
5030 Other,
5031}
5032
5033#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5034pub enum Grantee {
5035 User(UserName),
5036 Role(RoleName),
5037 #[default]
5038 Public,
5039}
5040
5041#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5042pub struct Grant {
5043 pub object_owner: SchemaName,
5044 pub object_name: ObjectName,
5045 pub privilege: GrantPrivilege,
5046 pub grantee: Grantee,
5047 pub grantable: bool,
5048 pub via_role: Option<RoleName>,
5049 pub with_hierarchy: bool,
5050}
5051
5052#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5053pub struct IndexMetadata {
5054 pub name: IndexName,
5055 pub table_owner: SchemaName,
5056 pub table_name: ObjectName,
5057 pub unique: bool,
5058 pub columns: Vec<ColumnName>,
5059 pub index_type: String,
5060 pub status: ObjectStatus,
5061}
5062
5063#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5064pub enum ConstraintType {
5065 PrimaryKey,
5066 ForeignKey,
5067 Unique,
5068 Check,
5069 NotNull,
5070 Ref,
5071 #[default]
5072 Other,
5073}
5074
5075#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5076pub struct ConstraintMetadata {
5077 pub name: ConstraintName,
5078 pub table_owner: SchemaName,
5079 pub table_name: ObjectName,
5080 pub constraint_type: ConstraintType,
5081 pub columns: Vec<ColumnName>,
5082 pub referenced_table_owner: Option<SchemaName>,
5083 pub referenced_table_name: Option<ObjectName>,
5084 pub referenced_columns: Vec<ColumnName>,
5085 pub search_condition: Option<String>,
5086 pub deferrable: Option<bool>,
5087 pub initially_deferred: Option<bool>,
5088}
5089
5090#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5091pub enum CatalogDependencyKind {
5092 Hard,
5093 Reference,
5094 Extended,
5095 #[default]
5096 Other,
5097}
5098
5099#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5100pub struct CatalogDependency {
5101 pub owner: SchemaName,
5102 pub name: ObjectName,
5103 pub object_type: ObjectType,
5104 pub referenced_owner: Option<SchemaName>,
5105 pub referenced_name: ObjectName,
5106 pub referenced_type: Option<ObjectType>,
5107 pub dependency_kind: CatalogDependencyKind,
5108 pub via_db_link: Option<String>,
5109}
5110
5111#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5112pub enum PlScopeAvailability {
5113 #[default]
5114 NotAvailable,
5115 AvailableButStale,
5116 IdentifiersOnly,
5117 IdentifiersAndStatements,
5118}
5119
5120#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5121pub struct CompilerIdentifier {
5122 pub owner: SchemaName,
5123 pub object_name: ObjectName,
5124 pub identifier_name: MemberName,
5125 pub identifier_type: String,
5126 pub usage: String,
5127 pub line: u32,
5128 pub column: u32,
5129}
5130
5131#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5132pub struct CompilerReference {
5133 pub owner: SchemaName,
5134 pub object_name: ObjectName,
5135 pub usage_line: u32,
5136 pub usage_column: u32,
5137 pub target_owner: Option<SchemaName>,
5138 pub target_object_name: Option<ObjectName>,
5139 pub target_identifier_name: Option<MemberName>,
5140}
5141
5142#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5143pub struct CompilerStatementUsage {
5144 pub owner: SchemaName,
5145 pub object_name: ObjectName,
5146 pub statement_kind: String,
5147 pub line: u32,
5148 pub column: u32,
5149}
5150
5151#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5152pub struct PlScopeSnapshot {
5153 pub availability: PlScopeAvailability,
5154 pub identifiers: Vec<CompilerIdentifier>,
5155 pub references: Vec<CompilerReference>,
5156 pub statements: Vec<CompilerStatementUsage>,
5157 pub collected_at: Option<DateTime<Utc>>,
5158 pub source_hash: Option<Hash>,
5159 pub warnings: Vec<CapabilityWarning>,
5160}
5161
5162#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5163pub struct DataTypeRef {
5164 pub owner: Option<SchemaName>,
5165 pub name: String,
5166 pub length: Option<u32>,
5167 pub precision: Option<u32>,
5168 pub scale: Option<i32>,
5169 pub char_semantics: Option<String>,
5170}
5171
5172#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5173pub struct ColumnMetadata {
5174 pub name: ColumnName,
5175 pub position: u32,
5176 pub data_type: DataTypeRef,
5177 pub nullable: bool,
5178 pub default_expression: Option<String>,
5179 pub generated_expression: Option<String>,
5180 pub hidden: bool,
5181}
5182
5183#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5184pub enum TemporaryTableDuration {
5185 #[default]
5186 Transaction,
5187 Session,
5188}
5189
5190#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5191pub struct TableMetadata {
5192 pub common: ObjectCommon,
5193 pub columns: HashMap<ColumnName, ColumnMetadata>,
5194 pub temporary: bool,
5195 pub temporary_duration: Option<TemporaryTableDuration>,
5196 pub index_organized: bool,
5197}
5198
5199#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5200pub struct ViewMetadata {
5201 pub common: ObjectCommon,
5202 pub columns: HashMap<ColumnName, ColumnMetadata>,
5203 pub query_hash: Option<Hash>,
5204 pub read_only: Option<bool>,
5205 pub check_option: Option<String>,
5206}
5207
5208#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5209pub struct MViewMetadata {
5210 pub common: ObjectCommon,
5211 pub columns: HashMap<ColumnName, ColumnMetadata>,
5212 pub refresh_mode: Option<String>,
5213 pub refresh_method: Option<String>,
5214 pub query_hash: Option<Hash>,
5215}
5216
5217#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5218pub struct SequenceMetadata {
5219 pub common: ObjectCommon,
5220 pub increment_by: i64,
5221 pub min_value: Option<i64>,
5222 pub max_value: Option<i64>,
5223 pub cycle: bool,
5224 pub ordered: bool,
5225 pub cache_size: Option<u64>,
5226}
5227
5228#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5229pub enum ParameterMode {
5230 #[default]
5231 In,
5232 Out,
5233 InOut,
5234}
5235
5236#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5237pub struct ArgumentMetadata {
5238 pub position: u32,
5239 pub name: Option<MemberName>,
5240 pub mode: ParameterMode,
5241 pub data_type: DataTypeRef,
5242 pub defaulted: bool,
5243}
5244
5245#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5246pub struct AccessibleByTarget {
5247 pub owner: Option<SchemaName>,
5248 pub object_name: ObjectName,
5249}
5250
5251#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5252pub struct RoutineSignature {
5253 pub routine_name: ObjectName,
5254 pub overload: Option<u32>,
5255 pub arguments: Vec<ArgumentMetadata>,
5256 pub return_type: Option<DataTypeRef>,
5257 pub authid_current_user: Option<bool>,
5258 pub accessible_by: Vec<AccessibleByTarget>,
5259}
5260
5261#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5262pub enum TypeFinality {
5263 Final,
5264 NotFinal,
5265 #[default]
5266 Unknown,
5267}
5268
5269#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5270pub enum TypeInstantiable {
5271 Instantiable,
5272 NotInstantiable,
5273 #[default]
5274 Unknown,
5275}
5276
5277#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5278pub struct TypeAttribute {
5279 pub name: MemberName,
5280 pub position: u32,
5281 pub data_type: DataTypeRef,
5282}
5283
5284#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5285pub struct TypeMetadata {
5286 pub common: ObjectCommon,
5287 pub attributes: Vec<TypeAttribute>,
5288 pub methods: Vec<RoutineSignature>,
5289 pub supertype_owner: Option<SchemaName>,
5290 pub supertype_name: Option<ObjectName>,
5291 pub finality: TypeFinality,
5292 pub instantiable: TypeInstantiable,
5293}
5294
5295#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5296pub struct PackageMetadata {
5297 pub common: ObjectCommon,
5298 pub procedures: Vec<RoutineSignature>,
5299 pub functions: Vec<RoutineSignature>,
5300 pub package_stateful: Option<bool>,
5301 pub authid_current_user: Option<bool>,
5302 pub accessible_by: Vec<AccessibleByTarget>,
5303}
5304
5305#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5306pub struct ProcedureMetadata {
5307 pub common: ObjectCommon,
5308 pub signature: RoutineSignature,
5309}
5310
5311#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5312pub struct FunctionMetadata {
5313 pub common: ObjectCommon,
5314 pub signature: RoutineSignature,
5315 pub deterministic: bool,
5316 pub pipelined: bool,
5317}
5318
5319#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5320pub enum TriggerTiming {
5321 Before,
5322 After,
5323 InsteadOf,
5324 #[default]
5325 Unknown,
5326}
5327
5328#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5329pub enum TriggerLevel {
5330 Statement,
5331 Row,
5332 #[default]
5333 Unknown,
5334}
5335
5336#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5337pub enum TriggerEvent {
5338 Insert,
5339 Update,
5340 Delete,
5341 Logon,
5342 Logoff,
5343 Ddl,
5344 #[default]
5345 Other,
5346}
5347
5348#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5349pub struct TriggerMetadata {
5350 pub common: ObjectCommon,
5351 pub target_owner: SchemaName,
5352 pub target_name: ObjectName,
5353 pub timing: TriggerTiming,
5354 pub level: TriggerLevel,
5355 pub events: Vec<TriggerEvent>,
5356 pub when_clause: Option<String>,
5357 pub body_hash: Option<Hash>,
5358}
5359
5360#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5361pub struct SchedulerJobMetadata {
5362 pub common: ObjectCommon,
5363 pub enabled: bool,
5364 pub job_type: String,
5365 pub program_name: Option<ObjectName>,
5366 pub schedule_name: Option<ObjectName>,
5367 pub job_action: Option<String>,
5368}
5369
5370#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
5371pub struct EditioningViewMetadata {
5372 pub common: ObjectCommon,
5373 pub base_table_owner: SchemaName,
5374 pub base_table_name: ObjectName,
5375 pub columns: HashMap<ColumnName, ColumnMetadata>,
5376}
5377
5378#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
5379pub enum CatalogObject {
5380 Table(TableMetadata),
5381 View(ViewMetadata),
5382 MaterializedView(MViewMetadata),
5383 Sequence(SequenceMetadata),
5384 Type(TypeMetadata),
5385 Package(PackageMetadata),
5386 Procedure(ProcedureMetadata),
5387 Function(FunctionMetadata),
5388 Trigger(TriggerMetadata),
5389 SchedulerJob(SchedulerJobMetadata),
5390 EditioningView(EditioningViewMetadata),
5391}
5392
5393#[cfg(test)]
5394mod tests {
5395 use std::collections::{HashMap, HashSet};
5396 use std::future::Future;
5397
5398 use asupersync::{Cx, runtime::RuntimeBuilder};
5399 use chrono::{DateTime, Utc};
5400 use plsql_core::{AnalysisProfile, ColumnName, MemberName, ObjectName, SchemaName, SymbolId};
5401 use tempfile::tempdir;
5402
5403 use crate::{
5404 AccessibleByTarget, CATALOG_SNAPSHOT_SCHEMA_ID, CATALOG_SNAPSHOT_SCHEMA_VERSION,
5405 CapabilityWarning, CatalogCapabilities, CatalogDependencyKind, CatalogError,
5406 CatalogLoadRequest, CatalogObject, CatalogSchemaFilter, CatalogSnapshot,
5407 CatalogSnapshotDocument, CatalogSource, CatalogSourceKind, CompilerIdentifier,
5408 ConstraintType, DataTypeRef, Hash, ObjectCommon, ObjectStatus, ObjectType, OracleBackend,
5409 OracleBind, OracleConnectOptions, OracleConnection, OracleConnectionInfo, OracleRow,
5410 PackageMetadata, PlScopeAvailability, PlScopeSnapshot, RoutineSignature, SchemaCatalog,
5411 SynonymName, SynonymTarget, TableMetadata, TriggerEvent, TriggerLevel, TriggerName,
5412 TriggerTiming, TypeFinality, TypeInstantiable, export_snapshot_to_json,
5413 grantee_from_dictionary_value, load_catalog_users, load_from_dbms_metadata_dir,
5414 load_snapshot_from_connection, load_snapshot_from_json, negotiate_capabilities,
5415 populate_dbms_metadata_ddl,
5416 };
5417
5418 #[derive(Clone, Debug, Eq, PartialEq)]
5419 struct QueryExpectation {
5420 sql_contains: String,
5421 params: Vec<OracleBind>,
5422 rows: Vec<OracleRow>,
5423 }
5424
5425 #[derive(Clone, Debug)]
5426 struct StaticConnection {
5427 rows: Vec<OracleRow>,
5428 row_count: u64,
5429 expected_queries: Vec<QueryExpectation>,
5430 connection_info: OracleConnectionInfo,
5431 }
5432
5433 impl Default for StaticConnection {
5434 fn default() -> Self {
5435 Self {
5436 rows: Vec::new(),
5437 row_count: 0,
5438 expected_queries: Vec::new(),
5439 connection_info: OracleConnectionInfo {
5440 backend: OracleBackend::RustOracle,
5441 connect_string: String::from("//localhost/XE"),
5442 current_schema: Some(String::from("BILLING")),
5443 server_version: String::from("23.0.0.0.0"),
5444 db_name: String::from("XE"),
5445 db_domain: String::new(),
5446 service_name: String::from("XE"),
5447 instance_name: String::from("xe"),
5448 server_type: String::from("Dedicated"),
5449 max_identifier_length: 128,
5450 max_open_cursors: 500,
5451 },
5452 }
5453 }
5454 }
5455
5456 #[async_trait::async_trait(?Send)]
5457 impl OracleConnection for StaticConnection {
5458 fn backend(&self) -> OracleBackend {
5459 OracleBackend::RustOracle
5460 }
5461
5462 async fn ping(&self, cx: &Cx) -> Result<(), CatalogError> {
5463 let _ = cx;
5464 Ok(())
5465 }
5466
5467 async fn describe(&self, cx: &Cx) -> Result<OracleConnectionInfo, CatalogError> {
5468 let _ = cx;
5469 Ok(self.connection_info.clone())
5470 }
5471
5472 async fn query_rows(
5473 &self,
5474 cx: &Cx,
5475 sql: &str,
5476 params: &[OracleBind],
5477 ) -> Result<Vec<OracleRow>, CatalogError> {
5478 let _ = cx;
5479 if !self.expected_queries.is_empty() {
5480 if let Some(expectation) = self.expected_queries.iter().find(|expectation| {
5481 sql.contains(expectation.sql_contains.as_str())
5482 && params.eq(expectation.params.as_slice())
5483 }) {
5484 return Ok(expectation.rows.clone());
5485 }
5486
5487 return Err(CatalogError::OracleBackendError {
5488 backend: OracleBackend::RustOracle,
5489 message: format!("unexpected query `{sql}` with params {params:?}"),
5490 });
5491 }
5492
5493 Ok(self.rows.clone())
5494 }
5495
5496 async fn execute(
5497 &self,
5498 cx: &Cx,
5499 _sql: &str,
5500 _params: &[OracleBind],
5501 ) -> Result<u64, CatalogError> {
5502 let _ = cx;
5503 Ok(self.row_count)
5504 }
5505 }
5506
5507 fn run_catalog_future<F: Future>(future: F) -> F::Output {
5508 RuntimeBuilder::current_thread()
5509 .build()
5510 .expect("test asupersync runtime")
5511 .block_on(future)
5512 }
5513
5514 fn test_cx() -> Cx {
5515 Cx::current().expect("test runtime installs a request Cx")
5516 }
5517
5518 fn load_snapshot_for_test<C: OracleConnection>(
5519 connection: &C,
5520 request: &CatalogLoadRequest,
5521 ) -> Result<CatalogSnapshot, CatalogError> {
5522 run_catalog_future(async {
5523 let cx = test_cx();
5524 load_snapshot_from_connection(&cx, connection, request).await
5525 })
5526 }
5527
5528 fn load_catalog_users_for_test<C: OracleConnection>(
5529 connection: &C,
5530 snapshot: &mut CatalogSnapshot,
5531 ) -> Result<(), CatalogError> {
5532 run_catalog_future(async {
5533 let cx = test_cx();
5534 load_catalog_users(&cx, connection, snapshot).await
5535 })
5536 }
5537
5538 fn populate_dbms_metadata_ddl_for_test<C: OracleConnection>(
5539 connection: &C,
5540 snapshot: &mut CatalogSnapshot,
5541 ) -> Result<(), CatalogError> {
5542 run_catalog_future(async {
5543 let cx = test_cx();
5544 populate_dbms_metadata_ddl(&cx, connection, snapshot).await
5545 })
5546 }
5547
5548 fn negotiate_capabilities_for_test<C: OracleConnection>(connection: &C) -> CatalogCapabilities {
5549 run_catalog_future(async {
5550 let cx = test_cx();
5551 negotiate_capabilities(&cx, connection).await
5552 })
5553 }
5554
5555 fn oracle_row(columns: &[(&str, &str, Option<&str>)]) -> OracleRow {
5556 let mut row = OracleRow::default();
5557 for (name, oracle_type, value) in columns {
5558 row.insert(*name, *oracle_type, value.map(String::from));
5559 }
5560 row
5561 }
5562
5563 fn capability_probe_expectations() -> Vec<QueryExpectation> {
5567 [
5568 "from all_objects where rownum = 0",
5569 "from dba_objects where rownum = 0",
5570 "from all_source where rownum = 0",
5571 "from all_scheduler_jobs where rownum = 0",
5572 "from all_tab_privs where rownum = 0",
5573 "from all_plsql_object_settings where rownum = 0",
5574 ]
5575 .into_iter()
5576 .map(|fragment| QueryExpectation {
5577 sql_contains: String::from(fragment),
5578 params: vec![],
5579 rows: vec![],
5580 })
5581 .collect()
5582 }
5583
5584 fn all_users_expectation(usernames: &[&str]) -> QueryExpectation {
5590 QueryExpectation {
5591 sql_contains: String::from("from all_users"),
5592 params: vec![],
5593 rows: usernames
5594 .iter()
5595 .map(|name| oracle_row(&[("USERNAME", "VARCHAR2(128)", Some(name))]))
5596 .collect(),
5597 }
5598 }
5599
5600 #[test]
5601 fn oracle_row_helpers_are_case_insensitive_and_typed() {
5602 let mut row = OracleRow::default();
5603 row.insert(
5604 "current_schema",
5605 "VARCHAR2(128)",
5606 Some(String::from("billing")),
5607 );
5608 row.insert("object_id", "NUMBER(10)", Some(String::from("42")));
5609 row.insert("editionable", "VARCHAR2(1)", Some(String::from("Y")));
5610 row.insert("ddl_text", "CLOB", None);
5611
5612 assert_eq!(row.text("CURRENT_SCHEMA"), Some("billing"));
5613 assert_eq!(row.parse_u64("object_id").ok(), Some(42));
5614 assert_eq!(row.parse_bool("EDITIONABLE").ok(), Some(true));
5615 assert!(matches!(
5616 row.require_text("ddl_text"),
5617 Err(CatalogError::NullColumnValue { column }) if column.eq("DDL_TEXT")
5618 ));
5619 }
5620
5621 #[test]
5622 fn parse_bool_honors_oracle_conventions_and_errors() {
5623 for t in ["Y", "y", "YES", " yes ", "TRUE", "true", "1"] {
5629 let row = oracle_row(&[("FLAG", "VARCHAR2(3)", Some(t))]);
5630 assert_eq!(row.parse_bool("flag").ok(), Some(true), "{t:?} -> true");
5631 }
5632 for f in ["N", "n", "NO", " no ", "FALSE", "false", "0"] {
5633 let row = oracle_row(&[("FLAG", "VARCHAR2(3)", Some(f))]);
5634 assert_eq!(row.parse_bool("flag").ok(), Some(false), "{f:?} -> false");
5635 }
5636 let bad = oracle_row(&[("FLAG", "VARCHAR2(5)", Some("MAYBE"))]);
5638 assert!(matches!(
5639 bad.parse_bool("FLAG"),
5640 Err(CatalogError::InvalidColumnValue {
5641 column,
5642 expected: "bool",
5643 ..
5644 }) if column.eq("FLAG")
5645 ));
5646 let empty = oracle_row(&[]);
5648 assert!(matches!(
5649 empty.parse_bool("FLAG"),
5650 Err(CatalogError::MissingColumn { column }) if column.eq("FLAG")
5651 ));
5652 let nullv = oracle_row(&[("FLAG", "VARCHAR2(1)", None)]);
5653 assert!(matches!(
5654 nullv.parse_bool("FLAG"),
5655 Err(CatalogError::NullColumnValue { column }) if column.eq("FLAG")
5656 ));
5657
5658 let neg = oracle_row(&[("N", "NUMBER", Some("-1"))]);
5661 assert!(matches!(
5662 neg.parse_u64("N"),
5663 Err(CatalogError::InvalidColumnValue {
5664 expected: "u64",
5665 ..
5666 })
5667 ));
5668 let i = oracle_row(&[("N", "NUMBER", Some("-42"))]);
5669 assert_eq!(i.parse_i64("N").ok(), Some(-42));
5670 }
5671
5672 #[test]
5673 fn catalog_load_request_defaults_to_current_schema() {
5674 let request = CatalogLoadRequest::default();
5675 assert_eq!(
5676 request.schema_filters,
5677 vec![CatalogSchemaFilter::CurrentSchema]
5678 );
5679 }
5680
5681 #[test]
5682 fn load_snapshot_from_connection_extracts_structural_metadata() {
5683 let object_rows = vec![
5684 oracle_row(&[
5685 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5686 ("OBJECT_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5687 ("OBJECT_TYPE", "VARCHAR2(30)", Some("PACKAGE")),
5688 ("STATUS", "VARCHAR2(7)", Some("VALID")),
5689 ]),
5690 oracle_row(&[
5691 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5692 ("OBJECT_NAME", "VARCHAR2(128)", Some("CALCULATE_TAX")),
5693 ("OBJECT_TYPE", "VARCHAR2(30)", Some("FUNCTION")),
5694 ("STATUS", "VARCHAR2(7)", Some("VALID")),
5695 ]),
5696 oracle_row(&[
5697 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5698 ("OBJECT_NAME", "VARCHAR2(128)", Some("CUSTOMERS")),
5699 ("OBJECT_TYPE", "VARCHAR2(30)", Some("TABLE")),
5700 ("STATUS", "VARCHAR2(7)", Some("VALID")),
5701 ]),
5702 oracle_row(&[
5703 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5704 ("OBJECT_NAME", "VARCHAR2(128)", Some("INVOICES")),
5705 ("OBJECT_TYPE", "VARCHAR2(30)", Some("TABLE")),
5706 ("STATUS", "VARCHAR2(7)", Some("VALID")),
5707 ]),
5708 oracle_row(&[
5709 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5710 ("OBJECT_NAME", "VARCHAR2(128)", Some("INVOICE_SUMMARY")),
5711 ("OBJECT_TYPE", "VARCHAR2(30)", Some("VIEW")),
5712 ("STATUS", "VARCHAR2(7)", Some("VALID")),
5713 ]),
5714 oracle_row(&[
5715 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5716 ("OBJECT_NAME", "VARCHAR2(128)", Some("INVOICES_BIU")),
5717 ("OBJECT_TYPE", "VARCHAR2(30)", Some("TRIGGER")),
5718 ("STATUS", "VARCHAR2(7)", Some("VALID")),
5719 ]),
5720 ];
5721 let column_rows = vec![
5722 oracle_row(&[
5723 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5724 ("TABLE_NAME", "VARCHAR2(128)", Some("CUSTOMERS")),
5725 ("COLUMN_NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
5726 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5727 ("DATA_TYPE_OWNER", "VARCHAR2(128)", None),
5728 ("DATA_TYPE", "VARCHAR2(128)", Some("NUMBER")),
5729 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5730 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
5731 ("DATA_SCALE", "NUMBER(10)", Some("0")),
5732 ("CHAR_USED", "VARCHAR2(1)", None),
5733 ("NULLABLE", "VARCHAR2(1)", Some("N")),
5734 ("DATA_DEFAULT_VC", "VARCHAR2(4000)", None),
5735 ("VIRTUAL_COLUMN", "VARCHAR2(3)", Some("NO")),
5736 ("HIDDEN_COLUMN", "VARCHAR2(3)", Some("NO")),
5737 ]),
5738 oracle_row(&[
5739 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5740 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5741 ("COLUMN_NAME", "VARCHAR2(128)", Some("INVOICE_ID")),
5742 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5743 ("DATA_TYPE_OWNER", "VARCHAR2(128)", None),
5744 ("DATA_TYPE", "VARCHAR2(128)", Some("NUMBER")),
5745 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5746 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
5747 ("DATA_SCALE", "NUMBER(10)", Some("0")),
5748 ("CHAR_USED", "VARCHAR2(1)", None),
5749 ("NULLABLE", "VARCHAR2(1)", Some("N")),
5750 ("DATA_DEFAULT_VC", "VARCHAR2(4000)", None),
5751 ("VIRTUAL_COLUMN", "VARCHAR2(3)", Some("NO")),
5752 ("HIDDEN_COLUMN", "VARCHAR2(3)", Some("NO")),
5753 ]),
5754 oracle_row(&[
5755 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5756 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5757 ("COLUMN_NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
5758 ("COLUMN_POSITION", "NUMBER(10)", Some("2")),
5759 ("DATA_TYPE_OWNER", "VARCHAR2(128)", None),
5760 ("DATA_TYPE", "VARCHAR2(128)", Some("NUMBER")),
5761 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5762 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
5763 ("DATA_SCALE", "NUMBER(10)", Some("0")),
5764 ("CHAR_USED", "VARCHAR2(1)", None),
5765 ("NULLABLE", "VARCHAR2(1)", Some("N")),
5766 ("DATA_DEFAULT_VC", "VARCHAR2(4000)", None),
5767 ("VIRTUAL_COLUMN", "VARCHAR2(3)", Some("NO")),
5768 ("HIDDEN_COLUMN", "VARCHAR2(3)", Some("NO")),
5769 ]),
5770 oracle_row(&[
5771 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5772 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICE_SUMMARY")),
5773 ("COLUMN_NAME", "VARCHAR2(128)", Some("TOTAL_DUE")),
5774 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5775 ("DATA_TYPE_OWNER", "VARCHAR2(128)", None),
5776 ("DATA_TYPE", "VARCHAR2(128)", Some("NUMBER")),
5777 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5778 ("DATA_PRECISION", "NUMBER(10)", Some("12")),
5779 ("DATA_SCALE", "NUMBER(10)", Some("2")),
5780 ("CHAR_USED", "VARCHAR2(1)", None),
5781 ("NULLABLE", "VARCHAR2(1)", Some("Y")),
5782 ("DATA_DEFAULT_VC", "VARCHAR2(4000)", Some("0")),
5783 ("VIRTUAL_COLUMN", "VARCHAR2(3)", Some("YES")),
5784 ("HIDDEN_COLUMN", "VARCHAR2(3)", Some("NO")),
5785 ]),
5786 ];
5787 let constraint_rows = vec![
5788 oracle_row(&[
5789 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5790 (
5791 "CONSTRAINT_NAME",
5792 "VARCHAR2(128)",
5793 Some("INVOICES_CUSTOMER_FK"),
5794 ),
5795 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5796 ("CONSTRAINT_TYPE", "VARCHAR2(1)", Some("R")),
5797 ("REFERENCED_TABLE_OWNER", "VARCHAR2(128)", Some("BILLING")),
5798 ("REFERENCED_TABLE_NAME", "VARCHAR2(128)", Some("CUSTOMERS")),
5799 ("SEARCH_CONDITION_VC", "VARCHAR2(4000)", None),
5800 ("IS_DEFERRABLE", "VARCHAR2(1)", Some("N")),
5801 ("IS_DEFERRED", "VARCHAR2(1)", Some("N")),
5802 ("COLUMN_NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
5803 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5804 (
5805 "REFERENCED_COLUMN_NAME",
5806 "VARCHAR2(128)",
5807 Some("CUSTOMER_ID"),
5808 ),
5809 ]),
5810 oracle_row(&[
5811 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5812 ("CONSTRAINT_NAME", "VARCHAR2(128)", Some("INVOICES_PK")),
5813 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5814 ("CONSTRAINT_TYPE", "VARCHAR2(1)", Some("P")),
5815 ("REFERENCED_TABLE_OWNER", "VARCHAR2(128)", None),
5816 ("REFERENCED_TABLE_NAME", "VARCHAR2(128)", None),
5817 ("SEARCH_CONDITION_VC", "VARCHAR2(4000)", None),
5818 ("IS_DEFERRABLE", "VARCHAR2(1)", Some("N")),
5819 ("IS_DEFERRED", "VARCHAR2(1)", Some("N")),
5820 ("COLUMN_NAME", "VARCHAR2(128)", Some("INVOICE_ID")),
5821 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5822 ("REFERENCED_COLUMN_NAME", "VARCHAR2(128)", None),
5823 ]),
5824 oracle_row(&[
5825 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5826 (
5827 "CONSTRAINT_NAME",
5828 "VARCHAR2(128)",
5829 Some("INVOICES_CUSTOMER_NN"),
5830 ),
5831 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5832 ("CONSTRAINT_TYPE", "VARCHAR2(1)", Some("C")),
5833 ("REFERENCED_TABLE_OWNER", "VARCHAR2(128)", None),
5834 ("REFERENCED_TABLE_NAME", "VARCHAR2(128)", None),
5835 (
5836 "SEARCH_CONDITION_VC",
5837 "VARCHAR2(4000)",
5838 Some("\"CUSTOMER_ID\" IS NOT NULL"),
5839 ),
5840 ("IS_DEFERRABLE", "VARCHAR2(1)", Some("N")),
5841 ("IS_DEFERRED", "VARCHAR2(1)", Some("N")),
5842 ("COLUMN_NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
5843 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5844 ("REFERENCED_COLUMN_NAME", "VARCHAR2(128)", None),
5845 ]),
5846 ];
5847 let index_rows = vec![oracle_row(&[
5848 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5849 ("INDEX_NAME", "VARCHAR2(128)", Some("INVOICES_PK_IDX")),
5850 ("TABLE_OWNER", "VARCHAR2(128)", Some("BILLING")),
5851 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5852 ("IS_UNIQUE", "VARCHAR2(1)", Some("Y")),
5853 ("INDEX_TYPE", "VARCHAR2(27)", Some("NORMAL")),
5854 ("STATUS", "VARCHAR2(8)", Some("VALID")),
5855 ("COLUMN_NAME", "VARCHAR2(128)", Some("INVOICE_ID")),
5856 ("COLUMN_POSITION", "NUMBER(10)", Some("1")),
5857 ])];
5858 let trigger_rows = vec![oracle_row(&[
5859 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5860 ("TRIGGER_NAME", "VARCHAR2(128)", Some("INVOICES_BIU")),
5861 ("TABLE_OWNER", "VARCHAR2(128)", Some("BILLING")),
5862 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5863 ("TRIGGER_TYPE", "VARCHAR2(80)", Some("BEFORE EACH ROW")),
5864 (
5865 "TRIGGERING_EVENT",
5866 "VARCHAR2(246)",
5867 Some("INSERT OR UPDATE"),
5868 ),
5869 ("WHEN_CLAUSE", "VARCHAR2(4000)", Some(":new.total_due >= 0")),
5870 ])];
5871 let synonym_rows = vec![
5872 oracle_row(&[
5873 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5874 ("SYNONYM_NAME", "VARCHAR2(128)", Some("LATEST_INVOICE")),
5875 ("TABLE_OWNER", "VARCHAR2(128)", Some("BILLING")),
5876 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5877 ("DB_LINK", "VARCHAR2(128)", None),
5878 ]),
5879 oracle_row(&[
5880 ("OWNER", "VARCHAR2(128)", Some("PUBLIC")),
5881 ("SYNONYM_NAME", "VARCHAR2(128)", Some("INVOICE_PUBLIC")),
5882 ("TABLE_OWNER", "VARCHAR2(128)", Some("BILLING")),
5883 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
5884 ("DB_LINK", "VARCHAR2(128)", None),
5885 ]),
5886 ];
5887 let procedure_rows = vec![
5888 oracle_row(&[
5889 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5890 ("OBJECT_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5891 ("PROCEDURE_NAME", "VARCHAR2(128)", Some("CREATE_INVOICE")),
5892 ("SUBPROGRAM_ID", "NUMBER(10)", Some("1")),
5893 ("OVERLOAD", "VARCHAR2(40)", None),
5894 ("OBJECT_TYPE", "VARCHAR2(13)", Some("PROCEDURE")),
5895 ("DETERMINISTIC", "VARCHAR2(3)", Some("NO")),
5896 ("PIPELINED", "VARCHAR2(3)", Some("NO")),
5897 ]),
5898 oracle_row(&[
5899 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5900 ("OBJECT_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5901 (
5902 "PROCEDURE_NAME",
5903 "VARCHAR2(128)",
5904 Some("TOTAL_FOR_CUSTOMER"),
5905 ),
5906 ("SUBPROGRAM_ID", "NUMBER(10)", Some("2")),
5907 ("OVERLOAD", "VARCHAR2(40)", Some("1")),
5908 ("OBJECT_TYPE", "VARCHAR2(13)", Some("FUNCTION")),
5909 ("DETERMINISTIC", "VARCHAR2(3)", Some("NO")),
5910 ("PIPELINED", "VARCHAR2(3)", Some("NO")),
5911 ]),
5912 oracle_row(&[
5913 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5914 ("OBJECT_NAME", "VARCHAR2(128)", Some("CALCULATE_TAX")),
5915 ("PROCEDURE_NAME", "VARCHAR2(128)", None),
5916 ("SUBPROGRAM_ID", "NUMBER(10)", Some("1")),
5917 ("OVERLOAD", "VARCHAR2(40)", None),
5918 ("OBJECT_TYPE", "VARCHAR2(13)", Some("FUNCTION")),
5919 ("DETERMINISTIC", "VARCHAR2(3)", Some("YES")),
5920 ("PIPELINED", "VARCHAR2(3)", Some("NO")),
5921 ]),
5922 ];
5923 let argument_rows = vec![
5924 oracle_row(&[
5925 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5926 ("PACKAGE_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5927 ("OBJECT_NAME", "VARCHAR2(128)", Some("CREATE_INVOICE")),
5928 ("SUBPROGRAM_ID", "NUMBER(10)", Some("1")),
5929 ("OVERLOAD", "VARCHAR2(40)", None),
5930 ("ARGUMENT_NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
5931 ("POSITION", "NUMBER(10)", Some("1")),
5932 ("SEQUENCE", "NUMBER(10)", Some("1")),
5933 ("DATA_TYPE", "VARCHAR2(30)", Some("NUMBER")),
5934 ("TYPE_OWNER", "VARCHAR2(128)", None),
5935 ("TYPE_NAME", "VARCHAR2(128)", None),
5936 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5937 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
5938 ("DATA_SCALE", "NUMBER(10)", Some("0")),
5939 ("IN_OUT", "VARCHAR2(9)", Some("IN")),
5940 ("DEFAULTED", "VARCHAR2(1)", Some("N")),
5941 ]),
5942 oracle_row(&[
5943 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5944 ("PACKAGE_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5945 ("OBJECT_NAME", "VARCHAR2(128)", Some("CREATE_INVOICE")),
5946 ("SUBPROGRAM_ID", "NUMBER(10)", Some("1")),
5947 ("OVERLOAD", "VARCHAR2(40)", None),
5948 ("ARGUMENT_NAME", "VARCHAR2(128)", Some("TOTAL_DUE")),
5949 ("POSITION", "NUMBER(10)", Some("2")),
5950 ("SEQUENCE", "NUMBER(10)", Some("2")),
5951 ("DATA_TYPE", "VARCHAR2(30)", Some("NUMBER")),
5952 ("TYPE_OWNER", "VARCHAR2(128)", None),
5953 ("TYPE_NAME", "VARCHAR2(128)", None),
5954 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5955 ("DATA_PRECISION", "NUMBER(10)", Some("12")),
5956 ("DATA_SCALE", "NUMBER(10)", Some("2")),
5957 ("IN_OUT", "VARCHAR2(9)", Some("IN")),
5958 ("DEFAULTED", "VARCHAR2(1)", Some("N")),
5959 ]),
5960 oracle_row(&[
5961 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5962 ("PACKAGE_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5963 ("OBJECT_NAME", "VARCHAR2(128)", Some("TOTAL_FOR_CUSTOMER")),
5964 ("SUBPROGRAM_ID", "NUMBER(10)", Some("2")),
5965 ("OVERLOAD", "VARCHAR2(40)", Some("1")),
5966 ("ARGUMENT_NAME", "VARCHAR2(128)", None),
5967 ("POSITION", "NUMBER(10)", Some("0")),
5968 ("SEQUENCE", "NUMBER(10)", Some("1")),
5969 ("DATA_TYPE", "VARCHAR2(30)", Some("NUMBER")),
5970 ("TYPE_OWNER", "VARCHAR2(128)", None),
5971 ("TYPE_NAME", "VARCHAR2(128)", None),
5972 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5973 ("DATA_PRECISION", "NUMBER(10)", Some("12")),
5974 ("DATA_SCALE", "NUMBER(10)", Some("2")),
5975 ("IN_OUT", "VARCHAR2(9)", Some("OUT")),
5976 ("DEFAULTED", "VARCHAR2(1)", Some("N")),
5977 ]),
5978 oracle_row(&[
5979 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5980 ("PACKAGE_NAME", "VARCHAR2(128)", Some("BILLING_API")),
5981 ("OBJECT_NAME", "VARCHAR2(128)", Some("TOTAL_FOR_CUSTOMER")),
5982 ("SUBPROGRAM_ID", "NUMBER(10)", Some("2")),
5983 ("OVERLOAD", "VARCHAR2(40)", Some("1")),
5984 ("ARGUMENT_NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
5985 ("POSITION", "NUMBER(10)", Some("1")),
5986 ("SEQUENCE", "NUMBER(10)", Some("2")),
5987 ("DATA_TYPE", "VARCHAR2(30)", Some("NUMBER")),
5988 ("TYPE_OWNER", "VARCHAR2(128)", None),
5989 ("TYPE_NAME", "VARCHAR2(128)", None),
5990 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
5991 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
5992 ("DATA_SCALE", "NUMBER(10)", Some("0")),
5993 ("IN_OUT", "VARCHAR2(9)", Some("IN")),
5994 ("DEFAULTED", "VARCHAR2(1)", Some("N")),
5995 ]),
5996 oracle_row(&[
5997 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
5998 ("PACKAGE_NAME", "VARCHAR2(128)", None),
5999 ("OBJECT_NAME", "VARCHAR2(128)", Some("CALCULATE_TAX")),
6000 ("SUBPROGRAM_ID", "NUMBER(10)", Some("1")),
6001 ("OVERLOAD", "VARCHAR2(40)", None),
6002 ("ARGUMENT_NAME", "VARCHAR2(128)", None),
6003 ("POSITION", "NUMBER(10)", Some("0")),
6004 ("SEQUENCE", "NUMBER(10)", Some("1")),
6005 ("DATA_TYPE", "VARCHAR2(30)", Some("NUMBER")),
6006 ("TYPE_OWNER", "VARCHAR2(128)", None),
6007 ("TYPE_NAME", "VARCHAR2(128)", None),
6008 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
6009 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
6010 ("DATA_SCALE", "NUMBER(10)", Some("2")),
6011 ("IN_OUT", "VARCHAR2(9)", Some("OUT")),
6012 ("DEFAULTED", "VARCHAR2(1)", Some("N")),
6013 ]),
6014 oracle_row(&[
6015 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6016 ("PACKAGE_NAME", "VARCHAR2(128)", None),
6017 ("OBJECT_NAME", "VARCHAR2(128)", Some("CALCULATE_TAX")),
6018 ("SUBPROGRAM_ID", "NUMBER(10)", Some("1")),
6019 ("OVERLOAD", "VARCHAR2(40)", None),
6020 ("ARGUMENT_NAME", "VARCHAR2(128)", Some("INVOICE_ID")),
6021 ("POSITION", "NUMBER(10)", Some("1")),
6022 ("SEQUENCE", "NUMBER(10)", Some("2")),
6023 ("DATA_TYPE", "VARCHAR2(30)", Some("NUMBER")),
6024 ("TYPE_OWNER", "VARCHAR2(128)", None),
6025 ("TYPE_NAME", "VARCHAR2(128)", None),
6026 ("DATA_LENGTH", "NUMBER(10)", Some("22")),
6027 ("DATA_PRECISION", "NUMBER(10)", Some("10")),
6028 ("DATA_SCALE", "NUMBER(10)", Some("0")),
6029 ("IN_OUT", "VARCHAR2(9)", Some("IN")),
6030 ("DEFAULTED", "VARCHAR2(1)", Some("N")),
6031 ]),
6032 ];
6033 let mut expected_queries = capability_probe_expectations();
6034 expected_queries.push(all_users_expectation(&["BILLING", "REPORTING"]));
6037 expected_queries.extend(vec![
6038 QueryExpectation {
6039 sql_contains: String::from("from all_objects"),
6040 params: vec![OracleBind::from("BILLING")],
6041 rows: object_rows,
6042 },
6043 QueryExpectation {
6044 sql_contains: String::from("from all_tab_cols"),
6045 params: vec![OracleBind::from("BILLING")],
6046 rows: column_rows,
6047 },
6048 QueryExpectation {
6049 sql_contains: String::from("from all_constraints c"),
6050 params: vec![OracleBind::from("BILLING")],
6051 rows: constraint_rows,
6052 },
6053 QueryExpectation {
6054 sql_contains: String::from("from all_indexes i"),
6055 params: vec![OracleBind::from("BILLING")],
6056 rows: index_rows,
6057 },
6058 QueryExpectation {
6059 sql_contains: String::from("from all_triggers"),
6060 params: vec![OracleBind::from("BILLING")],
6061 rows: trigger_rows,
6062 },
6063 QueryExpectation {
6064 sql_contains: String::from("from all_synonyms"),
6065 params: vec![OracleBind::from("BILLING")],
6066 rows: synonym_rows,
6067 },
6068 QueryExpectation {
6069 sql_contains: String::from("from all_procedures"),
6070 params: vec![OracleBind::from("BILLING")],
6071 rows: procedure_rows,
6072 },
6073 QueryExpectation {
6074 sql_contains: String::from("from all_arguments"),
6075 params: vec![OracleBind::from("BILLING")],
6076 rows: argument_rows,
6077 },
6078 QueryExpectation {
6079 sql_contains: String::from("from all_views"),
6080 params: vec![OracleBind::from("BILLING")],
6081 rows: vec![oracle_row(&[
6082 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6083 ("VIEW_NAME", "VARCHAR2(128)", Some("INVOICE_SUMMARY")),
6084 (
6085 "TEXT_VC",
6086 "VARCHAR2(4000)",
6087 Some(
6088 "select invoice_id, sum(amount) total_due from invoices group by invoice_id",
6089 ),
6090 ),
6091 ("READ_ONLY", "VARCHAR2(1)", Some("N")),
6092 ])],
6093 },
6094 QueryExpectation {
6095 sql_contains: String::from("from all_mviews"),
6096 params: vec![OracleBind::from("BILLING")],
6097 rows: vec![],
6098 },
6099 QueryExpectation {
6100 sql_contains: String::from("from all_sequences"),
6101 params: vec![OracleBind::from("BILLING")],
6102 rows: vec![],
6103 },
6104 QueryExpectation {
6105 sql_contains: String::from("from all_type_attrs"),
6106 params: vec![OracleBind::from("BILLING")],
6107 rows: vec![],
6108 },
6109 QueryExpectation {
6110 sql_contains: String::from("from all_tab_privs"),
6111 params: vec![OracleBind::from("BILLING")],
6112 rows: vec![oracle_row(&[
6113 ("TABLE_SCHEMA", "VARCHAR2(128)", Some("BILLING")),
6114 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
6115 ("GRANTEE", "VARCHAR2(128)", Some("REPORTING")),
6116 ("PRIVILEGE", "VARCHAR2(40)", Some("SELECT")),
6117 ("GRANTABLE", "VARCHAR2(3)", Some("NO")),
6118 ("HIERARCHY", "VARCHAR2(3)", Some("NO")),
6119 ])],
6120 },
6121 QueryExpectation {
6122 sql_contains: String::from("from all_db_links"),
6123 params: vec![OracleBind::from("BILLING")],
6124 rows: vec![],
6125 },
6126 QueryExpectation {
6127 sql_contains: String::from("from all_tab_comments"),
6128 params: vec![OracleBind::from("BILLING")],
6129 rows: vec![],
6130 },
6131 QueryExpectation {
6132 sql_contains: String::from("from all_col_comments"),
6133 params: vec![OracleBind::from("BILLING")],
6134 rows: vec![],
6135 },
6136 QueryExpectation {
6137 sql_contains: String::from("from all_editions"),
6138 params: vec![],
6139 rows: vec![],
6140 },
6141 QueryExpectation {
6142 sql_contains: String::from("from all_editioning_views"),
6143 params: vec![OracleBind::from("BILLING")],
6144 rows: vec![],
6145 },
6146 QueryExpectation {
6147 sql_contains: String::from("from all_policies"),
6148 params: vec![OracleBind::from("BILLING")],
6149 rows: vec![],
6150 },
6151 QueryExpectation {
6152 sql_contains: String::from("from all_dependencies"),
6153 params: vec![OracleBind::from("BILLING")],
6154 rows: vec![],
6155 },
6156 ]);
6157 let connection = StaticConnection {
6158 expected_queries,
6159 ..StaticConnection::default()
6160 };
6161
6162 let snapshot = load_snapshot_for_test(&connection, &CatalogLoadRequest::default()).unwrap();
6163
6164 let current_schema = snapshot
6165 .profile
6166 .current_schema
6167 .and_then(|name| snapshot.interner.resolve(name.symbol()));
6168 assert_eq!(current_schema, Some("BILLING"));
6169 assert_eq!(
6170 snapshot.profile.oracle_version,
6171 plsql_core::OracleVersion::Oracle23ai
6172 );
6173 assert!(snapshot.capabilities.can_query_all_views);
6174
6175 let billing_schema = snapshot
6176 .profile
6177 .current_schema
6178 .expect("current schema should be interned");
6179 let schema_catalog = snapshot
6180 .schemas
6181 .get(&billing_schema)
6182 .expect("billing schema should exist");
6183
6184 let invoices_name = schema_catalog
6185 .objects
6186 .keys()
6187 .find(|name| {
6188 snapshot
6189 .interner
6190 .resolve(name.symbol())
6191 .is_some_and(|label| label.eq("INVOICES"))
6192 })
6193 .copied()
6194 .expect("object name should intern");
6195 let invoice_summary_name = schema_catalog
6196 .objects
6197 .keys()
6198 .find(|name| {
6199 snapshot
6200 .interner
6201 .resolve(name.symbol())
6202 .is_some_and(|label| label.eq("INVOICE_SUMMARY"))
6203 })
6204 .copied()
6205 .expect("object name should intern");
6206 let package_object_name = schema_catalog
6207 .objects
6208 .keys()
6209 .find(|name| {
6210 snapshot
6211 .interner
6212 .resolve(name.symbol())
6213 .is_some_and(|label| label.eq("BILLING_API"))
6214 })
6215 .copied()
6216 .expect("package object name should intern");
6217 let tax_function_name = schema_catalog
6218 .objects
6219 .keys()
6220 .find(|name| {
6221 snapshot
6222 .interner
6223 .resolve(name.symbol())
6224 .is_some_and(|label| label.eq("CALCULATE_TAX"))
6225 })
6226 .copied()
6227 .expect("function object name should intern");
6228 let trigger_object_name = schema_catalog
6229 .objects
6230 .keys()
6231 .find(|name| {
6232 snapshot
6233 .interner
6234 .resolve(name.symbol())
6235 .is_some_and(|label| label.eq("INVOICES_BIU"))
6236 })
6237 .copied()
6238 .expect("trigger object name should intern");
6239
6240 let invoices_table = schema_catalog
6241 .objects
6242 .get(&invoices_name)
6243 .and_then(|object| {
6244 if let CatalogObject::Table(table) = object {
6245 Some(table)
6246 } else {
6247 None
6248 }
6249 });
6250 assert!(invoices_table.is_some());
6251 let invoice_id = invoices_table
6252 .and_then(|table| {
6253 table.columns.values().find(|column| {
6254 snapshot
6255 .interner
6256 .resolve(column.name.symbol())
6257 .is_some_and(|label| label.eq("INVOICE_ID"))
6258 })
6259 })
6260 .expect("table column should exist");
6261 assert_eq!(invoice_id.position, 1);
6262 assert_eq!(invoice_id.data_type.name, "NUMBER");
6263 assert_eq!(invoice_id.data_type.precision, Some(10));
6264 assert!(!invoice_id.nullable);
6265 assert!(!invoice_id.hidden);
6266 assert!(invoice_id.generated_expression.is_none());
6267
6268 let invoice_summary_view =
6269 schema_catalog
6270 .objects
6271 .get(&invoice_summary_name)
6272 .and_then(|object| {
6273 if let CatalogObject::View(view) = object {
6274 Some(view)
6275 } else {
6276 None
6277 }
6278 });
6279 assert!(invoice_summary_view.is_some());
6280 let total_due = invoice_summary_view
6281 .and_then(|view| {
6282 view.columns.values().find(|column| {
6283 snapshot
6284 .interner
6285 .resolve(column.name.symbol())
6286 .is_some_and(|label| label.eq("TOTAL_DUE"))
6287 })
6288 })
6289 .expect("view column should exist");
6290 assert_eq!(total_due.position, 1);
6291 assert!(total_due.nullable);
6292 assert_eq!(total_due.generated_expression.as_deref(), Some("0"));
6293 assert!(total_due.default_expression.is_none());
6294
6295 let invoices_pk_index = schema_catalog
6296 .indexes
6297 .values()
6298 .find(|index| {
6299 snapshot
6300 .interner
6301 .resolve(index.name.symbol())
6302 .is_some_and(|label| label.eq("INVOICES_PK_IDX"))
6303 })
6304 .expect("index metadata should exist");
6305 assert!(invoices_pk_index.unique);
6306 assert_eq!(invoices_pk_index.index_type, "NORMAL");
6307 assert_eq!(invoices_pk_index.status, ObjectStatus::Valid);
6308 assert_eq!(
6309 invoices_pk_index
6310 .columns
6311 .first()
6312 .and_then(|column| snapshot.interner.resolve(column.symbol())),
6313 Some("INVOICE_ID")
6314 );
6315
6316 let customer_fk = schema_catalog
6317 .constraints
6318 .values()
6319 .find(|constraint| {
6320 snapshot
6321 .interner
6322 .resolve(constraint.name.symbol())
6323 .is_some_and(|label| label.eq("INVOICES_CUSTOMER_FK"))
6324 })
6325 .expect("foreign key should exist");
6326 assert_eq!(customer_fk.constraint_type, ConstraintType::ForeignKey);
6327 assert_eq!(
6328 customer_fk
6329 .columns
6330 .first()
6331 .and_then(|column| snapshot.interner.resolve(column.symbol())),
6332 Some("CUSTOMER_ID")
6333 );
6334 assert_eq!(
6335 customer_fk
6336 .referenced_table_name
6337 .and_then(|name| snapshot.interner.resolve(name.symbol())),
6338 Some("CUSTOMERS")
6339 );
6340 assert_eq!(
6341 customer_fk
6342 .referenced_columns
6343 .first()
6344 .and_then(|column| snapshot.interner.resolve(column.symbol())),
6345 Some("CUSTOMER_ID")
6346 );
6347
6348 let customer_not_null = schema_catalog
6349 .constraints
6350 .values()
6351 .find(|constraint| {
6352 snapshot
6353 .interner
6354 .resolve(constraint.name.symbol())
6355 .is_some_and(|label| label.eq("INVOICES_CUSTOMER_NN"))
6356 })
6357 .expect("not-null constraint should exist");
6358 assert_eq!(customer_not_null.constraint_type, ConstraintType::NotNull);
6359
6360 let trigger_metadata = schema_catalog
6361 .triggers
6362 .values()
6363 .find(|trigger| {
6364 snapshot
6365 .interner
6366 .resolve(trigger.common.name.symbol())
6367 .is_some_and(|label| label.eq("INVOICES_BIU"))
6368 })
6369 .expect("trigger metadata should exist");
6370 assert_eq!(trigger_metadata.timing, TriggerTiming::Before);
6371 assert_eq!(trigger_metadata.level, TriggerLevel::Row);
6372 assert_eq!(
6373 trigger_metadata.events.as_slice(),
6374 &[TriggerEvent::Insert, TriggerEvent::Update]
6375 );
6376 assert_eq!(
6377 trigger_metadata.target_name.symbol().get(),
6378 invoices_name.symbol().get()
6379 );
6380 assert_eq!(
6381 trigger_metadata.when_clause.as_deref(),
6382 Some(":new.total_due >= 0")
6383 );
6384 assert!(matches!(
6385 schema_catalog.objects.get(&trigger_object_name),
6386 Some(CatalogObject::Trigger(_))
6387 ));
6388
6389 let latest_invoice_synonym = schema_catalog
6390 .synonyms
6391 .values()
6392 .find(|synonym| {
6393 snapshot
6394 .interner
6395 .resolve(synonym.target_name.symbol())
6396 .is_some_and(|label| label.eq("INVOICES"))
6397 })
6398 .expect("private synonym should exist");
6399 assert!(!latest_invoice_synonym.public_synonym);
6400
6401 let public_schema_name = snapshot
6402 .schemas
6403 .keys()
6404 .find(|name| {
6405 snapshot
6406 .interner
6407 .resolve(name.symbol())
6408 .is_some_and(|label| label.eq("PUBLIC"))
6409 })
6410 .copied()
6411 .expect("public schema should exist");
6412 let public_schema = snapshot
6413 .schemas
6414 .get(&public_schema_name)
6415 .expect("public schema catalog should exist");
6416 let public_synonym = public_schema
6417 .synonyms
6418 .values()
6419 .find(|synonym| {
6420 snapshot
6421 .interner
6422 .resolve(synonym.target_name.symbol())
6423 .is_some_and(|label| label.eq("INVOICES"))
6424 })
6425 .expect("public synonym should exist");
6426 assert!(public_synonym.public_synonym);
6427
6428 let package_metadata = schema_catalog
6429 .objects
6430 .get(&package_object_name)
6431 .and_then(|object| {
6432 if let CatalogObject::Package(metadata) = object {
6433 Some(metadata)
6434 } else {
6435 None
6436 }
6437 })
6438 .expect("package metadata should exist");
6439 let create_invoice = package_metadata
6440 .procedures
6441 .iter()
6442 .find(|signature| {
6443 snapshot
6444 .interner
6445 .resolve(signature.routine_name.symbol())
6446 .is_some_and(|label| label.eq("CREATE_INVOICE"))
6447 })
6448 .expect("package procedure should exist");
6449 assert_eq!(create_invoice.arguments.len(), 2);
6450 let total_for_customer = package_metadata
6451 .functions
6452 .iter()
6453 .find(|signature| {
6454 snapshot
6455 .interner
6456 .resolve(signature.routine_name.symbol())
6457 .is_some_and(|label| label.eq("TOTAL_FOR_CUSTOMER"))
6458 })
6459 .expect("package function should exist");
6460 assert_eq!(total_for_customer.overload, Some(1));
6461 assert_eq!(
6462 total_for_customer
6463 .return_type
6464 .as_ref()
6465 .map(|data_type| data_type.name.as_str()),
6466 Some("NUMBER")
6467 );
6468 assert_eq!(total_for_customer.arguments.len(), 1);
6469
6470 let tax_function = schema_catalog
6471 .objects
6472 .get(&tax_function_name)
6473 .and_then(|object| {
6474 if let CatalogObject::Function(metadata) = object {
6475 Some(metadata)
6476 } else {
6477 None
6478 }
6479 })
6480 .expect("top-level function should exist");
6481 assert!(tax_function.deterministic);
6482 assert!(!tax_function.pipelined);
6483 assert_eq!(tax_function.signature.arguments.len(), 1);
6484 assert_eq!(
6485 tax_function
6486 .signature
6487 .return_type
6488 .as_ref()
6489 .map(|data_type| data_type.name.as_str()),
6490 Some("NUMBER")
6491 );
6492
6493 let invoice_summary_view_with_hash = schema_catalog
6494 .objects
6495 .get(&invoice_summary_name)
6496 .and_then(|object| {
6497 if let CatalogObject::View(view) = object {
6498 Some(view)
6499 } else {
6500 None
6501 }
6502 })
6503 .expect("invoice summary view should be present");
6504 assert!(invoice_summary_view_with_hash.query_hash.is_some());
6505 assert_eq!(invoice_summary_view_with_hash.read_only, Some(false));
6506
6507 assert_eq!(schema_catalog.grants.len(), 1);
6508 let reporting_grant = &schema_catalog.grants[0];
6509 assert!(matches!(
6510 reporting_grant.privilege,
6511 crate::GrantPrivilege::Select
6512 ));
6513 assert!(matches!(reporting_grant.grantee, crate::Grantee::User(_)));
6516 assert!(!reporting_grant.grantable);
6517 }
6518
6519 #[test]
6527 fn grantee_classification_uses_loaded_user_set() {
6528 let mut snapshot = CatalogSnapshot::new(
6529 plsql_core::AnalysisProfile::default(),
6530 CatalogCapabilities::default(),
6531 CatalogSource::default(),
6532 DateTime::<Utc>::UNIX_EPOCH,
6533 );
6534
6535 assert!(snapshot.known_users.is_none());
6540 let undetermined =
6541 grantee_from_dictionary_value(&mut snapshot, "MYSTERY_GRANTEE").expect("grantee");
6542 assert!(
6543 matches!(undetermined, crate::Grantee::Role(_)),
6544 "undetermined grantee (ALL_USERS not loaded) must not be a direct user grant; got {undetermined:?}"
6545 );
6546
6547 let app_user = snapshot.intern_user_name("APP_USER").expect("user");
6549 let mut users = HashSet::new();
6550 users.insert(app_user);
6551 snapshot.known_users = Some(users);
6552
6553 assert!(matches!(
6555 grantee_from_dictionary_value(&mut snapshot, "PUBLIC").expect("grantee"),
6556 crate::Grantee::Public
6557 ));
6558 assert!(matches!(
6560 grantee_from_dictionary_value(&mut snapshot, "APP_USER").expect("grantee"),
6561 crate::Grantee::User(_)
6562 ));
6563 let role =
6566 grantee_from_dictionary_value(&mut snapshot, "APP_READER_ROLE").expect("grantee");
6567 let role_name = match role {
6568 crate::Grantee::Role(role_name) => Some(role_name),
6569 _ => None,
6570 };
6571 assert!(
6572 role_name.is_some(),
6573 "APP_READER_ROLE must classify as a role"
6574 );
6575 assert_eq!(
6576 snapshot
6577 .interner
6578 .resolve(role_name.expect("role assertion above").symbol()),
6579 Some("APP_READER_ROLE")
6580 );
6581 }
6582
6583 #[test]
6588 fn load_catalog_users_failure_is_nonfatal_and_marks_unknown() {
6589 let connection = StaticConnection {
6592 expected_queries: vec![QueryExpectation {
6593 sql_contains: String::from("from something_else"),
6594 params: vec![],
6595 rows: vec![],
6596 }],
6597 ..StaticConnection::default()
6598 };
6599 let mut snapshot = CatalogSnapshot::new(
6600 plsql_core::AnalysisProfile::default(),
6601 CatalogCapabilities::default(),
6602 CatalogSource::default(),
6603 DateTime::<Utc>::UNIX_EPOCH,
6604 );
6605
6606 load_catalog_users_for_test(&connection, &mut snapshot).expect("non-fatal");
6607 assert!(
6608 snapshot.known_users.is_none(),
6609 "failed ALL_USERS read must leave grantee universe undetermined"
6610 );
6611 assert!(
6612 snapshot
6613 .capabilities
6614 .warnings
6615 .iter()
6616 .any(|w| w.code.eq("all-users-probe")),
6617 "a capability warning must record the ALL_USERS read failure"
6618 );
6619 }
6620
6621 #[test]
6622 fn load_snapshot_from_connection_extracts_views_sequences_mviews_types_and_grants() {
6623 let object_rows = vec![
6624 oracle_row(&[
6625 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6626 ("OBJECT_NAME", "VARCHAR2(128)", Some("ACTIVE_CUSTOMERS")),
6627 ("OBJECT_TYPE", "VARCHAR2(30)", Some("VIEW")),
6628 ("STATUS", "VARCHAR2(7)", Some("VALID")),
6629 ]),
6630 oracle_row(&[
6631 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6632 ("OBJECT_NAME", "VARCHAR2(128)", Some("CUSTOMER_TOTALS_MV")),
6633 ("OBJECT_TYPE", "VARCHAR2(30)", Some("MATERIALIZED VIEW")),
6634 ("STATUS", "VARCHAR2(7)", Some("VALID")),
6635 ]),
6636 oracle_row(&[
6637 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6638 ("OBJECT_NAME", "VARCHAR2(128)", Some("INVOICE_SEQ")),
6639 ("OBJECT_TYPE", "VARCHAR2(30)", Some("SEQUENCE")),
6640 ("STATUS", "VARCHAR2(7)", Some("VALID")),
6641 ]),
6642 oracle_row(&[
6643 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6644 ("OBJECT_NAME", "VARCHAR2(128)", Some("ADDRESS_T")),
6645 ("OBJECT_TYPE", "VARCHAR2(30)", Some("TYPE")),
6646 ("STATUS", "VARCHAR2(7)", Some("VALID")),
6647 ]),
6648 ];
6649 let view_rows = vec![oracle_row(&[
6650 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6651 ("VIEW_NAME", "VARCHAR2(128)", Some("ACTIVE_CUSTOMERS")),
6652 (
6653 "TEXT_VC",
6654 "VARCHAR2(4000)",
6655 Some("select customer_id from customers where active = 'Y'"),
6656 ),
6657 ("READ_ONLY", "VARCHAR2(1)", Some("Y")),
6658 ])];
6659 let mview_rows = vec![oracle_row(&[
6660 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6661 ("MVIEW_NAME", "VARCHAR2(128)", Some("CUSTOMER_TOTALS_MV")),
6662 ("REFRESH_MODE", "VARCHAR2(6)", Some("DEMAND")),
6663 ("REFRESH_METHOD", "VARCHAR2(8)", Some("COMPLETE")),
6664 (
6665 "QUERY",
6666 "LONG",
6667 Some("select customer_id, sum(amount) from invoices group by customer_id"),
6668 ),
6669 ])];
6670 let sequence_rows = vec![oracle_row(&[
6671 ("SEQUENCE_OWNER", "VARCHAR2(128)", Some("BILLING")),
6672 ("SEQUENCE_NAME", "VARCHAR2(128)", Some("INVOICE_SEQ")),
6673 ("MIN_VALUE", "NUMBER(28)", Some("1")),
6674 ("MAX_VALUE", "NUMBER(28)", Some("9999999999")),
6675 ("INCREMENT_BY", "NUMBER(28)", Some("1")),
6676 ("CYCLE_FLAG", "VARCHAR2(1)", Some("N")),
6677 ("ORDER_FLAG", "VARCHAR2(1)", Some("N")),
6678 ("CACHE_SIZE", "NUMBER(28)", Some("20")),
6679 ])];
6680 let type_attr_rows = vec![
6681 oracle_row(&[
6682 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6683 ("TYPE_NAME", "VARCHAR2(128)", Some("ADDRESS_T")),
6684 ("ATTR_NAME", "VARCHAR2(128)", Some("STREET")),
6685 ("ATTR_NO", "NUMBER(10)", Some("1")),
6686 ("ATTR_TYPE_OWNER", "VARCHAR2(128)", None),
6687 ("ATTR_TYPE_NAME", "VARCHAR2(128)", Some("VARCHAR2")),
6688 ("LENGTH", "NUMBER(10)", Some("200")),
6689 ("PRECISION", "NUMBER(10)", None),
6690 ("SCALE", "NUMBER(10)", None),
6691 ]),
6692 oracle_row(&[
6693 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6694 ("TYPE_NAME", "VARCHAR2(128)", Some("ADDRESS_T")),
6695 ("ATTR_NAME", "VARCHAR2(128)", Some("ZIP")),
6696 ("ATTR_NO", "NUMBER(10)", Some("2")),
6697 ("ATTR_TYPE_OWNER", "VARCHAR2(128)", None),
6698 ("ATTR_TYPE_NAME", "VARCHAR2(128)", Some("VARCHAR2")),
6699 ("LENGTH", "NUMBER(10)", Some("10")),
6700 ("PRECISION", "NUMBER(10)", None),
6701 ("SCALE", "NUMBER(10)", None),
6702 ]),
6703 ];
6704 let grant_rows = vec![
6705 oracle_row(&[
6706 ("TABLE_SCHEMA", "VARCHAR2(128)", Some("BILLING")),
6707 ("TABLE_NAME", "VARCHAR2(128)", Some("ACTIVE_CUSTOMERS")),
6708 ("GRANTEE", "VARCHAR2(128)", Some("PUBLIC")),
6709 ("PRIVILEGE", "VARCHAR2(40)", Some("SELECT")),
6710 ("GRANTABLE", "VARCHAR2(3)", Some("NO")),
6711 ("HIERARCHY", "VARCHAR2(3)", Some("NO")),
6712 ]),
6713 oracle_row(&[
6714 ("TABLE_SCHEMA", "VARCHAR2(128)", Some("BILLING")),
6715 ("TABLE_NAME", "VARCHAR2(128)", Some("ACTIVE_CUSTOMERS")),
6716 ("GRANTEE", "VARCHAR2(128)", Some("REPORTING_ROLE")),
6717 ("PRIVILEGE", "VARCHAR2(40)", Some("UPDATE")),
6718 ("GRANTABLE", "VARCHAR2(3)", Some("YES")),
6719 ("HIERARCHY", "VARCHAR2(3)", Some("NO")),
6720 ]),
6721 ];
6722
6723 let edition_rows = vec![
6726 oracle_row(&[
6727 ("EDITION_NAME", "VARCHAR2(128)", Some("ORA$BASE")),
6728 ("PARENT_EDITION_NAME", "VARCHAR2(128)", None),
6729 ("USABLE", "VARCHAR2(1)", Some("Y")),
6730 ]),
6731 oracle_row(&[
6732 ("EDITION_NAME", "VARCHAR2(128)", Some("PATCH_2026_05")),
6733 ("PARENT_EDITION_NAME", "VARCHAR2(128)", Some("ORA$BASE")),
6734 ("USABLE", "VARCHAR2(1)", Some("Y")),
6735 ]),
6736 ];
6737 let editioning_view_rows = vec![oracle_row(&[
6738 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6739 ("VIEW_NAME", "VARCHAR2(128)", Some("INVOICES_E1")),
6740 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
6741 ])];
6742
6743 let vpd_policy_rows = vec![oracle_row(&[
6747 ("OBJECT_OWNER", "VARCHAR2(128)", Some("BILLING")),
6748 ("OBJECT_NAME", "VARCHAR2(128)", Some("INVOICES")),
6749 ("POLICY_GROUP", "VARCHAR2(128)", None),
6750 (
6751 "POLICY_NAME",
6752 "VARCHAR2(128)",
6753 Some("BILLING_TENANT_ISOLATION"),
6754 ),
6755 ("PF_OWNER", "VARCHAR2(128)", Some("SECURITY")),
6756 ("PACKAGE", "VARCHAR2(128)", Some("RLS_PKG")),
6757 ("FUNCTION", "VARCHAR2(128)", Some("TENANT_PREDICATE")),
6758 ("SEL", "VARCHAR2(3)", Some("YES")),
6759 ("INS", "VARCHAR2(3)", Some("NO")),
6760 ("UPD", "VARCHAR2(3)", Some("NO")),
6761 ("DEL", "VARCHAR2(3)", Some("NO")),
6762 ("ENABLE", "VARCHAR2(3)", Some("YES")),
6763 ])];
6764
6765 let table_comment_rows = vec![oracle_row(&[
6768 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6769 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
6770 ("TABLE_TYPE", "VARCHAR2(11)", Some("TABLE")),
6771 (
6772 "COMMENTS",
6773 "VARCHAR2(4000)",
6774 Some("Customer invoice header rows; one per invoice"),
6775 ),
6776 ])];
6777 let column_comment_rows = vec![oracle_row(&[
6778 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6779 ("TABLE_NAME", "VARCHAR2(128)", Some("INVOICES")),
6780 ("COLUMN_NAME", "VARCHAR2(128)", Some("INVOICE_ID")),
6781 (
6782 "COMMENTS",
6783 "VARCHAR2(4000)",
6784 Some("Primary key; surrogate, allocated from INVOICES_SEQ"),
6785 ),
6786 ])];
6787
6788 let db_link_rows = vec![
6792 oracle_row(&[
6793 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
6794 ("DB_LINK", "VARCHAR2(128)", Some("REPORTING_LINK")),
6795 ("HOST", "VARCHAR2(2000)", Some("reporting-db.internal")),
6796 ]),
6797 oracle_row(&[
6798 ("OWNER", "VARCHAR2(128)", Some("PUBLIC")),
6799 ("DB_LINK", "VARCHAR2(128)", Some("WORLDWIDE_LINK")),
6800 ("HOST", "VARCHAR2(2000)", Some("//world.example.com/PROD")),
6801 ]),
6802 ];
6803
6804 let billing_only = vec![OracleBind::from("BILLING")];
6805 let mut expected_queries = capability_probe_expectations();
6806 expected_queries.push(all_users_expectation(&["BILLING"]));
6809 expected_queries.extend(vec![
6810 QueryExpectation {
6811 sql_contains: String::from("from all_objects"),
6812 params: billing_only.clone(),
6813 rows: object_rows,
6814 },
6815 QueryExpectation {
6816 sql_contains: String::from("from all_tab_cols"),
6817 params: billing_only.clone(),
6818 rows: vec![],
6819 },
6820 QueryExpectation {
6821 sql_contains: String::from("from all_constraints c"),
6822 params: billing_only.clone(),
6823 rows: vec![],
6824 },
6825 QueryExpectation {
6826 sql_contains: String::from("from all_indexes i"),
6827 params: billing_only.clone(),
6828 rows: vec![],
6829 },
6830 QueryExpectation {
6831 sql_contains: String::from("from all_triggers"),
6832 params: billing_only.clone(),
6833 rows: vec![],
6834 },
6835 QueryExpectation {
6836 sql_contains: String::from("from all_synonyms"),
6837 params: billing_only.clone(),
6838 rows: vec![],
6839 },
6840 QueryExpectation {
6841 sql_contains: String::from("from all_procedures"),
6842 params: billing_only.clone(),
6843 rows: vec![],
6844 },
6845 QueryExpectation {
6846 sql_contains: String::from("from all_arguments"),
6847 params: billing_only.clone(),
6848 rows: vec![],
6849 },
6850 QueryExpectation {
6851 sql_contains: String::from("from all_views"),
6852 params: billing_only.clone(),
6853 rows: view_rows,
6854 },
6855 QueryExpectation {
6856 sql_contains: String::from("from all_mviews"),
6857 params: billing_only.clone(),
6858 rows: mview_rows,
6859 },
6860 QueryExpectation {
6861 sql_contains: String::from("from all_sequences"),
6862 params: billing_only.clone(),
6863 rows: sequence_rows,
6864 },
6865 QueryExpectation {
6866 sql_contains: String::from("from all_type_attrs"),
6867 params: billing_only.clone(),
6868 rows: type_attr_rows,
6869 },
6870 QueryExpectation {
6871 sql_contains: String::from("from all_tab_privs"),
6872 params: billing_only.clone(),
6873 rows: grant_rows,
6874 },
6875 QueryExpectation {
6876 sql_contains: String::from("from all_db_links"),
6877 params: billing_only.clone(),
6878 rows: db_link_rows,
6879 },
6880 QueryExpectation {
6881 sql_contains: String::from("from all_tab_comments"),
6882 params: billing_only.clone(),
6883 rows: table_comment_rows,
6884 },
6885 QueryExpectation {
6886 sql_contains: String::from("from all_col_comments"),
6887 params: billing_only.clone(),
6888 rows: column_comment_rows,
6889 },
6890 QueryExpectation {
6891 sql_contains: String::from("from all_editions"),
6892 params: vec![],
6893 rows: edition_rows,
6894 },
6895 QueryExpectation {
6896 sql_contains: String::from("from all_editioning_views"),
6897 params: billing_only.clone(),
6898 rows: editioning_view_rows,
6899 },
6900 QueryExpectation {
6901 sql_contains: String::from("from all_policies"),
6902 params: billing_only.clone(),
6903 rows: vpd_policy_rows,
6904 },
6905 QueryExpectation {
6906 sql_contains: String::from("from all_dependencies"),
6907 params: billing_only,
6908 rows: vec![],
6909 },
6910 ]);
6911 let connection = StaticConnection {
6912 expected_queries,
6913 ..StaticConnection::default()
6914 };
6915
6916 let snapshot = load_snapshot_for_test(&connection, &CatalogLoadRequest::default()).unwrap();
6917
6918 let schema = snapshot
6919 .profile
6920 .current_schema
6921 .expect("current schema interned");
6922 let schema_catalog = snapshot
6923 .schemas
6924 .get(&schema)
6925 .expect("billing schema catalog should exist");
6926
6927 let view = schema_catalog
6928 .objects
6929 .values()
6930 .find_map(|object| match object {
6931 CatalogObject::View(view) => Some(view),
6932 _ => None,
6933 })
6934 .expect("view metadata present");
6935 assert!(view.query_hash.is_some());
6936 assert_eq!(view.read_only, Some(true));
6937
6938 let mview = schema_catalog
6939 .objects
6940 .values()
6941 .find_map(|object| match object {
6942 CatalogObject::MaterializedView(metadata) => Some(metadata),
6943 _ => None,
6944 })
6945 .expect("mview metadata present");
6946 assert_eq!(mview.refresh_mode.as_deref(), Some("DEMAND"));
6947 assert_eq!(mview.refresh_method.as_deref(), Some("COMPLETE"));
6948 assert!(mview.query_hash.is_some());
6949
6950 let sequence = schema_catalog
6951 .objects
6952 .values()
6953 .find_map(|object| match object {
6954 CatalogObject::Sequence(metadata) => Some(metadata),
6955 _ => None,
6956 })
6957 .expect("sequence metadata present");
6958 assert_eq!(sequence.increment_by, 1);
6959 assert_eq!(sequence.min_value, Some(1));
6960 assert_eq!(sequence.max_value, Some(9999999999));
6961 assert!(!sequence.cycle);
6962 assert!(!sequence.ordered);
6963 assert_eq!(sequence.cache_size, Some(20));
6964
6965 let type_metadata = schema_catalog
6966 .objects
6967 .values()
6968 .find_map(|object| match object {
6969 CatalogObject::Type(metadata) => Some(metadata),
6970 _ => None,
6971 })
6972 .expect("type metadata present");
6973 assert_eq!(type_metadata.attributes.len(), 2);
6974 assert_eq!(type_metadata.attributes[0].position, 1);
6975 assert_eq!(type_metadata.attributes[1].position, 2);
6976 assert_eq!(type_metadata.attributes[0].data_type.name, "VARCHAR2");
6977 assert_eq!(type_metadata.attributes[0].data_type.length, Some(200));
6978
6979 let public_grant = schema_catalog
6980 .grants
6981 .iter()
6982 .find(|grant| matches!(grant.grantee, crate::Grantee::Public))
6983 .expect("public grant present");
6984 assert!(matches!(
6985 public_grant.privilege,
6986 crate::GrantPrivilege::Select
6987 ));
6988 let role_grant = schema_catalog
6992 .grants
6993 .iter()
6994 .find(|grant| matches!(grant.grantee, crate::Grantee::Role(_)))
6995 .expect("role grant present");
6996 let crate::Grantee::Role(role) = &role_grant.grantee else {
6997 unreachable!("matched Grantee::Role above");
6998 };
6999 assert_eq!(
7000 snapshot.interner.resolve(role.symbol()),
7001 Some("REPORTING_ROLE")
7002 );
7003 assert!(matches!(
7004 role_grant.privilege,
7005 crate::GrantPrivilege::Update
7006 ));
7007 assert!(role_grant.grantable);
7008 assert!(
7010 !schema_catalog
7011 .grants
7012 .iter()
7013 .any(|grant| matches!(grant.grantee, crate::Grantee::User(_)))
7014 );
7015
7016 let private_link = schema_catalog
7022 .db_links
7023 .iter()
7024 .find(|link| link.name.eq("REPORTING_LINK"))
7025 .expect("private db link present on BILLING schema");
7026 assert!(!private_link.public_link);
7027 assert_eq!(private_link.host.as_deref(), Some("reporting-db.internal"));
7028
7029 let public_link = snapshot
7034 .schemas
7035 .iter()
7036 .find_map(|(schema_id, schema_catalog)| {
7037 let label = snapshot.interner.resolve(schema_id.symbol())?;
7038 if label.eq_ignore_ascii_case("PUBLIC") {
7039 schema_catalog
7040 .db_links
7041 .iter()
7042 .find(|link| link.name.eq("WORLDWIDE_LINK"))
7043 } else {
7044 None
7045 }
7046 })
7047 .expect("public db link present on PUBLIC synthetic schema");
7048 assert!(public_link.public_link);
7049 assert_eq!(
7050 public_link.host.as_deref(),
7051 Some("//world.example.com/PROD")
7052 );
7053
7054 assert_eq!(schema_catalog.vpd_policies.len(), 1);
7058 let policy = &schema_catalog.vpd_policies[0];
7059 assert_eq!(policy.policy_name, "BILLING_TENANT_ISOLATION");
7060 assert_eq!(policy.function_name, "TENANT_PREDICATE");
7061 assert_eq!(policy.function_package.as_deref(), Some("RLS_PKG"));
7062 assert!(policy.policy_group.is_none());
7063 assert!(policy.on_select);
7064 assert!(!policy.on_insert);
7065 assert!(!policy.on_update);
7066 assert!(!policy.on_delete);
7067 assert!(policy.enabled);
7068
7069 assert_eq!(snapshot.editions.len(), 2);
7073 let root = snapshot
7074 .editions
7075 .iter()
7076 .find(|e| e.edition_name.eq("ORA$BASE"))
7077 .expect("root edition present");
7078 assert!(root.parent_edition_name.is_none());
7079 assert!(root.usable);
7080 let child = snapshot
7081 .editions
7082 .iter()
7083 .find(|e| e.edition_name.eq("PATCH_2026_05"))
7084 .expect("child edition present");
7085 assert_eq!(child.parent_edition_name.as_deref(), Some("ORA$BASE"));
7086 assert_eq!(schema_catalog.editioning_views.len(), 1);
7087 let ev = &schema_catalog.editioning_views[0];
7088 let view_label = snapshot.interner.resolve(ev.view_name.symbol()).unwrap();
7089 let table_label = snapshot.interner.resolve(ev.table_name.symbol()).unwrap();
7090 assert_eq!(view_label, "INVOICES_E1");
7091 assert_eq!(table_label, "INVOICES");
7092
7093 assert_eq!(schema_catalog.table_comments.len(), 1);
7097 let table_comment = &schema_catalog.table_comments[0];
7098 assert_eq!(table_comment.table_type, "TABLE");
7099 assert_eq!(
7100 table_comment.comments,
7101 "Customer invoice header rows; one per invoice"
7102 );
7103 assert_eq!(schema_catalog.column_comments.len(), 1);
7106 assert_eq!(
7107 schema_catalog.column_comments[0].comments,
7108 "Primary key; surrogate, allocated from INVOICES_SEQ"
7109 );
7110 }
7111
7112 #[test]
7113 fn load_snapshot_from_connection_requires_current_schema_when_requested() {
7114 let mut connection_info = StaticConnection::default().connection_info;
7115 connection_info.current_schema = None;
7116 let connection = StaticConnection {
7117 connection_info,
7118 ..StaticConnection::default()
7119 };
7120
7121 let error = load_snapshot_for_test(&connection, &CatalogLoadRequest::default());
7122
7123 assert!(matches!(error, Err(CatalogError::CurrentSchemaUnavailable)));
7124 }
7125
7126 #[test]
7127 fn oracle_connection_default_helpers_enforce_row_cardinality() {
7128 let mut row = OracleRow::default();
7129 row.insert(
7130 "schema_name",
7131 "VARCHAR2(128)",
7132 Some(String::from("billing")),
7133 );
7134
7135 let single = StaticConnection {
7136 rows: vec![row.clone()],
7137 row_count: 1,
7138 ..StaticConnection::default()
7139 };
7140 let multiple = StaticConnection {
7141 rows: vec![row.clone(), row],
7142 row_count: 2,
7143 ..StaticConnection::default()
7144 };
7145
7146 let single_result = run_catalog_future(async {
7147 let cx = test_cx();
7148 single.query_one_row(&cx, "select * from dual", &[]).await
7149 });
7150 assert!(single_result.is_ok());
7151 let multiple_result = run_catalog_future(async {
7152 let cx = test_cx();
7153 multiple
7154 .query_optional_row(&cx, "select * from dual", &[])
7155 .await
7156 });
7157 assert!(matches!(
7158 multiple_result,
7159 Err(CatalogError::UnexpectedRowCount { expected, actual })
7160 if expected.eq("0 or 1") && actual.eq(&2)
7161 ));
7162 }
7163
7164 #[test]
7165 fn oracle_connect_options_capture_session_overrides() {
7166 let options = OracleConnectOptions::new("scott", "tiger", "//localhost/XE")
7167 .with_current_schema("billing")
7168 .with_module("plsql-intelligence")
7169 .with_action("catalog-extract")
7170 .with_client_info("tests")
7171 .with_client_identifier("unit");
7172
7173 assert_eq!(options.current_schema.as_deref(), Some("billing"));
7174 assert_eq!(options.module.as_deref(), Some("plsql-intelligence"));
7175 assert_eq!(options.action.as_deref(), Some("catalog-extract"));
7176 assert_eq!(options.client_info.as_deref(), Some("tests"));
7177 assert_eq!(options.client_identifier.as_deref(), Some("unit"));
7178 }
7179
7180 #[test]
7181 fn catalog_snapshot_initializes_with_empty_schema_map() {
7182 let snapshot = CatalogSnapshot::new(
7183 AnalysisProfile::default(),
7184 CatalogCapabilities::default(),
7185 CatalogSource {
7186 kind: CatalogSourceKind::SyntheticTestCatalog,
7187 path: None,
7188 description: Some(String::from("fixture")),
7189 },
7190 DateTime::<Utc>::UNIX_EPOCH,
7191 );
7192
7193 assert!(snapshot.schemas.is_empty());
7194 assert!(snapshot.interner.is_empty());
7195 assert_eq!(snapshot.generated_at, DateTime::<Utc>::UNIX_EPOCH);
7196 assert_eq!(
7197 snapshot.source.kind,
7198 CatalogSourceKind::SyntheticTestCatalog
7199 );
7200 }
7201
7202 #[test]
7203 fn schema_catalog_can_hold_structural_lookup_maps() {
7204 let mut schema_catalog = SchemaCatalog::default();
7205 let object_name = ObjectName::from(SymbolId::new(2));
7206 let column_name = ColumnName::from(SymbolId::new(3));
7207 let owner = SchemaName::from(SymbolId::new(1));
7208
7209 let table = TableMetadata {
7210 common: ObjectCommon {
7211 owner,
7212 name: object_name,
7213 object_type: ObjectType::Table,
7214 status: ObjectStatus::Valid,
7215 source_hash: Some(Hash::new("abc123")),
7216 ..ObjectCommon::default()
7217 },
7218 columns: HashMap::from([(
7219 column_name,
7220 crate::ColumnMetadata {
7221 name: column_name,
7222 position: 1,
7223 data_type: DataTypeRef {
7224 name: String::from("NUMBER"),
7225 precision: Some(10),
7226 ..DataTypeRef::default()
7227 },
7228 nullable: false,
7229 ..crate::ColumnMetadata::default()
7230 },
7231 )]),
7232 ..TableMetadata::default()
7233 };
7234
7235 schema_catalog
7236 .objects
7237 .insert(object_name, CatalogObject::Table(table));
7238 schema_catalog.synonyms.insert(
7239 SynonymName::from(SymbolId::new(4)),
7240 SynonymTarget {
7241 target_owner: Some(owner),
7242 target_name: object_name,
7243 public_synonym: false,
7244 ..SynonymTarget::default()
7245 },
7246 );
7247
7248 assert_eq!(schema_catalog.objects.len(), 1);
7249 assert_eq!(schema_catalog.synonyms.len(), 1);
7250 }
7251
7252 #[test]
7253 fn package_and_plscope_models_capture_signature_state() {
7254 let owner = SchemaName::from(SymbolId::new(10));
7255 let package_name = ObjectName::from(SymbolId::new(11));
7256 let procedure_name = ObjectName::from(SymbolId::new(12));
7257 let member_name = MemberName::from(SymbolId::new(13));
7258
7259 let package = PackageMetadata {
7260 common: ObjectCommon {
7261 owner,
7262 name: package_name,
7263 object_type: ObjectType::Package,
7264 status: ObjectStatus::Valid,
7265 ..ObjectCommon::default()
7266 },
7267 procedures: vec![RoutineSignature {
7268 routine_name: procedure_name,
7269 arguments: vec![crate::ArgumentMetadata {
7270 position: 1,
7271 name: Some(member_name),
7272 data_type: DataTypeRef {
7273 name: String::from("VARCHAR2"),
7274 length: Some(30),
7275 ..DataTypeRef::default()
7276 },
7277 ..crate::ArgumentMetadata::default()
7278 }],
7279 accessible_by: vec![AccessibleByTarget {
7280 owner: Some(owner),
7281 object_name: package_name,
7282 }],
7283 ..RoutineSignature::default()
7284 }],
7285 ..PackageMetadata::default()
7286 };
7287
7288 let plscope = PlScopeSnapshot {
7289 availability: PlScopeAvailability::IdentifiersAndStatements,
7290 identifiers: vec![CompilerIdentifier {
7291 owner,
7292 object_name: package_name,
7293 identifier_name: member_name,
7294 identifier_type: String::from("FORMAL IN"),
7295 usage: String::from("DECLARATION"),
7296 line: 4,
7297 column: 12,
7298 }],
7299 ..PlScopeSnapshot::default()
7300 };
7301
7302 assert_eq!(package.procedures.len(), 1);
7303 assert_eq!(package.procedures[0].accessible_by.len(), 1);
7304 assert_eq!(
7305 plscope.availability,
7306 PlScopeAvailability::IdentifiersAndStatements
7307 );
7308 assert_eq!(plscope.identifiers[0].line, 4);
7309 assert_eq!(ConstraintType::ForeignKey, ConstraintType::ForeignKey);
7310 assert_eq!(TypeFinality::Unknown, TypeFinality::default());
7311 assert_eq!(TypeInstantiable::Unknown, TypeInstantiable::default());
7312 assert_eq!(TriggerName::from(SymbolId::new(14)).symbol().get(), 14);
7313 }
7314
7315 #[test]
7316 fn catalog_snapshot_round_trips_through_versioned_json_document() {
7317 let tempdir = tempdir();
7318 assert!(tempdir.is_ok());
7319 let tempdir = if let Ok(tempdir) = tempdir {
7320 tempdir
7321 } else {
7322 return;
7323 };
7324
7325 let mut snapshot = CatalogSnapshot::new(
7326 AnalysisProfile::default(),
7327 CatalogCapabilities::default(),
7328 CatalogSource {
7329 kind: CatalogSourceKind::JsonSnapshot,
7330 path: None,
7331 description: Some(String::from("roundtrip")),
7332 },
7333 DateTime::<Utc>::UNIX_EPOCH,
7334 );
7335 let billing = snapshot.intern_schema_name("billing");
7336 let claims = snapshot.intern_object_name("claims");
7337 assert!(billing.is_some());
7338 assert!(claims.is_some());
7339
7340 let path = tempdir.path().join("snapshot.json");
7341 let exported = export_snapshot_to_json(&snapshot, &path);
7342 assert!(exported.is_ok());
7343
7344 let loaded = load_snapshot_from_json(&path);
7345 assert!(loaded.is_ok());
7346 assert_eq!(loaded.ok(), Some(snapshot.clone()));
7347
7348 let rendered = std::fs::read_to_string(path);
7349 assert!(rendered.is_ok());
7350 let rendered = if let Ok(rendered) = rendered {
7351 rendered
7352 } else {
7353 return;
7354 };
7355 let document = serde_json::from_str::<CatalogSnapshotDocument>(&rendered);
7356 assert!(document.is_ok());
7357 let document = if let Ok(document) = document {
7358 document
7359 } else {
7360 return;
7361 };
7362
7363 assert!(document.schema_id.as_str().eq(CATALOG_SNAPSHOT_SCHEMA_ID));
7364 assert!(matches!(
7365 document
7366 .schema_version
7367 .cmp(&CATALOG_SNAPSHOT_SCHEMA_VERSION),
7368 std::cmp::Ordering::Equal
7369 ));
7370 assert_eq!(
7371 document
7372 .snapshot
7373 .interner
7374 .resolve(billing.unwrap_or_default().symbol()),
7375 Some("billing")
7376 );
7377 assert_eq!(
7378 document
7379 .snapshot
7380 .interner
7381 .resolve(claims.unwrap_or_default().symbol()),
7382 Some("claims")
7383 );
7384 }
7385
7386 #[test]
7387 fn load_from_dbms_metadata_dir_classifies_create_statements() {
7388 let dir = tempdir().unwrap();
7389 let root = dir.path();
7390
7391 std::fs::write(
7393 root.join("customers.sql"),
7394 "CREATE TABLE customers (id NUMBER, name VARCHAR2(100));",
7395 )
7396 .unwrap();
7397 std::fs::write(
7398 root.join("billing_api.sql"),
7399 "CREATE OR REPLACE PACKAGE billing_api AS\n PROCEDURE charge(p_id NUMBER);\nEND;",
7400 )
7401 .unwrap();
7402 std::fs::write(
7403 root.join("invoice_seq.sql"),
7404 "CREATE SEQUENCE invoice_seq START WITH 1 INCREMENT BY 1;",
7405 )
7406 .unwrap();
7407 std::fs::write(root.join("skip.txt"), "not a sql file").unwrap();
7408
7409 let snapshot = load_from_dbms_metadata_dir(root).unwrap();
7410 assert_eq!(snapshot.source.kind, CatalogSourceKind::DbmsMetadataFiles);
7411
7412 let total_objects: usize = snapshot.schemas.values().map(|s| s.objects.len()).sum();
7414 assert!(
7415 total_objects >= 2,
7416 "expected at least 2 objects, got {}",
7417 total_objects
7418 );
7419 }
7420
7421 #[test]
7422 fn load_from_dbms_metadata_dir_returns_error_for_nonexistent_dir() {
7423 let result = load_from_dbms_metadata_dir(std::path::Path::new("/nonexistent/path"));
7424 assert!(result.is_err());
7425 }
7426
7427 #[test]
7428 fn load_from_dbms_metadata_dir_handles_empty_dir() {
7429 let dir = tempdir().unwrap();
7430 let snapshot = load_from_dbms_metadata_dir(dir.path()).unwrap();
7431 assert!(
7432 snapshot.schemas.is_empty() || snapshot.schemas.values().all(|s| s.objects.is_empty())
7433 );
7434 }
7435
7436 #[test]
7441 fn load_from_dbms_metadata_dir_records_real_schema_owner() {
7442 let dir = tempdir().unwrap();
7443 let root = dir.path();
7444 std::fs::write(
7445 root.join("hr_employees.sql"),
7446 "CREATE TABLE hr.employees (id NUMBER PRIMARY KEY);",
7447 )
7448 .unwrap();
7449 std::fs::write(
7450 root.join("billing_invoices.sql"),
7451 "CREATE TABLE billing.invoices (id NUMBER, amount NUMBER(12,2));",
7452 )
7453 .unwrap();
7454 let snapshot = load_from_dbms_metadata_dir(root).unwrap();
7455
7456 let schema_names: std::collections::HashSet<String> = snapshot
7458 .schemas
7459 .keys()
7460 .filter_map(|s| snapshot.interner.resolve(s.symbol()).map(str::to_string))
7461 .collect();
7462 assert!(
7463 schema_names.contains("HR"),
7464 "HR schema bucket must exist; got {schema_names:?}"
7465 );
7466 assert!(
7467 schema_names.contains("BILLING"),
7468 "BILLING schema bucket must exist; got {schema_names:?}"
7469 );
7470
7471 for (schema, bucket) in &snapshot.schemas {
7474 let name = snapshot.interner.resolve(schema.symbol()).unwrap();
7475 assert_eq!(
7476 bucket.objects.len(),
7477 1,
7478 "{name} bucket must hold exactly one object"
7479 );
7480 }
7481 }
7482
7483 #[test]
7488 fn load_from_dbms_metadata_dir_uses_named_default_schema_for_unqualified_ddl() {
7489 let dir = tempdir().unwrap();
7490 let root = dir.path();
7491 std::fs::write(
7492 root.join("customers.sql"),
7493 "CREATE TABLE customers (id NUMBER, name VARCHAR2(100));",
7494 )
7495 .unwrap();
7496 let snapshot = load_from_dbms_metadata_dir(root).unwrap();
7497
7498 let schema_names: std::collections::HashSet<String> = snapshot
7499 .schemas
7500 .keys()
7501 .filter_map(|s| snapshot.interner.resolve(s.symbol()).map(str::to_string))
7502 .collect();
7503 assert!(
7504 schema_names.contains("PUBLIC"),
7505 "default schema bucket (PUBLIC) must exist for unqualified DDL; got {schema_names:?}"
7506 );
7507 }
7508
7509 #[test]
7513 fn load_from_dbms_metadata_dir_records_raw_ddl_text_on_object() {
7514 let dir = tempdir().unwrap();
7515 let root = dir.path();
7516 let raw = "CREATE TABLE hr.orders (id NUMBER PRIMARY KEY, total NUMBER(12,2));";
7517 std::fs::write(root.join("orders.sql"), raw).unwrap();
7518 let snapshot = load_from_dbms_metadata_dir(root).unwrap();
7519
7520 let bucket = snapshot
7521 .schemas
7522 .values()
7523 .find(|b| !b.objects.is_empty())
7524 .expect("at least one schema bucket with an object");
7525 let obj = bucket.objects.values().next().unwrap();
7526 let common = match obj {
7527 CatalogObject::Table(t) => Some(&t.common),
7528 _ => None,
7529 }
7530 .expect("expected Table");
7531 let ddl = common
7532 .ddl
7533 .as_ref()
7534 .expect("CatalogObject must carry its raw DDL text");
7535 assert_eq!(ddl.ddl_text, raw, "ddl_text must round-trip the source DDL");
7536 }
7537
7538 #[test]
7539 fn load_snapshot_populates_object_metadata_and_dependency_rows() {
7540 let object_rows = vec![
7541 oracle_row(&[
7542 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
7543 ("OBJECT_NAME", "VARCHAR2(128)", Some("BILLING_PKG")),
7544 ("OBJECT_TYPE", "VARCHAR2(30)", Some("PACKAGE")),
7545 ("STATUS", "VARCHAR2(7)", Some("INVALID")),
7546 (
7547 "LAST_DDL_TIME_ISO",
7548 "VARCHAR2(19)",
7549 Some("2026-05-01T13:14:15"),
7550 ),
7551 ("EDITIONABLE", "VARCHAR2(1)", Some("Y")),
7552 ("EDITION_NAME", "VARCHAR2(128)", Some("ORA$BASE")),
7553 ]),
7554 oracle_row(&[
7555 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
7556 ("OBJECT_NAME", "VARCHAR2(128)", Some("CUSTOMERS")),
7557 ("OBJECT_TYPE", "VARCHAR2(30)", Some("TABLE")),
7558 ("STATUS", "VARCHAR2(7)", Some("VALID")),
7559 (
7560 "LAST_DDL_TIME_ISO",
7561 "VARCHAR2(19)",
7562 Some("2026-04-22T08:30:00"),
7563 ),
7564 ("EDITIONABLE", "VARCHAR2(1)", None),
7565 ("EDITION_NAME", "VARCHAR2(128)", None),
7566 ]),
7567 ];
7568
7569 let dependency_rows = vec![
7570 oracle_row(&[
7571 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
7572 ("NAME", "VARCHAR2(128)", Some("BILLING_PKG")),
7573 ("TYPE", "VARCHAR2(30)", Some("PACKAGE")),
7574 ("REFERENCED_OWNER", "VARCHAR2(128)", Some("BILLING")),
7575 ("REFERENCED_NAME", "VARCHAR2(128)", Some("CUSTOMERS")),
7576 ("REFERENCED_TYPE", "VARCHAR2(30)", Some("TABLE")),
7577 ("DEPENDENCY_TYPE", "VARCHAR2(4)", Some("HARD")),
7578 ]),
7579 oracle_row(&[
7580 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
7581 ("NAME", "VARCHAR2(128)", Some("BILLING_PKG")),
7582 ("TYPE", "VARCHAR2(30)", Some("PACKAGE")),
7583 ("REFERENCED_OWNER", "VARCHAR2(128)", Some("SYS")),
7584 ("REFERENCED_NAME", "VARCHAR2(128)", Some("DBMS_OUTPUT")),
7585 ("REFERENCED_TYPE", "VARCHAR2(30)", Some("PACKAGE")),
7586 ("DEPENDENCY_TYPE", "VARCHAR2(4)", Some("REF")),
7587 ]),
7588 ];
7589
7590 let billing_only = vec![OracleBind::from("BILLING")];
7591 let mut expected_queries = capability_probe_expectations();
7592 expected_queries.push(all_users_expectation(&["BILLING"]));
7595 expected_queries.extend(vec![
7596 QueryExpectation {
7597 sql_contains: String::from("from all_objects"),
7598 params: billing_only.clone(),
7599 rows: object_rows,
7600 },
7601 QueryExpectation {
7602 sql_contains: String::from("from all_tab_cols"),
7603 params: billing_only.clone(),
7604 rows: vec![],
7605 },
7606 QueryExpectation {
7607 sql_contains: String::from("from all_constraints c"),
7608 params: billing_only.clone(),
7609 rows: vec![],
7610 },
7611 QueryExpectation {
7612 sql_contains: String::from("from all_indexes i"),
7613 params: billing_only.clone(),
7614 rows: vec![],
7615 },
7616 QueryExpectation {
7617 sql_contains: String::from("from all_triggers"),
7618 params: billing_only.clone(),
7619 rows: vec![],
7620 },
7621 QueryExpectation {
7622 sql_contains: String::from("from all_synonyms"),
7623 params: billing_only.clone(),
7624 rows: vec![],
7625 },
7626 QueryExpectation {
7627 sql_contains: String::from("from all_procedures"),
7628 params: billing_only.clone(),
7629 rows: vec![],
7630 },
7631 QueryExpectation {
7632 sql_contains: String::from("from all_arguments"),
7633 params: billing_only.clone(),
7634 rows: vec![],
7635 },
7636 QueryExpectation {
7637 sql_contains: String::from("from all_views"),
7638 params: billing_only.clone(),
7639 rows: vec![],
7640 },
7641 QueryExpectation {
7642 sql_contains: String::from("from all_mviews"),
7643 params: billing_only.clone(),
7644 rows: vec![],
7645 },
7646 QueryExpectation {
7647 sql_contains: String::from("from all_sequences"),
7648 params: billing_only.clone(),
7649 rows: vec![],
7650 },
7651 QueryExpectation {
7652 sql_contains: String::from("from all_type_attrs"),
7653 params: billing_only.clone(),
7654 rows: vec![],
7655 },
7656 QueryExpectation {
7657 sql_contains: String::from("from all_tab_privs"),
7658 params: billing_only.clone(),
7659 rows: vec![],
7660 },
7661 QueryExpectation {
7662 sql_contains: String::from("from all_db_links"),
7663 params: billing_only.clone(),
7664 rows: vec![],
7665 },
7666 QueryExpectation {
7667 sql_contains: String::from("from all_tab_comments"),
7668 params: billing_only.clone(),
7669 rows: vec![],
7670 },
7671 QueryExpectation {
7672 sql_contains: String::from("from all_col_comments"),
7673 params: billing_only.clone(),
7674 rows: vec![],
7675 },
7676 QueryExpectation {
7677 sql_contains: String::from("from all_editions"),
7678 params: vec![],
7679 rows: vec![],
7680 },
7681 QueryExpectation {
7682 sql_contains: String::from("from all_editioning_views"),
7683 params: billing_only.clone(),
7684 rows: vec![],
7685 },
7686 QueryExpectation {
7687 sql_contains: String::from("from all_policies"),
7688 params: billing_only.clone(),
7689 rows: vec![],
7690 },
7691 QueryExpectation {
7692 sql_contains: String::from("from all_dependencies"),
7693 params: billing_only,
7694 rows: dependency_rows,
7695 },
7696 ]);
7697 let connection = StaticConnection {
7698 expected_queries,
7699 ..StaticConnection::default()
7700 };
7701
7702 let snapshot = load_snapshot_for_test(&connection, &CatalogLoadRequest::default()).unwrap();
7703 let schema = snapshot
7704 .profile
7705 .current_schema
7706 .expect("current schema interned");
7707 let schema_catalog = snapshot.schemas.get(&schema).expect("billing schema");
7708
7709 let billing_pkg = schema_catalog
7710 .objects
7711 .values()
7712 .find_map(|object| match object {
7713 CatalogObject::Package(metadata) => Some(metadata),
7714 _ => None,
7715 })
7716 .expect("billing pkg present");
7717 assert_eq!(billing_pkg.common.status, ObjectStatus::Invalid);
7718 assert_eq!(billing_pkg.common.editionable, Some(true));
7719 assert!(billing_pkg.common.edition_name.is_some());
7720 let last_ddl = billing_pkg
7721 .common
7722 .last_ddl_time
7723 .expect("last_ddl_time populated");
7724 assert_eq!(last_ddl.timestamp(), 1_777_641_255);
7726
7727 let customers_table = schema_catalog
7728 .objects
7729 .values()
7730 .find_map(|object| match object {
7731 CatalogObject::Table(metadata) => Some(metadata),
7732 _ => None,
7733 })
7734 .expect("customers table");
7735 assert!(customers_table.common.editionable.is_none());
7736 assert!(customers_table.common.edition_name.is_none());
7737
7738 assert_eq!(schema_catalog.dependencies.len(), 2);
7739 let pkg_to_customers = schema_catalog
7740 .dependencies
7741 .iter()
7742 .find(|d| matches!(d.dependency_kind, CatalogDependencyKind::Hard))
7743 .expect("hard dependency");
7744 assert!(matches!(pkg_to_customers.object_type, ObjectType::Package));
7745 assert!(matches!(
7746 pkg_to_customers.referenced_type,
7747 Some(ObjectType::Table)
7748 ));
7749 let pkg_to_sys = schema_catalog
7750 .dependencies
7751 .iter()
7752 .find(|d| matches!(d.dependency_kind, CatalogDependencyKind::Reference))
7753 .expect("ref dependency");
7754 assert!(matches!(
7755 pkg_to_sys.referenced_type,
7756 Some(ObjectType::Package)
7757 ));
7758 assert_eq!(
7759 pkg_to_sys
7760 .referenced_owner
7761 .and_then(|owner| snapshot.interner.resolve(owner.symbol())),
7762 Some("SYS")
7763 );
7764 }
7765
7766 #[test]
7767 fn doctor_report_counts_objects_columns_and_indexes() {
7768 let connection = StaticConnection::default();
7769 let snapshot = load_snapshot_for_test(&connection, &CatalogLoadRequest::default()).unwrap();
7770 let mut snapshot = snapshot;
7771 let billing = snapshot.intern_schema_name("BILLING").expect("schema name");
7772 let invoices = snapshot
7773 .intern_object_name("INVOICES")
7774 .expect("object name");
7775 let invoice_view = snapshot
7776 .intern_object_name("INVOICE_VIEW")
7777 .expect("object name");
7778 let billing_seq = snapshot
7779 .intern_object_name("BILLING_SEQ")
7780 .expect("sequence name");
7781
7782 let invoice_id = snapshot
7783 .intern_column_name("INVOICE_ID")
7784 .expect("column name");
7785 let table_metadata = crate::TableMetadata {
7786 common: ObjectCommon {
7787 owner: billing,
7788 name: invoices,
7789 object_type: ObjectType::Table,
7790 status: ObjectStatus::Valid,
7791 ..ObjectCommon::default()
7792 },
7793 columns: HashMap::from([(
7794 invoice_id,
7795 crate::ColumnMetadata {
7796 name: invoice_id,
7797 position: 1,
7798 ..crate::ColumnMetadata::default()
7799 },
7800 )]),
7801 ..crate::TableMetadata::default()
7802 };
7803
7804 let view_metadata = crate::ViewMetadata {
7805 common: ObjectCommon {
7806 owner: billing,
7807 name: invoice_view,
7808 object_type: ObjectType::View,
7809 status: ObjectStatus::Invalid,
7810 ..ObjectCommon::default()
7811 },
7812 ..crate::ViewMetadata::default()
7813 };
7814
7815 let sequence_metadata = crate::SequenceMetadata {
7816 common: ObjectCommon {
7817 owner: billing,
7818 name: billing_seq,
7819 object_type: ObjectType::Sequence,
7820 status: ObjectStatus::Valid,
7821 ..ObjectCommon::default()
7822 },
7823 ..crate::SequenceMetadata::default()
7824 };
7825
7826 let schema_catalog = snapshot.schemas.entry(billing).or_default();
7827 schema_catalog
7828 .objects
7829 .insert(invoices, CatalogObject::Table(table_metadata));
7830 schema_catalog
7831 .objects
7832 .insert(invoice_view, CatalogObject::View(view_metadata));
7833 schema_catalog
7834 .objects
7835 .insert(billing_seq, CatalogObject::Sequence(sequence_metadata));
7836
7837 let report = snapshot.doctor_report();
7838 assert_eq!(report.totals.objects_total, 3);
7839 assert_eq!(report.totals.columns_total, 1);
7840 let table_tile = report
7841 .object_counts
7842 .iter()
7843 .find(|tile| matches!(tile.object_type, ObjectType::Table))
7844 .expect("table tile");
7845 assert_eq!(table_tile.total, 1);
7846 assert_eq!(table_tile.valid, 1);
7847 let view_tile = report
7848 .object_counts
7849 .iter()
7850 .find(|tile| matches!(tile.object_type, ObjectType::View))
7851 .expect("view tile");
7852 assert_eq!(view_tile.invalid, 1);
7853 assert!(report.can_query_all_views);
7858 assert!(report.can_use_dbms_metadata);
7859 assert!(
7860 report.missing_permissions.is_empty(),
7861 "all probes succeeded on the mock so no permissions should be flagged"
7862 );
7863 }
7864
7865 #[test]
7866 fn doctor_report_suggests_grants_when_capabilities_are_missing() {
7867 let mut snapshot = CatalogSnapshot::new(
7868 plsql_core::AnalysisProfile::default(),
7869 CatalogCapabilities {
7870 can_query_all_views: true,
7871 can_query_dba_views: false,
7872 can_use_dbms_metadata: false,
7873 can_read_source: false,
7874 plscope_enabled: false,
7875 can_query_scheduler: false,
7876 can_query_roles_and_grants: false,
7877 warnings: vec![CapabilityWarning {
7878 code: String::from("catalog-version-parse-fallback"),
7879 message: String::from("server version did not parse"),
7880 remediation: None,
7881 }],
7882 },
7883 CatalogSource {
7884 kind: CatalogSourceKind::LiveConnection,
7885 path: None,
7886 description: Some(String::from("live extraction via oraclemcp-db from xe")),
7887 },
7888 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
7889 );
7890 let _ = snapshot.intern_schema_name("BILLING");
7891
7892 let report = snapshot.doctor_report();
7893 assert!(matches!(
7894 report.source_kind,
7895 CatalogSourceKind::LiveConnection
7896 ));
7897 assert_eq!(report.capability_warnings.len(), 1);
7898 let view_names: Vec<&str> = report
7899 .missing_permissions
7900 .iter()
7901 .map(|m| m.view_name.as_str())
7902 .collect();
7903 assert!(view_names.iter().any(|v| v.contains("DBA_OBJECTS")));
7904 assert!(view_names.iter().any(|v| v.contains("DBMS_METADATA")));
7905 assert!(view_names.iter().any(|v| v.contains("ALL_SOURCE")));
7906 assert!(view_names.iter().any(|v| v.contains("PLSCOPE_SETTINGS")));
7907 assert!(view_names.iter().any(|v| v.contains("SCHEDULER_JOBS")));
7908 assert!(view_names.iter().any(|v| v.contains("ROLE_PRIVS")));
7909 }
7910
7911 #[test]
7912 fn load_snapshot_populates_plscope_availability_from_object_settings() {
7913 let plscope_rows = vec![
7914 oracle_row(&[
7915 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
7916 (
7917 "PLSCOPE_SETTINGS",
7918 "VARCHAR2(255)",
7919 Some("IDENTIFIERS:ALL,STATEMENTS:ALL"),
7920 ),
7921 ]),
7922 oracle_row(&[
7923 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
7924 (
7925 "PLSCOPE_SETTINGS",
7926 "VARCHAR2(255)",
7927 Some("IDENTIFIERS:ALL,STATEMENTS:NONE"),
7928 ),
7929 ]),
7930 oracle_row(&[
7931 ("OWNER", "VARCHAR2(128)", Some("REPORTING")),
7932 (
7933 "PLSCOPE_SETTINGS",
7934 "VARCHAR2(255)",
7935 Some("IDENTIFIERS:NONE,STATEMENTS:NONE"),
7936 ),
7937 ]),
7938 ];
7939
7940 let billing_only = vec![OracleBind::from("BILLING")];
7941 let mut expected_queries = capability_probe_expectations();
7942 expected_queries.push(all_users_expectation(&["BILLING", "REPORTING"]));
7944 for fragment in [
7945 "from all_objects",
7946 "from all_tab_cols",
7947 "from all_constraints c",
7948 "from all_indexes i",
7949 "from all_triggers",
7950 "from all_synonyms",
7951 "from all_procedures",
7952 "from all_arguments",
7953 "from all_views",
7954 "from all_mviews",
7955 "from all_sequences",
7956 "from all_type_attrs",
7957 "from all_tab_privs",
7958 "from all_db_links",
7959 "from all_tab_comments",
7960 "from all_col_comments",
7961 "from all_editions",
7962 "from all_editioning_views",
7963 "from all_policies",
7964 "from all_dependencies",
7965 ] {
7966 let params = if fragment.eq("from all_editions") {
7968 vec![]
7969 } else {
7970 billing_only.clone()
7971 };
7972 expected_queries.push(QueryExpectation {
7973 sql_contains: String::from(fragment),
7974 params,
7975 rows: vec![],
7976 });
7977 }
7978 expected_queries.push(QueryExpectation {
7979 sql_contains: String::from("from all_plsql_object_settings"),
7980 params: billing_only.clone(),
7981 rows: plscope_rows,
7982 });
7983 let connection = StaticConnection {
7984 expected_queries,
7985 ..StaticConnection::default()
7986 };
7987 let snapshot = load_snapshot_for_test(&connection, &CatalogLoadRequest::default()).unwrap();
7988
7989 let billing = snapshot
7990 .schemas
7991 .keys()
7992 .find(|name| {
7993 snapshot
7994 .interner
7995 .resolve(name.symbol())
7996 .is_some_and(|label| label.eq("BILLING"))
7997 })
7998 .copied()
7999 .expect("billing schema");
8000 let plscope = snapshot
8001 .schemas
8002 .get(&billing)
8003 .and_then(|s| s.plscope.as_ref())
8004 .expect("billing plscope");
8005 assert!(matches!(
8006 plscope.availability,
8007 PlScopeAvailability::IdentifiersAndStatements
8008 ));
8009
8010 let reporting = snapshot
8011 .schemas
8012 .keys()
8013 .find(|name| {
8014 snapshot
8015 .interner
8016 .resolve(name.symbol())
8017 .is_some_and(|label| label.eq("REPORTING"))
8018 })
8019 .copied()
8020 .expect("reporting schema");
8021 let reporting_plscope = snapshot
8022 .schemas
8023 .get(&reporting)
8024 .and_then(|s| s.plscope.as_ref())
8025 .expect("reporting plscope");
8026 assert!(matches!(
8028 reporting_plscope.availability,
8029 PlScopeAvailability::AvailableButStale
8030 ));
8031 }
8032
8033 #[test]
8034 fn load_snapshot_extracts_all_identifiers_into_plscope() {
8035 let identifier_rows = vec![
8036 oracle_row(&[
8037 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
8038 ("NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
8039 ("TYPE", "VARCHAR2(30)", Some("VARIABLE")),
8040 ("USAGE", "VARCHAR2(20)", Some("DECLARATION")),
8041 ("LINE", "NUMBER(10)", Some("12")),
8042 ("COL", "NUMBER(10)", Some("5")),
8043 ("OBJECT_NAME", "VARCHAR2(128)", Some("BILLING_PKG")),
8044 ]),
8045 oracle_row(&[
8046 ("OWNER", "VARCHAR2(128)", Some("BILLING")),
8047 ("NAME", "VARCHAR2(128)", Some("CUSTOMER_ID")),
8048 ("TYPE", "VARCHAR2(30)", Some("VARIABLE")),
8049 ("USAGE", "VARCHAR2(20)", Some("REFERENCE")),
8050 ("LINE", "NUMBER(10)", Some("18")),
8051 ("COL", "NUMBER(10)", Some("21")),
8052 ("OBJECT_NAME", "VARCHAR2(128)", Some("BILLING_PKG")),
8053 ]),
8054 ];
8055
8056 let billing_only = vec![OracleBind::from("BILLING")];
8057 let mut expected_queries = capability_probe_expectations();
8058 expected_queries.push(all_users_expectation(&["BILLING"]));
8060 for fragment in [
8061 "from all_objects",
8062 "from all_tab_cols",
8063 "from all_constraints c",
8064 "from all_indexes i",
8065 "from all_triggers",
8066 "from all_synonyms",
8067 "from all_procedures",
8068 "from all_arguments",
8069 "from all_views",
8070 "from all_mviews",
8071 "from all_sequences",
8072 "from all_type_attrs",
8073 "from all_tab_privs",
8074 "from all_db_links",
8075 "from all_tab_comments",
8076 "from all_col_comments",
8077 "from all_editions",
8078 "from all_editioning_views",
8079 "from all_policies",
8080 "from all_dependencies",
8081 "from all_plsql_object_settings",
8082 ] {
8083 let params = if fragment.eq("from all_editions") {
8084 vec![]
8085 } else {
8086 billing_only.clone()
8087 };
8088 expected_queries.push(QueryExpectation {
8089 sql_contains: String::from(fragment),
8090 params,
8091 rows: vec![],
8092 });
8093 }
8094 expected_queries.push(QueryExpectation {
8095 sql_contains: String::from("from all_identifiers"),
8096 params: billing_only.clone(),
8097 rows: identifier_rows,
8098 });
8099 let connection = StaticConnection {
8100 expected_queries,
8101 ..StaticConnection::default()
8102 };
8103 let snapshot = load_snapshot_for_test(&connection, &CatalogLoadRequest::default()).unwrap();
8104
8105 let billing = snapshot
8106 .schemas
8107 .keys()
8108 .find(|name| {
8109 snapshot
8110 .interner
8111 .resolve(name.symbol())
8112 .is_some_and(|label| label.eq("BILLING"))
8113 })
8114 .copied()
8115 .expect("billing schema");
8116 let plscope = snapshot
8117 .schemas
8118 .get(&billing)
8119 .and_then(|s| s.plscope.as_ref())
8120 .expect("plscope present");
8121 assert_eq!(plscope.identifiers.len(), 2);
8122 let first = &plscope.identifiers[0];
8123 assert_eq!(first.identifier_type, "VARIABLE");
8124 assert_eq!(first.usage, "DECLARATION");
8125 assert_eq!(first.line, 12);
8126 assert_eq!(first.column, 5);
8127 }
8128
8129 #[test]
8130 fn normalize_dbms_metadata_ddl_collapses_whitespace_and_trims_slash() {
8131 use crate::normalize_dbms_metadata_ddl;
8132 let input = " CREATE TABLE FOO ( ID NUMBER ) \n/ ";
8133 let normalized = normalize_dbms_metadata_ddl(input);
8134 assert_eq!(normalized, "CREATE TABLE FOO ( ID NUMBER )");
8135 }
8136
8137 #[test]
8138 fn object_type_to_dbms_metadata_value_covers_all_known_types() {
8139 use crate::object_type_to_dbms_metadata_value;
8140 for object_type in [
8142 ObjectType::Table,
8143 ObjectType::View,
8144 ObjectType::MaterializedView,
8145 ObjectType::Sequence,
8146 ObjectType::Type,
8147 ObjectType::Package,
8148 ObjectType::Procedure,
8149 ObjectType::Function,
8150 ObjectType::Trigger,
8151 ObjectType::EditioningView,
8152 ObjectType::SchedulerJob,
8153 ObjectType::Synonym,
8154 ObjectType::Index,
8155 ] {
8156 assert!(object_type_to_dbms_metadata_value(object_type).is_some());
8157 }
8158 assert!(object_type_to_dbms_metadata_value(ObjectType::Constraint).is_none());
8160 assert!(object_type_to_dbms_metadata_value(ObjectType::Unknown).is_none());
8161 }
8162
8163 #[test]
8164 fn populate_dbms_metadata_ddl_records_warnings_on_fetch_failure() {
8165 use crate::CatalogCapabilities;
8166
8167 let mut snapshot = CatalogSnapshot::new(
8172 plsql_core::AnalysisProfile::default(),
8173 CatalogCapabilities {
8174 can_use_dbms_metadata: true,
8175 ..CatalogCapabilities::default()
8176 },
8177 CatalogSource {
8178 kind: CatalogSourceKind::LiveConnection,
8179 path: None,
8180 description: Some(String::from("test")),
8181 },
8182 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
8183 );
8184 let billing = snapshot.intern_schema_name("BILLING").expect("schema");
8185 let invoices = snapshot.intern_object_name("INVOICES").expect("object");
8186 let table = crate::TableMetadata {
8187 common: ObjectCommon {
8188 owner: billing,
8189 name: invoices,
8190 object_type: ObjectType::Table,
8191 status: ObjectStatus::Valid,
8192 ..ObjectCommon::default()
8193 },
8194 ..crate::TableMetadata::default()
8195 };
8196 snapshot
8197 .schemas
8198 .entry(billing)
8199 .or_default()
8200 .objects
8201 .insert(invoices, CatalogObject::Table(table));
8202
8203 let failing_connection = StaticConnection {
8205 expected_queries: vec![QueryExpectation {
8206 sql_contains: String::from("dbms_metadata.get_ddl"),
8207 params: vec![OracleBind::from("UNREACHABLE_SENTINEL")],
8208 rows: vec![],
8209 }],
8210 ..StaticConnection::default()
8211 };
8212
8213 populate_dbms_metadata_ddl_for_test(&failing_connection, &mut snapshot).unwrap();
8214
8215 assert!(
8216 snapshot
8217 .capabilities
8218 .warnings
8219 .iter()
8220 .any(|w| w.code.eq("dbms-metadata-fetch-failed"))
8221 );
8222
8223 snapshot.capabilities.can_use_dbms_metadata = false;
8225 let baseline_warnings = snapshot.capabilities.warnings.len();
8226 populate_dbms_metadata_ddl_for_test(&failing_connection, &mut snapshot).unwrap();
8227 assert_eq!(snapshot.capabilities.warnings.len(), baseline_warnings);
8228 }
8229
8230 #[test]
8231 fn populate_dbms_metadata_ddl_writes_ddl_field_on_success() {
8232 use crate::CatalogCapabilities;
8233
8234 let mut snapshot = CatalogSnapshot::new(
8235 plsql_core::AnalysisProfile::default(),
8236 CatalogCapabilities {
8237 can_use_dbms_metadata: true,
8238 ..CatalogCapabilities::default()
8239 },
8240 CatalogSource {
8241 kind: CatalogSourceKind::LiveConnection,
8242 path: None,
8243 description: Some(String::from("test")),
8244 },
8245 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
8246 );
8247 let billing = snapshot.intern_schema_name("BILLING").expect("schema");
8248 let invoices = snapshot.intern_object_name("INVOICES").expect("object");
8249 let table = crate::TableMetadata {
8250 common: ObjectCommon {
8251 owner: billing,
8252 name: invoices,
8253 object_type: ObjectType::Table,
8254 status: ObjectStatus::Valid,
8255 ..ObjectCommon::default()
8256 },
8257 ..crate::TableMetadata::default()
8258 };
8259 snapshot
8260 .schemas
8261 .entry(billing)
8262 .or_default()
8263 .objects
8264 .insert(invoices, CatalogObject::Table(table));
8265
8266 let connection = StaticConnection {
8267 expected_queries: vec![QueryExpectation {
8268 sql_contains: String::from("dbms_metadata.get_ddl"),
8269 params: vec![
8270 OracleBind::from("TABLE"),
8271 OracleBind::from("INVOICES"),
8272 OracleBind::from("BILLING"),
8273 ],
8274 rows: vec![oracle_row(&[
8275 (
8276 "DDL_TEXT",
8277 "CLOB",
8278 Some(" CREATE TABLE BILLING.INVOICES (ID NUMBER) \n "),
8279 ),
8280 (
8281 "XML_TEXT",
8282 "CLOB",
8283 Some("<TABLE_T><NAME>INVOICES</NAME></TABLE_T>"),
8284 ),
8285 ])],
8286 }],
8287 ..StaticConnection::default()
8288 };
8289
8290 populate_dbms_metadata_ddl_for_test(&connection, &mut snapshot).unwrap();
8291
8292 let stored = snapshot
8293 .schemas
8294 .get(&billing)
8295 .and_then(|schema| schema.objects.get(&invoices));
8296 assert!(
8297 matches!(stored, Some(CatalogObject::Table(_))),
8298 "expected a Table catalog object for billing.invoices"
8299 );
8300 let Some(CatalogObject::Table(table)) = stored else {
8301 return;
8302 };
8303 let ddl = table.common.ddl.as_ref().expect("ddl populated");
8304 assert!(ddl.ddl_text.contains("CREATE TABLE BILLING.INVOICES"));
8305 assert_eq!(
8306 ddl.normalized_ddl.as_deref(),
8307 Some("CREATE TABLE BILLING.INVOICES (ID NUMBER)")
8308 );
8309 assert_eq!(
8310 ddl.xml_text.as_deref(),
8311 Some("<TABLE_T><NAME>INVOICES</NAME></TABLE_T>")
8312 );
8313 }
8314
8315 #[test]
8316 fn negotiate_capabilities_reports_failures_as_warnings() {
8317 let connection = StaticConnection {
8318 expected_queries: capability_probe_expectations()
8319 .into_iter()
8320 .map(|mut q| {
8321 q.params = vec![OracleBind::from("UNREACHABLE_SENTINEL")];
8324 q
8325 })
8326 .collect(),
8327 ..StaticConnection::default()
8328 };
8329
8330 let capabilities = negotiate_capabilities_for_test(&connection);
8331
8332 assert!(!capabilities.can_query_all_views);
8333 assert!(!capabilities.can_query_dba_views);
8334 assert!(!capabilities.can_read_source);
8335 assert!(!capabilities.can_query_scheduler);
8336 assert!(!capabilities.can_query_roles_and_grants);
8337 assert!(!capabilities.plscope_enabled);
8338 assert!(capabilities.can_use_dbms_metadata);
8341 assert_eq!(capabilities.warnings.len(), 6);
8343 assert!(
8344 capabilities
8345 .warnings
8346 .iter()
8347 .any(|w| w.code.eq("all-views-probe"))
8348 );
8349 assert!(
8350 capabilities
8351 .warnings
8352 .iter()
8353 .any(|w| w.code.eq("plscope-probe"))
8354 );
8355 }
8356
8357 #[test]
8358 fn negotiate_capabilities_marks_every_probe_succeeded_on_open_mock() {
8359 let connection = StaticConnection::default();
8362 let capabilities = negotiate_capabilities_for_test(&connection);
8363 assert!(capabilities.can_query_all_views);
8364 assert!(capabilities.can_query_dba_views);
8365 assert!(capabilities.can_read_source);
8366 assert!(capabilities.can_query_scheduler);
8367 assert!(capabilities.can_query_roles_and_grants);
8368 assert!(capabilities.plscope_enabled);
8369 assert!(capabilities.can_use_dbms_metadata);
8370 assert!(capabilities.warnings.is_empty());
8371 }
8372
8373 #[test]
8374 fn doctor_report_skips_grant_suggestions_for_json_snapshot_source() {
8375 let snapshot = CatalogSnapshot::new(
8376 plsql_core::AnalysisProfile::default(),
8377 CatalogCapabilities {
8378 can_query_all_views: false,
8379 can_query_dba_views: false,
8380 can_use_dbms_metadata: false,
8381 can_read_source: false,
8382 plscope_enabled: false,
8383 can_query_scheduler: false,
8384 can_query_roles_and_grants: false,
8385 warnings: vec![],
8386 },
8387 CatalogSource {
8388 kind: CatalogSourceKind::JsonSnapshot,
8389 path: Some(std::path::PathBuf::from("/tmp/snapshot.json")),
8390 description: None,
8391 },
8392 DateTime::<Utc>::from_timestamp(1_700_000_000, 0).unwrap(),
8393 );
8394
8395 let report = snapshot.doctor_report();
8396 assert!(matches!(
8397 report.source_kind,
8398 CatalogSourceKind::JsonSnapshot
8399 ));
8400 assert!(report.missing_permissions.is_empty());
8403 }
8404
8405 fn classify_single(ddl: &str) -> (ObjectType, CatalogObject) {
8419 let dir = tempdir().unwrap();
8420 std::fs::write(dir.path().join("obj.sql"), ddl).unwrap();
8421 let snapshot = load_from_dbms_metadata_dir(dir.path()).unwrap();
8422 let mut found: Vec<CatalogObject> = snapshot
8423 .schemas
8424 .values()
8425 .flat_map(|s| s.objects.values().cloned())
8426 .collect();
8427 assert_eq!(
8428 found.len(),
8429 1,
8430 "expected exactly one classified object for DDL: {ddl}"
8431 );
8432 let obj = found.remove(0);
8433 let common = match &obj {
8434 CatalogObject::Table(m) => &m.common,
8435 CatalogObject::View(m) => &m.common,
8436 CatalogObject::MaterializedView(m) => &m.common,
8437 CatalogObject::Sequence(m) => &m.common,
8438 CatalogObject::Type(m) => &m.common,
8439 CatalogObject::Package(m) => &m.common,
8440 CatalogObject::Procedure(m) => &m.common,
8441 CatalogObject::Function(m) => &m.common,
8442 CatalogObject::Trigger(m) => &m.common,
8443 CatalogObject::SchedulerJob(m) => &m.common,
8444 CatalogObject::EditioningView(m) => &m.common,
8445 };
8446 (common.object_type, obj)
8447 }
8448
8449 #[test]
8454 fn classify_view_with_table_in_body_is_view_not_table() {
8455 let ddl = "CREATE OR REPLACE VIEW hr.v_emp AS SELECT * FROM hr.emp WHERE 'TABLE'='TABLE';";
8456 let (kind, obj) = classify_single(ddl);
8457 assert_eq!(
8458 kind,
8459 ObjectType::View,
8460 "VIEW with 'TABLE' literal in body must classify as View, got {kind:?}",
8461 );
8462 assert!(
8463 matches!(obj, CatalogObject::View(_)),
8464 "expected CatalogObject::View, got {obj:?}",
8465 );
8466 }
8467
8468 #[test]
8470 fn classify_trigger_with_table_in_body_is_trigger() {
8471 let ddl = "CREATE OR REPLACE TRIGGER hr.t_audit \
8472 AFTER INSERT ON hr.employees \
8473 BEGIN INSERT INTO hr.audit_table VALUES (:NEW.id); END;";
8474 let (kind, _obj) = classify_single(ddl);
8475 assert_eq!(kind, ObjectType::Trigger);
8476 }
8477
8478 #[test]
8480 fn classify_procedure_with_table_in_body_is_procedure() {
8481 let ddl = "CREATE OR REPLACE PROCEDURE hr.p_load \
8482 AS BEGIN INSERT INTO hr.staging_table SELECT * FROM hr.src; END;";
8483 let (kind, _obj) = classify_single(ddl);
8484 assert_eq!(kind, ObjectType::Procedure);
8485 }
8486
8487 #[test]
8489 fn classify_function_with_table_in_body_is_function() {
8490 let ddl = "CREATE OR REPLACE FUNCTION hr.f_count RETURN NUMBER \
8491 AS n NUMBER; BEGIN SELECT COUNT(*) INTO n FROM hr.big_table; RETURN n; END;";
8492 let (kind, _obj) = classify_single(ddl);
8493 assert_eq!(kind, ObjectType::Function);
8494 }
8495
8496 #[test]
8500 fn classify_package_body_with_table_is_not_package_or_table() {
8501 let ddl = "CREATE OR REPLACE PACKAGE BODY hr.billing_api AS \
8502 PROCEDURE charge IS BEGIN INSERT INTO hr.charges_table VALUES (1); END; END;";
8503 let dir = tempdir().unwrap();
8504 std::fs::write(dir.path().join("body.sql"), ddl).unwrap();
8505 let snapshot = load_from_dbms_metadata_dir(dir.path()).unwrap();
8506 let total: usize = snapshot.schemas.values().map(|s| s.objects.len()).sum();
8507 assert_eq!(
8508 total, 0,
8509 "PACKAGE BODY must not produce a classified object (got {total})",
8510 );
8511 }
8512
8513 #[test]
8517 fn classify_materialized_view_is_materialized_view_not_view() {
8518 let ddl = "CREATE MATERIALIZED VIEW hr.mv_emp_summary AS SELECT dept, COUNT(*) FROM hr.emp GROUP BY dept;";
8519 let (kind, obj) = classify_single(ddl);
8520 assert_eq!(
8521 kind,
8522 ObjectType::MaterializedView,
8523 "MATERIALIZED VIEW must classify as MaterializedView, got {kind:?}",
8524 );
8525 assert!(
8526 matches!(obj, CatalogObject::MaterializedView(_)),
8527 "expected CatalogObject::MaterializedView, got {obj:?}",
8528 );
8529 }
8530
8531 #[test]
8534 fn classify_view_with_leading_block_comment_mentioning_create_table_is_view() {
8535 let ddl = "/* comment with CREATE TABLE x; */\n\
8536 CREATE OR REPLACE VIEW hr.v_dept AS SELECT * FROM hr.dept;";
8537 let (kind, _obj) = classify_single(ddl);
8538 assert_eq!(kind, ObjectType::View);
8539 }
8540
8541 #[test]
8544 fn classify_procedure_with_leading_line_comments_is_procedure() {
8545 let ddl = "-- CREATE TABLE oops (x NUMBER);\n\
8546 -- another line mentioning VIEW and TABLE\n\
8547 CREATE OR REPLACE PROCEDURE hr.p_noop AS BEGIN NULL; END;";
8548 let (kind, _obj) = classify_single(ddl);
8549 assert_eq!(kind, ObjectType::Procedure);
8550 }
8551
8552 #[test]
8556 fn classify_editionable_view_is_view() {
8557 let ddl = "CREATE OR REPLACE EDITIONABLE VIEW hr.v_emp AS SELECT * FROM hr.emp;";
8558 let (kind, _obj) = classify_single(ddl);
8559 assert_eq!(kind, ObjectType::View);
8560 }
8561
8562 #[test]
8565 fn classify_quoted_table_owner_name_is_table() {
8566 let ddl = "CREATE TABLE \"HR\".\"EMP\" (id NUMBER);";
8567 let (kind, _obj) = classify_single(ddl);
8568 assert_eq!(kind, ObjectType::Table);
8569 }
8570
8571 #[test]
8578 fn extract_owner_and_name_keeps_whitespace_in_quoted_identifiers() {
8579 assert_eq!(
8581 crate::extract_owner_and_name("\"HR\".\"MY TABLE\" (ID NUMBER);"),
8582 Some((Some("HR".to_string()), "MY TABLE".to_string())),
8583 );
8584 assert_eq!(
8586 crate::extract_owner_and_name("\"MY OWNER\".\"EMP\" (ID NUMBER);"),
8587 Some((Some("MY OWNER".to_string()), "EMP".to_string())),
8588 );
8589 assert_eq!(
8591 crate::extract_owner_and_name("\"MY TABLE\" (ID NUMBER);"),
8592 Some((None, "MY TABLE".to_string())),
8593 );
8594 assert_eq!(
8596 crate::extract_owner_and_name("\"HR\".\"EMP\" (ID NUMBER);"),
8597 Some((Some("HR".to_string()), "EMP".to_string())),
8598 );
8599 assert_eq!(
8600 crate::extract_owner_and_name("HR.ORDERS (ID NUMBER);"),
8601 Some((Some("HR".to_string()), "ORDERS".to_string())),
8602 );
8603 }
8604
8605 #[test]
8609 fn quoted_whitespace_names_intern_as_distinct_keys() {
8610 let dir = tempdir().unwrap();
8611 std::fs::write(
8612 dir.path().join("a.sql"),
8613 "CREATE TABLE \"HR\".\"MY TABLE\" (id NUMBER);",
8614 )
8615 .unwrap();
8616 std::fs::write(
8617 dir.path().join("b.sql"),
8618 "CREATE TABLE \"HR\".\"MY OTHER\" (id NUMBER);",
8619 )
8620 .unwrap();
8621 let snapshot = load_from_dbms_metadata_dir(dir.path()).unwrap();
8622
8623 let hr = snapshot
8625 .schemas
8626 .iter()
8627 .find(|(s, _)| {
8628 snapshot
8629 .interner
8630 .resolve(s.symbol())
8631 .is_some_and(|label| label.eq("HR"))
8632 })
8633 .map(|(_, c)| c)
8634 .expect("HR schema present");
8635 assert_eq!(hr.objects.len(), 2, "two distinct objects, no collision");
8636
8637 let mut names: Vec<&str> = hr
8638 .objects
8639 .values()
8640 .filter_map(|o| {
8641 let CatalogObject::Table(m) = o else {
8642 return None;
8643 };
8644 snapshot.interner.resolve(m.common.name.symbol())
8645 })
8646 .collect();
8647 assert_eq!(names.len(), 2, "all HR objects must be tables");
8648 names.sort_unstable();
8649 assert_eq!(names, vec!["MY OTHER", "MY TABLE"]);
8650 }
8651
8652 #[test]
8654 fn classify_plain_table_is_table() {
8655 let ddl = "CREATE TABLE hr.orders (id NUMBER PRIMARY KEY, total NUMBER(12,2));";
8656 let (kind, obj) = classify_single(ddl);
8657 assert_eq!(kind, ObjectType::Table);
8658 assert!(matches!(obj, CatalogObject::Table(_)));
8659 }
8660
8661 #[test]
8664 fn classify_package_spec_mentioning_view_and_table_is_package() {
8665 let ddl = "CREATE OR REPLACE PACKAGE hr.report_api AS \
8666 -- builds a VIEW over a TABLE \n\
8667 PROCEDURE rebuild_view_from_table; END;";
8668 let (kind, _obj) = classify_single(ddl);
8669 assert_eq!(kind, ObjectType::Package);
8670 }
8671}