Skip to content

Commit 7834f14

Browse files
committed
mk-oracle: report non-CDB Oracle jobs in the legacy row format
A CDB jobs row carries eleven fields and a non-CDB row ten, and no single statement produces both, so a non-CDB was reported in the CDB shape. The 12.1 query is therefore tagged CDB-only, and a non-CDB falls through to the ten-field query, which matches the legacy output. Tagging the query CDB-only also makes its fallback branch for a non-CDB unreachable, so that branch is removed here. It matched a con_id of 0, which only a non-CDB reports. Two further divergences from legacy are fixed on the way. - the CDB query mapped :IGNORE_DB_NAME = 1 to the database name, so by default it keyed the item on the instance name where legacy keys it on the database name - a job of class SCHED$_LOG_ON_ERRORS_CLASS kept the status of its last run across a restart, in both arms, which turned a legacy WARN into a permanent CRIT TESTS: Manual and automated. Unit tests cover the tenant split from the section name down to the selected statement, and cover both arms for the item key and for the log-on-errors class. Confirmed against real Oracle databases in Docker, using 12.1 as a non-CDB and 19.3 and 23.26 as CDBs. The non-CDB now emits ten-field rows with a bare database item, and the CDBs keep their eleven fields. CMK-37363 Change-Id: I4602011274c7d19afefa6597364b17877d605687
1 parent 68b2a76 commit 7834f14

8 files changed

Lines changed: 261 additions & 45 deletions

File tree

packages/mk-oracle/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ rust_library(
6464
"sqls/instance.12010002.all.sql",
6565
"sqls/io_stats.0.all.sql",
6666
"sqls/jobs.10020000.all.sql",
67-
"sqls/jobs.12010000.all.sql",
67+
"sqls/jobs.12010000.cdb.sql",
6868
"sqls/locks.10020000.all.sql",
6969
"sqls/locks.12010000.all.sql",
7070
"sqls/logswitches.0.all.sql",

packages/mk-oracle/sqls/jobs.10020000.all.sql

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ SELECT UPPER(
3131
TO_DATE('1970-01-01', 'YYYY-MM-DD'))
3232
) AS next_run_date, -- Next scheduled run (defaulted if null)
3333
NVL(j.schedule_name, '-') AS schedule_name, -- Schedule name associated with the job (or '-' if none)
34-
jd.status -- Status of the most recent run (e.g., SUCCEEDED, FAILED, STOPPED)
34+
-- Blank a log-on-errors job that restarted since its last log row:
35+
-- the previous outcome is stale, not its current status.
36+
CASE
37+
WHEN j.job_class = 'SCHED$_LOG_ON_ERRORS_CLASS'
38+
AND j.last_start_date > jd.actual_start_date THEN ''
39+
ELSE jd.status
40+
END -- Status of the most recent run (e.g., SUCCEEDED, FAILED, STOPPED)
3541
FROM dba_scheduler_jobs j -- Data dictionary view of all Scheduler jobs
3642
JOIN v$database vd ON 1 = 1 -- Database metadata (name, role, mode)
3743
JOIN v$instance i ON 1 = 1 -- Instance metadata (instance name)

packages/mk-oracle/sqls/jobs.12010000.all.sql renamed to packages/mk-oracle/sqls/jobs.12010000.cdb.sql

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ SELECT vp.instance_name, -- Instance name (or DB na
2929
TO_DATE('1970-01-01', 'YYYY-MM-DD'))
3030
) AS next_run_date, -- Next scheduled run date/time (defaulted if null)
3131
NVL(j.schedule_name, '-') AS schedule_name, -- Associated schedule name (or '-' if none)
32-
jd.status -- Status of the most recent run (SUCCEEDED, FAILED, STOPPED, etc.)
32+
-- Blank a log-on-errors job that restarted since its last log row:
33+
-- the previous outcome is stale, not its current status.
34+
CASE
35+
WHEN j.job_class = 'SCHED$_LOG_ON_ERRORS_CLASS'
36+
AND j.last_start_date > jd.actual_start_date THEN ''
37+
ELSE jd.status
38+
END -- Status of the most recent run (SUCCEEDED, FAILED, STOPPED, etc.)
3339
FROM cdb_scheduler_jobs j -- CDB view of scheduler jobs across all PDBs
3440
-- Join with Subquery: maps container ID to instance and container names
3541
JOIN (SELECT c.con_id,
3642
UPPER(DECODE(NVL(:IGNORE_DB_NAME, 0),
37-
1, d.NAME, -- If :IGNORE_DB_NAME = 1 → show DB name
43+
0, d.NAME, -- If :IGNORE_DB_NAME = 0 → show DB name
3844
i.instance_name -- Else → show instance name
3945
)) AS instance_name,
4046
c.name AS container_name -- PDB name
@@ -46,16 +52,6 @@ FROM cdb_scheduler_jobs j -- CDB view of scheduler jobs across all PDBs
4652
AND c.con_id <> 2 -- Exclude seed PDB (con_id = 2)
4753
AND d.database_role = 'PRIMARY' -- Only primary DB (exclude standby)
4854
AND d.open_mode = 'READ WRITE' -- Only open databases
49-
UNION ALL
50-
-- Handles non-CDB-like representation (fallback case)
51-
SELECT 0, d.name, c.name
52-
FROM v$database d
53-
JOIN v$instance i
54-
ON i.con_id = d.con_id
55-
LEFT JOIN v$containers c
56-
ON c.dbid = d.dbid
57-
WHERE d.database_role = 'PRIMARY'
58-
AND d.open_mode = 'READ WRITE'
5955
) vp
6056
ON j.con_id = vp.con_id
6157
-- Join with Subquery: get latest run log ID per job

packages/mk-oracle/src/ora_sql/instance.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,44 @@ oracle:
11231123
);
11241124
}
11251125

1126+
// CMK-37363: derivation (v$database.cdb -> Tenant) + plumbing + selection.
1127+
// Four numeric components, or the version parses to None and this is vacuous.
1128+
#[test]
1129+
fn test_jobs_non_cdb_selects_ten_field_query() {
1130+
let yaml = one_builtin_section_yaml("jobs");
1131+
let statements = selected_statements(&yaml, "TESTDB", "19.28.0.0.0", "NO");
1132+
1133+
assert_eq!(statements.len(), 1);
1134+
assert!(
1135+
statements[0].contains("dba_scheduler_jobs"),
1136+
"{}",
1137+
statements[0]
1138+
);
1139+
assert!(
1140+
!statements[0].contains("container_name"),
1141+
"{}",
1142+
statements[0]
1143+
);
1144+
}
1145+
1146+
#[test]
1147+
fn test_jobs_cdb_keeps_container_column() {
1148+
let yaml = one_builtin_section_yaml("jobs");
1149+
let statements = selected_statements(&yaml, "TESTCDB", "19.28.0.0.0", "YES");
1150+
1151+
assert_eq!(statements.len(), 1);
1152+
assert!(
1153+
statements[0].contains("cdb_scheduler_jobs"),
1154+
"{}",
1155+
statements[0]
1156+
);
1157+
assert!(
1158+
statements[0].contains("container_name"),
1159+
"{}",
1160+
statements[0]
1161+
);
1162+
}
1163+
11261164
// CMK-37361: the item expression must branch on d.cdb, otherwise a non-CDB
11271165
// (con_id 0) gets the item `TESTDB.TESTDB` instead of `TESTDB`.
11281166
#[test]

packages/mk-oracle/src/ora_sql/section.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,66 @@ oracle:
10481048
assert!(!pats[0].is_match("PDB4"));
10491049
}
10501050

1051+
/// The factory statements of the builtin section `name`. Search dirs are
1052+
/// empty on purpose, so no stray on-disk SQL file can shadow them.
1053+
fn builtin_queries(name: &str, version: u32, tenant: Tenant) -> Vec<String> {
1054+
Section::new(
1055+
&section::SectionBuilder::new(name).build(),
1056+
0,
1057+
&Options::default(),
1058+
)
1059+
.find_queries_with_search_dirs(InstanceNumVersion::from(version), tenant, &[], &[])
1060+
.unwrap_or_else(|| panic!("builtin section '{name}' must resolve"))
1061+
.iter()
1062+
.map(|q| q.as_str().to_owned())
1063+
.collect()
1064+
}
1065+
1066+
// CMK-37363: covers the whole hop section name -> Id -> get_factory_query.
1067+
#[test]
1068+
fn test_jobs_section_query_selected_per_tenant() {
1069+
let no_cdb = builtin_queries(names::JOBS, 19_01_00_00, Tenant::NoCdb);
1070+
assert_eq!(no_cdb.len(), 1);
1071+
assert!(no_cdb[0].contains("dba_scheduler_jobs"), "{}", no_cdb[0]);
1072+
assert!(!no_cdb[0].contains("v$containers"), "{}", no_cdb[0]);
1073+
1074+
let cdb = builtin_queries(names::JOBS, 19_01_00_00, Tenant::Cdb);
1075+
assert_eq!(cdb.len(), 1);
1076+
assert!(cdb[0].contains("cdb_scheduler_jobs"), "{}", cdb[0]);
1077+
assert!(cdb[0].contains("v$containers"), "{}", cdb[0]);
1078+
}
1079+
1080+
// Legacy keys the item on the database name by default
1081+
// (agents/plugins/mk_oracle), and both arms must agree with it.
1082+
#[test]
1083+
fn test_jobs_both_tenants_key_the_item_on_the_db_name_by_default() {
1084+
for tenant in [Tenant::Cdb, Tenant::NoCdb] {
1085+
let sql = builtin_queries(names::JOBS, 19_01_00_00, tenant).join("\n");
1086+
// Flattened, so neither reindentation nor a CRLF checkout matters.
1087+
let flat = sql.split_whitespace().collect::<Vec<_>>().join(" ");
1088+
assert!(
1089+
flat.contains("DECODE(NVL(:IGNORE_DB_NAME, 0), 0,"),
1090+
"{tenant:?} arm does not map IGNORE_DB_NAME=0 to the DB name: {sql}"
1091+
);
1092+
}
1093+
}
1094+
1095+
// Without this the check goes permanently CRIT where legacy warned.
1096+
#[test]
1097+
fn test_jobs_both_tenants_blank_a_restarted_log_on_errors_job() {
1098+
for tenant in [Tenant::Cdb, Tenant::NoCdb] {
1099+
let sql = builtin_queries(names::JOBS, 19_01_00_00, tenant).join("\n");
1100+
assert!(
1101+
sql.contains("SCHED$_LOG_ON_ERRORS_CLASS"),
1102+
"{tenant:?} arm does not special-case the log-on-errors class: {sql}"
1103+
);
1104+
assert!(
1105+
sql.contains("j.last_start_date > jd.actual_start_date"),
1106+
"{tenant:?} arm does not compare the restart against the recorded run: {sql}"
1107+
);
1108+
}
1109+
}
1110+
10511111
fn split_trimmed(sql: &str) -> Vec<&str> {
10521112
split_sql_statements(sql)
10531113
.into_iter()

packages/mk-oracle/src/ora_sql/sqls.rs

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ pub mod query {
8888
}];
8989
pub const JOBS_META: &[RawMetadata] = &[
9090
RawMetadata {
91-
sql: include_str!("../../sqls/jobs.12010000.all.sql"),
91+
sql: include_str!("../../sqls/jobs.12010000.cdb.sql"),
9292
min_version: 12010000,
93-
tenant: Tenant::All,
93+
tenant: Tenant::Cdb,
9494
},
9595
RawMetadata {
9696
sql: include_str!("../../sqls/jobs.10020000.all.sql"),
@@ -502,6 +502,97 @@ mod tests {
502502
assert!(query_nothing.is_err());
503503
assert_ne!(query_old, query_new);
504504
assert_eq!(query_last, query_new);
505+
// A non-CDB must never get the 11-field CDB query, even at version `None`.
506+
assert_eq!(find_helper(id, 0, Tenant::NoCdb).unwrap(), query_old);
507+
// A pre-12.1 CDB must still resolve.
508+
assert_eq!(find_helper(id, 11020000, Tenant::Cdb).unwrap(), query_old);
509+
}
510+
511+
/// The only section whose non-CDB shape differs in arity.
512+
#[test]
513+
fn test_find_jobs_tenant_split() {
514+
let cdb = find_helper(Id::Jobs, 19010000, Tenant::Cdb).unwrap();
515+
let no_cdb = find_helper(Id::Jobs, 19010000, Tenant::NoCdb).unwrap();
516+
517+
assert_ne!(cdb, no_cdb);
518+
assert!(cdb.contains("cdb_scheduler_jobs"), "{cdb}");
519+
assert!(no_cdb.contains("dba_scheduler_jobs"), "{no_cdb}");
520+
assert!(!no_cdb.contains("container_name"), "{no_cdb}");
521+
assert!(!no_cdb.contains("v$containers"), "{no_cdb}");
522+
}
523+
524+
/// A tenant tag must never make a whole section vanish.
525+
#[test]
526+
fn test_every_section_resolves_for_both_tenants() {
527+
const ALL_IDS: &[Id] = &[
528+
Id::IoStats,
529+
Id::TsQuotas,
530+
Id::Jobs,
531+
Id::Resumable,
532+
Id::UndoStat,
533+
Id::RecoveryArea,
534+
Id::AsmDiskGroup,
535+
Id::Locks,
536+
Id::LogSwitches,
537+
Id::LongActiveSessions,
538+
Id::Processes,
539+
Id::RecoveryStatus,
540+
Id::Rman,
541+
Id::Sessions,
542+
Id::SystemParameter,
543+
Id::TableSpaces,
544+
Id::DataGuardStats,
545+
Id::Instance,
546+
Id::AsmInstance,
547+
Id::Performance,
548+
];
549+
assert_eq!(ALL_IDS.len(), QUERY_MAP.len(), "new Id not covered here");
550+
for id in ALL_IDS {
551+
for version in [12_01_00_00u32, 19_01_00_00, 21_03_00_00, 23_08_00_25] {
552+
for tenant in [Tenant::Cdb, Tenant::NoCdb] {
553+
assert!(
554+
find_helper(*id, version, tenant).is_ok(),
555+
"{id:?} does not resolve at {version} for {tenant:?}"
556+
);
557+
}
558+
}
559+
}
560+
}
561+
562+
/// Tripwire for the shadowing rule in `build_query_metadata`. No section
563+
/// declares two entries at one min_version today, so the body does not run.
564+
#[test]
565+
fn test_no_all_entry_shares_min_version_with_a_tenant_entry() {
566+
for (id, metas) in QUERY_MAP.iter() {
567+
for (a, b) in metas
568+
.iter()
569+
.enumerate()
570+
.flat_map(|(i, a)| metas[i + 1..].iter().map(move |b| (a, b)))
571+
{
572+
if a.min_version != b.min_version {
573+
continue;
574+
}
575+
assert!(
576+
a.tenant != Tenant::All && b.tenant != Tenant::All && a.tenant != b.tenant,
577+
"{id:?} has ambiguous entries at min_version {}: {:?} and {:?}",
578+
a.min_version,
579+
a.tenant,
580+
b.tenant
581+
);
582+
}
583+
}
584+
}
585+
586+
/// ASM instances resolve as `NoCdb`, so these must resolve for both tenants.
587+
#[test]
588+
fn test_asm_reachable_sections_are_tenant_agnostic() {
589+
for id in [Id::AsmDiskGroup, Id::AsmInstance, Id::Processes] {
590+
assert_eq!(
591+
find_helper(id, 19010000, Tenant::NoCdb).unwrap(),
592+
find_helper(id, 19010000, Tenant::Cdb).unwrap(),
593+
"{id:?} must not depend on the tenant"
594+
);
595+
}
505596
}
506597
#[test]
507598
fn test_find_resumable() {

packages/mk-oracle/tests/test_ora_no_db.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,11 +958,14 @@ mod custom_path_tests {
958958

959959
/// First resolved query body for `section` at `version`, using `search_dirs`
960960
/// as the relative-`path:` search roots (irrelevant for absolute paths).
961+
///
962+
/// `Cdb`, not `All`: an `All` argument matches only `All`-tagged entries and
963+
/// would bypass the tenant-specific jobs query.
961964
fn first_query(section: &Section, version: u32, search_dirs: &[PathBuf]) -> Option<String> {
962965
section
963966
.find_queries_with_search_dirs(
964967
InstanceNumVersion::from(version),
965-
Tenant::All,
968+
Tenant::Cdb,
966969
&[],
967970
search_dirs,
968971
)

0 commit comments

Comments
 (0)