diff --git a/autonomous-ai-agents/data_quality_agent/README.md b/autonomous-ai-agents/data_quality_agent/README.md new file mode 100644 index 0000000..6f0b8a5 --- /dev/null +++ b/autonomous-ai-agents/data_quality_agent/README.md @@ -0,0 +1,190 @@ +# Select AI - Data Quality Check Agent for Oracle Autonomous Database + +## Release Metadata + +- Release Version: `1.1` +- Release Date: `19-May-2026` + +## Overview + +The **Data Quality Check Agent** provides schema-aware data quality assessment for Oracle Autonomous Database tables using Select AI Agent tools. + +It supports: + +- Table profiling +- Null/duplicate/outlier detection +- Quality score computation with history tracking +- Drift detection based on recent vs baseline score windows +- Issue listing with severity and remediation guidance +- Safe remediation preview and controlled apply mode +- OML Services monitoring setup and run trigger hooks + +For definitions of **Tool**, **Task**, **Agent**, and **Agent Team**, see the top-level guide: [README](../README.md#simple-agent-execution-flow). + +--- + +## Repository Contents + +```text +. +├── database_quality_check_tools.sql +│ ├── Installer bootstrap and grants +│ ├── DATABASE_QUALITY package (core DQ logic) +│ ├── SELECT_AI_DATA_QUALITY_AGENT package (tool wrappers) +│ └── Tool registration +│ +├── database_quality_check_agent.sql +│ ├── Task definition (DATA_QUALITY_TASKS) +│ ├── Agent creation (DATA_QUALITY_ADVISOR) +│ ├── Team creation (DATA_QUALITY_TEAM) +│ └── Default target schema behavior (DQ_TARGET_SCHEMA) +│ +└── README.md +``` + +--- + +## Architecture Overview + +```text +User Request + ↓ +DATA_QUALITY_TASKS + ↓ +DATA_QUALITY_ADVISOR Reasoning + ├── PROFILE_TABLE_TOOL + ├── DETECT_NULLS_TOOL + ├── DETECT_DUPLICATES_TOOL + ├── DETECT_OUTLIERS_TOOL + ├── DETECT_DRIFT_TOOL + ├── GENERATE_QUALITY_RULES_TOOL + ├── EVALUATE_QUALITY_SCORE_TOOL + ├── LIST_QUALITY_ISSUES_TOOL + ├── SUGGEST_REMEDIATION_TOOL + ├── APPLY_REMEDIATION_TOOL + ├── SETUP_OML_DATA_MONITORING_TOOL + └── RUN_OML_DATA_MONITORING_TOOL + ↓ +Issue Summary + Severity + Quality Score + Next Action +``` + +--- + +## Prerequisites + +- Oracle Autonomous AI Database (26ai recommended) +- Select AI and `DBMS_CLOUD_AI_AGENT` enabled +- `ADMIN` (or equivalent privileged user) for installation +- A valid AI profile (`DBMS_CLOUD_AI.CREATE_PROFILE`) +- Object privileges from install schema to target data schema tables (for cross-schema checks) + +For OML monitoring tools: + +- `SELECTAI_AGENT_CONFIG` entries for `AGENT='DATA_QUALITY'`: + - `OML_MONITORING_ENDPOINT` + - `OML_MONITORING_CREDENTIAL` + +For controlled remediation apply: + +- `SELECTAI_AGENT_CONFIG` entry for `AGENT='DATA_QUALITY'`: + - `REMEDIATION_APPROVAL_CODE` + +--- + +## Installation + +Run as `ADMIN` (or privileged user) from this folder: + +```sql +sqlplus admin@ @database_quality_check_tools.sql +sqlplus admin@ @database_quality_check_agent.sql +``` + +Prompts in tools script: + +- `SCHEMA_NAME` (schema where package/tools are installed) + +Prompts in agent script: + +- `SCHEMA_NAME` (same install schema) +- `AI_PROFILE_NAME` +- `DQ_TARGET_SCHEMA` (default schema for DQ checks; if blank uses `SCHEMA_NAME`) + +Important: + +- Re-run `database_quality_check_agent.sql` whenever task instructions are changed. + +--- + +## Internal Tables + +Created in install schema (if missing): + +- `DQ_RUN_HISTORY$`: + - score history per table/run + - stores score component metrics and worst severity +- `DQ_FINDINGS$`: + - issue registry with severity, recommendation, and optional fix SQL +- `DQ_OML_MONITORS$`: + - registered OML monitor metadata and last run response + +--- + +## Tool-to-Function Mapping + +| Tool | Function | Purpose | +|---|---|---| +| `PROFILE_TABLE_TOOL` | `select_ai_data_quality_agent.profile_table` | Baseline profile | +| `DETECT_NULLS_TOOL` | `select_ai_data_quality_agent.detect_nulls` | Null issue detection | +| `DETECT_DUPLICATES_TOOL` | `select_ai_data_quality_agent.detect_duplicates` | Duplicate detection | +| `DETECT_OUTLIERS_TOOL` | `select_ai_data_quality_agent.detect_outliers` | Outlier detection | +| `DETECT_DRIFT_TOOL` | `select_ai_data_quality_agent.detect_drift` | Drift analysis | +| `GENERATE_QUALITY_RULES_TOOL` | `select_ai_data_quality_agent.generate_quality_rules` | Rule suggestions | +| `EVALUATE_QUALITY_SCORE_TOOL` | `select_ai_data_quality_agent.evaluate_quality_score` | Score + persistence | +| `LIST_QUALITY_ISSUES_TOOL` | `select_ai_data_quality_agent.list_quality_issues` | Issue review | +| `SUGGEST_REMEDIATION_TOOL` | `select_ai_data_quality_agent.suggest_remediation` | SQL guidance | +| `APPLY_REMEDIATION_TOOL` | `select_ai_data_quality_agent.apply_remediation` | Preview/apply fix SQL | +| `SETUP_OML_DATA_MONITORING_TOOL` | `select_ai_data_quality_agent.setup_oml_data_monitoring` | Register OML monitor | +| `RUN_OML_DATA_MONITORING_TOOL` | `select_ai_data_quality_agent.run_oml_data_monitoring` | Trigger OML monitor run | + +--- + +## Operational Behavior + +- If `owner_name` is omitted, agent defaults to `DQ_TARGET_SCHEMA`. +- For schema-wide requests (for example, “all tables”), task instruction is configured to auto-discover tables and not ask user to list table names. +- `APPLY_REMEDIATION_TOOL`: + - default mode is `PREVIEW` + - `APPLY` requires matching `approval_code` + - SQL safety checks block unsafe statements + +--- + +## Example Prompts + +- `Check null issues in SALES and show columns with null_count, null_rate_pct, and severity.` +- `Detect duplicates in SALES using all columns and show duplicate_row_count, duplicate_rate_pct, and severity.` +- `Find numeric outliers in SALES using z-score threshold 3 and rank by severity.` +- `Evaluate quality score for SALES and explain null, duplicate, outlier, and drift components.` +- `Evaluate quality score for every table in the default target schema and return table-wise summary.` +- `List open HIGH severity quality issues for SALES with recommendation and generated_fix_sql.` +- `Preview remediation for issue_id 1 on SALES.` +- `Apply remediation for issue_id 1 on SALES with execute_mode APPLY and approval_code .` + +OML examples: + +- `Set up OML data monitoring for SH.SALES with monitor name SH_SALES_DQ_MON, baseline query "", and new-data query "".` +- `Run OML data monitoring for monitor SH_SALES_DQ_MON and return the job response.` + +--- + +## Troubleshooting + +- `ORA-00942` during package compilation: + - Re-run `database_quality_check_tools.sql`; it pre-creates `DQ_*` tables. +- Agent asks for table list during “all tables” request: + - Re-run `database_quality_check_agent.sql` to recreate task with latest instructions. +- OML monitoring tool errors with missing config: + - Insert required keys in `SELECTAI_AGENT_CONFIG` for `AGENT='DATA_QUALITY'`. +- Apply remediation blocked: + - Ensure `REMEDIATION_APPROVAL_CODE` is configured and supplied as `approval_code`. diff --git a/autonomous-ai-agents/data_quality_agent/database_quality_check_agent.sql b/autonomous-ai-agents/data_quality_agent/database_quality_check_agent.sql new file mode 100644 index 0000000..8e589a4 --- /dev/null +++ b/autonomous-ai-agents/data_quality_agent/database_quality_check_agent.sql @@ -0,0 +1,192 @@ +rem ============================================================================ +rem LICENSE +rem Copyright (c) 2026 Oracle and/or its affiliates. +rem Licensed under the Universal Permissive License (UPL), Version 1.0 +rem https://oss.oracle.com/licenses/upl/ +rem +rem NAME +rem database_quality_check_agent.sql +rem +rem DESCRIPTION +rem Installer and configuration script for Data Quality Check AI Agent Team. +rem +rem RELEASE VERSION +rem 1.0 +rem +rem RELEASE DATE +rem 18-May-2026 +rem ============================================================================ + +SET SERVEROUTPUT ON +SET VERIFY OFF + +PROMPT ====================================================== +PROMPT Data Quality Check AI Agent Installer +PROMPT ====================================================== + +VAR v_schema VARCHAR2(128) +EXEC :v_schema := '&SCHEMA_NAME'; + +VAR v_ai_profile_name VARCHAR2(128) +EXEC :v_ai_profile_name := '&AI_PROFILE_NAME'; + +PROMPT +PROMPT DQ_TARGET_SCHEMA: +PROMPT Schema to inspect by default for data quality checks. +PROMPT If blank, SCHEMA_NAME is used as default. +PROMPT + +VAR v_dq_target_schema VARCHAR2(128) +EXEC :v_dq_target_schema := '&DQ_TARGET_SCHEMA'; + +DECLARE + l_sql VARCHAR2(500); + l_schema VARCHAR2(128); + l_session_user VARCHAR2(128); +BEGIN + l_schema := DBMS_ASSERT.SIMPLE_SQL_NAME(:v_schema); + l_session_user := SYS_CONTEXT('USERENV', 'SESSION_USER'); + + IF UPPER(l_schema) <> UPPER(l_session_user) THEN + l_sql := 'GRANT EXECUTE ON DBMS_CLOUD_AI_AGENT TO ' || l_schema; + EXECUTE IMMEDIATE l_sql; + + l_sql := 'GRANT EXECUTE ON DBMS_CLOUD_AI TO ' || l_schema; + EXECUTE IMMEDIATE l_sql; + + l_sql := 'GRANT EXECUTE ON DBMS_CLOUD TO ' || l_schema; + EXECUTE IMMEDIATE l_sql; + ELSE + DBMS_OUTPUT.PUT_LINE('Skipping grants for schema ' || l_schema || + ' (same as session user).'); + END IF; + + DBMS_OUTPUT.PUT_LINE('Grants completed.'); +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'ALTER SESSION SET CURRENT_SCHEMA = ' || :v_schema; +END; +/ + +CREATE OR REPLACE PROCEDURE install_data_quality_check_agent( + p_install_schema IN VARCHAR2, + p_profile_name IN VARCHAR2, + p_dq_target_schema IN VARCHAR2 +) +AUTHID DEFINER +AS + l_target_schema VARCHAR2(128); +BEGIN + l_target_schema := UPPER(TRIM(NVL(p_dq_target_schema, p_install_schema))); + + DBMS_OUTPUT.PUT_LINE('--------------------------------------------'); + DBMS_OUTPUT.PUT_LINE('Starting Data Quality Check AI installation'); + DBMS_OUTPUT.PUT_LINE('--------------------------------------------'); + + BEGIN + DBMS_CLOUD_AI_AGENT.DROP_TASK('DATA_QUALITY_TASKS'); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + + DBMS_CLOUD_AI_AGENT.CREATE_TASK( + task_name => 'DATA_QUALITY_TASKS', + description => 'Task for data quality profiling, scoring, and remediation planning', + attributes => '{ + "instruction": "You are a Data Quality specialist for Oracle Autonomous Database. ' + || 'Default target schema for data quality checks is ' || l_target_schema || '. ' + || 'If the user does not provide owner_name, use owner_name=' || l_target_schema || '. ' + || 'If the user provides a different schema explicitly, use that schema. ' + || 'Cross-schema analysis is allowed when object privileges are granted to the install schema. ' + || 'When the user asks for all tables or schema-wide analysis, automatically discover table names from the target schema and run checks without asking the user to provide table lists. ' + || 'If no owner_name is provided in schema-wide requests, use owner_name=' || l_target_schema || '. ' + || 'Do not ask the user for table names when this can be derived from ALL_TABLES/USER_TABLES metadata. ' + || 'Use PROFILE_TABLE_TOOL first to establish table baseline when user provides owner/table. ' + || 'Use DETECT_NULLS_TOOL, DETECT_DUPLICATES_TOOL, and DETECT_OUTLIERS_TOOL to identify quality issues with severity. ' + || 'Use GENERATE_QUALITY_RULES_TOOL to propose enforceable quality rules. ' + || 'Use EVALUATE_QUALITY_SCORE_TOOL to compute/store overall quality score and history point. ' + || 'Use DETECT_DRIFT_TOOL to identify recent score drift against baseline history. ' + || 'Use SETUP_OML_DATA_MONITORING_TOOL for automated OML Services data monitoring setup when requested. ' + || 'Use RUN_OML_DATA_MONITORING_TOOL to trigger OML monitoring jobs and report run response. ' + || 'Use LIST_QUALITY_ISSUES_TOOL for issue review. ' + || 'Use SUGGEST_REMEDIATION_TOOL to produce practical SQL-based fixes. ' + || 'Only use APPLY_REMEDIATION_TOOL in PREVIEW mode unless the user explicitly asks to apply changes and provides approval_code. ' + || 'Always return: issue summary, severity, quality score, and next remediation step. ' + || 'User request: {query}", + "tools": [ + "PROFILE_TABLE_TOOL", + "DETECT_NULLS_TOOL", + "DETECT_DUPLICATES_TOOL", + "DETECT_OUTLIERS_TOOL", + "DETECT_DRIFT_TOOL", + "SETUP_OML_DATA_MONITORING_TOOL", + "RUN_OML_DATA_MONITORING_TOOL", + "GENERATE_QUALITY_RULES_TOOL", + "EVALUATE_QUALITY_SCORE_TOOL", + "LIST_QUALITY_ISSUES_TOOL", + "SUGGEST_REMEDIATION_TOOL", + "APPLY_REMEDIATION_TOOL" + ], + "enable_human_tool": "true" + }' + ); + DBMS_OUTPUT.PUT_LINE('Created task DATA_QUALITY_TASKS'); + + BEGIN + DBMS_CLOUD_AI_AGENT.DROP_AGENT('DATA_QUALITY_ADVISOR'); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + + DBMS_CLOUD_AI_AGENT.CREATE_AGENT( + agent_name => 'DATA_QUALITY_ADVISOR', + attributes => + '{' || + '"profile_name":"' || p_profile_name || '",' || + '"role":"You are a Data Quality Advisor. You profile data, detect anomalies and drift, compute quality scores, and recommend safe remediation steps for Oracle Autonomous Database tables."' || + '}', + description => 'AI agent for Oracle Autonomous Database data quality monitoring and remediation guidance' + ); + DBMS_OUTPUT.PUT_LINE('Created agent DATA_QUALITY_ADVISOR'); + + BEGIN + DBMS_CLOUD_AI_AGENT.DROP_TEAM('DATA_QUALITY_TEAM'); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + + DBMS_CLOUD_AI_AGENT.CREATE_TEAM( + team_name => 'DATA_QUALITY_TEAM', + attributes => '{ + "agents":[{"name":"DATA_QUALITY_ADVISOR","task":"DATA_QUALITY_TASKS"}], + "process":"sequential" + }' + ); + + DBMS_OUTPUT.PUT_LINE('Created team DATA_QUALITY_TEAM'); + DBMS_OUTPUT.PUT_LINE('--------------------------------------------'); + DBMS_OUTPUT.PUT_LINE('Data Quality Check AI installation COMPLETE'); + DBMS_OUTPUT.PUT_LINE('--------------------------------------------'); +END install_data_quality_check_agent; +/ + +PROMPT Executing installer procedure ... +BEGIN + install_data_quality_check_agent( + p_install_schema => :v_schema, + p_profile_name => :v_ai_profile_name, + p_dq_target_schema => :v_dq_target_schema + ); +END; +/ + +ALTER SESSION SET CURRENT_SCHEMA = ADMIN; + +PROMPT ====================================================== +PROMPT Installation finished successfully +PROMPT ====================================================== diff --git a/autonomous-ai-agents/data_quality_agent/database_quality_check_tools.sql b/autonomous-ai-agents/data_quality_agent/database_quality_check_tools.sql new file mode 100644 index 0000000..ba37ee0 --- /dev/null +++ b/autonomous-ai-agents/data_quality_agent/database_quality_check_tools.sql @@ -0,0 +1,1400 @@ +rem ============================================================================ +rem LICENSE +rem Copyright (c) 2026 Oracle and/or its affiliates. +rem Licensed under the Universal Permissive License (UPL), Version 1.0 +rem https://oss.oracle.com/licenses/upl/ +rem +rem NAME +rem database_quality_check_tools.sql +rem +rem DESCRIPTION +rem Installer script for Data Quality Select AI tools. +rem +rem RELEASE VERSION +rem 1.0 +rem +rem RELEASE DATE +rem 18-May-2026 +rem ============================================================================ + +SET SERVEROUTPUT ON +SET VERIFY OFF + +VAR v_schema VARCHAR2(128) +EXEC :v_schema := '&SCHEMA_NAME'; + +CREATE OR REPLACE PROCEDURE initialize_data_quality_agent( + p_install_schema_name IN VARCHAR2 +) +IS + l_schema_name VARCHAR2(128); + + TYPE priv_list_t IS VARRAY(20) OF VARCHAR2(4000); + l_priv_list CONSTANT priv_list_t := priv_list_t( + 'DBMS_CLOUD_AI_AGENT', + 'DBMS_CLOUD_AI', + 'DBMS_CLOUD' + ); + + PROCEDURE execute_grants(p_schema IN VARCHAR2, p_objects IN priv_list_t) IS + l_session_user VARCHAR2(128); + BEGIN + l_session_user := SYS_CONTEXT('USERENV', 'SESSION_USER'); + + IF UPPER(p_schema) = UPPER(l_session_user) THEN + DBMS_OUTPUT.PUT_LINE('Skipping grants for schema ' || p_schema || + ' (same as session user).'); + RETURN; + END IF; + + FOR i IN 1 .. p_objects.COUNT LOOP + BEGIN + EXECUTE IMMEDIATE 'GRANT EXECUTE ON ' || p_objects(i) || ' TO ' || p_schema; + EXCEPTION + WHEN OTHERS THEN + DBMS_OUTPUT.PUT_LINE('Warning: failed to grant ' || p_objects(i) || + ' to ' || p_schema || ' - ' || SQLERRM); + END; + END LOOP; + END execute_grants; +BEGIN + l_schema_name := DBMS_ASSERT.SIMPLE_SQL_NAME(p_install_schema_name); + + execute_grants(l_schema_name, l_priv_list); + + BEGIN + EXECUTE IMMEDIATE + 'CREATE TABLE ' || l_schema_name || '.SELECTAI_AGENT_CONFIG ( + "ID" NUMBER GENERATED BY DEFAULT AS IDENTITY, + "KEY" VARCHAR2(200) NOT NULL, + "VALUE" CLOB, + "AGENT" VARCHAR2(128) NOT NULL, + CONSTRAINT SELECTAI_AGENT_CONFIG_PK PRIMARY KEY ("ID"), + CONSTRAINT SELECTAI_AGENT_CONFIG_UK UNIQUE ("KEY","AGENT") + )'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + + DBMS_OUTPUT.PUT_LINE('initialize_data_quality_agent completed for schema ' || + l_schema_name); +END initialize_data_quality_agent; +/ + +BEGIN + initialize_data_quality_agent(:v_schema); +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'ALTER SESSION SET CURRENT_SCHEMA = ' || :v_schema; +END; +/ + +BEGIN + BEGIN + EXECUTE IMMEDIATE q'[ + CREATE TABLE DQ_RUN_HISTORY$( + run_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + owner_name VARCHAR2(128) NOT NULL, + table_name VARCHAR2(128) NOT NULL, + run_ts TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, + quality_score NUMBER, + null_rate_pct NUMBER, + duplicate_rate_pct NUMBER, + outlier_rate_pct NUMBER, + drift_score_pct NUMBER, + worst_severity VARCHAR2(20), + details CLOB + ) + ]'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + + BEGIN + EXECUTE IMMEDIATE q'[ + CREATE TABLE DQ_FINDINGS$( + issue_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + run_id NUMBER, + owner_name VARCHAR2(128) NOT NULL, + table_name VARCHAR2(128) NOT NULL, + issue_type VARCHAR2(64) NOT NULL, + column_name VARCHAR2(128), + severity VARCHAR2(20) NOT NULL, + issue_value NUMBER, + threshold_value NUMBER, + recommendation CLOB, + generated_fix_sql CLOB, + status VARCHAR2(30) DEFAULT 'OPEN', + created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT dq_findings_run_fk FOREIGN KEY (run_id) + REFERENCES DQ_RUN_HISTORY$(run_id) + ) + ]'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + + BEGIN + EXECUTE IMMEDIATE q'[ + CREATE TABLE DQ_OML_MONITORS$( + monitor_name VARCHAR2(256) PRIMARY KEY, + target_schema VARCHAR2(128) NOT NULL, + target_table VARCHAR2(128) NOT NULL, + baseline_expr CLOB NOT NULL, + newdata_expr CLOB NOT NULL, + oml_job_id VARCHAR2(256), + status VARCHAR2(30) DEFAULT 'REGISTERED', + last_run_ts TIMESTAMP, + last_run_response CLOB, + created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL + ) + ]'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; +END; +/ + +CREATE OR REPLACE PACKAGE database_quality AUTHID CURRENT_USER AS + FUNCTION profile_table( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + sample_rows IN NUMBER DEFAULT 10000 + ) RETURN CLOB; + + FUNCTION detect_nulls( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION detect_duplicates( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_key_columns_csv IN VARCHAR2 DEFAULT NULL + ) RETURN CLOB; + + FUNCTION detect_outliers( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_z_limit IN NUMBER DEFAULT 3 + ) RETURN CLOB; + + FUNCTION detect_drift( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_baseline_days IN NUMBER DEFAULT 30, + p_current_days IN NUMBER DEFAULT 7, + p_drift_threshold IN NUMBER DEFAULT 20 + ) RETURN CLOB; + + FUNCTION generate_quality_rules( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION evaluate_quality_score( + owner_name IN VARCHAR2, + table_name IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION list_quality_issues( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_severity_level IN VARCHAR2 DEFAULT NULL + ) RETURN CLOB; + + FUNCTION suggest_remediation( + owner_name IN VARCHAR2, + table_name IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION apply_remediation( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_issue_id IN NUMBER, + p_execute_mode IN VARCHAR2 DEFAULT 'PREVIEW', + p_approval_code IN VARCHAR2 DEFAULT NULL + ) RETURN CLOB; + + FUNCTION setup_oml_data_monitoring( + p_monitor_name IN VARCHAR2, + p_target_schema IN VARCHAR2, + p_target_table IN VARCHAR2, + p_baseline_expr IN VARCHAR2, + p_newdata_expr IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION run_oml_data_monitoring( + p_monitor_name IN VARCHAR2 + ) RETURN CLOB; +END database_quality; +/ + +CREATE OR REPLACE PACKAGE BODY database_quality AS + FUNCTION quote_ident(p_name IN VARCHAR2) RETURN VARCHAR2 IS + BEGIN + RETURN DBMS_ASSERT.ENQUOTE_NAME(UPPER(TRIM(p_name)), FALSE); + END quote_ident; + + FUNCTION full_table_name(p_owner IN VARCHAR2, p_table IN VARCHAR2) RETURN VARCHAR2 IS + BEGIN + RETURN quote_ident(p_owner) || '.' || quote_ident(p_table); + END full_table_name; + + FUNCTION severity_rank(p_severity IN VARCHAR2) RETURN NUMBER IS + BEGIN + RETURN CASE UPPER(NVL(TRIM(p_severity), 'LOW')) + WHEN 'CRITICAL' THEN 4 + WHEN 'HIGH' THEN 3 + WHEN 'MEDIUM' THEN 2 + ELSE 1 + END; + END severity_rank; + + FUNCTION severity_name(p_rank IN NUMBER) RETURN VARCHAR2 IS + BEGIN + RETURN CASE + WHEN p_rank >= 4 THEN 'CRITICAL' + WHEN p_rank = 3 THEN 'HIGH' + WHEN p_rank = 2 THEN 'MEDIUM' + ELSE 'LOW' + END; + END severity_name; + + FUNCTION get_cfg_value(p_key IN VARCHAR2) RETURN CLOB IS + l_val CLOB; + BEGIN + SELECT value + INTO l_val + FROM SELECTAI_AGENT_CONFIG + WHERE UPPER(agent) = 'DATA_QUALITY' + AND UPPER(key) = UPPER(p_key); + RETURN l_val; + EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN NULL; + END get_cfg_value; + + FUNCTION is_safe_remediation_sql( + p_sql IN CLOB, + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2 + ) RETURN BOOLEAN IS + l_sql CLOB := UPPER(TRIM(p_sql)); + l_target VARCHAR2(300) := UPPER(TRIM(p_owner_name)) || '.' || UPPER(TRIM(p_table_name)); + BEGIN + IF l_sql IS NULL THEN + RETURN FALSE; + END IF; + + IF INSTR(l_sql, ';') > 0 OR INSTR(l_sql, '/*') > 0 OR INSTR(l_sql, '--') > 0 THEN + RETURN FALSE; + END IF; + + IF NOT (l_sql LIKE 'UPDATE %' OR l_sql LIKE 'DELETE %' OR l_sql LIKE 'MERGE %') THEN + RETURN FALSE; + END IF; + + IF l_sql LIKE '%DROP %' OR l_sql LIKE '%TRUNCATE %' OR l_sql LIKE '%ALTER %' OR + l_sql LIKE '%CREATE %' OR l_sql LIKE '%GRANT %' THEN + RETURN FALSE; + END IF; + + IF INSTR(l_sql, l_target) = 0 THEN + RETURN FALSE; + END IF; + + RETURN TRUE; + END is_safe_remediation_sql; + + PROCEDURE ensure_internal_tables IS + BEGIN + BEGIN + EXECUTE IMMEDIATE q'[ + CREATE TABLE DQ_RUN_HISTORY$( + run_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + owner_name VARCHAR2(128) NOT NULL, + table_name VARCHAR2(128) NOT NULL, + run_ts TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, + quality_score NUMBER, + null_rate_pct NUMBER, + duplicate_rate_pct NUMBER, + outlier_rate_pct NUMBER, + drift_score_pct NUMBER, + worst_severity VARCHAR2(20), + details CLOB + ) + ]'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + + BEGIN + EXECUTE IMMEDIATE q'[ + CREATE TABLE DQ_FINDINGS$( + issue_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + run_id NUMBER, + owner_name VARCHAR2(128) NOT NULL, + table_name VARCHAR2(128) NOT NULL, + issue_type VARCHAR2(64) NOT NULL, + column_name VARCHAR2(128), + severity VARCHAR2(20) NOT NULL, + issue_value NUMBER, + threshold_value NUMBER, + recommendation CLOB, + generated_fix_sql CLOB, + status VARCHAR2(30) DEFAULT 'OPEN', + created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT dq_findings_run_fk FOREIGN KEY (run_id) + REFERENCES DQ_RUN_HISTORY$(run_id) + ) + ]'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + + BEGIN + EXECUTE IMMEDIATE q'[ + CREATE TABLE DQ_OML_MONITORS$( + monitor_name VARCHAR2(256) PRIMARY KEY, + target_schema VARCHAR2(128) NOT NULL, + target_table VARCHAR2(128) NOT NULL, + baseline_expr CLOB NOT NULL, + newdata_expr CLOB NOT NULL, + oml_job_id VARCHAR2(256), + status VARCHAR2(30) DEFAULT 'REGISTERED', + last_run_ts TIMESTAMP, + last_run_response CLOB, + created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL + ) + ]'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + END ensure_internal_tables; + + FUNCTION profile_table( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + sample_rows IN NUMBER DEFAULT 10000 + ) RETURN CLOB IS + l_sql VARCHAR2(32767); + l_row_count NUMBER := 0; + l_col_count NUMBER := 0; + l_resp JSON_OBJECT_T := JSON_OBJECT_T(); + BEGIN + ensure_internal_tables; + + l_sql := 'SELECT COUNT(*) FROM ' || full_table_name(p_owner_name, p_table_name); + EXECUTE IMMEDIATE l_sql INTO l_row_count; + + SELECT COUNT(*) + INTO l_col_count + FROM ALL_TAB_COLUMNS + WHERE OWNER = UPPER(p_owner_name) + AND TABLE_NAME = UPPER(p_table_name); + + l_resp.put('status', 'ok'); + l_resp.put('owner_name', UPPER(p_owner_name)); + l_resp.put('table_name', UPPER(p_table_name)); + l_resp.put('sample_rows_requested', NVL(sample_rows, 10000)); + l_resp.put('row_count', l_row_count); + l_resp.put('column_count', l_col_count); + l_resp.put('message', 'Table profiling completed.'); + RETURN l_resp.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('profile_table failed: ' || SQLERRM)); + END profile_table; + + FUNCTION detect_nulls( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2 + ) RETURN CLOB IS + l_sql VARCHAR2(32767); + l_row_count NUMBER := 0; + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + l_arr JSON_ARRAY_T := JSON_ARRAY_T(); + l_col VARCHAR2(128); + l_nulls NUMBER; + l_rate NUMBER; + BEGIN + ensure_internal_tables; + + l_sql := 'SELECT COUNT(*) FROM ' || full_table_name(p_owner_name, p_table_name); + EXECUTE IMMEDIATE l_sql INTO l_row_count; + + FOR r IN ( + SELECT COLUMN_NAME + FROM ALL_TAB_COLUMNS + WHERE OWNER = UPPER(p_owner_name) + AND TABLE_NAME = UPPER(p_table_name) + ORDER BY COLUMN_ID + ) LOOP + l_col := quote_ident(r.COLUMN_NAME); + l_sql := 'SELECT COUNT(*) FROM ' || full_table_name(p_owner_name, p_table_name) || + ' WHERE ' || l_col || ' IS NULL'; + EXECUTE IMMEDIATE l_sql INTO l_nulls; + + l_rate := CASE WHEN l_row_count = 0 THEN 0 ELSE ROUND((l_nulls / l_row_count) * 100, 4) END; + + IF l_nulls > 0 THEN + l_arr.append(JSON_OBJECT( + 'column_name' VALUE r.COLUMN_NAME, + 'null_count' VALUE l_nulls, + 'null_rate_pct' VALUE l_rate, + 'severity' VALUE CASE WHEN l_rate >= 20 THEN 'HIGH' WHEN l_rate >= 5 THEN 'MEDIUM' ELSE 'LOW' END + )); + END IF; + END LOOP; + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(p_owner_name)); + l_obj.put('table_name', UPPER(p_table_name)); + l_obj.put('row_count', l_row_count); + l_obj.put('issues', l_arr); + RETURN l_obj.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('detect_nulls failed: ' || SQLERRM)); + END detect_nulls; + + FUNCTION detect_duplicates( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_key_columns_csv IN VARCHAR2 DEFAULT NULL + ) RETURN CLOB IS + l_sql VARCHAR2(32767); + l_rows NUMBER := 0; + l_distinct_rows NUMBER := 0; + l_dup_rows NUMBER := 0; + l_rate NUMBER := 0; + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + BEGIN + ensure_internal_tables; + + l_sql := 'SELECT COUNT(*) FROM ' || full_table_name(p_owner_name, p_table_name); + EXECUTE IMMEDIATE l_sql INTO l_rows; + + IF p_key_columns_csv IS NULL OR TRIM(p_key_columns_csv) IS NULL THEN + l_sql := 'SELECT COUNT(*) FROM (SELECT DISTINCT * FROM ' || + full_table_name(p_owner_name, p_table_name) || ')'; + ELSE + l_sql := 'SELECT COUNT(*) FROM (SELECT DISTINCT ' || p_key_columns_csv || + ' FROM ' || full_table_name(p_owner_name, p_table_name) || ')'; + END IF; + + EXECUTE IMMEDIATE l_sql INTO l_distinct_rows; + + l_dup_rows := GREATEST(l_rows - l_distinct_rows, 0); + l_rate := CASE WHEN l_rows = 0 THEN 0 ELSE ROUND((l_dup_rows / l_rows) * 100, 4) END; + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(p_owner_name)); + l_obj.put('table_name', UPPER(p_table_name)); + l_obj.put('row_count', l_rows); + l_obj.put('distinct_row_count', l_distinct_rows); + l_obj.put('duplicate_row_count', l_dup_rows); + l_obj.put('duplicate_rate_pct', l_rate); + l_obj.put('severity', CASE WHEN l_rate >= 10 THEN 'HIGH' WHEN l_rate >= 2 THEN 'MEDIUM' ELSE 'LOW' END); + RETURN l_obj.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('detect_duplicates failed: ' || SQLERRM)); + END detect_duplicates; + + FUNCTION detect_outliers( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_z_limit IN NUMBER DEFAULT 3 + ) RETURN CLOB IS + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + l_arr JSON_ARRAY_T := JSON_ARRAY_T(); + l_sql VARCHAR2(32767); + l_outlier_cnt NUMBER; + l_row_count NUMBER; + l_rate NUMBER; + BEGIN + ensure_internal_tables; + + l_sql := 'SELECT COUNT(*) FROM ' || full_table_name(p_owner_name, p_table_name); + EXECUTE IMMEDIATE l_sql INTO l_row_count; + + FOR c IN ( + SELECT COLUMN_NAME + FROM ALL_TAB_COLUMNS + WHERE OWNER = UPPER(p_owner_name) + AND TABLE_NAME = UPPER(p_table_name) + AND DATA_TYPE IN ('NUMBER', 'FLOAT', 'BINARY_DOUBLE', 'BINARY_FLOAT') + ORDER BY COLUMN_ID + ) LOOP + l_sql := + 'WITH s AS (' || + ' SELECT ' || quote_ident(c.COLUMN_NAME) || ' v FROM ' || full_table_name(p_owner_name, p_table_name) || + ' WHERE ' || quote_ident(c.COLUMN_NAME) || ' IS NOT NULL' || + '), m AS (' || + ' SELECT AVG(v) mu, STDDEV(v) sigma FROM s' || + ')' || + ' SELECT COUNT(*) FROM s, m' || + ' WHERE m.sigma > 0 AND ABS((s.v - m.mu) / m.sigma) > :z'; + + EXECUTE IMMEDIATE l_sql INTO l_outlier_cnt USING NVL(p_z_limit, 3); + l_rate := CASE WHEN l_row_count = 0 THEN 0 ELSE ROUND((l_outlier_cnt / l_row_count) * 100, 4) END; + + IF l_outlier_cnt > 0 THEN + l_arr.append(JSON_OBJECT( + 'column_name' VALUE c.COLUMN_NAME, + 'outlier_count' VALUE l_outlier_cnt, + 'outlier_rate_pct' VALUE l_rate, + 'severity' VALUE CASE WHEN l_rate >= 10 THEN 'HIGH' WHEN l_rate >= 3 THEN 'MEDIUM' ELSE 'LOW' END + )); + END IF; + END LOOP; + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(p_owner_name)); + l_obj.put('table_name', UPPER(p_table_name)); + l_obj.put('issues', l_arr); + RETURN l_obj.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('detect_outliers failed: ' || SQLERRM)); + END detect_outliers; + + FUNCTION detect_drift( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_baseline_days IN NUMBER DEFAULT 30, + p_current_days IN NUMBER DEFAULT 7, + p_drift_threshold IN NUMBER DEFAULT 20 + ) RETURN CLOB IS + l_hist_avg NUMBER := 0; + l_curr NUMBER := 0; + l_change NUMBER := 0; + l_sql VARCHAR2(32767); + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + BEGIN + ensure_internal_tables; + + l_sql := q'[ + SELECT NVL(AVG(quality_score), 0) + FROM DQ_RUN_HISTORY$ + WHERE owner_name = UPPER(:1) + AND table_name = UPPER(:2) + AND run_ts >= SYSTIMESTAMP - NUMTODSINTERVAL(:3, 'DAY') + AND run_ts < SYSTIMESTAMP - NUMTODSINTERVAL(:4, 'DAY') + ]'; + EXECUTE IMMEDIATE l_sql INTO l_hist_avg USING p_owner_name, p_table_name, + NVL(p_baseline_days, 30), NVL(p_current_days, 7); + + l_sql := q'[ + SELECT NVL(AVG(quality_score), 0) + FROM DQ_RUN_HISTORY$ + WHERE owner_name = UPPER(:1) + AND table_name = UPPER(:2) + AND run_ts >= SYSTIMESTAMP - NUMTODSINTERVAL(:3, 'DAY') + ]'; + EXECUTE IMMEDIATE l_sql INTO l_curr USING p_owner_name, p_table_name, + NVL(p_current_days, 7); + + IF l_hist_avg = 0 THEN + l_change := 0; + ELSE + l_change := ROUND(((l_curr - l_hist_avg) / l_hist_avg) * 100, 4); + END IF; + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(p_owner_name)); + l_obj.put('table_name', UPPER(p_table_name)); + l_obj.put('baseline_avg_score', l_hist_avg); + l_obj.put('current_avg_score', l_curr); + l_obj.put('drift_pct', l_change); + l_obj.put('threshold_pct', NVL(p_drift_threshold, 20)); + l_obj.put('is_drift_flagged', CASE WHEN ABS(l_change) >= NVL(p_drift_threshold, 20) THEN 'true' ELSE 'false' END); + RETURN l_obj.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('detect_drift failed: ' || SQLERRM)); + END detect_drift; + + FUNCTION generate_quality_rules( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2 + ) RETURN CLOB IS + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + l_rules JSON_ARRAY_T := JSON_ARRAY_T(); + BEGIN + ensure_internal_tables; + + l_rules.append(JSON_OBJECT('rule_type' VALUE 'NOT_NULL', + 'rule' VALUE 'Enforce NOT NULL for mandatory business columns after cleanup.')); + l_rules.append(JSON_OBJECT('rule_type' VALUE 'UNIQUENESS', + 'rule' VALUE 'Create unique constraints or indexes for natural/business keys.')); + l_rules.append(JSON_OBJECT('rule_type' VALUE 'RANGE_CHECK', + 'rule' VALUE 'Set acceptable min/max ranges for numeric measures.')); + l_rules.append(JSON_OBJECT('rule_type' VALUE 'REFERENCE_INTEGRITY', + 'rule' VALUE 'Apply FK constraints for parent-child relationships where applicable.')); + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(p_owner_name)); + l_obj.put('table_name', UPPER(p_table_name)); + l_obj.put('rules', l_rules); + RETURN l_obj.to_clob; + END generate_quality_rules; + + FUNCTION evaluate_quality_score( + owner_name IN VARCHAR2, + table_name IN VARCHAR2 + ) RETURN CLOB IS + l_null_rate NUMBER := 0; + l_dup_rate NUMBER := 0; + l_out_rate NUMBER := 0; + l_drift_score NUMBER := 0; + l_score NUMBER := 100; + l_sev_rank NUMBER := 1; + l_worst_sev VARCHAR2(20) := 'LOW'; + l_run_id NUMBER; + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + l_sql VARCHAR2(32767); + l_hist_avg NUMBER := 0; + l_curr_avg NUMBER := 0; + l_drift_pct NUMBER := 0; + BEGIN + ensure_internal_tables; + + BEGIN + l_sql := 'SELECT AVG(issue_value) FROM DQ_FINDINGS$ WHERE owner_name = UPPER(:1) AND table_name = UPPER(:2) AND issue_type = ''NULL_RATE'' AND status = ''OPEN'''; + EXECUTE IMMEDIATE l_sql INTO l_null_rate USING owner_name, table_name; + EXCEPTION + WHEN OTHERS THEN l_null_rate := 0; + END; + + BEGIN + l_sql := 'SELECT AVG(issue_value) FROM DQ_FINDINGS$ WHERE owner_name = UPPER(:1) AND table_name = UPPER(:2) AND issue_type = ''DUPLICATE_RATE'' AND status = ''OPEN'''; + EXECUTE IMMEDIATE l_sql INTO l_dup_rate USING owner_name, table_name; + EXCEPTION + WHEN OTHERS THEN l_dup_rate := 0; + END; + + BEGIN + l_sql := 'SELECT AVG(issue_value) FROM DQ_FINDINGS$ WHERE owner_name = UPPER(:1) AND table_name = UPPER(:2) AND issue_type = ''OUTLIER_RATE'' AND status = ''OPEN'''; + EXECUTE IMMEDIATE l_sql INTO l_out_rate USING owner_name, table_name; + EXCEPTION + WHEN OTHERS THEN l_out_rate := 0; + END; + + BEGIN + l_sql := 'SELECT MAX(severity) KEEP (DENSE_RANK LAST ORDER BY CASE severity WHEN ''LOW'' THEN 1 WHEN ''MEDIUM'' THEN 2 WHEN ''HIGH'' THEN 3 ELSE 4 END) FROM DQ_FINDINGS$ WHERE owner_name = UPPER(:1) AND table_name = UPPER(:2) AND status = ''OPEN'''; + EXECUTE IMMEDIATE l_sql INTO l_worst_sev USING owner_name, table_name; + EXCEPTION + WHEN OTHERS THEN l_worst_sev := 'LOW'; + END; + + l_null_rate := NVL(l_null_rate, 0); + l_dup_rate := NVL(l_dup_rate, 0); + l_out_rate := NVL(l_out_rate, 0); + BEGIN + l_sql := q'[ + SELECT NVL(AVG(quality_score), 0) + FROM DQ_RUN_HISTORY$ + WHERE owner_name = UPPER(:1) + AND table_name = UPPER(:2) + AND run_ts >= SYSTIMESTAMP - NUMTODSINTERVAL(30, 'DAY') + AND run_ts < SYSTIMESTAMP - NUMTODSINTERVAL(7, 'DAY') + ]'; + EXECUTE IMMEDIATE l_sql INTO l_hist_avg USING owner_name, table_name; + + l_sql := q'[ + SELECT NVL(AVG(quality_score), 0) + FROM DQ_RUN_HISTORY$ + WHERE owner_name = UPPER(:1) + AND table_name = UPPER(:2) + AND run_ts >= SYSTIMESTAMP - NUMTODSINTERVAL(7, 'DAY') + ]'; + EXECUTE IMMEDIATE l_sql INTO l_curr_avg USING owner_name, table_name; + + IF l_hist_avg > 0 THEN + l_drift_pct := ABS(ROUND(((l_curr_avg - l_hist_avg) / l_hist_avg) * 100, 4)); + ELSE + l_drift_pct := 0; + END IF; + EXCEPTION + WHEN OTHERS THEN + l_drift_pct := 0; + END; + l_drift_score := LEAST(l_drift_pct, 100); + + l_score := GREATEST(0, ROUND(100 - (l_null_rate * 0.35) - (l_dup_rate * 0.35) - (l_out_rate * 0.20) - (l_drift_score * 0.10), 2)); + + l_sev_rank := severity_rank(l_worst_sev); + l_worst_sev := severity_name(l_sev_rank); + + INSERT INTO DQ_RUN_HISTORY$( + owner_name, + table_name, + run_ts, + quality_score, + null_rate_pct, + duplicate_rate_pct, + outlier_rate_pct, + drift_score_pct, + worst_severity, + details + ) VALUES ( + UPPER(owner_name), + UPPER(table_name), + SYSTIMESTAMP, + l_score, + l_null_rate, + l_dup_rate, + l_out_rate, + l_drift_score, + l_worst_sev, + JSON_OBJECT('source' VALUE 'evaluate_quality_score v1') + ) RETURNING run_id INTO l_run_id; + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(owner_name)); + l_obj.put('table_name', UPPER(table_name)); + l_obj.put('run_id', l_run_id); + l_obj.put('quality_score', l_score); + l_obj.put('worst_severity', l_worst_sev); + l_obj.put('components', JSON_OBJECT( + 'null_rate_pct' VALUE l_null_rate, + 'duplicate_rate_pct' VALUE l_dup_rate, + 'outlier_rate_pct' VALUE l_out_rate, + 'drift_score_pct' VALUE l_drift_score + )); + RETURN l_obj.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('evaluate_quality_score failed: ' || SQLERRM)); + END evaluate_quality_score; + + FUNCTION list_quality_issues( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_severity_level IN VARCHAR2 DEFAULT NULL + ) RETURN CLOB IS + l_arr JSON_ARRAY_T := JSON_ARRAY_T(); + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + BEGIN + ensure_internal_tables; + + FOR r IN ( + SELECT issue_id, + issue_type, + column_name, + severity, + issue_value, + threshold_value, + recommendation, + generated_fix_sql, + status, + created_at + FROM DQ_FINDINGS$ + WHERE owner_name = UPPER(p_owner_name) + AND table_name = UPPER(p_table_name) + AND (p_severity_level IS NULL OR UPPER(severity) = UPPER(p_severity_level)) + ORDER BY created_at DESC + FETCH FIRST 100 ROWS ONLY + ) LOOP + l_arr.append(JSON_OBJECT( + 'issue_id' VALUE r.issue_id, + 'issue_type' VALUE r.issue_type, + 'column_name' VALUE r.column_name, + 'severity' VALUE r.severity, + 'issue_value' VALUE r.issue_value, + 'threshold_value' VALUE r.threshold_value, + 'recommendation' VALUE r.recommendation, + 'generated_fix_sql' VALUE r.generated_fix_sql, + 'status' VALUE r.status, + 'created_at' VALUE TO_CHAR(r.created_at, 'YYYY-MM-DD"T"HH24:MI:SS') + )); + END LOOP; + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(p_owner_name)); + l_obj.put('table_name', UPPER(p_table_name)); + l_obj.put('issues', l_arr); + RETURN l_obj.to_clob; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('list_quality_issues failed: ' || SQLERRM)); + END list_quality_issues; + + FUNCTION suggest_remediation( + owner_name IN VARCHAR2, + table_name IN VARCHAR2 + ) RETURN CLOB IS + l_arr JSON_ARRAY_T := JSON_ARRAY_T(); + l_obj JSON_OBJECT_T := JSON_OBJECT_T(); + BEGIN + ensure_internal_tables; + + l_arr.append(JSON_OBJECT( + 'issue_type' VALUE 'NULL_RATE', + 'action' VALUE 'Backfill defaults/business-safe values for null-heavy columns.', + 'sql_template' VALUE ('UPDATE ' || UPPER(owner_name) || '.' || UPPER(table_name) || + ' SET = WHERE IS NULL') + )); + + l_arr.append(JSON_OBJECT( + 'issue_type' VALUE 'DUPLICATE_RATE', + 'action' VALUE 'Deduplicate using row_number over key columns and keep latest record.', + 'sql_template' VALUE ('DELETE FROM ' || UPPER(owner_name) || '.' || UPPER(table_name) || + ' t WHERE ROWID IN (SELECT rid FROM (SELECT ROWID rid, ROW_NUMBER() OVER (PARTITION BY ORDER BY DESC) rn FROM ' || + UPPER(owner_name) || '.' || UPPER(table_name) || ') WHERE rn > 1)') + )); + + l_arr.append(JSON_OBJECT( + 'issue_type' VALUE 'OUTLIER_RATE', + 'action' VALUE 'Cap or isolate outliers based on statistical bounds and business rules.', + 'sql_template' VALUE ('SELECT * FROM ' || UPPER(owner_name) || '.' || UPPER(table_name) || + ' WHERE ABS(( - ) / NULLIF(,0)) > 3') + )); + + l_obj.put('status', 'ok'); + l_obj.put('owner_name', UPPER(owner_name)); + l_obj.put('table_name', UPPER(table_name)); + l_obj.put('recommendations', l_arr); + RETURN l_obj.to_clob; + END suggest_remediation; + + FUNCTION apply_remediation( + p_owner_name IN VARCHAR2, + p_table_name IN VARCHAR2, + p_issue_id IN NUMBER, + p_execute_mode IN VARCHAR2 DEFAULT 'PREVIEW', + p_approval_code IN VARCHAR2 DEFAULT NULL + ) RETURN CLOB IS + l_sql CLOB; + l_mode VARCHAR2(30) := UPPER(NVL(p_execute_mode, 'PREVIEW')); + l_required_approval CLOB; + l_rows NUMBER := 0; + l_savepoint_set BOOLEAN := FALSE; + BEGIN + ensure_internal_tables; + + SELECT generated_fix_sql + INTO l_sql + FROM DQ_FINDINGS$ + WHERE issue_id = p_issue_id + AND owner_name = UPPER(p_owner_name) + AND table_name = UPPER(p_table_name); + + IF l_mode = 'APPLY' THEN + l_required_approval := get_cfg_value('REMEDIATION_APPROVAL_CODE'); + IF NVL(TRIM(l_required_approval), 'APPROVED') <> NVL(TRIM(p_approval_code), 'NO_APPROVAL') THEN + RETURN JSON_OBJECT( + 'status' VALUE 'error', + 'execute_mode' VALUE l_mode, + 'message' VALUE 'Approval code mismatch. Execution blocked.', + 'hint' VALUE 'Set SELECTAI_AGENT_CONFIG key REMEDIATION_APPROVAL_CODE for AGENT=DATA_QUALITY, then pass matching approval code.' + ); + END IF; + + IF l_sql IS NULL THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE 'No generated_fix_sql available for this issue.'); + END IF; + + IF NOT is_safe_remediation_sql(l_sql, p_owner_name, p_table_name) THEN + RETURN JSON_OBJECT( + 'status' VALUE 'error', + 'execute_mode' VALUE l_mode, + 'message' VALUE 'Unsafe remediation SQL blocked by policy.', + 'policy' VALUE 'Only single-statement UPDATE/DELETE/MERGE on target table is allowed; DDL/comments/multi-statement are blocked.', + 'preview_sql' VALUE l_sql + ); + END IF; + + SAVEPOINT dq_apply_start; + l_savepoint_set := TRUE; + EXECUTE IMMEDIATE l_sql; + l_rows := SQL%ROWCOUNT; + + UPDATE DQ_FINDINGS$ + SET status = 'RESOLVED' + WHERE issue_id = p_issue_id; + + RETURN JSON_OBJECT('status' VALUE 'ok', + 'execute_mode' VALUE l_mode, + 'rows_affected' VALUE l_rows, + 'message' VALUE 'Remediation applied successfully.'); + END IF; + + RETURN JSON_OBJECT('status' VALUE 'ok', + 'execute_mode' VALUE l_mode, + 'preview_sql' VALUE l_sql, + 'message' VALUE 'Preview mode only. Re-run with execute_mode=APPLY and approval_code to execute.'); + EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE 'Issue not found for the given table/scope.'); + WHEN OTHERS THEN + IF l_savepoint_set THEN + ROLLBACK TO dq_apply_start; + END IF; + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('apply_remediation failed: ' || SQLERRM)); + END apply_remediation; + + FUNCTION setup_oml_data_monitoring( + p_monitor_name IN VARCHAR2, + p_target_schema IN VARCHAR2, + p_target_table IN VARCHAR2, + p_baseline_expr IN VARCHAR2, + p_newdata_expr IN VARCHAR2 + ) RETURN CLOB IS + l_endpoint CLOB; + l_cred CLOB; + l_uri VARCHAR2(4000); + l_body CLOB; + l_resp DBMS_CLOUD_TYPES.resp; + l_resp_txt CLOB; + l_job_id VARCHAR2(256); + BEGIN + ensure_internal_tables; + l_endpoint := get_cfg_value('OML_MONITORING_ENDPOINT'); + l_cred := get_cfg_value('OML_MONITORING_CREDENTIAL'); + + IF l_endpoint IS NULL OR l_cred IS NULL THEN + RETURN JSON_OBJECT( + 'status' VALUE 'error', + 'message' VALUE 'OML monitoring config missing.', + 'required_keys' VALUE JSON_ARRAY('OML_MONITORING_ENDPOINT', 'OML_MONITORING_CREDENTIAL'), + 'agent' VALUE 'DATA_QUALITY' + ); + END IF; + + l_uri := RTRIM(l_endpoint, '/') || '/omlmod/v1/jobs'; + l_body := JSON_OBJECT( + 'jobName' VALUE p_monitor_name, + 'jobType' VALUE 'DATA_MONITORING', + 'baselineDataQuery' VALUE p_baseline_expr, + 'newDataQuery' VALUE p_newdata_expr, + 'targetTable' VALUE UPPER(p_target_schema) || '.' || UPPER(p_target_table) + ); + + l_resp := DBMS_CLOUD.SEND_REQUEST( + credential_name => l_cred, + uri => l_uri, + method => DBMS_CLOUD.METHOD_POST, + body => UTL_RAW.CAST_TO_RAW(l_body) + ); + l_resp_txt := DBMS_CLOUD.GET_RESPONSE_TEXT(l_resp); + + BEGIN + l_job_id := JSON_VALUE(l_resp_txt, '$.jobId'); + EXCEPTION + WHEN OTHERS THEN + l_job_id := NULL; + END; + + MERGE INTO DQ_OML_MONITORS$ t + USING ( + SELECT UPPER(p_monitor_name) monitor_name FROM dual + ) s + ON (t.monitor_name = s.monitor_name) + WHEN MATCHED THEN + UPDATE SET + t.target_schema = UPPER(p_target_schema), + t.target_table = UPPER(p_target_table), + t.baseline_expr = p_baseline_expr, + t.newdata_expr = p_newdata_expr, + t.oml_job_id = l_job_id, + t.status = 'REGISTERED', + t.last_run_ts = SYSTIMESTAMP, + t.last_run_response = l_resp_txt + WHEN NOT MATCHED THEN + INSERT (monitor_name, target_schema, target_table, baseline_expr, newdata_expr, oml_job_id, status, last_run_ts, last_run_response) + VALUES (UPPER(p_monitor_name), UPPER(p_target_schema), UPPER(p_target_table), p_baseline_expr, p_newdata_expr, l_job_id, 'REGISTERED', SYSTIMESTAMP, l_resp_txt); + + RETURN JSON_OBJECT( + 'status' VALUE 'ok', + 'monitor_name' VALUE UPPER(p_monitor_name), + 'job_id' VALUE l_job_id, + 'response' VALUE l_resp_txt + ); + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('setup_oml_data_monitoring failed: ' || SQLERRM)); + END setup_oml_data_monitoring; + + FUNCTION run_oml_data_monitoring( + p_monitor_name IN VARCHAR2 + ) RETURN CLOB IS + l_endpoint CLOB; + l_cred CLOB; + l_job_id VARCHAR2(256); + l_uri VARCHAR2(4000); + l_resp DBMS_CLOUD_TYPES.resp; + l_resp_txt CLOB; + BEGIN + ensure_internal_tables; + l_endpoint := get_cfg_value('OML_MONITORING_ENDPOINT'); + l_cred := get_cfg_value('OML_MONITORING_CREDENTIAL'); + + IF l_endpoint IS NULL OR l_cred IS NULL THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE 'OML monitoring config missing in SELECTAI_AGENT_CONFIG for AGENT=DATA_QUALITY.'); + END IF; + + SELECT oml_job_id + INTO l_job_id + FROM DQ_OML_MONITORS$ + WHERE monitor_name = UPPER(p_monitor_name); + + IF l_job_id IS NULL THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE 'No OML job id mapped for this monitor. Run setup first.'); + END IF; + + l_uri := RTRIM(l_endpoint, '/') || '/omlmod/v1/jobs/' || UTL_URL.ESCAPE(l_job_id, TRUE) || '/run'; + l_resp := DBMS_CLOUD.SEND_REQUEST( + credential_name => l_cred, + uri => l_uri, + method => DBMS_CLOUD.METHOD_POST + ); + l_resp_txt := DBMS_CLOUD.GET_RESPONSE_TEXT(l_resp); + + UPDATE DQ_OML_MONITORS$ + SET status = 'RUN_TRIGGERED', + last_run_ts = SYSTIMESTAMP, + last_run_response = l_resp_txt + WHERE monitor_name = UPPER(p_monitor_name); + + RETURN JSON_OBJECT( + 'status' VALUE 'ok', + 'monitor_name' VALUE UPPER(p_monitor_name), + 'job_id' VALUE l_job_id, + 'response' VALUE l_resp_txt + ); + EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE 'Monitor not found. Run setup_oml_data_monitoring first.'); + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', + 'message' VALUE ('run_oml_data_monitoring failed: ' || SQLERRM)); + END run_oml_data_monitoring; +END database_quality; +/ + +CREATE OR REPLACE PACKAGE select_ai_data_quality_agent AS + FUNCTION profile_table( + owner_name VARCHAR2, + table_name VARCHAR2, + sample_rows NUMBER DEFAULT 10000 + ) RETURN CLOB; + + FUNCTION detect_nulls( + owner_name VARCHAR2, + table_name VARCHAR2 + ) RETURN CLOB; + + FUNCTION detect_duplicates( + owner_name VARCHAR2, + table_name VARCHAR2, + key_columns_csv VARCHAR2 DEFAULT NULL + ) RETURN CLOB; + + FUNCTION detect_outliers( + owner_name VARCHAR2, + table_name VARCHAR2, + z_limit NUMBER DEFAULT 3 + ) RETURN CLOB; + + FUNCTION detect_drift( + owner_name VARCHAR2, + table_name VARCHAR2, + baseline_days NUMBER DEFAULT 30, + current_days NUMBER DEFAULT 7, + drift_threshold NUMBER DEFAULT 20 + ) RETURN CLOB; + + FUNCTION generate_quality_rules( + owner_name VARCHAR2, + table_name VARCHAR2 + ) RETURN CLOB; + + FUNCTION evaluate_quality_score( + owner_name VARCHAR2, + table_name VARCHAR2 + ) RETURN CLOB; + + FUNCTION list_quality_issues( + owner_name VARCHAR2, + table_name VARCHAR2, + severity_level VARCHAR2 DEFAULT NULL + ) RETURN CLOB; + + FUNCTION suggest_remediation( + owner_name VARCHAR2, + table_name VARCHAR2 + ) RETURN CLOB; + + FUNCTION apply_remediation( + owner_name VARCHAR2, + table_name VARCHAR2, + issue_id NUMBER, + execute_mode VARCHAR2 DEFAULT 'PREVIEW', + approval_code VARCHAR2 DEFAULT NULL + ) RETURN CLOB; + + FUNCTION setup_oml_data_monitoring( + monitor_name IN VARCHAR2, + target_schema IN VARCHAR2, + target_table IN VARCHAR2, + baseline_expr IN VARCHAR2, + newdata_expr IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION run_oml_data_monitoring( + monitor_name IN VARCHAR2 + ) RETURN CLOB; +END select_ai_data_quality_agent; +/ + +CREATE OR REPLACE PACKAGE BODY select_ai_data_quality_agent AS + FUNCTION profile_table( + owner_name VARCHAR2, + table_name VARCHAR2, + sample_rows NUMBER DEFAULT 10000 + ) RETURN CLOB IS + BEGIN + RETURN database_quality.profile_table(owner_name, table_name, sample_rows); + END profile_table; + + FUNCTION detect_nulls(owner_name VARCHAR2, table_name VARCHAR2) RETURN CLOB IS + BEGIN + RETURN database_quality.detect_nulls(owner_name, table_name); + END detect_nulls; + + FUNCTION detect_duplicates( + owner_name VARCHAR2, + table_name VARCHAR2, + key_columns_csv VARCHAR2 DEFAULT NULL + ) RETURN CLOB IS + BEGIN + RETURN database_quality.detect_duplicates(owner_name, table_name, key_columns_csv); + END detect_duplicates; + + FUNCTION detect_outliers( + owner_name VARCHAR2, + table_name VARCHAR2, + z_limit NUMBER DEFAULT 3 + ) RETURN CLOB IS + BEGIN + RETURN database_quality.detect_outliers(owner_name, table_name, z_limit); + END detect_outliers; + + FUNCTION detect_drift( + owner_name VARCHAR2, + table_name VARCHAR2, + baseline_days NUMBER DEFAULT 30, + current_days NUMBER DEFAULT 7, + drift_threshold NUMBER DEFAULT 20 + ) RETURN CLOB IS + BEGIN + RETURN database_quality.detect_drift(owner_name, table_name, baseline_days, current_days, drift_threshold); + END detect_drift; + + FUNCTION generate_quality_rules(owner_name VARCHAR2, table_name VARCHAR2) RETURN CLOB IS + BEGIN + RETURN database_quality.generate_quality_rules(owner_name, table_name); + END generate_quality_rules; + + FUNCTION evaluate_quality_score(owner_name VARCHAR2, table_name VARCHAR2) RETURN CLOB IS + BEGIN + RETURN database_quality.evaluate_quality_score(owner_name, table_name); + END evaluate_quality_score; + + FUNCTION list_quality_issues( + owner_name VARCHAR2, + table_name VARCHAR2, + severity_level VARCHAR2 DEFAULT NULL + ) RETURN CLOB IS + BEGIN + RETURN database_quality.list_quality_issues(owner_name, table_name, severity_level); + END list_quality_issues; + + FUNCTION suggest_remediation(owner_name VARCHAR2, table_name VARCHAR2) RETURN CLOB IS + BEGIN + RETURN database_quality.suggest_remediation(owner_name, table_name); + END suggest_remediation; + + FUNCTION apply_remediation( + owner_name VARCHAR2, + table_name VARCHAR2, + issue_id NUMBER, + execute_mode VARCHAR2 DEFAULT 'PREVIEW', + approval_code VARCHAR2 DEFAULT NULL + ) RETURN CLOB IS + BEGIN + RETURN database_quality.apply_remediation(owner_name, table_name, issue_id, execute_mode, approval_code); + END apply_remediation; + + FUNCTION setup_oml_data_monitoring( + monitor_name IN VARCHAR2, + target_schema IN VARCHAR2, + target_table IN VARCHAR2, + baseline_expr IN VARCHAR2, + newdata_expr IN VARCHAR2 + ) RETURN CLOB IS + BEGIN + RETURN database_quality.setup_oml_data_monitoring(monitor_name, target_schema, target_table, baseline_expr, newdata_expr); + END setup_oml_data_monitoring; + + FUNCTION run_oml_data_monitoring( + monitor_name IN VARCHAR2 + ) RETURN CLOB IS + BEGIN + RETURN database_quality.run_oml_data_monitoring(monitor_name); + END run_oml_data_monitoring; +END select_ai_data_quality_agent; +/ + +CREATE OR REPLACE PROCEDURE initialize_data_quality_tools +IS + PROCEDURE drop_tool_if_exists(p_tool_name IN VARCHAR2) IS + l_tool_count NUMBER; + BEGIN + SELECT COUNT(*) + INTO l_tool_count + FROM USER_AI_AGENT_TOOLS + WHERE TOOL_NAME = p_tool_name; + + IF l_tool_count > 0 THEN + DBMS_CLOUD_AI_AGENT.DROP_TOOL(p_tool_name); + END IF; + END drop_tool_if_exists; +BEGIN + drop_tool_if_exists('PROFILE_TABLE_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'PROFILE_TABLE_TOOL', + attributes => '{"instruction":"Profile a table for baseline data quality metrics.","function":"select_ai_data_quality_agent.profile_table"}', + description => 'Profile a table for data quality' + ); + + drop_tool_if_exists('DETECT_NULLS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'DETECT_NULLS_TOOL', + attributes => '{"instruction":"Detect null-heavy columns and null rates.","function":"select_ai_data_quality_agent.detect_nulls"}', + description => 'Detect null issues' + ); + + drop_tool_if_exists('DETECT_DUPLICATES_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'DETECT_DUPLICATES_TOOL', + attributes => '{"instruction":"Detect duplicates using all columns or provided key columns.","function":"select_ai_data_quality_agent.detect_duplicates"}', + description => 'Detect duplicate records' + ); + + drop_tool_if_exists('DETECT_OUTLIERS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'DETECT_OUTLIERS_TOOL', + attributes => '{"instruction":"Detect numeric outliers using z-score.","function":"select_ai_data_quality_agent.detect_outliers"}', + description => 'Detect outliers' + ); + + drop_tool_if_exists('DETECT_DRIFT_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'DETECT_DRIFT_TOOL', + attributes => '{"instruction":"Detect quality-score drift from baseline history.","function":"select_ai_data_quality_agent.detect_drift"}', + description => 'Detect data quality drift' + ); + + drop_tool_if_exists('GENERATE_QUALITY_RULES_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'GENERATE_QUALITY_RULES_TOOL', + attributes => '{"instruction":"Generate recommended quality rules for the table.","function":"select_ai_data_quality_agent.generate_quality_rules"}', + description => 'Generate quality rules' + ); + + drop_tool_if_exists('EVALUATE_QUALITY_SCORE_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'EVALUATE_QUALITY_SCORE_TOOL', + attributes => '{"instruction":"Compute and store the overall quality score for a table.","function":"select_ai_data_quality_agent.evaluate_quality_score"}', + description => 'Evaluate quality score' + ); + + drop_tool_if_exists('LIST_QUALITY_ISSUES_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'LIST_QUALITY_ISSUES_TOOL', + attributes => '{"instruction":"List open quality issues and severity.","function":"select_ai_data_quality_agent.list_quality_issues"}', + description => 'List quality issues' + ); + + drop_tool_if_exists('SUGGEST_REMEDIATION_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'SUGGEST_REMEDIATION_TOOL', + attributes => '{"instruction":"Suggest remediation actions and SQL templates.","function":"select_ai_data_quality_agent.suggest_remediation"}', + description => 'Suggest remediation' + ); + + drop_tool_if_exists('APPLY_REMEDIATION_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'APPLY_REMEDIATION_TOOL', + attributes => '{"instruction":"Preview or apply generated remediation SQL for a known issue. APPLY requires approval_code.","function":"select_ai_data_quality_agent.apply_remediation"}', + description => 'Apply remediation for a quality issue' + ); + + drop_tool_if_exists('SETUP_OML_DATA_MONITORING_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'SETUP_OML_DATA_MONITORING_TOOL', + attributes => '{"instruction":"Create/register an OML Services data monitoring job for a table using baseline/new-data SQL expressions.","function":"select_ai_data_quality_agent.setup_oml_data_monitoring"}', + description => 'Setup OML Services data monitoring' + ); + + drop_tool_if_exists('RUN_OML_DATA_MONITORING_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'RUN_OML_DATA_MONITORING_TOOL', + attributes => '{"instruction":"Trigger a previously registered OML Services data monitoring job run.","function":"select_ai_data_quality_agent.run_oml_data_monitoring"}', + description => 'Run OML Services data monitoring' + ); + + DBMS_OUTPUT.PUT_LINE('initialize_data_quality_tools completed.'); +END initialize_data_quality_tools; +/ + +BEGIN + initialize_data_quality_tools; +END; +/ + +ALTER SESSION SET CURRENT_SCHEMA = ADMIN; + +PROMPT DATA_QUALITY agent tools installation completed diff --git a/autonomous-ai-agents/dbms_scheduler_jobs/README.md b/autonomous-ai-agents/dbms_scheduler_jobs/README.md new file mode 100644 index 0000000..51b0a9a --- /dev/null +++ b/autonomous-ai-agents/dbms_scheduler_jobs/README.md @@ -0,0 +1,179 @@ +# Select AI - DBMS Scheduler Monitoring Agent + +## Release Metadata + +- Release Version: `1.0` +- Release Date: `19-May-2026` + +## Overview + +The **Scheduler Monitoring Agent** is a production-style Select AI agent for Oracle `DBMS_SCHEDULER` activity, job history, and CPU/load diagnostics. + +It supports: + +- Running/scheduled/completed/failed/disabled/broken job visibility +- Historical interval analysis (counts, success/failure, p95 elapsed/CPU) +- Per-job detail and metadata (program/schedule/class/arguments) +- Full local-schema job inventory listing +- CPU and load trend analysis +- Hotspot and recurring-failure detection +- Chart-ready and export-table payload generation +- Executive summary + diagnostics + recommendations format + +--- + +## Repository Contents + +```text +. +├── dbms_scheduler_jobs.sql (legacy draft script) +├── dbms_scheduler_monitor_tools.sql (tools layer) +├── dbms_scheduler_monitor_agent.sql (agent layer) +└── README.md +``` + +--- + +## Architecture Pattern + +This implementation follows the same two-layer pattern as other agents: + +1. **Tools layer** (`dbms_scheduler_monitor_tools.sql`) +- Installs `SCHEDULER_MONITORING` core package +- Installs `SELECT_AI_SCHEDULER_MONITOR` wrapper package +- Registers all Select AI tools + +2. **Agent layer** (`dbms_scheduler_monitor_agent.sql`) +- Creates task (`SCHEDULER_MONITOR_TASKS`) +- Creates agent (`SCHEDULER_MONITOR_ADVISOR`) +- Creates team (`DBMS_SCHEDULER_MONITOR_TEAM`) + +--- + +## Tools Implemented + +- `GET_DATABASE_CURRENT_TIME_TOOL` +- `LIST_ALL_JOBS_TOOL` +- `LIST_RUNNING_JOBS_TOOL` +- `LIST_JOB_HISTORY_TOOL` +- `GET_JOB_DETAILS_TOOL` +- `ANALYZE_CPU_BY_JOB_TOOL` +- `ANALYZE_LOAD_BY_JOB_TOOL` +- `COMPARE_JOBS_TOOL` +- `IDENTIFY_HOTSPOTS_TOOL` +- `DETECT_FAILURES_TOOL` +- `SUMMARIZE_SCHEDULER_HEALTH_TOOL` +- `GENERATE_CHARTS_TOOL` +- `EXPORT_TABLES_TOOL` + +--- + +## Primary Data Sources + +The package is designed around local-schema scheduler views: + +- `USER_SCHEDULER_JOBS` +- `USER_SCHEDULER_RUNNING_JOBS` +- `USER_SCHEDULER_JOB_RUN_DETAILS` +- `USER_SCHEDULER_JOB_ARGS` + +It returns explicit error/proxy messaging when specific sources are unavailable. + +Important scope note: + +- This agent is intentionally **local-schema scoped** via `USER_SCHEDULER_*`. +- If a user mentions another schema name, the agent should clarify scope and continue with current schema data. + +--- + +## Installation + +Run as `ADMIN` (or equivalent privileged user): + +```sql +sqlplus admin@ @dbms_scheduler_monitor_tools.sql +sqlplus admin@ @dbms_scheduler_monitor_agent.sql +``` + +Prompts: + +- Tools script: + - `SCHEMA_NAME` +- Agent script: + - `SCHEMA_NAME` + - `AI_PROFILE_NAME` +- `SCHED_TARGET_OWNER` (default owner label for responses; data remains local schema scope) + +--- + +## Output Contract + +Task instructions enforce output sections: + +- Executive summary +- Detailed tables +- Chart section +- Diagnostics section +- Recommendations section + +For correlations: + +- label as `exact` or `inferred` +- include confidence +- avoid strong attribution when confidence is weak +- relative date requests (today/yesterday/previous day) must be resolved from database `SYSTIMESTAMP` via `GET_DATABASE_CURRENT_TIME_TOOL` + +For full-list requests: + +- Use `LIST_ALL_JOBS_TOOL` first. +- Do not infer total jobs from history/running jobs. +- Do not return sample-only output unless explicitly requested. +- Do not use ellipsis placeholders; if needed, paginate deterministically (`Page 1`, `Page 2`, ...). + +--- + +## Sample Prompts + +1. `Show currently running scheduler jobs with session id, elapsed time, and cpu used.` +1. `Give me full list of all scheduler jobs in table format.` +2. `Show scheduler run history for last 7 days with success/failure breakdown and p95 elapsed time.` +3. `Get detailed diagnostics for job ..` +4. `Analyze top CPU-consuming jobs in last 3 days.` +5. `Identify scheduler hotspots with minimum elapsed 600 seconds and minimum cpu 120 seconds.` +6. `Detect recurring scheduler failures in last 14 days and show common error patterns.` +7. `Compare jobs JOB_A and JOB_B for last 30 days.` +8. `Generate chart datasets for scheduler health trends by hour for last 2 days.` +9. `Export tables for running jobs, history, cpu analysis, and failures for last 24 hours.` +10. `Give scheduler health overview for last 7 days with recommendations.` + +--- + +## Notes + +- Time filters accept timestamp text; if timezone is supplied it is preserved. +- Aggregations are bounded with `top_n` caps to avoid oversized payloads. +- Logging table `SCHED_MONITOR_LOG$` captures operation duration and failures. +- `list_all_jobs` returns: + - `summary` (total/enabled/disabled/running/failed/broken) + - `returned_rows` + - `truncated` (`true`/`false`) + - `rows` (detailed job list) +- Legacy draft script (`dbms_scheduler_jobs.sql`) is retained for reference. + +--- + +## Troubleshooting + +1. `ORA-20051: Task attributes are not in valid JSON format` +- Cause: unescaped double quotes inside task instruction text. +- Fix: avoid embedded `"` in free text within `attributes` JSON. + +2. `ORA-40478` during large JSON payloads +- Cause: JSON result exceeds default `VARCHAR2` JSON return size. +- Fix: use `RETURNING CLOB` on outer JSON constructors (already applied). + +3. Package compilation errors in `SCHEDULER_MONITORING` +- Re-run tools script after edits: + - `@dbms_scheduler_monitor_tools.sql` +- Then recreate task/agent: + - `@dbms_scheduler_monitor_agent.sql` diff --git a/autonomous-ai-agents/dbms_scheduler_jobs/dbms_scheduler_monitor_agent.sql b/autonomous-ai-agents/dbms_scheduler_jobs/dbms_scheduler_monitor_agent.sql new file mode 100644 index 0000000..0f2d502 --- /dev/null +++ b/autonomous-ai-agents/dbms_scheduler_jobs/dbms_scheduler_monitor_agent.sql @@ -0,0 +1,192 @@ +rem ============================================================================ +rem LICENSE +rem Copyright (c) 2026 Oracle and/or its affiliates. +rem Licensed under the Universal Permissive License (UPL), Version 1.0 +rem https://oss.oracle.com/licenses/upl/ +rem +rem NAME +rem dbms_scheduler_monitor_agent.sql +rem +rem DESCRIPTION +rem Installer and configuration script for Scheduler Monitoring AI Agent Team. +rem +rem RELEASE VERSION +rem 1.0 +rem +rem RELEASE DATE +rem 19-May-2026 +rem ============================================================================ + +SET SERVEROUTPUT ON +SET VERIFY OFF + +PROMPT ====================================================== +PROMPT Scheduler Monitoring AI Agent Installer +PROMPT ====================================================== + +VAR v_schema VARCHAR2(128) +EXEC :v_schema := '&SCHEMA_NAME'; + +VAR v_ai_profile_name VARCHAR2(128) +EXEC :v_ai_profile_name := '&AI_PROFILE_NAME'; + +PROMPT +PROMPT SCHED_TARGET_OWNER: +PROMPT Logical default owner label for responses. +PROMPT Data is sourced from USER_SCHEDULER_* in the install schema. +PROMPT If blank, SCHEMA_NAME is used. +PROMPT + +VAR v_sched_target_owner VARCHAR2(128) +EXEC :v_sched_target_owner := '&SCHED_TARGET_OWNER'; + +DECLARE + l_sql VARCHAR2(500); + l_schema VARCHAR2(128); + l_session_user VARCHAR2(128); +BEGIN + l_schema := DBMS_ASSERT.SIMPLE_SQL_NAME(:v_schema); + l_session_user := SYS_CONTEXT('USERENV', 'SESSION_USER'); + + IF UPPER(l_schema) <> UPPER(l_session_user) THEN + l_sql := 'GRANT EXECUTE ON DBMS_CLOUD_AI_AGENT TO ' || l_schema; + EXECUTE IMMEDIATE l_sql; + + l_sql := 'GRANT EXECUTE ON DBMS_CLOUD_AI TO ' || l_schema; + EXECUTE IMMEDIATE l_sql; + + l_sql := 'GRANT EXECUTE ON DBMS_CLOUD TO ' || l_schema; + EXECUTE IMMEDIATE l_sql; + ELSE + DBMS_OUTPUT.PUT_LINE('Skipping grants for schema ' || l_schema || + ' (same as session user).'); + END IF; + + DBMS_OUTPUT.PUT_LINE('Grants completed.'); +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'ALTER SESSION SET CURRENT_SCHEMA = ' || :v_schema; +END; +/ + +CREATE OR REPLACE PROCEDURE install_scheduler_monitor_agent( + p_install_schema IN VARCHAR2, + p_profile_name IN VARCHAR2, + p_sched_target_owner IN VARCHAR2 +) +AUTHID DEFINER +AS + l_target_owner VARCHAR2(128); +BEGIN + l_target_owner := UPPER(TRIM(NVL(p_sched_target_owner, p_install_schema))); + + BEGIN + DBMS_CLOUD_AI_AGENT.DROP_TASK('SCHEDULER_MONITOR_TASKS'); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + + DBMS_CLOUD_AI_AGENT.CREATE_TASK( + task_name => 'SCHEDULER_MONITOR_TASKS', + description => 'Task for Oracle DBMS_SCHEDULER monitoring, history, and CPU/load analysis', + attributes => '{ + "instruction": "You are an Oracle DBMS_SCHEDULER monitoring expert. ' + || 'Default owner label is ' || l_target_owner || ' when owner is omitted. ' + || 'This agent monitors USER_SCHEDULER_* objects in the install schema (local schema scope). ' + || 'For schema-wide requests, auto-discover local jobs from USER_SCHEDULER metadata and do not ask users to provide object lists. ' + || 'When user asks how many jobs or all job details, call LIST_ALL_JOBS_TOOL first and answer with count plus detailed rows. ' + || 'If user asks for full list of jobs, return the complete job-name list from LIST_ALL_JOBS_TOOL rows (not a sample). ' + || 'Do not show sample tables unless user explicitly asks for sample/top-N. ' + || 'Never use ellipsis like dot-dot-dot (N more jobs) for full-list requests. ' + || 'For full-list table requests, output every row from LIST_ALL_JOBS_TOOL rows. ' + || 'If output is too large for one response, paginate deterministically as Page 1, Page 2, ... with fixed row ranges until all rows are shown. ' + || 'Do not infer total jobs from job history or running jobs when LIST_ALL_JOBS_TOOL is available. ' + || 'If user mentions another schema name, explain this agent is local-schema scoped (USER_SCHEDULER_*), then continue using current schema data unless cross-schema support is explicitly added. ' + || 'Always return: executive summary, detailed tables, chart section, diagnostics, and recommendations. ' + || 'For any relative date/time request (today, yesterday, last day, previous day, last week), call GET_DATABASE_CURRENT_TIME_TOOL first and derive interval boundaries from database time, not model time. ' + || 'Use LIST_RUNNING_JOBS_TOOL for running jobs. ' + || 'Use LIST_JOB_HISTORY_TOOL for historical interval analysis with success/failure and p95 metrics. ' + || 'Use GET_JOB_DETAILS_TOOL for selected job metadata, args, schedule/program details, and recent run diagnostics. ' + || 'Use ANALYZE_CPU_BY_JOB_TOOL and ANALYZE_LOAD_BY_JOB_TOOL for CPU and load correlation. ' + || 'Use COMPARE_JOBS_TOOL when user asks comparison. ' + || 'Use IDENTIFY_HOTSPOTS_TOOL for contention and overlap hotspots. ' + || 'Use DETECT_FAILURES_TOOL for recurring failures and error patterns. ' + || 'Use SUMMARIZE_SCHEDULER_HEALTH_TOOL for fleet overview. ' + || 'Use GENERATE_CHARTS_TOOL for chart-ready output. ' + || 'Use EXPORT_TABLES_TOOL for exportable table payloads. ' + || 'Clearly label correlations as exact or inferred and include confidence level; do not overstate weak attribution. ' + || 'If performance views are unavailable, say so and provide nearest proxy metrics. ' + || 'User request: {query}", + "tools": [ + "GET_DATABASE_CURRENT_TIME_TOOL", + "LIST_ALL_JOBS_TOOL", + "LIST_RUNNING_JOBS_TOOL", + "LIST_JOB_HISTORY_TOOL", + "GET_JOB_DETAILS_TOOL", + "ANALYZE_CPU_BY_JOB_TOOL", + "ANALYZE_LOAD_BY_JOB_TOOL", + "COMPARE_JOBS_TOOL", + "IDENTIFY_HOTSPOTS_TOOL", + "DETECT_FAILURES_TOOL", + "SUMMARIZE_SCHEDULER_HEALTH_TOOL", + "GENERATE_CHARTS_TOOL", + "EXPORT_TABLES_TOOL" + ], + "enable_human_tool": "true" + }' + ); + + BEGIN + DBMS_CLOUD_AI_AGENT.DROP_AGENT('SCHEDULER_MONITOR_ADVISOR'); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + + DBMS_CLOUD_AI_AGENT.CREATE_AGENT( + agent_name => 'SCHEDULER_MONITOR_ADVISOR', + attributes => + '{' || + '"profile_name":"' || p_profile_name || '",' || + '"role":"You are a senior Oracle scheduler operations and performance advisor. You diagnose running and historical scheduler behavior, correlate workload with CPU/load, and provide actionable operational recommendations."' || + '}', + description => 'AI agent for DBMS_SCHEDULER monitoring and performance diagnostics' + ); + + BEGIN + DBMS_CLOUD_AI_AGENT.DROP_TEAM('DBMS_SCHEDULER_MONITOR_TEAM'); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + + DBMS_CLOUD_AI_AGENT.CREATE_TEAM( + team_name => 'DBMS_SCHEDULER_MONITOR_TEAM', + attributes => '{ + "agents":[{"name":"SCHEDULER_MONITOR_ADVISOR","task":"SCHEDULER_MONITOR_TASKS"}], + "process":"sequential" + }' + ); + + DBMS_OUTPUT.PUT_LINE('Scheduler monitor task, agent, and team created successfully.'); +END install_scheduler_monitor_agent; +/ + +PROMPT Executing installer procedure ... +BEGIN + install_scheduler_monitor_agent( + p_install_schema => :v_schema, + p_profile_name => :v_ai_profile_name, + p_sched_target_owner => :v_sched_target_owner + ); +END; +/ + +ALTER SESSION SET CURRENT_SCHEMA = ADMIN; + +PROMPT ====================================================== +PROMPT Scheduler Monitoring Agent installation complete +PROMPT ====================================================== diff --git a/autonomous-ai-agents/dbms_scheduler_jobs/dbms_scheduler_monitor_tools.sql b/autonomous-ai-agents/dbms_scheduler_jobs/dbms_scheduler_monitor_tools.sql new file mode 100644 index 0000000..f4c164e --- /dev/null +++ b/autonomous-ai-agents/dbms_scheduler_jobs/dbms_scheduler_monitor_tools.sql @@ -0,0 +1,1381 @@ +rem ============================================================================ +rem LICENSE +rem Copyright (c) 2026 Oracle and/or its affiliates. +rem Licensed under the Universal Permissive License (UPL), Version 1.0 +rem https://oss.oracle.com/licenses/upl/ +rem +rem NAME +rem dbms_scheduler_monitor_tools.sql +rem +rem DESCRIPTION +rem Installer script for DBMS_SCHEDULER monitoring and performance tools. +rem +rem RELEASE VERSION +rem 1.0 +rem +rem RELEASE DATE +rem 19-May-2026 +rem ============================================================================ + +SET SERVEROUTPUT ON +SET VERIFY OFF + +VAR v_schema VARCHAR2(128) +EXEC :v_schema := '&SCHEMA_NAME'; + +CREATE OR REPLACE PROCEDURE initialize_scheduler_monitoring( + p_install_schema_name IN VARCHAR2 +) +IS + l_schema_name VARCHAR2(128); + + TYPE priv_list_t IS VARRAY(20) OF VARCHAR2(4000); + l_priv_list CONSTANT priv_list_t := priv_list_t( + 'DBMS_CLOUD_AI_AGENT', + 'DBMS_CLOUD_AI', + 'DBMS_CLOUD' + ); + + PROCEDURE execute_grants(p_schema IN VARCHAR2, p_objects IN priv_list_t) IS + l_session_user VARCHAR2(128); + BEGIN + l_session_user := SYS_CONTEXT('USERENV', 'SESSION_USER'); + + IF UPPER(p_schema) = UPPER(l_session_user) THEN + DBMS_OUTPUT.PUT_LINE('Skipping grants for schema ' || p_schema || + ' (same as session user).'); + RETURN; + END IF; + + FOR i IN 1 .. p_objects.COUNT LOOP + BEGIN + EXECUTE IMMEDIATE 'GRANT EXECUTE ON ' || p_objects(i) || ' TO ' || p_schema; + EXCEPTION + WHEN OTHERS THEN + DBMS_OUTPUT.PUT_LINE('Warning: failed to grant ' || p_objects(i) || + ' to ' || p_schema || ' - ' || SQLERRM); + END; + END LOOP; + END execute_grants; +BEGIN + l_schema_name := DBMS_ASSERT.SIMPLE_SQL_NAME(p_install_schema_name); + execute_grants(l_schema_name, l_priv_list); + + BEGIN + EXECUTE IMMEDIATE + 'CREATE TABLE ' || l_schema_name || '.SCHED_MONITOR_LOG$( + id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + logged_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL, + operation_name VARCHAR2(200), + duration_ms NUMBER, + status VARCHAR2(30), + error_message CLOB + )'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + + DBMS_OUTPUT.PUT_LINE('initialize_scheduler_monitoring completed for schema ' || l_schema_name); +END initialize_scheduler_monitoring; +/ + +BEGIN + initialize_scheduler_monitoring(:v_schema); +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'ALTER SESSION SET CURRENT_SCHEMA = ' || :v_schema; +END; +/ + +CREATE OR REPLACE PACKAGE scheduler_monitoring AUTHID CURRENT_USER AS + FUNCTION format_duration(p_seconds IN NUMBER) RETURN VARCHAR2; + + FUNCTION get_database_current_time RETURN CLOB; + + FUNCTION list_all_jobs( + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 1000 + ) RETURN CLOB; + + FUNCTION list_running_jobs( + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 200 + ) RETURN CLOB; + + FUNCTION list_job_history( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_status IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 1000 + ) RETURN CLOB; + + FUNCTION get_job_details( + p_owner IN VARCHAR2, + p_job_name IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION analyze_cpu_by_job( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 20 + ) RETURN CLOB; + + FUNCTION analyze_load_by_job( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_granularity IN VARCHAR2 DEFAULT 'HOUR' + ) RETURN CLOB; + + FUNCTION compare_jobs( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2, + p_job_a IN VARCHAR2, + p_job_b IN VARCHAR2 + ) RETURN CLOB; + + FUNCTION identify_hotspots( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_top_n IN NUMBER DEFAULT 20, + p_min_elapsed IN NUMBER DEFAULT 0, + p_min_cpu IN NUMBER DEFAULT 0 + ) RETURN CLOB; + + FUNCTION detect_failures( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 100 + ) RETURN CLOB; + + FUNCTION summarize_scheduler_health( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 10 + ) RETURN CLOB; + + FUNCTION generate_charts( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_granularity IN VARCHAR2 DEFAULT 'HOUR', + p_top_n IN NUMBER DEFAULT 10 + ) RETURN CLOB; + + FUNCTION export_tables( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 500 + ) RETURN CLOB; +END scheduler_monitoring; +/ + +CREATE OR REPLACE PACKAGE BODY scheduler_monitoring AS + PROCEDURE write_log( + p_operation_name IN VARCHAR2, + p_start_ts IN TIMESTAMP, + p_status IN VARCHAR2, + p_error_message IN CLOB DEFAULT NULL + ); + + FUNCTION parse_ts(p_text IN VARCHAR2) RETURN TIMESTAMP WITH TIME ZONE IS + BEGIN + IF p_text IS NULL THEN + RETURN NULL; + END IF; + + BEGIN + RETURN TO_TIMESTAMP_TZ(p_text, 'YYYY-MM-DD"T"HH24:MI:SS.FF TZH:TZM'); + EXCEPTION + WHEN OTHERS THEN + BEGIN + RETURN TO_TIMESTAMP_TZ(p_text, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'); + EXCEPTION + WHEN OTHERS THEN + RETURN TO_TIMESTAMP_TZ( + p_text || ' ' || TO_CHAR(SYSTIMESTAMP, 'TZH:TZM'), + 'YYYY-MM-DD HH24:MI:SS TZH:TZM' + ); + END; + END; + END parse_ts; + + FUNCTION clamp_top_n(p_top_n IN NUMBER, p_default IN NUMBER, p_max IN NUMBER) RETURN NUMBER IS + BEGIN + RETURN LEAST(GREATEST(NVL(p_top_n, p_default), 1), p_max); + END clamp_top_n; + + FUNCTION sanitize_granularity(p_val IN VARCHAR2) RETURN VARCHAR2 IS + l_val VARCHAR2(20) := UPPER(TRIM(NVL(p_val, 'HOUR'))); + BEGIN + IF l_val NOT IN ('MINUTE', 'HOUR', 'DAY') THEN + RETURN 'HOUR'; + END IF; + RETURN l_val; + END sanitize_granularity; + + FUNCTION sanitize_status(p_val IN VARCHAR2) RETURN VARCHAR2 IS + l_val VARCHAR2(40) := UPPER(TRIM(p_val)); + BEGIN + IF l_val IS NULL THEN + RETURN NULL; + END IF; + IF l_val IN ('SUCCEEDED','FAILED','RUNNING','RETRY SCHEDULED','STOPPED','BROKEN') THEN + RETURN l_val; + END IF; + RETURN NULL; + END sanitize_status; + + FUNCTION format_duration(p_seconds IN NUMBER) RETURN VARCHAR2 IS + l_secs NUMBER := NVL(p_seconds, 0); + BEGIN + IF l_secs < 60 THEN + RETURN TO_CHAR(ROUND(l_secs, 3)) || ' sec'; + ELSIF l_secs < 3600 THEN + RETURN TO_CHAR(ROUND(l_secs / 60, 3)) || ' min'; + ELSIF l_secs < 86400 THEN + RETURN TO_CHAR(ROUND(l_secs / 3600, 3)) || ' hr'; + ELSE + RETURN TO_CHAR(ROUND(l_secs / 86400, 3)) || ' day'; + END IF; + END format_duration; + + FUNCTION get_database_current_time RETURN CLOB IS + l_json CLOB; + BEGIN + SELECT JSON_OBJECT( + 'database_systimestamp' VALUE TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD\"T\"HH24:MI:SS.FF TZH:TZM'), + 'database_sysdate' VALUE TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), + 'session_timezone' VALUE SESSIONTIMEZONE, + 'dbtimezone' VALUE DBTIMEZONE + ) + INTO l_json + FROM dual; + + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END get_database_current_time; + + FUNCTION list_all_jobs( + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 1000 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_top_n NUMBER := clamp_top_n(p_top_n, 1000, 10000); + BEGIN + SELECT JSON_OBJECT( + 'summary' VALUE ( + SELECT JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'total_jobs' VALUE COUNT(*), + 'enabled_jobs' VALUE SUM(CASE WHEN enabled = 'TRUE' THEN 1 ELSE 0 END), + 'disabled_jobs' VALUE SUM(CASE WHEN enabled = 'FALSE' THEN 1 ELSE 0 END), + 'running_jobs' VALUE SUM(CASE WHEN state = 'RUNNING' THEN 1 ELSE 0 END), + 'failed_state_jobs' VALUE SUM(CASE WHEN state = 'FAILED' THEN 1 ELSE 0 END), + 'broken_jobs' VALUE SUM(CASE WHEN state = 'BROKEN' THEN 1 ELSE 0 END) + ) + FROM USER_SCHEDULER_JOBS + WHERE (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + ), + 'returned_rows' VALUE ( + SELECT COUNT(*) + FROM ( + SELECT 1 + FROM USER_SCHEDULER_JOBS + WHERE (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + ORDER BY job_name + FETCH FIRST l_top_n ROWS ONLY + ) + ), + 'truncated' VALUE ( + SELECT CASE + WHEN COUNT(*) > l_top_n THEN 'true' + ELSE 'false' + END + FROM USER_SCHEDULER_JOBS + WHERE (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + ), + 'rows' VALUE COALESCE(( + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE job_name, + 'enabled' VALUE enabled, + 'state' VALUE state, + 'job_style' VALUE job_style, + 'job_type' VALUE job_type, + 'job_action' VALUE job_action, + 'program_name' VALUE program_name, + 'schedule_name' VALUE schedule_name, + 'job_class' VALUE job_class, + 'repeat_interval' VALUE repeat_interval, + 'last_start_date' VALUE TO_CHAR(last_start_date, 'YYYY-MM-DD\"T\"HH24:MI:SS TZH:TZM'), + 'next_run_date' VALUE TO_CHAR(next_run_date, 'YYYY-MM-DD\"T\"HH24:MI:SS TZH:TZM'), + 'run_count' VALUE run_count, + 'failure_count' VALUE failure_count, + 'retry_count' VALUE retry_count, + 'max_failures' VALUE max_failures, + 'max_runs' VALUE max_runs, + 'auto_drop' VALUE auto_drop, + 'restartable' VALUE restartable, + 'logging_level' VALUE logging_level, + 'stop_on_window_close' VALUE stop_on_window_close + ) + RETURNING CLOB) + FROM ( + SELECT * + FROM USER_SCHEDULER_JOBS + WHERE (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + ORDER BY job_name + FETCH FIRST l_top_n ROWS ONLY + ) + ), TO_CLOB('[]')) + RETURNING CLOB) INTO l_json + FROM dual; + + write_log('list_all_jobs', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('list_all_jobs', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END list_all_jobs; + + PROCEDURE write_log( + p_operation_name IN VARCHAR2, + p_start_ts IN TIMESTAMP, + p_status IN VARCHAR2, + p_error_message IN CLOB DEFAULT NULL + ) IS + l_duration_ms NUMBER; + l_delta INTERVAL DAY TO SECOND; + BEGIN + l_delta := SYSTIMESTAMP - CAST(p_start_ts AS TIMESTAMP WITH TIME ZONE); + l_duration_ms := + (EXTRACT(DAY FROM l_delta) * 86400000) + + (EXTRACT(HOUR FROM l_delta) * 3600000) + + (EXTRACT(MINUTE FROM l_delta) * 60000) + + (EXTRACT(SECOND FROM l_delta) * 1000); + BEGIN + INSERT INTO SCHED_MONITOR_LOG$(operation_name, duration_ms, status, error_message) + VALUES (p_operation_name, l_duration_ms, p_status, p_error_message); + EXCEPTION + WHEN OTHERS THEN + NULL; + END; + END write_log; + + FUNCTION list_running_jobs( + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 200 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_top_n NUMBER := clamp_top_n(p_top_n, 200, 5000); + BEGIN + SELECT JSON_OBJECT( + 'summary' VALUE JSON_OBJECT( + 'running_jobs' VALUE COUNT(*) + ), + 'rows' VALUE COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE job_name, + 'session_id' VALUE session_id, + 'running_instance' VALUE running_instance, + 'elapsed_time' VALUE scheduler_monitoring.format_duration(elapsed_time), + 'cpu_used_seconds' VALUE cpu_used, + 'slave_os_process_id' VALUE slave_os_process_id + ) + ), + JSON_ARRAY() + ) + ) + INTO l_json + FROM ( + SELECT job_name, + session_id, + running_instance, + EXTRACT(DAY FROM elapsed_time) * 86400 + EXTRACT(HOUR FROM elapsed_time) * 3600 + + EXTRACT(MINUTE FROM elapsed_time) * 60 + EXTRACT(SECOND FROM elapsed_time) AS elapsed_time, + EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used) AS cpu_used, + slave_os_process_id + FROM USER_SCHEDULER_RUNNING_JOBS + WHERE (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + ORDER BY elapsed_time DESC + FETCH FIRST l_top_n ROWS ONLY + ); + + write_log('list_running_jobs', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('list_running_jobs', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END list_running_jobs; + + FUNCTION list_job_history( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_status IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 1000 + ) RETURN CLOB IS + l_start_ts_log TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_status VARCHAR2(40) := sanitize_status(p_status); + l_top_n NUMBER := clamp_top_n(p_top_n, 1000, 10000); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + IF l_from_ts IS NULL OR l_to_ts IS NULL THEN + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE 'start_time and end_time are required.'); + END IF; + + SELECT JSON_OBJECT( + 'interval' VALUE JSON_OBJECT( + 'start_time' VALUE TO_CHAR(l_from_ts, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'), + 'end_time' VALUE TO_CHAR(l_to_ts, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM') + ), + 'summary' VALUE JSON_OBJECT( + 'total_runs' VALUE COUNT(*), + 'succeeded_runs' VALUE SUM(CASE WHEN status = 'SUCCEEDED' THEN 1 ELSE 0 END), + 'failed_runs' VALUE SUM(CASE WHEN status = 'FAILED' THEN 1 ELSE 0 END), + 'avg_elapsed_time' VALUE scheduler_monitoring.format_duration(ROUND(AVG(elapsed_seconds), 3)), + 'min_elapsed_time' VALUE scheduler_monitoring.format_duration(MIN(elapsed_seconds)), + 'max_elapsed_time' VALUE scheduler_monitoring.format_duration(MAX(elapsed_seconds)), + 'p95_elapsed_time' VALUE scheduler_monitoring.format_duration(ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY elapsed_seconds), 3)), + 'avg_cpu_seconds' VALUE ROUND(AVG(cpu_seconds), 3), + 'min_cpu_seconds' VALUE MIN(cpu_seconds), + 'max_cpu_seconds' VALUE MAX(cpu_seconds), + 'p95_cpu_seconds' VALUE ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY cpu_seconds), 3) + ), + 'rows' VALUE COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'log_id' VALUE log_id, + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE job_name, + 'status' VALUE status, + 'actual_start_time' VALUE TO_CHAR(actual_start_date, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'), + 'run_duration' VALUE run_duration, + 'elapsed_time' VALUE scheduler_monitoring.format_duration(elapsed_seconds), + 'cpu_seconds' VALUE cpu_seconds, + 'error_code' VALUE error#, + 'additional_info' VALUE additional_info, + 'instance_id' VALUE instance_id + ) + ), JSON_ARRAY() + ) + ) + INTO l_json + FROM ( + SELECT rd.log_id, + rd.job_name, + rd.status, + rd.actual_start_date, + rd.run_duration, + EXTRACT(DAY FROM rd.run_duration) * 86400 + EXTRACT(HOUR FROM rd.run_duration) * 3600 + + EXTRACT(MINUTE FROM rd.run_duration) * 60 + EXTRACT(SECOND FROM rd.run_duration) AS elapsed_seconds, + EXTRACT(DAY FROM rd.cpu_used) * 86400 + EXTRACT(HOUR FROM rd.cpu_used) * 3600 + + EXTRACT(MINUTE FROM rd.cpu_used) * 60 + EXTRACT(SECOND FROM rd.cpu_used) AS cpu_seconds, + rd.error#, + rd.additional_info, + rd.instance_id + FROM USER_SCHEDULER_JOB_RUN_DETAILS rd + WHERE rd.actual_start_date BETWEEN l_from_ts AND l_to_ts + AND (p_job_name_pattern IS NULL OR rd.job_name LIKE UPPER(p_job_name_pattern)) + AND (l_status IS NULL OR rd.status = l_status) + ORDER BY rd.actual_start_date DESC + FETCH FIRST l_top_n ROWS ONLY + ); + + write_log('list_job_history', l_start_ts_log, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('list_job_history', l_start_ts_log, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END list_job_history; + + FUNCTION get_job_details( + p_owner IN VARCHAR2, + p_job_name IN VARCHAR2 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_owner VARCHAR2(128) := UPPER(TRIM(NVL(p_owner, SYS_CONTEXT('USERENV','CURRENT_SCHEMA')))); + l_job_name VARCHAR2(128) := UPPER(TRIM(p_job_name)); + BEGIN + SELECT JSON_OBJECT( + 'job_metadata' VALUE ( + SELECT JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE j.job_name, + 'enabled' VALUE j.enabled, + 'state' VALUE j.state, + 'job_style' VALUE j.job_style, + 'job_type' VALUE j.job_type, + 'job_action' VALUE j.job_action, + 'program_name' VALUE j.program_name, + 'schedule_name' VALUE j.schedule_name, + 'job_class' VALUE j.job_class, + 'logging_level' VALUE j.logging_level, + 'restartable' VALUE j.restartable, + 'auto_drop' VALUE j.auto_drop, + 'max_failures' VALUE j.max_failures, + 'max_runs' VALUE j.max_runs, + 'repeat_interval' VALUE j.repeat_interval, + 'last_start_date' VALUE TO_CHAR(j.last_start_date, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'), + 'next_run_date' VALUE TO_CHAR(j.next_run_date, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'), + 'failure_count' VALUE j.failure_count, + 'credential_name' VALUE j.credential_name + ) + FROM USER_SCHEDULER_JOBS j + WHERE j.job_name = l_job_name + ), + 'job_arguments' VALUE COALESCE(( + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'argument_position' VALUE a.argument_position, + 'argument_name' VALUE a.argument_name, + 'argument_type' VALUE a.argument_type, + 'value' VALUE a.value, + 'out_argument' VALUE a.out_argument + ) + ) + FROM USER_SCHEDULER_JOB_ARGS a + WHERE a.job_name = l_job_name + ), JSON_ARRAY()), + 'recent_runs' VALUE COALESCE(( + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'log_id' VALUE r.log_id, + 'status' VALUE r.status, + 'actual_start_time' VALUE TO_CHAR(r.actual_start_date, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'), + 'run_duration' VALUE r.run_duration, + 'cpu_used' VALUE r.cpu_used, + 'error_code' VALUE r.error#, + 'additional_info' VALUE r.additional_info + ) + ) + FROM ( + SELECT * FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE job_name = l_job_name + ORDER BY actual_start_date DESC + FETCH FIRST 200 ROWS ONLY + ) r + ), JSON_ARRAY()) + ) INTO l_json + FROM dual; + + write_log('get_job_details', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('get_job_details', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END get_job_details; + + FUNCTION analyze_cpu_by_job( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 20 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_top_n NUMBER := clamp_top_n(p_top_n, 20, 500); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'summary' VALUE JSON_OBJECT( + 'total_cpu_seconds' VALUE ROUND(SUM(cpu_seconds), 3), + 'total_elapsed_seconds' VALUE ROUND(SUM(elapsed_seconds), 3) + ), + 'top_cpu_jobs' VALUE COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE job_name, + 'runs' VALUE run_count, + 'total_cpu_seconds' VALUE total_cpu_seconds, + 'total_elapsed_time' VALUE scheduler_monitoring.format_duration(total_elapsed_seconds), + 'cpu_per_run' VALUE cpu_per_run, + 'cpu_per_minute' VALUE cpu_per_minute, + 'cpu_to_elapsed_ratio' VALUE cpu_elapsed_ratio + ) + ), JSON_ARRAY() + ) + ) + INTO l_json + FROM ( + SELECT job_name, + COUNT(*) AS run_count, + ROUND(SUM(cpu_seconds), 3) AS total_cpu_seconds, + ROUND(SUM(elapsed_seconds), 3) AS total_elapsed_seconds, + ROUND(SUM(cpu_seconds)/NULLIF(COUNT(*),0), 3) AS cpu_per_run, + ROUND((SUM(cpu_seconds)/NULLIF(SUM(elapsed_seconds),0))*60, 3) AS cpu_per_minute, + ROUND(SUM(cpu_seconds)/NULLIF(SUM(elapsed_seconds),0), 4) AS cpu_elapsed_ratio, + SUM(cpu_seconds) AS cpu_seconds, + SUM(elapsed_seconds) AS elapsed_seconds + FROM ( + SELECT rd.job_name, + EXTRACT(DAY FROM rd.cpu_used) * 86400 + EXTRACT(HOUR FROM rd.cpu_used) * 3600 + + EXTRACT(MINUTE FROM rd.cpu_used) * 60 + EXTRACT(SECOND FROM rd.cpu_used) AS cpu_seconds, + EXTRACT(DAY FROM rd.run_duration) * 86400 + EXTRACT(HOUR FROM rd.run_duration) * 3600 + + EXTRACT(MINUTE FROM rd.run_duration) * 60 + EXTRACT(SECOND FROM rd.run_duration) AS elapsed_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS rd + WHERE rd.actual_start_date BETWEEN l_from_ts AND l_to_ts + AND (p_job_name_pattern IS NULL OR rd.job_name LIKE UPPER(p_job_name_pattern)) + ) + GROUP BY job_name + ORDER BY total_cpu_seconds DESC + FETCH FIRST l_top_n ROWS ONLY + ); + + write_log('analyze_cpu_by_job', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('analyze_cpu_by_job', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END analyze_cpu_by_job; + + FUNCTION analyze_load_by_job( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_granularity IN VARCHAR2 DEFAULT 'HOUR' + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + l_gran VARCHAR2(20) := sanitize_granularity(p_granularity); + BEGIN + SELECT JSON_OBJECT( + 'correlation_mode' VALUE 'inferred_time_window', + 'confidence' VALUE 'medium', + 'rows' VALUE COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'bucket_time' VALUE bucket_time, + 'runs' VALUE runs, + 'failed_runs' VALUE failed_runs, + 'avg_cpu_seconds' VALUE avg_cpu_seconds, + 'avg_elapsed_time' VALUE scheduler_monitoring.format_duration(avg_elapsed_seconds), + 'cpu_load_proxy' VALUE cpu_load_proxy + ) + ), JSON_ARRAY() + ) + ) + INTO l_json + FROM ( + SELECT TO_CHAR( + CASE l_gran + WHEN 'DAY' THEN CAST(TRUNC(CAST(rd.actual_start_date AS DATE)) AS TIMESTAMP) + WHEN 'HOUR' THEN CAST(TRUNC(CAST(rd.actual_start_date AS DATE), 'HH24') AS TIMESTAMP) + ELSE CAST(TRUNC(CAST(rd.actual_start_date AS DATE), 'MI') AS TIMESTAMP) + END, + 'YYYY-MM-DD HH24:MI:SS' + ) AS bucket_time, + COUNT(*) AS runs, + SUM(CASE WHEN rd.status = 'FAILED' THEN 1 ELSE 0 END) AS failed_runs, + ROUND(AVG(EXTRACT(DAY FROM rd.cpu_used) * 86400 + EXTRACT(HOUR FROM rd.cpu_used) * 3600 + + EXTRACT(MINUTE FROM rd.cpu_used) * 60 + EXTRACT(SECOND FROM rd.cpu_used)), 3) AS avg_cpu_seconds, + ROUND(AVG(EXTRACT(DAY FROM rd.run_duration) * 86400 + EXTRACT(HOUR FROM rd.run_duration) * 3600 + + EXTRACT(MINUTE FROM rd.run_duration) * 60 + EXTRACT(SECOND FROM rd.run_duration)), 3) AS avg_elapsed_seconds, + ROUND(AVG((EXTRACT(DAY FROM rd.cpu_used) * 86400 + EXTRACT(HOUR FROM rd.cpu_used) * 3600 + + EXTRACT(MINUTE FROM rd.cpu_used) * 60 + EXTRACT(SECOND FROM rd.cpu_used)) / + NULLIF((EXTRACT(DAY FROM rd.run_duration) * 86400 + EXTRACT(HOUR FROM rd.run_duration) * 3600 + + EXTRACT(MINUTE FROM rd.run_duration) * 60 + EXTRACT(SECOND FROM rd.run_duration)), 0)), 4) AS cpu_load_proxy + FROM USER_SCHEDULER_JOB_RUN_DETAILS rd + WHERE rd.actual_start_date BETWEEN l_from_ts AND l_to_ts + AND (p_job_name_pattern IS NULL OR rd.job_name LIKE UPPER(p_job_name_pattern)) + GROUP BY CASE l_gran + WHEN 'DAY' THEN TRUNC(CAST(rd.actual_start_date AS DATE)) + WHEN 'HOUR' THEN TRUNC(CAST(rd.actual_start_date AS DATE), 'HH24') + ELSE TRUNC(CAST(rd.actual_start_date AS DATE), 'MI') + END + ORDER BY bucket_time + ); + + write_log('analyze_load_by_job', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('analyze_load_by_job', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END analyze_load_by_job; + + FUNCTION compare_jobs( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2, + p_job_a IN VARCHAR2, + p_job_b IN VARCHAR2 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_job_a VARCHAR2(128) := UPPER(TRIM(p_job_a)); + l_job_b VARCHAR2(128) := UPPER(TRIM(p_job_b)); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'job_a' VALUE ( + SELECT JSON_OBJECT( + 'job_name' VALUE l_job_a, + 'runs' VALUE COUNT(*), + 'failed_runs' VALUE SUM(CASE WHEN status='FAILED' THEN 1 ELSE 0 END), + 'avg_elapsed_time' VALUE scheduler_monitoring.format_duration(ROUND(AVG(elapsed_seconds), 3)), + 'avg_cpu_seconds' VALUE ROUND(AVG(cpu_seconds), 3), + 'p95_elapsed_time' VALUE scheduler_monitoring.format_duration(ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY elapsed_seconds), 3)) + ) + FROM ( + SELECT status, + EXTRACT(DAY FROM run_duration) * 86400 + EXTRACT(HOUR FROM run_duration) * 3600 + + EXTRACT(MINUTE FROM run_duration) * 60 + EXTRACT(SECOND FROM run_duration) AS elapsed_seconds, + EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used) AS cpu_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE job_name = l_job_a + AND actual_start_date BETWEEN l_from_ts AND l_to_ts + ) + ), + 'job_b' VALUE ( + SELECT JSON_OBJECT( + 'job_name' VALUE l_job_b, + 'runs' VALUE COUNT(*), + 'failed_runs' VALUE SUM(CASE WHEN status='FAILED' THEN 1 ELSE 0 END), + 'avg_elapsed_time' VALUE scheduler_monitoring.format_duration(ROUND(AVG(elapsed_seconds), 3)), + 'avg_cpu_seconds' VALUE ROUND(AVG(cpu_seconds), 3), + 'p95_elapsed_time' VALUE scheduler_monitoring.format_duration(ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY elapsed_seconds), 3)) + ) + FROM ( + SELECT status, + EXTRACT(DAY FROM run_duration) * 86400 + EXTRACT(HOUR FROM run_duration) * 3600 + + EXTRACT(MINUTE FROM run_duration) * 60 + EXTRACT(SECOND FROM run_duration) AS elapsed_seconds, + EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used) AS cpu_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE job_name = l_job_b + AND actual_start_date BETWEEN l_from_ts AND l_to_ts + ) + ) + ) INTO l_json + FROM dual; + + write_log('compare_jobs', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('compare_jobs', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END compare_jobs; + + FUNCTION identify_hotspots( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_top_n IN NUMBER DEFAULT 20, + p_min_elapsed IN NUMBER DEFAULT 0, + p_min_cpu IN NUMBER DEFAULT 0 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_top_n NUMBER := clamp_top_n(p_top_n, 20, 500); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'rows' VALUE COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE job_name, + 'runs' VALUE runs, + 'total_cpu_seconds' VALUE total_cpu_seconds, + 'total_elapsed_time' VALUE scheduler_monitoring.format_duration(total_elapsed_seconds), + 'avg_cpu_to_elapsed_ratio' VALUE cpu_elapsed_ratio, + 'hotspot_reason' VALUE hotspot_reason + ) + ), JSON_ARRAY() + ) + ) INTO l_json + FROM ( + SELECT job_name, + COUNT(*) runs, + ROUND(SUM(cpu_seconds), 3) total_cpu_seconds, + ROUND(SUM(elapsed_seconds), 3) total_elapsed_seconds, + ROUND(AVG(cpu_seconds/NULLIF(elapsed_seconds,0)), 4) cpu_elapsed_ratio, + CASE + WHEN SUM(cpu_seconds) >= p_min_cpu AND SUM(elapsed_seconds) >= p_min_elapsed THEN 'high_cpu_and_long_runtime' + WHEN SUM(cpu_seconds) >= p_min_cpu THEN 'high_cpu' + ELSE 'long_runtime' + END hotspot_reason + FROM ( + SELECT job_name, + EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used) AS cpu_seconds, + EXTRACT(DAY FROM run_duration) * 86400 + EXTRACT(HOUR FROM run_duration) * 3600 + + EXTRACT(MINUTE FROM run_duration) * 60 + EXTRACT(SECOND FROM run_duration) AS elapsed_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE actual_start_date BETWEEN l_from_ts AND l_to_ts + ) + GROUP BY job_name + HAVING SUM(elapsed_seconds) >= NVL(p_min_elapsed, 0) + OR SUM(cpu_seconds) >= NVL(p_min_cpu, 0) + ORDER BY total_cpu_seconds DESC, total_elapsed_seconds DESC + FETCH FIRST l_top_n ROWS ONLY + ); + + write_log('identify_hotspots', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('identify_hotspots', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END identify_hotspots; + + FUNCTION detect_failures( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 100 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_top_n NUMBER := clamp_top_n(p_top_n, 100, 2000); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'summary' VALUE JSON_OBJECT( + 'failed_runs' VALUE COUNT(*), + 'distinct_jobs_failed' VALUE COUNT(DISTINCT job_name) + ), + 'top_failures' VALUE COALESCE( + JSON_ARRAYAGG( + JSON_OBJECT( + 'owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'), + 'job_name' VALUE job_name, + 'failures' VALUE failures, + 'last_failure_time' VALUE TO_CHAR(last_failure_time, 'YYYY-MM-DD"T"HH24:MI:SS TZH:TZM'), + 'common_error_code' VALUE common_error_code, + 'common_error_message' VALUE common_error_message + ) + ), JSON_ARRAY() + ) + ) + INTO l_json + FROM ( + SELECT job_name, + COUNT(*) failures, + MAX(actual_start_date) last_failure_time, + MIN(error#) KEEP (DENSE_RANK FIRST ORDER BY COUNT(*) DESC) common_error_code, + MIN(additional_info) KEEP (DENSE_RANK FIRST ORDER BY COUNT(*) DESC) common_error_message + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE actual_start_date BETWEEN l_from_ts AND l_to_ts + AND status = 'FAILED' + AND (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + GROUP BY job_name + ORDER BY failures DESC, last_failure_time DESC + FETCH FIRST l_top_n ROWS ONLY + ); + + write_log('detect_failures', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('detect_failures', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END detect_failures; + + FUNCTION summarize_scheduler_health( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 10 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_top_n NUMBER := clamp_top_n(p_top_n, 10, 100); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'executive_summary' VALUE JSON_OBJECT( + 'total_jobs' VALUE (SELECT COUNT(*) FROM USER_SCHEDULER_JOBS j), + 'running_jobs' VALUE (SELECT COUNT(*) FROM USER_SCHEDULER_RUNNING_JOBS r), + 'disabled_jobs' VALUE (SELECT COUNT(*) FROM USER_SCHEDULER_JOBS j WHERE j.enabled = 'FALSE'), + 'broken_jobs' VALUE (SELECT COUNT(*) FROM USER_SCHEDULER_JOBS j WHERE j.state = 'BROKEN'), + 'jobs_executed_in_interval' VALUE (SELECT COUNT(*) FROM USER_SCHEDULER_JOB_RUN_DETAILS d WHERE d.actual_start_date BETWEEN l_from_ts AND l_to_ts), + 'failed_runs_in_interval' VALUE (SELECT COUNT(*) FROM USER_SCHEDULER_JOB_RUN_DETAILS d WHERE d.actual_start_date BETWEEN l_from_ts AND l_to_ts AND d.status = 'FAILED') + ), + 'top_10_cpu_jobs' VALUE ( + SELECT COALESCE(JSON_ARRAYAGG( + JSON_OBJECT('owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'),'job_name' VALUE job_name,'total_cpu_seconds' VALUE total_cpu_seconds) + ), JSON_ARRAY()) + FROM ( + SELECT job_name, + ROUND(SUM(EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used)), 3) total_cpu_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE actual_start_date BETWEEN l_from_ts AND l_to_ts + GROUP BY job_name + ORDER BY total_cpu_seconds DESC + FETCH FIRST l_top_n ROWS ONLY + ) + ), + 'top_10_failure_jobs' VALUE ( + SELECT COALESCE(JSON_ARRAYAGG( + JSON_OBJECT('owner' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'),'job_name' VALUE job_name,'failed_runs' VALUE failed_runs) + ), JSON_ARRAY()) + FROM ( + SELECT job_name, COUNT(*) failed_runs + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE actual_start_date BETWEEN l_from_ts AND l_to_ts + AND status = 'FAILED' + GROUP BY job_name + ORDER BY failed_runs DESC + FETCH FIRST l_top_n ROWS ONLY + ) + ), + 'recommendations' VALUE JSON_ARRAY( + 'Reschedule jobs with repeated failures to non-peak windows.', + 'Stagger top CPU jobs to reduce concurrency hotspots.', + 'Review jobs with high CPU-to-elapsed ratio for SQL optimization.', + 'Increase instrumentation (module/action/client_identifier) for weakly correlated runs.' + ) + ) INTO l_json + FROM dual; + + write_log('summarize_scheduler_health', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('summarize_scheduler_health', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END summarize_scheduler_health; + + FUNCTION generate_charts( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_granularity IN VARCHAR2 DEFAULT 'HOUR', + p_top_n IN NUMBER DEFAULT 10 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_gran VARCHAR2(20) := sanitize_granularity(p_granularity); + l_top_n NUMBER := clamp_top_n(p_top_n, 10, 200); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'chart_specs' VALUE JSON_ARRAY( + JSON_OBJECT('chart_id' VALUE 'cpu_usage_trend','type' VALUE 'line','x' VALUE 'bucket_time','y' VALUE 'avg_cpu_seconds'), + JSON_OBJECT('chart_id' VALUE 'load_trend','type' VALUE 'line','x' VALUE 'bucket_time','y' VALUE 'runs'), + JSON_OBJECT('chart_id' VALUE 'top_cpu_jobs','type' VALUE 'bar','x' VALUE 'job_name','y' VALUE 'total_cpu_seconds'), + JSON_OBJECT('chart_id' VALUE 'top_longest_jobs','type' VALUE 'bar','x' VALUE 'job_name','y' VALUE 'total_elapsed_seconds'), + JSON_OBJECT('chart_id' VALUE 'failure_by_job','type' VALUE 'bar','x' VALUE 'job_name','y' VALUE 'failed_runs'), + JSON_OBJECT('chart_id' VALUE 'elapsed_vs_cpu','type' VALUE 'scatter','x' VALUE 'elapsed_seconds','y' VALUE 'cpu_seconds') + ), + 'datasets' VALUE JSON_OBJECT( + 'cpu_usage_trend' VALUE ( + SELECT COALESCE(JSON_ARRAYAGG( + JSON_OBJECT('bucket_time' VALUE bucket_time,'avg_cpu_seconds' VALUE avg_cpu_seconds) + ), JSON_ARRAY()) + FROM ( + SELECT TO_CHAR( + CASE l_gran + WHEN 'DAY' THEN TRUNC(CAST(actual_start_date AS DATE)) + WHEN 'HOUR' THEN TRUNC(CAST(actual_start_date AS DATE), 'HH24') + ELSE TRUNC(CAST(actual_start_date AS DATE), 'MI') + END, + 'YYYY-MM-DD HH24:MI:SS' + ) bucket_time, + ROUND(AVG(EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used)), 3) avg_cpu_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE actual_start_date BETWEEN l_from_ts AND l_to_ts + AND (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + GROUP BY CASE l_gran + WHEN 'DAY' THEN TRUNC(CAST(actual_start_date AS DATE)) + WHEN 'HOUR' THEN TRUNC(CAST(actual_start_date AS DATE), 'HH24') + ELSE TRUNC(CAST(actual_start_date AS DATE), 'MI') + END + ORDER BY bucket_time + ) + ), + 'top_cpu_jobs' VALUE ( + SELECT COALESCE(JSON_ARRAYAGG( + JSON_OBJECT('job_name' VALUE job_name,'total_cpu_seconds' VALUE total_cpu_seconds) + ), JSON_ARRAY()) + FROM ( + SELECT job_name, + ROUND(SUM(EXTRACT(DAY FROM cpu_used) * 86400 + EXTRACT(HOUR FROM cpu_used) * 3600 + + EXTRACT(MINUTE FROM cpu_used) * 60 + EXTRACT(SECOND FROM cpu_used)), 3) total_cpu_seconds + FROM USER_SCHEDULER_JOB_RUN_DETAILS + WHERE actual_start_date BETWEEN l_from_ts AND l_to_ts + AND (p_job_name_pattern IS NULL OR job_name LIKE UPPER(p_job_name_pattern)) + GROUP BY job_name + ORDER BY total_cpu_seconds DESC + FETCH FIRST l_top_n ROWS ONLY + ) + ) + ) + ) INTO l_json + FROM dual; + + write_log('generate_charts', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('generate_charts', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END generate_charts; + + FUNCTION export_tables( + p_start_time IN VARCHAR2, + p_end_time IN VARCHAR2, + p_owner IN VARCHAR2 DEFAULT NULL, + p_job_name_pattern IN VARCHAR2 DEFAULT NULL, + p_top_n IN NUMBER DEFAULT 500 + ) RETURN CLOB IS + l_start_ts TIMESTAMP := SYSTIMESTAMP; + l_json CLOB; + l_owner VARCHAR2(128) := UPPER(TRIM(p_owner)); + l_top_n NUMBER := clamp_top_n(p_top_n, 500, 5000); + l_from_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_start_time); + l_to_ts TIMESTAMP WITH TIME ZONE := parse_ts(p_end_time); + BEGIN + SELECT JSON_OBJECT( + 'all_jobs_table' VALUE list_all_jobs(p_job_name_pattern, LEAST(l_top_n, 10000)), + 'running_jobs_table' VALUE list_running_jobs(l_owner, p_job_name_pattern, l_top_n), + 'history_table' VALUE list_job_history(p_start_time, p_end_time, l_owner, p_job_name_pattern, NULL, l_top_n), + 'cpu_table' VALUE analyze_cpu_by_job(p_start_time, p_end_time, l_owner, p_job_name_pattern, LEAST(l_top_n, 1000)), + 'failure_table' VALUE detect_failures(p_start_time, p_end_time, l_owner, p_job_name_pattern, LEAST(l_top_n, 1000)) + ) INTO l_json + FROM dual; + + write_log('export_tables', l_start_ts, 'SUCCESS'); + RETURN l_json; + EXCEPTION + WHEN OTHERS THEN + write_log('export_tables', l_start_ts, 'ERROR', SQLERRM); + RETURN JSON_OBJECT('status' VALUE 'error', 'message' VALUE SQLERRM); + END export_tables; +END scheduler_monitoring; +/ + +CREATE OR REPLACE PACKAGE select_ai_scheduler_monitor AS + FUNCTION get_database_current_time RETURN CLOB; + + FUNCTION list_all_jobs( + job_name_pattern VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 1000 + ) RETURN CLOB; + + FUNCTION list_running_jobs( + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 200 + ) RETURN CLOB; + + FUNCTION list_job_history( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + status_filter VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 1000 + ) RETURN CLOB; + + FUNCTION get_job_details( + owner_name VARCHAR2, + job_name VARCHAR2 + ) RETURN CLOB; + + FUNCTION analyze_cpu_by_job( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 20 + ) RETURN CLOB; + + FUNCTION analyze_load_by_job( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + granularity VARCHAR2 DEFAULT 'HOUR' + ) RETURN CLOB; + + FUNCTION compare_jobs( + start_time VARCHAR2, + end_time VARCHAR2, + owner_name VARCHAR2, + job_a VARCHAR2, + job_b VARCHAR2 + ) RETURN CLOB; + + FUNCTION identify_hotspots( + start_time VARCHAR2, + end_time VARCHAR2, + top_n NUMBER DEFAULT 20, + min_elapsed NUMBER DEFAULT 0, + min_cpu NUMBER DEFAULT 0 + ) RETURN CLOB; + + FUNCTION detect_failures( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 100 + ) RETURN CLOB; + + FUNCTION summarize_scheduler_health( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 10 + ) RETURN CLOB; + + FUNCTION generate_charts( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + granularity VARCHAR2 DEFAULT 'HOUR', + top_n NUMBER DEFAULT 10 + ) RETURN CLOB; + + FUNCTION export_tables( + start_time VARCHAR2, + end_time VARCHAR2, + owner_filter VARCHAR2 DEFAULT NULL, + job_name_pattern VARCHAR2 DEFAULT NULL, + top_n NUMBER DEFAULT 500 + ) RETURN CLOB; +END select_ai_scheduler_monitor; +/ + +CREATE OR REPLACE PACKAGE BODY select_ai_scheduler_monitor AS + FUNCTION get_database_current_time RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.get_database_current_time; + END get_database_current_time; + + FUNCTION list_all_jobs(job_name_pattern VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 1000) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.list_all_jobs(job_name_pattern, top_n); + END list_all_jobs; + + FUNCTION list_running_jobs(owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 200) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.list_running_jobs(owner_filter, job_name_pattern, top_n); + END list_running_jobs; + + FUNCTION list_job_history(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, status_filter VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 1000) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.list_job_history(start_time, end_time, owner_filter, job_name_pattern, status_filter, top_n); + END list_job_history; + + FUNCTION get_job_details(owner_name VARCHAR2, job_name VARCHAR2) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.get_job_details(owner_name, job_name); + END get_job_details; + + FUNCTION analyze_cpu_by_job(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 20) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.analyze_cpu_by_job(start_time, end_time, owner_filter, job_name_pattern, top_n); + END analyze_cpu_by_job; + + FUNCTION analyze_load_by_job(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, granularity VARCHAR2 DEFAULT 'HOUR') RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.analyze_load_by_job(start_time, end_time, owner_filter, job_name_pattern, granularity); + END analyze_load_by_job; + + FUNCTION compare_jobs(start_time VARCHAR2, end_time VARCHAR2, owner_name VARCHAR2, job_a VARCHAR2, job_b VARCHAR2) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.compare_jobs(start_time, end_time, owner_name, job_a, job_b); + END compare_jobs; + + FUNCTION identify_hotspots(start_time VARCHAR2, end_time VARCHAR2, top_n NUMBER DEFAULT 20, min_elapsed NUMBER DEFAULT 0, min_cpu NUMBER DEFAULT 0) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.identify_hotspots(start_time, end_time, top_n, min_elapsed, min_cpu); + END identify_hotspots; + + FUNCTION detect_failures(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 100) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.detect_failures(start_time, end_time, owner_filter, job_name_pattern, top_n); + END detect_failures; + + FUNCTION summarize_scheduler_health(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 10) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.summarize_scheduler_health(start_time, end_time, owner_filter, top_n); + END summarize_scheduler_health; + + FUNCTION generate_charts(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, granularity VARCHAR2 DEFAULT 'HOUR', top_n NUMBER DEFAULT 10) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.generate_charts(start_time, end_time, owner_filter, job_name_pattern, granularity, top_n); + END generate_charts; + + FUNCTION export_tables(start_time VARCHAR2, end_time VARCHAR2, owner_filter VARCHAR2 DEFAULT NULL, job_name_pattern VARCHAR2 DEFAULT NULL, top_n NUMBER DEFAULT 500) RETURN CLOB IS + BEGIN + RETURN scheduler_monitoring.export_tables(start_time, end_time, owner_filter, job_name_pattern, top_n); + END export_tables; +END select_ai_scheduler_monitor; +/ + +CREATE OR REPLACE PROCEDURE initialize_scheduler_tools +IS + PROCEDURE drop_tool_if_exists(p_tool_name IN VARCHAR2) IS + l_tool_count NUMBER; + BEGIN + SELECT COUNT(*) INTO l_tool_count + FROM USER_AI_AGENT_TOOLS + WHERE TOOL_NAME = p_tool_name; + + IF l_tool_count > 0 THEN + DBMS_CLOUD_AI_AGENT.DROP_TOOL(p_tool_name); + END IF; + END drop_tool_if_exists; +BEGIN + drop_tool_if_exists('GET_DATABASE_CURRENT_TIME_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'GET_DATABASE_CURRENT_TIME_TOOL', + attributes => '{"instruction":"Get database current time (SYSTIMESTAMP/SYSDATE) and timezone values. Use this to resolve relative date requests like today/yesterday/last day.","function":"select_ai_scheduler_monitor.get_database_current_time"}', + description => 'Get database current timestamp and timezone' + ); + + drop_tool_if_exists('LIST_ALL_JOBS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'LIST_ALL_JOBS_TOOL', + attributes => '{"instruction":"List all scheduler jobs in the current schema with state, schedule, run/failure counts, and control attributes. For full-list requests, return complete rows/job names (do not sample), do not abbreviate with ellipsis, and set top_n high enough to cover all jobs.","function":"select_ai_scheduler_monitor.list_all_jobs"}', + description => 'List all scheduler jobs in current schema' + ); + + drop_tool_if_exists('LIST_RUNNING_JOBS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'LIST_RUNNING_JOBS_TOOL', + attributes => '{"instruction":"List currently running scheduler jobs with session, elapsed, cpu and destination details.","function":"select_ai_scheduler_monitor.list_running_jobs"}', + description => 'List running scheduler jobs' + ); + + drop_tool_if_exists('LIST_JOB_HISTORY_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'LIST_JOB_HISTORY_TOOL', + attributes => '{"instruction":"Return historical scheduler job runs for the requested interval with status, duration, cpu, and error details.","function":"select_ai_scheduler_monitor.list_job_history"}', + description => 'List scheduler job run history' + ); + + drop_tool_if_exists('GET_JOB_DETAILS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'GET_JOB_DETAILS_TOOL', + attributes => '{"instruction":"Return full metadata and recent history for a selected scheduler job.","function":"select_ai_scheduler_monitor.get_job_details"}', + description => 'Get scheduler job detail view' + ); + + drop_tool_if_exists('ANALYZE_CPU_BY_JOB_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'ANALYZE_CPU_BY_JOB_TOOL', + attributes => '{"instruction":"Analyze CPU usage by scheduler job over an interval; include top cpu consumers and cpu-to-elapsed ratio.","function":"select_ai_scheduler_monitor.analyze_cpu_by_job"}', + description => 'Analyze per-job CPU usage' + ); + + drop_tool_if_exists('ANALYZE_LOAD_BY_JOB_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'ANALYZE_LOAD_BY_JOB_TOOL', + attributes => '{"instruction":"Analyze execution/load trends by minute/hour/day over an interval and provide inferred load correlation.","function":"select_ai_scheduler_monitor.analyze_load_by_job"}', + description => 'Analyze scheduler load trends' + ); + + drop_tool_if_exists('COMPARE_JOBS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'COMPARE_JOBS_TOOL', + attributes => '{"instruction":"Compare two jobs for runs, failures, average/p95 elapsed and cpu metrics.","function":"select_ai_scheduler_monitor.compare_jobs"}', + description => 'Compare two scheduler jobs' + ); + + drop_tool_if_exists('IDENTIFY_HOTSPOTS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'IDENTIFY_HOTSPOTS_TOOL', + attributes => '{"instruction":"Identify cpu/runtime hotspots and contention candidates across scheduler jobs.","function":"select_ai_scheduler_monitor.identify_hotspots"}', + description => 'Identify scheduler hotspots' + ); + + drop_tool_if_exists('DETECT_FAILURES_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'DETECT_FAILURES_TOOL', + attributes => '{"instruction":"Detect recurring failures, common errors, and failure-prone jobs in the interval.","function":"select_ai_scheduler_monitor.detect_failures"}', + description => 'Detect scheduler failures' + ); + + drop_tool_if_exists('SUMMARIZE_SCHEDULER_HEALTH_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'SUMMARIZE_SCHEDULER_HEALTH_TOOL', + attributes => '{"instruction":"Return scheduler health overview with executive summary, top cpu jobs, top failed jobs, and recommendations.","function":"select_ai_scheduler_monitor.summarize_scheduler_health"}', + description => 'Summarize scheduler health' + ); + + drop_tool_if_exists('GENERATE_CHARTS_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'GENERATE_CHARTS_TOOL', + attributes => '{"instruction":"Generate chart-ready datasets/specifications for scheduler performance and reliability analysis.","function":"select_ai_scheduler_monitor.generate_charts"}', + description => 'Generate chart datasets' + ); + + drop_tool_if_exists('EXPORT_TABLES_TOOL'); + DBMS_CLOUD_AI_AGENT.CREATE_TOOL( + tool_name => 'EXPORT_TABLES_TOOL', + attributes => '{"instruction":"Return export-friendly table payloads for running jobs, history, cpu, and failures.","function":"select_ai_scheduler_monitor.export_tables"}', + description => 'Export scheduler analysis tables' + ); + + DBMS_OUTPUT.PUT_LINE('initialize_scheduler_tools completed.'); +END initialize_scheduler_tools; +/ + +BEGIN + initialize_scheduler_tools; +END; +/ + +ALTER SESSION SET CURRENT_SCHEMA = ADMIN; + +PROMPT DBMS_SCHEDULER monitoring tools installation completed