Discoverer Migration Rules Copy link

Description
Category: Migration
Plugin: Blitz Report Development (discoverer-migration)
Works with: Claude Code
Type: Rules loaded automatically when a task needs them

Reviewing and finishing Oracle Discoverer workbooks converted to Blitz Reports.

When to use it

  • Use when working on Discoverer-converted reports.
Install

Claude Code

  1. Add the Enginatics marketplace in Claude Code.
    /plugin marketplace add enginatics/ai-skills
  2. Install the plugin.
    /plugin install blitz-report-ai-skills@enginatics
  3. Run the environment setup once; it describes your EBS development instance (never production).
    /blitz-report-ai-skills:blitz-environment-setup
  4. Nothing to run: Claude loads these rules automatically when a task needs them.
  5. Keep it current: enable auto-update for the enginatics marketplace, or update manually.
    /plugin marketplace update enginatics

Requirements: Blitz Report installed on your Oracle EBS development instance, and database access from Claude Code to that instance (Oracle SQLcl MCP server, SQLcl or SQL*Plus). Never point the skills at production.

Pipeline

  1. Export (DIS Workbook Export Script report) generates dis51adm.exe /export ... /xmlworkbook commands per workbook (if not exist re-run safety, ping stagger). Set Executable Path, DB Service Name, EUL + password. Ignore Access History/Workbook Id list workbooks without statistics.
  2. Upload (Setup>Tools>Import>XML Upload, Win10-style zip — fix-zip-compatibility skill) → xxen_discoverer_workbook_xmls keyed (eul, doc_id), EUL from profile XXEN_REPORT_DISCOVERER_DEFAULT_EUL (set it BEFORE upload when >1 EUL). Workbooks whose owner is unknown on the target are rejected.
  3. Select (DIS Workbook Conversion Selection upload, XXEN_DIS_UPLOAD) → convert_flag NULL / Y selected / C converted; Submit Import=Yes submits and waits for the import and reports per workbook; Report Name Prefix param.
  4. Import (Blitz Report Discoverer Import = xxen_xdo.import_discoverer_folders_cp; params End User Layer, Report Name Prefix, Object Use Key (;-list), Existing Reports (table value set XXEN_DISCOVERER_EXISTING_REPORTS over a union all from dual, codes UPDATE/SKIP/REPLACE; Skip is offered only without an Object Use Key and Delete and Recreate only with one, through :$FLEX$.XXEN_DISCOVERER_OBJECT_USE_KEY:NULL; folder sets without a report are always imported)).
  5. Validate with the Blitz validation reports (Parameter Uniqueness, LOV SQL, SQL, Bind Variable, Table Alias, DIS Migration identify missing EulConditions, DIS Workbook Import Validation) and the discoverer_*_fix.sql scripts (static date defaults, All/% sentinels, output file name token, pivot/having, range parameter order) after every round.
Profiles
ProfilePurposeDefault
XXEN_REPORT_DISCOVERER_DEFAULT_EUL ("Blitz Report Discoverer Default EUL")EUL that the XML Upload tags all uploaded .eex with, via xxen_util.dis_default_eul. Set this before upload if there is more than one EUL.—
XXEN_REPORT_DISCOVERER_IMPORT_LOV_ACCESS_HISTORY_DAYSlook-back window for the export script's "recently used" default (Accessed After). The import no longer reads it — see the stats-free section below600 days
XXEN_REPORT_DISCOVERER_IMPORT_REPORT_NAME_PREFIXfallback report-name prefix when the conc Report Name Prefix param is blank—

Staging tables

object_use_key = dot-separated list of the Discoverer folder obj_ids a worksheet draws from, derived from the flattened .eex by xxen_xdo.sheet_object_use_key. It is the grouping key: one Blitz Report is built per distinct object_use_key. The created report records its identity in the description as Object IDs: <object_use_key> + EUL: <eul> — this marker drives the "already imported" exclusion, the targeted Object Use Key reload, and the delta refresh's report lookup. It is NOT stored in a column, so re-deriving it is the only way to find an existing report for a folder set.


Condition handling — and the "hardcoded conditions ≠ defaulted parameters" rule

Condition kindExamplecomplex_conditionDefault import result
Parameter-drivenitem = :MyParamN (parameter_value not null)visible parameter — the genuine prompt
Hardcoded, simple operatorstatus = 'ACTIVE', dept in (…), name like 'X%'N (literal, simple op, no calc)visible parameter (e.g. Status) pre-filled with the literal; SQL rewritten to status=:status
Hardcoded, complex / no literalcalc-based, between, is nullYhidden Yes-toggle (negative display_seq), default Y, full condition baked into SQL via 2=2

Purge per EUL

Per-EUL purge (from the report description) — deletes the 5 staging tables for l_eul, then all Discoverer-category reports + their orphaned (all-uppercase-description) LOVs:

declare l_eul varchar2(30):='eul_us';
begin
  delete xxen_discoverer_workbook_xmls xdwx where xdwx.eul=l_eul;
  delete xxen_discoverer_fnd_user xdfu where xdfu.eul=l_eul;
  delete xxen_discoverer_pivot_fields xdpf where xdpf.eul=l_eul;
  delete xxen_discoverer_sheets xds where xds.eul=l_eul;
  delete xxen_discoverer_workbooks xdw where xdw.eul=l_eul;
  for c in (select xrca.report_id from xxen_report_categories_v xrcv, xxen_report_category_assigns xrca where xrcv.category='Discoverer' and xrcv.category_id=xrca.category_id) loop
    xxen_api.delete_report(c.report_id,'Y');
  end loop;
  for c in (select xrplv.lov_id from xxen_report_parameter_lovs_v xrplv where xrplv.description=upper(xrplv.description) and xrplv.lov_id not in (select xrp.lov_id from xxen_report_parameters xrp where xrp.parameter_type='LOV' and xrp.lov_id is not null)) loop
    xxen_api.delete_lov(c.lov_id);
  end loop;
  commit;
end;

For a single folder set, prefer the conc program's Object Use Key parameter (deletes+recreates just that report) over a full purge.

Phase D — Post-import validation (the real cleanup)

Run these after import (from the export script description); each catches a class of Discoverer-ism that doesn't translate cleanly:

ReportCatches
Blitz Report Parameter Uniqueness Validationduplicate parameter names within a report (Discoverer allows dupes; Blitz needs unique)
Blitz Report LOV SQL ValidationLOVs that error; slow distinct-style LOVs to rewrite as fast SQL
Blitz Report SQL Validationmain SQLs that don't parse
Blitz Report Parameter Bind Variable Validationbinds that don't resolve
DIS Migration identify missing EulConditionsDiscoverer conditions not carried over
Blitz Report Parameter Table Alias Validationalias mismatches
Blitz Reports (col required_parameters)reports that came in over-required (see C4 step 10)
Blitz Report Parameter Default Valuesdefaults containing partition (see C4 step 11)
Blitz Report Templates (desc Subtotals: Y)compact-pivot sheets — retrain users to switch to tabular
DIS Workbook Import Validationcoverage gaps: which workbooks/worksheets are uploaded, selected, converted, and which lack a migrated report/template

DIS Workbook Import Validation was reworked to the stats-free methodology: worksheet spine from the flattened staged xmls (eul5_documents FULL-joined to xxen_discoverer_workbook_xmls, so both EUL-only and staged-after-deletion workbooks appear), Object Use Key derived per sheet via xxen_xdo.sheet_object_use_key, Selected For Conversion/Converted from the flag, report matched by the derived object use key (OUK) marker, template by the Doc Id:+Sheet: markers; usage statistics OUTER-joined as informational columns bounded by Accessed after. The Include Inactive parameter (the old &full stats join toggle) was removed and Workbook Owner retargeted from the stats rows to the workbook owner. A helper sheet ([9,n]-referenced) shows its folder set's report but no template — the Not yet imported filter (template_name is null) therefore lists helper sheets too; expected, not a gap. An underivable workbook (foreign-EUL folders) shows uploaded+selected with a blank OUK and nothing matched — exactly the gap the report exists to surface.

Post-import DATA fixes — six scripts, run after EVERY import round

The validation reports above find what does not parse. These repair what parses and is still wrong — so nothing errors and only a human notices. Each takes PREVIEW / APPLY (no commit) and lists rather than guesses; re-run them after every round, because the conversion keeps emitting what they repair.

scriptrepairs
discoverer_static_date_default_fix.sqla static date defaulted into a Date parameter. A Discoverer worksheet stores the LAST RUN values, not defaults, so the migrated report opens pinned to one historical window. Dynamic defaults (sysdate, trunc(sysdate,'MM'), a select, :$flex$) are legitimate and kept; locked defaults and defaults on an EXCLUDED parameter (keep (hidden) -- a converted hardcoded date condition, where the default IS the restriction) are never touched. The converter now writes no visible static date default itself (a hardcoded date is stored canonically on an excluded parameter, a prompted date default is skipped), so the script's remaining targets are older conversions and the AI fallout generator, which does not check the parameter type
discoverer_all_sentinel_fix.sqlDiscoverer word sentinels (All/Yes/NULL): defaults corrupted to <multiple_values>X;All;X, and the decode(:b,'All',<self>,:b) construct → (:b='All' or col like :b)
discoverer_pct_%_fix_sql_texts.sqldead '%' decodes in parameter SQL, only where '%' can no longer reach the bind
discoverer_pct_%_wildcard_fix.sql'%' wildcard defaults (always-true predicate → wide-open scan) + the stranded required='Y'
discoverer_output_file_name_fix.sqlthe underscore token <template_name> the import writes (see the Gotchas entry on the two spellings)
discoverer_template_pivot_and_having_fix.sqla &template_having_clause default with no column aggregation (a HAVING with nothing aggregated filters nothing → SUM is set on the number columns), and templates over the 40 pivot-ROW-field limit that must fall back to tabular
discoverer_range_parameter_order_fix.sqlrange parameter pairs prompted upper-bound-first on reports converted before the converter's ordering correction — replays the converter's adjacent-pair and grouped-block merges (same range-name reduction, same shared-name guard) on already-converted reports and resequences; needed because a refresh run preserves matched parameters' sequences, so the import-time correction never reaches existing reports. Name-based only — no dependency on the flattened staging tables or the document-order column, so it works on instances gathered before that column existed
The parameter ceiling — a report can carry only as many parameters as the concurrent program has formals

xxen_report_ebs.report_host passes parameters positionally through XXEN_REPORT: it numbers them row_number() over (order by display_sequence, parameter_id) over the rows where display_sequence is not null (or the whole same-name group is invisible), and dispatches with a literal elsif c.parameter_number=<n> and parameter<n> is not null chain. There is no branch past the last formal, so a parameter beyond it is SILENTLY DROPPED at submit — the report runs with one filter not applied. A wrong answer, not an error.

  • A migration hits this because grafting merges parameter sets. Each Discoverer worksheet brings its own prompts onto the shared report, and the count only grows.
  • Hiding does not free a slot. The test is display_sequence IS NOT NULL: a per-template PARAMETER_EXCLUSION leaves the sequence intact, and a negative sequence is not null either. Only a row with no DISPLAY_SEQUENCE at all (a sub-row) is free — and a name whose rows are all sub-rows still takes one slot. A template parameter default frees nothing.
  • Remedies: merge duplicate prompts that mean the same thing; demote a pure anchor-injection parameter to a sub-row; or split the report with discoverer_split_template_to_copy.sql so each copy carries only its own templates' parameters (then re-export the baseline and regenerate). Never delete a live parameter to make room — that is destructive and needs a ruling.

A converted report with MORE THAN 85 PARAMETERS cannot be submitted — consolidate the hidden worksheet-condition toggles into matching-value parameters

The hard limit: bin/XXEN_REPORT.prog / xxen_report_ebs.report_host declare parameter1..parameter85, so a report whose parameter count exceeds 85 cannot run at all (heavy Discoverer workbooks reach this — one worksheet condition = one hidden Yes-toggle parameter each). Only DISPLAY rows count: the report_host argument-mapping cursor filters display_sequence is not null (a name's sub-rows are excluded via the min(display_sequence) over (partition by parameter_name) test) — so matching-value sub-rows sharing one parameter name consume NO argument slot, which is what makes consolidation possible without touching the run screen.

The recipe (worked example: a report with 93 parameters and 150 templates, reduced to 71 parameters): the conversion's hidden Yes-toggle condition parameters (negative display_sequence, shared Yes LOV, literal bind-free SQL_TEXT, activated ONLY by a per-template default 'Y', never excluded by any template) merge into a few hidden matching-value parameters:

  • One merged parameter Template Condition <n> per group: primary row carries DISPLAY_SEQUENCE (negative), LOV custom listing the codes (select '<CODE>' value, '<original condition name>' description from dual union all …), VALIDATE_FROM_LIST_DSP=Y; each condition becomes a row with MATCHING_VALUE=<code> + its original SQL_TEXT; each template's default row changes from (toggle,'Y') to (merged param, <code>).
  • Grouping constraint: two conditions defaulted by the SAME template must land in DIFFERENT merged parameters (one parameter holds one value). Build the co-occurrence conflict graph over the template defaults and color it — the max co-occurrence dictates the merged-parameter count (e.g. max 3 co-occurring conditions → 3 parameters absorb 24 conditions).
  • When the max co-occurrence is too high for coloring, use ONE merged parameter with one code per DISTINCT per-template condition SET instead — each matching row's SQL_TEXT is that set's member conditions each wrapped in parentheses and AND-joined. Worked example: a report (98 → 75 parameters) had a template defaulting 8 conditions at once (per-condition coloring would only reach 82), but only 20 distinct condition sets across 174 templates → one parameter, 20 matching rows, each template defaults its set code. The trade-off is SQL duplication across combo rows (a condition edited later must be edited in every set containing it) — acceptable because these are frozen Discoverer worksheet conditions, and it is actually truer to the original (one code = one worksheet's condition set).
  • A toggle defaulted by NO template is dead (hidden + no report default = never injects) — delete it outright.
  • Template defaults resolve by NAME to the display row only (import join xrp.display_sequence is not null), so the sub-rows can never receive a default. Exclusions were never written for the toggles by the conversion, so no exclusion rewiring is needed — and the visible parameters (with their thousands of per-template exclusion rows) stay byte-identical.
  • Import-on-top is safe when kept rows keep their exact SORT_ORDER (the upsert key) and the new merged rows use fresh sort orders past the old max; the removed toggles' rows are seeded and get deleted by the parameter sync, cascading their old default rows.

Verify three ways: (a) offline — per template, the set of injected condition SQLs before vs after must be identical; (b) post-import DB — display-param count, defaults land on exactly the merged display rows, no dangling exclusion/default rows; (c) runtime — xxen_report.initialize_parameter_values(report_id, null, template_id) puts the CODE into the tmp values, xxen_api.create_run_id harvests it, and the executed SQL (v$sql) contains exactly the defaulted conditions and none of the sibling matching rows. A missing customer view is stood in per the established stand-in pattern (typed null columns from the referenced-column list), then dropped. When asserting injection via v$sql, filter on length(sql_fulltext) and probe sql_fulltext with dbms_lob.instr — a bare sql_text like '%<literal>%' check matches your OWN monitoring query (self-observation), and sql_text holds only the first 1000 chars, so the real run cursor (whose FROM clause sits ~75k chars in) is missed while the false self-match "passes".

Converter design (statistics-independent)

  • One Blitz Report per object_use_key; worksheets become templates (template_name = Workbook[: Sheet], Doc Id:/Sheet: markers). Category Discoverer. Anchor 2=2.
  • Update (Existing Reports=UPDATE): builds a new report and transplants it (refresh_report); parameters matched by normalised sql_text (else anchor), unmatched target parameters kept; templates matched by worksheet name, hand-made ones untouched; template defaults/exclusions inserted only where absent. Always reconciles parameters (no report-only option). Old stats-path Object IDs: markers may not match derived keys — compare before re-running.
  • Post-import corrections done by the converter: duplicate names (lower(trim()) comparison + unique_parameter_name), range-pair ordering (adjacent and grouped merges keyed on the direction-word-stripped range name), prompt order from .eex document order (display_sequence captured at flatten), % defaults dropped, degenerate/oversized (>40 row fields) pivots removed, operator suffixes in names (from/to unbracketed, others (equals) etc.), ORA-00936 items skipped. No post-build parse (use Blitz Report SQL Validation).
  • Hardcoded conditions (parameter_value null, complex_condition='N') become per-template EXCLUDED parameters carrying a template default that the engine still applies (initialize_parameter_values change); dates stored canonical YYYY/MM/DD; prompted date defaults are skipped (last-run values). Identification is by origin only — name/operator heuristics over-matched twice.
  • Reports over 85 parameters cannot be submitted (report_host formals; hidden and negative sequences still count): consolidate hidden condition toggles into matching-value parameters (one per co-occurrence colour, or one code per distinct condition set); verify via v$sql sql_fulltext.
  • Never render the literal token [5,1,"NULL"] as null: Discoverer itself generates it as the string 'NULL' (its saved worksheet SQL in ams_discoverer_sql shows TO_NUMBER(DECODE(...,'NULL')), ELSE 'NULL', NVL(x,'NULL') IN (...,'NULL')), and xml_expression_to_rpn/dis_formula already reproduce that. The NULL keyword is a different token, the function [1,115](), rendered null. A quoted 'NULL' in a numeric position therefore raises ORA-01722 in Discoverer too — a latent error in the source worksheet, fixed per report, not in the converter.
  • The SQL Discoverer generated for a worksheet is in ams_discoverer_sql (chunks by sequence_order, assembled by xxen_util.dis_worksheet_sql), written by the EUL function eul_trigger$post_save_document on every save; workbook_owner_name is the EUL user name (#<user_id>) and there is no doc id. Use it to settle what Discoverer did with an expression before changing the converter.
  • Query@IsDistinct → distinct_flag → select distinct wrap, which collapses identical detail rows; a Discoverer output showing duplicates ran without the flag.
  • A <multiple_values> parameter on a converted report over a complex customer view (a DISTINCT view with per-row lookups or scalar subqueries) runs the view UNFILTERED. The engine rewrites col=:bind to col in (select dbms_lob.substr(xrrpv.value) ... from xxen_report_run_param_values xrrpv ...); a subquery predicate is not pushed into the view the way Discoverer's literal in ('…') is — Oracle unnests it to a semi-join ABOVE the view, so the view executes for every row before the item list applies. On 12c+ the CLOB reference is what excludes the subquery from complex view merging (a merge hint cannot force it; the identical subquery over a varchar2 column merges); on 11.2 neither form merges. Plan signature: the VIEW as the outer side of a NESTED LOOPS SEMI probing XXEN_REPORT_RUN_PARAM_VALUES last, with a normal-looking cost. Single-value runs are unaffected (=:bind pushes like a literal) and are the immediate workaround; push_pred on the view is refused (not legal into a DISTINCT view). Per-report fix: a hidden same-name sub-row on an &item_filter lexical anchor whose SQL_TEXT reads the run's values from xxen_report_run_param_values by :run_id (always bound in dynamic_sql_text) and parameter name, splits them with xxen_util.rowgen into a quote-escaped literal in list, capped (case when count(*) between 1 and 200) so oversize selections fall back to the bind subquery; the visible parameter and its bind predicate stay untouched. Two lexical-evaluation traps: other binds inside a lexical resolve to ONE value even for multi-select (insert_run_bind_values keeps min keep dense_rank first per bind — never reference the parameter's own bind), and lexicals evaluate BEFORE build_sql expands the stored <multiple_values>a;b;c row into N rows, so the lexical must handle both storage forms. Engine-level literal inlining is the general fix (product decision pending). Never test view mergeability with a no_merge harness (it suppresses the transformation in question) and never extrapolate merge behaviour across database versions.
  • "Columns missing": diff the Discoverer SQL against xxen_reports.sql_text; negative template display_sequence = column absent from the SQL; fix the SQL, never the template; <name>2 suffixed columns select the source column twice.
  • Class-A ORA-00904 "<ALIAS>"."<COL>" on old-engine reports = orphan FROM folder; re-convert on the current engine.
  • Purge per EUL: delete the five staging tables, delete_report for Discoverer category, orphan LOVs; prefer the Object Use Key parameter for one folder set.