Custom Reports Migration Rules Copy link
Plugin: Blitz Report Development (
custom-reports-migration)Works with: Claude Code
Type: Rules loaded automatically when a task needs them
Rules for migrating custom Oracle EBS reports gathered by the Blitz Report Custom Reports Converter into Blitz Reports.
When to use it
- Use with the migrate-custom-reports skill.
Claude Code
- Add the Enginatics marketplace in Claude Code.
/plugin marketplace add enginatics/ai-skills - Install the plugin.
/plugin install blitz-report-ai-skills@enginatics - Run the environment setup once; it describes your EBS development instance (never production).
/blitz-report-ai-skills:blitz-environment-setup - Nothing to run: Claude loads these rules automatically when a task needs them.
- 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.
Engagement workflow: /migrate-custom-reports. Phase 1 = the customer runs the Blitz Report Custom Reports Converter upload (gathers RDF/host/SQL, .ldt, BIP _ds.ldt/.rtf, the custom dependency closure, <PROG>_assignments.xml, value-set/lookup ldts, Noetix closures) into a zip that is the gather request's OUTPUT (download_filename falls back to outfile_name). Phase 2 = classify and build on the conversion instance (instance-agnostic).
Gathered folder layout (Phase-1 output)
A converter run produces CONV_<ABBR>_<reqid>_<timestamp>/ containing manifest.txt, gather_summary.txt, import_reports.sh, and one subfolder per selected program:
| File | Meaning | Reliable when… |
|---|---|---|
<PROG>.rdf | Raw Oracle Reports binary (execution method P) | always present for method P |
<PROG>_rdf.xml | rwconverter output (the data-model SQL) | only if rwconverter succeeded — absent when it segfaults (see gotcha) |
<PROG>.ldt | Concurrent-program definition: parameters + value sets + defaults | always — the authoritative parameter source |
<PROG>_ds.ldt | BIP data-definition registration (template metadata) | BIP reports only — contains no SQL |
<PROG>_<lang>_<PROG>.rtf | BIP layout template — <?FIELD?> codes = the columns the customer's layout actually outputs | BIP reports only |
_xdo_metadata.txt | BIP template→layout mapping | BIP reports only |
<OWNER>.<NAME>.<type>.sql | Custom dependency DDL (table/view/package/function/index) | present only when the report has custom DB objects — a key Outcome-3 signal |
Classification
Evidence-based, never by name: standard value sets + stock tables + no custom DDL → Outcome 1 recommend the shipped Blitz Report (no clone by default); partial → Outcome 2 enhance the existing report; bespoke SQL/custom objects/user security → Outcome 3 new report via convert-bip-and-rdf-reports-to-blitz. Category always Custom Reports <ABBR>. <DESCRIPTION> names the source program + user name and says what the report does.
Description — MANDATORY (state the source report + what it does)
Every migrated/built custom report's <DESCRIPTION> must record (a) the original report it was migrated from, as its last two lines Source: <user concurrent program name> and Short Name: <program short name> (the view name for a custom view; e.g. Source: Work Order Schedule Detail Report / Short Name: XXWOSCHDTL), and (b) a brief, plain-language description of what the report is for (the entity it lists, the key columns/measures, and the main filters). The two lines are the format Blitz Report's own concurrent program import writes, and Blitz Report Custom Reports Migration Tracker links each program to its converted report through the Short Name: line, so write them exactly.
- Placement + format follow
report-developmentskill: the<DESCRIPTION>goes inside<REPORT_TRANSLATIONS_ROW>(next to<REPORT_NAME>), not loose inREPORTS_ROW(a misplaced one is silently ignored on import → null description). Break it into short paragraphs with blank lines (flush-left), never one run-on block. - Shape: paragraph 1 = what it lists / key columns; paragraph 2 = the main parameters; then the
Source:andShort Name:lines, whichbuild_xml.pyappends from the spec'sPROGRAMandSHORT_NAME. A report cloned from shipped XML gets the two lines by hand. - Applies to Outcome 3 (new build) and any Outcome 1/2 clone. For a pure Outcome-1 recommendation (no report built) there is nothing to describe — the shipped report keeps its own description.
- Verify after import:
select dbms_lob.getlength(xrt.description) len, regexp_count(xrt.description,chr(10)) nl from xxen_reports_tl xrt, xxen_reports_v xrv where xrt.report_id=xrv.report_id and xrt.language='US' and xrv.report_name='<name>'— a NULL length means wrong placement;nl=0means it's a run-on block; the text must end with theShort Name:line of the source program.
Data-model SQL
Prefer _rdf.xml; .ldt is the authoritative parameter source; .rtf <?FIELD?> codes are the real output columns (keep multiplicity); method-P data is in the RDF, not _ds.ldt. Strip trailing -- comment lines and replace a connector-carrying RDF lexical with and 1=1 (both raise ORA-00933). PL/SQL (method I) programs: reconcile against the elements the package EMITS. An RDF whose data model reads only a temp table filled by its BeforeReport trigger converts to one query: the trigger's insert ... select becomes an inline view (the data model's group by over every column is a distinct), aggregated to the group the layout prints; compare the trigger's if branches first, they often differ only in from order.
A customer program that reads
xla_transaction_entities_upgkeeps it. The APPS viewxla_transaction_entitiesreturned no rows for Cost Management receiving entities on the conversion instance, in a plain application session and inside the concurrent request alike, so a converted report that swaps in the view passes every parse gate and returns the header only; the base-table synonym is what the customer's program proved. For RCV_ACCOUNTING_EVENTS entitiessource_id_int_1is the receiving transaction id, so a direct join torcv_transactionsis correct.In a PL/SQL cursor an identifier that is both a column of the queried table or view and a procedure parameter resolves to the column:
x.p_type = P_TYPEover a view that has ap_typecolumn is always true and filters nothing, so every branch of the view prints. Read each such predicate for name capture before converting it to a filter; inv$sqlthe ported cursor shows real parameters as binds and a captured name as the column.Which BI Publisher layout prints is decided by the template registrations in
<PROG>_ds.ldt(start and end dates, the default template), not by the file names: a layout whose end date has passed is not converted, and a_MAINfile with sub-templates can be an abandoned test. One layout can hold several tables behindif:conditions on a parameter; that is one report whose parameter selects the column list through a lexical.Dynamic SQL in the source converts to a lexical anchor, never to a frozen copy of what the SQL happened to be on the day of the gather. Whenever the program assembles a fragment at run time - a select list read from a lookup or a setup table, a FROM or WHERE fragment chosen by a parameter, a hint, an order by - the Blitz Report keeps that behaviour: a hidden Char parameter (or the visible parameter that drives the choice) with anchor
&<name>at the fragment's position and a SQL_TEXT that returns the fragment. For a column list the SQL_TEXT returns one line per column,<expression> <alias>with a comma on every line but the last (row_number() over (order by ... desc)>1), ordered as the program orders; a constant default (the lookup type) gives the parameter a value on every run and can be referenced as<parameter_value>. The engine fetches every row of that select into a clob and substitutes it, so thelistagg4000-byte limit does not apply (dynamic_sql_text). The FROM aliases must be the ones the stored expressions reference, and the column alias is the code the program printed as the heading, double-quoted ('"'||flvv.lookup_code||'"'; a lookup code is at most 30 bytes). Decision by the product owner: a customer's runtime-driven column list is delivered dynamic, even when a static list would pass every test.Every output column shows the heading the original printed, in its exact case and punctuation: the RTF header cell or the label beside the field, the header line a PL/SQL program writes, the lookup code a dynamic program prints. Blitz Report shows a double-quoted alias verbatim; any other alias renders as
initcapwith underscores as spaces, or through the global column translations (xxen_report.column_translation), which differ between versions. Keep a plain alias only where its plain rendering already equals the heading; otherwise alias the top-level select column as the quoted heading (x.debit_inr_amt "Debit Amount (INR)") and leave the inner queries on plain names. A template names a quoted column by the bare heading. Never ship an RDF formula, placeholder or summary name (CF_,CP_,CS_);build_xml.pyrefuses them.Heading limits: an alias is at most 30 bytes, so abbreviate a longer heading minimally; a heading printed twice needs a suffix, because Excel headers must be unique; an all-upper-case heading that is a plain identifier folds to the unquoted name even when quoted, so Blitz Report initcaps or translates it. Write such a heading with its underscores as spaces (
"ITEM CODE"forITEM_CODE), which keeps the printed capitals; a single upper-case word (TDS,STATUS) cannot be kept and shows asTds/Status- list those for the customer. A single-level query whoseorder bynames a select alias must follow the rename. A heading built from parameter values ('0-'||p_age1||' QTY') is a hidden lexical whose SQL_TEXT binds those parameters and returns the aliased columns;dynamic_sql_textbinds from the run's bind values. Verify after import by rendering every parsed column throughxxen_report.column_translationand comparing with the printed list.A BI Publisher cross-tab (
horizontal-break-table,for-each-group@column) converts to an embedded tabular pivot template (PIVOT_LAYOUT: 'T',REPEAT_LABELS: 'Y'inbuild_xml.py): the printed line columns as row fields, the grouping column as the column field, the amount as a value. Every row field must hold one value per printed line, or the pivot splits the line: a data-template column that varies by the cross-tab column (the layout prints only the first row's value) is made per line first. Excel places row fields left of the column field and shows an empty row field as(blank); say so to the customer. A layout's<?position()?>isrow_number() over (<the report's order by>); with a top-leveldistinct, usegroup byinstead so the numbering is applied after duplicates are removed.Do not use a dynamic column translation rule for customer-specific headings: the importer processes rules before reports and skips a report-restricted rule whose report does not exist yet, and
delete_reportremoves the rule's report association, after which the rule applies to every report.Lexical gates:
xxen_api.create_run_id_applies no defaults, so a scripted run must pass the hidden parameter explicitly ("Item Columns=<lookup type>") or the anchor is stripped and the run fails withORA-00936; the design-time parse needsxxen_report.initialize_run_param_vals_tmp(id)theninitialize_parameter_values(id,'Y')beforeparse_report_sql_columns, as the form does - a bare parse strips the unresolved anchor and reportsError parsing SQL.. Run the lexical's own select standalone first and count its lines against the source.
Parameter fidelity — the authoritative process (MANDATORY, NEVER shortcut)
The data-model SQL bind list is NOT the parameter source. The concurrent-program .ldt is. Building parameters from the RDF/rwconverter SQL binds is the single biggest conversion failure mode — it silently drops parameters, invents wrong ones, and ships with no LOVs. Typical defect: an LDT with 8 properly-prompted, value-set-backed parameters (incl. a required Stock Type) converted into 8 bare bind parameters with wrong names, no LOVs, no required flags — because the build read the SQL binds instead of the LDT. Follow every step below, in order, every time. Do not assume. Do not skip the verification gate.
Step P1 — Build the parameter inventory from the LDT (authoritative)
Parse the concurrent-program .ldt's parameter entities. For each parameter capture all of:
FORM_LEFT_PROMPT→ the Blitz parameter name (e.g.Stock Type,Item Category) — NOT the bind token, NOT a guess.FLEX_VALUE_SET_NAME→ the value set (its LOV).REQUIRED_FLAG→ BlitzREQUIRED.DISPLAY_FLAG = 'N'→ hidden parameter (negativeDISPLAY_SEQUENCE).DEFAULT_TYPE/DEFAULT_VALUE→ the default (e.g. a hiddenUSER_IDdefaulting tofnd_global.user_id, never a hardcoded1).SRW_PARAM→ the RDF user-parameter / bind token. THIS is the authoritative mapping between the concurrent-program parameter and the RDF, not the bind name guessed from the SQL.
Step P2 — Decide the parameter TYPE from the value set's validation_type (do NOT blanket-convert to LOV Oracle)
Each parameter's value set has a validation_type (select validation_type, format_type from fnd_flex_value_sets where flex_value_set_name='<name>') that decides the Blitz parameter type. Reuse the value set as LOV Oracle ONLY when it actually has a list/SQL behind it; otherwise use a plain type by format (or a custom LOV if a pick-list is wanted).
validation_type | Has a usable LOV? | Blitz parameter |
|---|---|---|
I Independent, D Dependent, T/Y Translatable, F Table | Yes (value list / table query) | PARAMETER_TYPE_DSP = LOV Oracle + LOV_NAME = <value-set name> — reuse it (no LOV_GUID, no LOV_QUERY_DSP); Blitz resolves the value set at runtime |
N None (format only) | No | plain type by format_type (C→Char, N→Number, X/date→Date), or a LOV custom if a pick-list is wanted |
P Pair / R Range Pair | No (from/to validation mechanism, no list) | plain type by format_type — the From/To pair is already modelled as the two Blitz parameters |
Putting LOV Oracle on an N/P value set is the ORA-00903 trap. Such a set has no list, so the run-screen value validation builds select x.value,x.id from () x where x.value=:bind (empty from () x) → ORA-00903 "invalid table name" the moment a value (incl. a default) is validated. Symptom: "Error validating value 'X' for parameter '...'. ORA-00903 ... from () x". Decide the type from validation_type up front.
F= Table is a real LOV — RETAIN it, don't replace it. The common mistake is treatingF(and "special") sets as "no list".Fis table-validated (has afnd_flex_validation_tablesrow), soLOV Oraclereuses the original pick-list (e.g.WIP_SRS_DISCRETE_JOBS_OPT→ discrete-job names; resolved at runtime soxxen_report_parameters_v.lov_queryis null — that's expected, not a failure). Confirm table-backed:select count(*) from fnd_flex_validation_tables ffvt, fnd_flex_value_sets ffvs where ffvt.flex_value_set_id=ffvs.flex_value_set_id and ffvs.flex_value_set_name='<name>'> 0.LOV customwhen the list isn't portable. For a customer-custom value set that won't exist on the conversion/validation instance (or to drop a cross-instance dependency), a portableLOV customis fine — match the SQL's value semantics (e.g. a report comparing the literal'Yes'/'No'→select 'Yes' id,'Yes' value,null description from dual union all select 'No' id,'No' value,null description from dual).- A
LOV customthat replaces an Independent value set copies itsVSET_VALUE/DESCRIPTIONpairs verbatim from the_vs_<name>_.ldt— id = the flex value, label = its description — and every id is then matched against the literals the SQL compares the bind to (decode(:currency,'FC',…)). Never write the labels from memory: a list whose ids are right but whose labels are swapped ('FC'labelled Invoice Currency) passes every parse and run check and reaches the customer as "values not reflected" / "parameters not matched". Prove it by running the report once per list value and checking the amounts change as the value set's descriptions say. LOV customquoting — author with REAL single quotes. The XML escaper turns'→'exactly once, which the import decodes back to'. Pre-escaping the LOV text with'double-escapes (&→&) and the import then stores the literal'→ the LOV won't compile. Verify by EXECUTING the STORED LOV (execute immediate 'select count(*) from ('||p.lov_query||') x'overxxen_report_parameters_v p), never a hand-typed copy.- A literal DEFAULT_VALUE on a custom-LOV param must be one of its VALUES (the run screen validates
x.value=:default). For an id≠value LOV a literal default is the display value (e.g. Drawer defaults toAll jobs, notA); an evaluated default — a bare function call such asfnd_profile.value('ORG_ID'), noselect … from dualneeded — returns the id and is validated as such (report-developmentskill "Parameter LOV Defaults").
Step P2a — Before authoring ANY LOV custom, search the shared Blitz Report LOV catalogue
Reuse ladder, highest first. A LOV custom is the LAST resort, never the default.
| # | Option | How |
|---|---|---|
| 1 | A shared Blitz Report LOV | PARAMETER_TYPE_DSP=LOV + LOV_NAME + LOV_GUID (content/lovs.xml / xxen_report_parameter_lovs_v) |
| 2 | The source program's own Oracle value set | LOV Oracle + LOV_NAME=<value-set name> (Step P2) |
| 3 | LOV custom | only when neither fits — and then the security rule below is mandatory |
Find the candidates for the master table the parameter picks from — run this for EVERY list-bearing parameter, and record the answer:
select xrplv.lov_name, xrplv.guid,
case when regexp_like(dbms_lob.substr(xrplv.lov_query,4000,1),'[[:space:],]id[[:space:],]','i') then 'has id' else 'name only' end idness,
case when dbms_lob.instr(xrplv.lov_query,'gl_access_set')+dbms_lob.instr(xrplv.lov_query,'org_access_view')+dbms_lob.instr(xrplv.lov_query,'mo_glob_org_access_tmp')>0 then 'security aware' else 'plain' end security
from xxen_report_parameter_lovs_v xrplv
where xrplv.lov_query is not null and
dbms_lob.instr(xrplv.lov_query,'<master table, e.g. gl_ledgers>')>0
order by xrplv.lov_name;Why reuse outranks convenience: the shared LOVs carry SECURITY and edge cases you will not reproduce by hand. Dozens of the shared LOVs are GL-access-set, org_access_view or MOAC aware. A converted report is replacing an Oracle report whose value set or form applied that security; a hand-written select ... from <master table> silently drops it. A converted report's LOV must never be a bare unfiltered select over a security-scoped master table (gl_ledgers, hr_operating_units, mtl_parameters, hr_all_organization_units, …).
When id-ness forces a custom LOV, LIFT THE SHIPPED LOV'S where CLAUSE VERBATIM — never improvise the security predicate. The shared LOV is often name-only (rule: never add an id column to it — see the P4 LOV gate), so a parameter binding a numeric *_id legitimately needs its own LOV. That is a reason to re-author the select list, never the filter.
- Worked example, and the exact trap. A converted report binds
:ledger_id, so the name-only sharedGL LedgerLOV could not be reused. The custom LOV shipped as a bareselect gl.ledger_id id, gl.name value, gl.description description from gl_ledgers gl order by gl.name— all 101 ledgers, no access-set restriction, while the parameter's ownDEFAULT_VALUEwas alreadygl_access_set_security_pkg.get_default_ledger_id(...). Customer-reported: the list and the default disagreed. - The improvised fix was ALSO wrong, and worse than the bug. A hand-written
gl.ledger_id in (select gasnav.ledger_id from gl_access_set_norm_assign_v gasnav where gasnav.access_set_id=fnd_profile.value('GL_ACCESS_SET_ID') and gasnav.access_privilege_code in ('R','B'))under-returns: an access set may grant a LEDGER SET, andgl_access_set_norm_assign_vdoes NOT expand it to its member ledgers. Measured: access sets 1160/1161 returned 1 ledger instead of 5, 1180/1225 1 instead of 3. Under-returning blocks a user from a ledger they are entitled to — a worse failure than listing too many. Theaccess_privilege_codefilter was additionally a no-op (all 115 assignments wereB) and would itself kill the expanded rows, which have no_vrow at all. - The correct predicate is the shipped
GL Ledgerone, copied unchanged:gl.ledger_id in (select nvl(glsnav.ledger_id,gasna.ledger_id) from gl_access_set_norm_assign gasna, gl_ledger_set_norm_assign_v glsnav where gasna.access_set_id=fnd_profile.value('GL_ACCESS_SET_ID') and gasna.ledger_id=glsnav.ledger_set_id(+))Note it uses the base table and the outer-join expansion, and carries no privilege filter. Verified after the correction: 5/5/3/3 for the ledger-set access sets, 3 for a 3-ledger set, 1 for a single-ledger set, default always present in the list.
- Generalise it: the shipped LOV is the specification. Read its
lov_query, take itswhereclause as-is, and change only the select list. If you find yourself writing security SQL from first principles during a conversion, you are about to ship one of these.
Step P3 — Reconcile each parameter against how it's actually used in the report
Principle (per the Blitz developer guide): prefer a 1=1 WHERE-anchor with the predicate in SQL_TEXT over embedding the value as a :bind in the main SQL — wherever possible. Convert every optional/independent filter to 1=1 + SQL_TEXT; keep a bare :bind anchor ONLY for non-filter logic that must stay inline (see the exception below). For every LDT parameter, find how its SRW_PARAM token is consumed and wire the Blitz anchor accordingly:
- Used as a
:bindfor an OPTIONAL equality filter in the SELECT's WHERE → prefer a1=1WHERE-anchor with the predicate inSQL_TEXT, not the bind embedded in the main SQL. RDFs almost always express optional filters as a self-referencing no-op bind, e.g.and nvl(mc.segment3,0) = nvl(:P_SEGMENT3, nvl(mc.segment3,0)). Convert that to the Blitz idiom: put a single1=1anchor at that spot in the SQL and move the predicate to the parameter asSQL_TEXT = nvl(mc.segment3,0)=:P_SEGMENT3(the bare predicate — NEVER prefixand/or; the framework ANDs it at the anchor — seereport-developmentskill "WHERE Clause Anchors"). The framework then injects the condition only when the user supplies a value, and the verbosenvl(...)=nvl(:bind,nvl(...))no-op trick disappears. Multiple optional filters share the one1=1anchor (they AND together). Keep a parameter as a bare:bindanchor (no SQL_TEXT) only when the bind is used in non-filter logic that must stay inline — e.g.:user_idinside(:user_id=1 and ...) or (:user_id=2 and ...)row-security, or a bind referenced inside aCASE/formula column. Match the LOV id/value semantics (seereport-developmentskill "LOV Binding").1=1anchor must be unique in the SQL (the framework injects at every literal1=1); rewrite any incidental1=1inside a subquery to a real predicate. Single- vs multi-value:nvl(col,0)=:bindis single-value; for multi-select use the unwrappedcol=:bindso the framework auto-expands toIN (...)(report-developmentskill "Multiple Values").- Worked example: Organization + the 4 category segments were converted from embedded
nvl(mc.segmentN,0)=nvl(:P_SEGMENTN,nvl(mc.segmentN,0))binds to1=1anchors withSQL_TEXT = nvl(mc.segmentN,0)=:P_SEGMENTN/d.organization_id=:p_inv_org_id.User IDandDecode/Encode Typestayed bind anchors (row-security + CASE column). - The self-referencing no-op also comes in a
DECODEform:nvl(col,x) = DECODE(:bind,'Y',y,nvl(col,x))is the same pattern as thenvlno-op — a Yes/No parameter whose 'Y' restricts and whose other values no-op. Convert to a matching-value parameter:LOV Oracle+LOV_NAME=Yes_No,MATCHING_VALUE=Y,1=1anchor,SQL_TEXT= the simplified restriction (nvl(xipcq.asst_sub_inv,2)=1⇔xipcq.asst_sub_inv=1); selecting No matches no row and injects nothing = the DECODE fallback. - The no-op's treatment of NULL is part of the row set.
col=nvl(:p,col)with an empty parameter iscol=col, which drops every row whose column is null: convert it to the predicatecol=:pplus a staticcol is not nullin the main SQL, unless the column is declared not null. An orphan bind in that form (no LDT parameter) is justcol is not null. With a stand-in on the column side (nvl(col,'X')=nvl(:p,nvl(col,'X')), or a dynamically appendedand nvl(col,1)=<value>) the predicate keeps the stand-in,nvl(col,'X')=:p, so a value equal to the stand-in still matches the empty rows as it did in the original;upper()stays on the side the original has it. - A free-text parameter on an upper-case value set is upper-cased. A format-only value set with
UPPERCASE_ONLY_FLAG = "Y"converted what the user typed in the submit form to upper case; a BlitzCharparameter passes the text as typed, so wrap the bind inupper(). - A legitimately-retained bind anchor still needs a pick-list. When a bind stays inline because it feeds SELECT-list logic (currency lookup, CASE/formula column), the parameter must NOT be a raw numeric-id entry (coding-standards #46 — users never type ids): give it an id/value LOV so users pick the display value while the bind receives the id. When the SQL compares the bind to a numeric literal or id column (e.g.
when :ledger_id=2223), the LOV MUST pass the id — a name-only shared LOV would bind the name into a numeric comparison (wrong result / ORA-01722) → a custom id/value LOV is required. Re-author only the SELECT LIST; take the shipped LOV'swhereclause verbatim (Step P2a). A bareselect gl.ledger_id id, gl.name value, gl.description description from gl_ledgers gllists every ledger with no GL access-set restriction. The correct form keeps that select list and adds the shippedGL Ledgerpredicate unchanged.
- Used only in RDF PL/SQL (formula columns, before-report trigger, lexical
¶m) and absent from the SELECT bind list → you MUST read the RDF PL/SQL (rwconverter XML<programUnits>/<formula>/triggers, orrdf-analyzer) to learn what it filters, then express that as a Blitz&lexicalor WHERE-clauseSQL_TEXT. A parameter missing from the SELECT binds is NOT an absent parameter — it is a PL/SQL-driven one. Never drop it. (aStock Type/P_RTYPEorDecode/Encode Type/P_DE_TYPEparameter used only in formula columns is exactly this case.) - Orphan binds in the SELECT with no LDT parameter (e.g.
:p_org_id, a commented-out--...:p_item) → do NOT create a parameter for them. Resolve to their RDF default, drop the dead predicate, or confirm intent. Never expose an internal/dead RDF bind as a user parameter.
Step P4 — Verification gate (a build is NOT done until ALL pass)
"It parses and runs wide-open" is NOT acceptance — a bind-only, LOV-less parameter parses and runs perfectly while being completely wrong. Before declaring the conversion complete, produce and check a parameter-by-parameter table proving fidelity to the LDT:
| Check | Pass condition |
|---|---|
| Count | Blitz parameter count = LDT parameter count (hidden included) |
| Names | Every Blitz name = the LDT FORM_LEFT_PROMPT (no invented/generic names) |
| LOVs | Parameter TYPE matches the value set's validation_type (P2): list/table sets (I/D/T/F) → LOV Oracle + correct LOV_NAME, and confirm the LOV passes the framework-wrapper gate below, not a bare row count; N/P sets → plain type (or LOV custom), never LOV Oracle (else ORA-00903 from () x). Custom LOVs verified through the framework wrapper below (not a bare select count(*)) |
| Required | Every LDT REQUIRED_FLAG='Y' parameter is REQUIRED in Blitz |
| Hidden/defaults | DISPLAY_FLAG='N' → hidden; defaults match (e.g. fnd_global.user_id, not a literal) |
| Wiring | Every parameter is reconciled per Step P3 (bind / SQL_TEXT / lexical) — none left as a naked bind that doesn't filter |
| Bind-anchor gate (mechanical — run it, don't eyeball) | select xrpv.parameter_name, xrpv.anchor from xxen_report_parameters_v xrpv where xrpv.report_id=xxen_report.report_id('<name>') and xrpv.anchor like ':%' — EVERY returned row needs a written P3-exception justification: the bind's ONLY uses in the main SQL are SELECT-list/CASE/row-security expressions. Automatic FAIL when the bind token sits in a WHERE filter predicate (plain equality, LIKE, or a self-referencing nvl/DECODE no-op), or when a retained numeric-id bind parameter has no id/value LOV. Bind-anchor filters have shipped before — the fidelity table is produced BEFORE declaring done, not after the user complains |
| LOV gate (mechanical — but CALIBRATE it before believing a failure) | Run each LOV through select x.value, x.id from (<lov>) x, NOT a bare select count(*) from (<lov>) — the bare count hides a genuine id/bind mismatch. A raw x.id failure is NOT by itself a defect, and "every LOV needs an id column" is WRONG — the framework describes the LOV's select list to detect id-presence and wraps accordingly, so a name-only LOV is fully supported. Proven on the conversion instance: the shipped shared LOVs HR Operating Unit, GL Ledger, FND User Name, AR Customer Name and INV Organization Code all fail the raw x.id wrapper and pass value-only, and report-development skill documents them as name-only by design. So calibrate the gate against 3-5 known-good shipped LOVs first; if they fail too, your harness is over-strict and its hits are false positives (a typical uncalibrated run: 17 hits, 16 of them false). Never "fix" a name-only LOV by adding an id column — with an id present the bind receives the id instead of the display value (see report-development skill "LOV Binding"), which silently breaks every SQL_TEXT that compares a name column. The real defect is a mismatch: the LOV's id-ness vs what the consuming SQL_TEXT/:$flex$ actually binds — a name-only LOV feeding a numeric *_id column (ORA-01722, or 0 rows), or an id LOV compared against a name column. Judge each hit by reading its SQL_TEXT, not by the wrapper's exit code. Substitute a real parent value for each :$flex$.<parent> when testing dependent LOVs: xxen_api.lov_query_exec_nosort reads parents from the session's run-parameter table and returns 0 rows for a dependent LOV in a bare session, so execute the stored lov_query with the parent substituted. Org-access-aware shared LOVs (INV Organization Code) return rows only under a responsibility with organization access, never as System Administrator. |
| Shared-LOV reuse gate (mechanical — list every custom LOV and justify it) | select xrpv.parameter_name, dbms_lob.substr(xrpv.lov_query,300,1) from xxen_report_parameters_v xrpv where xrpv.report_id=xxen_report.report_id('<name>') and xrpv.lov_query is not null; — every row returned is a hand-written LOV (a LOV Oracle value set and a shared-LOV reference both store null here, so the query lists exactly the ones needing justification). For each: name the shared Blitz LOV considered and why it was rejected (Step P2a), the only valid reason being id-ness or genuine non-portability. Automatic FAIL: the LOV selects from a security-scoped master table (gl_ledgers, hr_operating_units, hr_all_organization_units, mtl_parameters, …) with no security predicate, or with a predicate improvised instead of copied from the shipped LOV. |
| Output-column reconciliation (BIP/RDF/PL-SQL completeness) | Diff the built column set against the AUTHORITATIVE rendered output — the RTF <?FIELD?> codes for BIP, the SELECT list for RDF, and for a PL/SQL (method I) program the elements the package EMITS (its '<TAG>' XML literals or fnd_file.put_line column list) — preserving multiplicity and order (never dedup the field list). Counts must match and every rendered field must map to exactly one Blitz column; produce the per-column mapping table and count it — a "core subset" delivery is only acceptable when the dropped columns are LISTED and the customer has agreed to the reduced scope in writing. A layout can render the SAME field code in two columns under different headers" and "(Latest Revision)"); a de-duplicated field scan drops the second column and ships a report one column short. Skipping this gate for PL/SQL programs ships registers missing most of their output elements (e.g. every tax column of a GST register) — found by the customer, not the build |
| Anchor SQL_TEXT not blank | No 1=1/&lexical-anchor parameter has empty SQL_TEXT (anchor like '%=%' or '&%' ⇒ SQL_TEXT must be non-empty) |
| Assignments | The report has <ASSIGNMENTS> (top-level, per report-development skill) so a responsibility can see it — consumed from the gathered <PROG>_assignments.xml; xxen_report_assignments_v returns the expected rows |
| Description | <DESCRIPTION> (inside REPORT_TRANSLATIONS_ROW) names the source Oracle report (<PROG> + user name) AND briefly says what the report does; broken into paragraphs (not run-on). Verify it imported non-null with line breaks |
| E2E with real values | Run through the Blitz frontend run screen (or with real filter values set, never wide-open) and confirm the filters actually restrict the output |
If any row fails, the conversion is not done — fix and re-verify. Never report a conversion as complete on a wide-open parse+run.
Mechanical rules for generating the parameter XML (each of these shipped a real defect first)
- A
LOV customquery returnsvalueanddescriptioncolumns,idoptional (null descriptionwhen there is nothing to show). The run form reads every custom list throughselect x.value, x.id, x.description from (select null id, x.value, x.description from (<query>) x) x where rownum<3, so a value-only list opens withError reading LOV value ... ORA-00904: "X"."DESCRIPTION": invalid identifieron every run screen whileselect count(*) from (<query>)gates pass.build_xml.pyrefuses a query without both aliases; the instance gate isselect x.value, x.description from (<query>) x where rownum<3for everylov_queryof the category. - An LDT SQL default (
DEFAULT_TYPE = "S",select <expr> from dual) may drop theselectonly when<expr>is somethingxxen_report.is_functionevaluates (fnd_global.,fnd_profile.,xxen_util.,xxen_report*.,sys_context(,:$flex$., a standard SQL function,sysdateon a Date,case ... end); anything else,mo_global.get_current_org_idincluded, is stored as literal text. On a Number parameter every run screen then stops withError validating value '<expr>' for parameter '<name>' of type Number ... ORA-06502, and the add-in, which echoes the literal back as the parameter value, fails the run withORA-06502: character to number conversion error.build_xml.pyrefuses such a default. Gate: printinitialize_parameter_values' message on its own (reusing its variable for the parse result hides it), and callxxen_webservices.report_metadataplusreport_param_lovfor the independent lists with the metadata defaults as binds, which is what the add-in does when the run screen opens. - A single operating unit program (
MULTI_ORG_CATEGORY = "S"in the LDT) took its operating unit from the submit form, andmo_global.get_current_org_id/fnd_global.org_idinside it returned that unit. A Blitz Report without a concurrent program link runs in multi-org mode, wheremo_global.get_current_org_idis null for a responsibility with several operating units andfnd_global.org_idis -1 without MO: Operating Unit, so a hidden parameter defaulted from either returns no rows there. Convert it to a visible required id/value list with the shipped HR Operating Unit where clause (mo_glob_org_access_tmp) and defaultfnd_profile.value('DEFAULT_ORG_ID'); the engine fills a required list that has a single row, so a single operating unit responsibility needs no default.
Whether you hand-author the XML or generate it, these four decide correctness and none is visible to a framework parse:
A
--PARAMpredicate can be lifted into ann=nanchor only when it sits at the SAME bracket depth as the anchors — not "depth 0". When the report is wrapped asselect ... from ( <branches> ) x, the anchors live at depth 1 and so do the liftable predicates; a depth-0 test then finds nothing liftable and every parameter degenerates to a:bindanchor — silently turning optional filters into always-applied ones and failing the Step-P4 bind-anchor gate. Compute the anchor's depth first, then lift only predicates whose every occurrence is at that depth. A predicate deeper than the anchor is inside a correlated subquery and must stay inline behind a:bind(the P3 exception). Verify all anchors sit at one depth and abort if they don't.The visible row (DISPLAY_SEQUENCE + LOV + default + required) belongs on the FIRST branch that actually carries a predicate for that parameter — never hardcoded to branch 1. In a multi-branch report a parameter used only in, say, branches 6 and 7 otherwise gets sub-rows only and no display row at all: it never appears on the run screen and
xxen_api.set_parameter_value_fails withORA-20001: Parameter "X" not found. Audit before importing — every distinct parameter name must have exactly one row containing<DISPLAY_SEQUENCE>.A plain-typed parameter (Date/Number/Char) must NOT be given an empty
LOV_QUERY_DSP. The framework buildsselect x.value, x.id from () x→ORA-00903 invalid table nameat run time (the same trap as puttingLOV Oracleon anN/Pvalue set, Step P2). EmitPARAMETER_TYPE_DSPalone and no LOV element.A bind that lands in an
org_id/*_idNUMBER column needs an id/value LOV. An OU LOV written ashou.name valuewith noidbinds the name intoorg_id→ORA-01722: invalid number. The failure can hide: if other predicates filter every row first, the run completes "Normal" with 0 rows instead of erroring, so a 0-row result is never proof of correctness — check the bind against the column type.Assignments are CONSUMED from the gathered
<PROG>_assignments.xml— never hand-listed, and every row keeps its OWNAPPLICATION_SHORT_NAME. The gathered file is the authoritative set (the source program's real request groups). Hand-typing a few group names into a config and hardcoding one application drops most of the access and points the rest at the wrong application — the reports then appear for almost nobody. A customer's real set typically spans several applications (the custom one plusJA/SQLAP/AR/JE/SQLGL).- Gate it OFFLINE against the gathered file — the instance-side count is NOT a substitute. On a conversion instance the customer's request groups do not exist, so the import legitimately reports 0 assignments and can tell you nothing about whether the XML was right. A zero there is unverifiable, not passed; treating it as passed is what let the above ship. Diff the built XML's
<ASSIGNMENTS_ROW>set against the gathered file (level_value + application_short_name, count exact, no extras) with no DB at all — reference implementationverify_assignments.pyin themigrate-custom-reportsskill. - The level code in a gathered assignments file may be the OBSOLETE single letter — always normalise it, never trust it. The converter hardcoded
Gwhile the product switched to full words, and an unrecognised code is silently skipped on import — so on any customer at that build or later the converter's own gathered file imports zero rows with no error. The converter is fixed to emitRequest Group, but every package gathered before that fix still carriesG, and nothing reprocesses them. Map the gathered code through the legacy table below and emit what the TARGET instance's build expects.
- Gate it OFFLINE against the gathered file — the instance-side count is NOT a substitute. On a conversion instance the customer's request groups do not exist, so the import legitimately reports 0 assignments and can tell you nothing about whether the XML was right. A zero there is unverifiable, not passed; treating it as passed is what let the above ship. Diff the built XML's
E2E harness quirks (bare xxen_api API, not report defects): set_parameter_value_ stores the value raw, so pass what the stored column expects — a DD-MON-YYYY date (canonical YYYY/MM/DD HH24:MI:SS raises ORA-01861 at bind time), and for an id/value LOV the id, not the display value. A run that reaches "Parameters and binds" in the log and then fails under FND_DATE is this, not the report.
A 0-row E2E on the conversion instance is usually a missing customer object, and you can prove which one: re-run the SQL with that single join relaxed and count. On one report this isolated an ungathered customer FND lookup (inner-joined to the ledger name) — 0 rows as shipped, 45,197 with the join relaxed. That is a complete, honest E2E result: the report is correct and the dependency is the customer's to supply.
The two mechanical gates above (LOV framework-wrapper, output-column reconciliation) exist because parse-only validation is blind to them; both defect classes have shipped and were caught by the customer, not the build. (1) A Period LOV missing its id column parsed fine and even returned rows to a bare count, but failed the framework's select x.value, x.id wrapper, so the report could not be run at all. (2) The Pending PO report was one column short because the RTF renders CP_LINE_PRICE twice and the field-code scan had deduplicated it. Neither is a parse failure — xxen_report.parse_report_sql_columns never opens a LOV nor compares the column set to the source, so a clean framework parse proves neither. When the conversion instance lacks the customer's master data (so a real frontend run is impossible), these two gates — plus, ideally, a column-and-value diff against one real Oracle output the customer supplies — are the substitute; run them before declaring done.
Four more classes that shipped in converted batches and were caught by customer UAT, each now a mandatory check:
- A visible parameter with NO type and NO LOV is a shipped trap — a "Ledger" parameter with neither (
PARAMETER_TYPE_DSPabsent) lets the run screen accept free text, so the user's ledger NAME goes into the numericate.ledger_idbind → ORA-01722 on every run. Same class: a "Supplier Site" LOV with noidcolumn while its SQL_TEXT bindsvendor_site_id. Both are exactly what the P4 bind-anchor + LOV framework-wrapper gates test — run those gates over EVERY parameter of EVERY report in the batch, not just the ones being actively worked on. - A STUB of a customer function must never guess the return type — normalize the consuming union positions to varchar2 instead. A stub declared
return datefor a customer function that really returns a character value makes every run die with ORA-01790 (union datatype mismatch) on their instance while parsing clean on ours. Fix pattern: wrap every select-list call of an unknown-signature customer function into_char(...)(legal for both date and varchar2 returns) and align the other union branches' expressions at those positions to text — the report then cannot depend on the real signatures at all. - Report SQL is a CLOB and is NOT capped at 32,767 bytes — do NOT flag a long SQL as a defect. Blitz Report supports report SQL beyond 32k: the engine reads the base CLOB, and a long-SQL report parses and runs normally. A
nullcoming back from a varchar2-bound path in your own ad-hoc probe is an artifact of that probe, not a product limit. When a multi-branch union needs per-branch columns, wrap the union inselect x.*, <new columns> from (<union>) xanyway — not for size, but so the additions are written ONCE and correlate via the union's exposed id columns (this also gives per-branch fallback semantics for free: a branch that exposes NULL ids makes that source's subquery return null). - Scalar lookups keyed on non-unique customer data need
max()— a bank-code lookup joiningce_bank_accountsby account name+number, which is not unique on the customer's data, raises ORA-01427 at run time. The original RDF swallowed the duplicate inside a formula-column exception handler, so the custom report "worked" — Blitz surfaces it. When converting an RDF formula column withWHEN OTHERS RETURN NULL/0around aselect into, aggregate the converted subquery (max()/sum()) so multi-row data degrades the same way the original did. Applymax()also to a cursor lookup without a handler whose key is unique only within one operating unit (order_numberalone): a report without an operating-unit filter raises ORA-01427 as soon as the number exists in two operating units.
Gathering gaps
- A method-P program whose folder holds only the
.ldtplus value sets (1 file(s) writtencounts only whatwrite_program_fileswrites; the.rdfcopy is the shell's) is usually a program whose EXECUTABLE belongs to another application than the program: the converter resolvesAPP_TOPfrom the concurrent program's application basepath (fcp.application_id), while the.rdflives under the executable's application top (fcp.executable_application_id), so the gather logsRDF file ... not found. CompareBEGIN PROGRAM "<X>" "<app>"withBEGIN EXECUTABLE "<X>" "<app>"in the LDT; ask the customer for$<EXEC_APP>_TOP/reports/US/<X>.rdfand leave the tracker row blocked; never classify from the parameters alone. classify_probe.pyreports a customer-owned object only for a<prefix>*table or a bare<prefix>*(function call; a customer dependency accessed some other way (a synonym under a standard prefix, an object referenced only from a trigger) still needs the gathered.sqllist read by hand. It does read a schema-qualifiedfrom <prefix>.<table>— an earlier version did not and silently returned "Outcome 1/2 candidate" for a report whose whole data source was a customer table, so re-check that behaviour after any change toFROM_RE.xxen_report_sql_columnsis a SESSION-duration GTT, so the output-column gate must parse and count in ONE session. Reading it in a fresh session returns zero rows, which reads exactly like a parse failure on a report that parsed fine. Putxxen_report.parse_report_sql_columns(<id>)and theselect ... from xxen_report_sql_columns where id=<id>in the same script (acommitbetween them is harmless).- Strip
fnd_date.canonical_to_date()from every converted date predicate. The RDF received its dates as canonical strings; a BlitzDateparameter binds a real DATE, andcanonical_to_date(<date>)returns NULL, so every predicate using it is silently never true and the report completes Normal with zero rows. Same family as the RDF'sp_<x>PL/SQL variables left unbound in a gathered package body — grep the converted SQL forcanonical_to_dateand for barep_[a-z_]+(no colon) before the first run. - A value set's LDT carries its security grants, and FNDLOAD aborts the whole file on the first responsibility the conversion instance lacks (
ORA-20002: LDR.GET_RESP(P) is not able to find ... RESP KEY), leaving the value set absent and the report'sLOV Oracledegraded. Drop theVSET_SECURITY_USAGE/VSET_SECURITY_LINE/VSET_SECURITY_RULEblocks before loading; the values and the table definition are what the LOV gate needs. - A value set's validation TABLE is never gathered, so its
LOV Oraclesilently degrades on the conversion instance.import_xmlstores aLOV Oracleparameter as a bareLOVwith no name when the named value set is absent, which reads exactly like a build defect (and is the "visible parameter with no LOV" trap) while the delivered XML is correct. Load the gathered<PROG>_vs_<NAME>_.ldtwith FNDLOAD (afffload.lct), create a stub for the table named in itsAPPLICATION_TABLE_NAME(shape comes fromVALUE_COLUMN_NAME/ID_COLUMN_NAME), then DELETE and re-import the report and re-readparameter_type_dsp— only then does the gate mean anything. Request the real objects from the customer in the same round. - A value set whose validation table is registered under the customer application fails FNDLOAD on the conversion instance (
ORA-20002: LDR.GET_APP(SHORT_NAME, P) is not able to find APP SHORT NAME:'XXCUST'): rewriteTABLE_APPLICATION_SHORT_NAME = "XXCUST"to"FND"in the copy you load. Test accommodation only; the delivered XML names the value set, not its application. - An LDT parameter with
ENABLED_FLAG=Nis invisible in the original and is not converted; list it in the mapping. Rendered fields that echo parameter values in the layout header (organization name, the from/to dates) are not report columns either, and must not turn their binds into:bindanchors. - Never use a
modelclause in report SQL.dbms_sql.parseaccepts it, but the framework'sdescribe_columns3fails withORA-00984: column not allowed here, so the report parses to a single error row. A PL/SQL loop that accumulates receipts until the running total is positive, allocatesleast(total, remaining)and resets is the same as, for cumulative receiptsCnewest first and the running maximumMof the previous rows floored at 0,greatest(0,least(C,onhand)-least(M,onhand))per row, withonhand-least(max(C),onhand)left unexplained — two analytic layers, no recursion. - A
1=1anchor works inside aconnect by ... start with (org,item) in (select ... where 1=1 ...)subquery: the framework injects textually, so the top-item filters of a BOM explosion belong there. A program that callsbompxinq.exploder_userexitis converted by reproducingBOMPBXIN.bom_exploder(read it on the instance): a recursivewithoverbom_bill_of_materialsxbom_inventory_components(primary bill,common_bill_sequence_id,implementation_date is not null), depthleast(50, bom_parameters.maximum_bom_level),eco_for_production=1components skipped, no explosion of a model or option class under a standard item, Explode Option 1 below the first level only for components whose effectivity overlaps the parent's, and extended quantitycomponent_quantity*decode(basis_type,null,<parent extended quantity>,1)/decode(component_yield_factor,0,1,component_yield_factor)in plain NUMBER arithmetic. Neverexp(sum(ln())): it is inexact, so aunion/distincton the result misses duplicates, andln(0)raises ORA-01428 on any zero quantity. A leaf is a component exploded nowhere in the top item's explosion, notconnect_by_isleaf. Prove it against the exploder itself: it writes the session-privateBOM_SMALL_EXPL_TEMP, so run it and roll back. - A template
COLUMN_NAMEmust be the report's SQL alias; a name copied from the RTF field (DOCUMENT_NUMBERfordocument_seq_value) is silently dropped from the output. When evaluating defaults in the E2E harness, skip parameters whose default references:$flex$(binding the literal textnullraised ORA-01722 only once rows reached that predicate), and set hidden operating-unit parameters explicitly:fnd_global.org_idis -1 in a bareapps_initializesession. - rwconverter segfaults on some RDFs (no
<PROG>_rdf.xml, ten retries do not help): ask the customer for the.rextext export of the report (Reports Builder File > Convert, report definition to text) and run${CLAUDE_PLUGIN_ROOT}/skills/migrate-custom-reports/rex_parse.py <folder> <PROG>- it writes every query's SQL and every PL/SQL unit (formula columns, triggers, after-parameter-form) next to the.rex. Queries cloned per sort/group variant collapse into one Blitz SQL;FND FLEXSQL/FLEXIDVALuser exits becomemtl_categories_b_kfv.concatenated_segmentspredicates and columns. - The dependency closure is PREFIX-driven; the prefix field takes a delimited list (
XX,XXC) — ask for every prefix, and cross-check gathered SQL for object references without a.sqlfile. Objects named only in a value set's validation table are never collected, whatever the prefix: readTABLE_NAMEin the_vs_*.ldtfiles and ask for those objects separately. .ldtfrom a newer EBS may not FNDLOAD on an older conversion instance.- Wrapper + child (temp table + BIP child) programs convert to ONE report via
<DB_PACKAGE>before/after-report triggers (xxen_xdo.before_reportwrites bind-anchor values into same-named package variables;afterreportcleans up). Warm per-row lookup caches set-based inbeforereport. - Noetix views: closure gathered into
noetix/; transform catalogue (#wrapper views → base tables, ACL maps →hr_operating_units/gl_ledgers/MOAC,xxk_gl_acct→gcck+&segment_columns,noetix_env_pkg.get_language→userenv('lang'), dropA$*); parameters designed from the standard library.
Value fidelity (the acceptance of every delivery)
- A converted report, and every correction of one, is delivered only after the value fidelity gate of the
migrate-custom-reportsskill (step 6,references/value-fidelity.md) passes. The parse, run, row-count, parameter and heading gates prove the report is well-formed, not that its values are the original's. - Request one output of each original program from the customer's system, with its parameters, at engagement start. It is the arbiter for printed columns, headings, row grain and every value it lets you recompute. Our own earlier Blitz output is never a reference.
- A correction implements the original's logic for the field and cites the source line it follows. Never choose a date basis, a rate or a set operator by analogy with another report.
- A defect class found in one report is searched for in every report of the category before the round goes out.
- Before the first delivery, a second agent audits each report against the original's files (
references/fidelity-audit-prompt.md), and every DIFFERENT or MISSING item is resolved before the file is sent. - Without a reference output from the customer, run the original itself on the conversion instance where its tables have data: an RDF with
appsrwrun.shon the gathered.rdf(XML output), its RTF through the BI Publisher engine on that XML; a PL/SQL program as its entry procedure copied verbatim into a harness procedure that captures the output lines. Where the instance has no data, both sides read the same seeded stand-in rows. A port generated by script from the source lines proves more than a retyped one. - A standard table of the conversion instance can lack columns the customer's release has (
jai_tax_det_factors.eway_bill_numon 12.1.3): read it through a stand-in view in the instance copy of the XML only, the delivered XML keeps the table.
Performance of a converted report (checked before delivery, not after the customer's first run)
- Measure a converted or changed report with a full fetch of every output column, old against new over the same window. A
count(*)lets Oracle skip the select-list subqueries, so it hides exactly the per-row lookups a like-for-like conversion adds. - A converted union of many branches that filled a temp table branch by branch must carry the run-screen filters INSIDE every branch, as the original's insert statements did: an outer
1=1over the union is not pushed into branches that aggregate (group by,distinct) or join through inline views, so those scan their full source (a 34-branch GL drill-down took 20 minutes for 2,399 lines). The framework replaces every occurrence of ann=nanchor, so put the same1=1in each branch's where clause (inside the aggregating inline view where the filter columns live) and write the predicates on the branch aliases;build_xml.pytakes the intended count asREPEATED_ANCHOR: {'1=1': n}. - Before delivery, read the
#VALUE_SETSdefinition of EVERYLOV Oracleparameter of the batch and replace any list that scans a transactional table or an ungathered customer view per open (distinctovermtl_item_categories, an on-hand view, a PO print view): the run screen executes a required list twice on open and once per list open. Take the values from the setup table instead (mtl_categories_bfor the category set,po_document_types_all_vl, the shippedINV Item (master)list). A list over a setup table (categories, value sets, suppliers, organizations, FA books) is fine. - Never carry over a customer value set whose list is a
distinctovergl_code_combinationswith a description function per row (gl_flexfields_pkg.get_description_sql): the run screen executes a required list parameter's query twice on open and once per list open, minutes each on 600,000 combinations. Replace it with a segment value list over the ledger's chart of accounts throughxxen_util.kff_segment_lov(the select list of the shippedGL Segment<n>lists), keyed the same way the report resolves its ledger. - A report that computes balances through correlated scalar sub-queries per row (an aging: applications and adjustments per bucket, 262 of them) is rewritten before delivery: enumerate the distinct sub-query shapes (normalise whitespace and count them), compute each as one aggregate per key (
group bypayment schedule / transaction / receipt, the as-of date inside), outer-join the aggregates and substitute the column into the original wrapper (nvl(-1*...,0),abs(...), baresumstays nullable). Drive an as-of aging from open schedules (aps.gl_date_closed>:p_as_of_date); a schedule closed by GL date on or before the as-of date has a zero balance in any arithmetic that dates applications bynvl(gl_date,apply_date). Prove it with an order-insensitive row diff and column sums against the previous version, not by reading the plan. - A report whose selection is wide by design (a whole operating unit and category as of a date) reads
mtl_material_transactionswith one full scan hash joined to the listed items and lots, never per organization (N5) or per item (N1) through an index: without bind peeking the date range is estimated at 5% of the table, so the optimizer picks the index path, which takes hours on a customer table of ~50M rows while the full scan takes minutes; a small dev instance cannot show the difference. When an optional parameter makes the selection narrow (an item), switch the hint with a hidden lexical inside the hint comment,/*+ leading(i mmt mtln) &mmt_access */with SQL_TEXTselect nvl2(:p_item_code,'<index path hints>','<full scan hints>') from dual; the engine substitutes a lexical anchor anywhere in the text, comments included. - An
xmlaggover aselect distinctof a whole table inside a per-row scalar sub-query is aggregated once (group by the correlation key) and outer-joined; aselect distinct ... rownum=1lookup drops thedistinct. - E2E harness scripts that several runs share must use per-run temp file names: two concurrent
_e2e.pyruns that shared/tmp/_e2e_wait.sqlpolled each other's request and downloaded the wrong output.
UAT round bookkeeping
- A conversion, and every later correction, replicates what the original computes outside its main query, not only the query: RDF formula columns, placeholders assigned inside formulas, summaries a formula or the layout reads, format triggers that hide rows, group filters (a filter on a parent group suppresses every child query linked to it), BeforeReport/AfterPForm lexicals and derived binds, BI Publisher data triggers, and for a PL/SQL (execution method I) program every variable the loop writes to the output. A cursor column selected as a literal (
0 basic,NULL batch_source_name) is a placeholder the loop fills later; shipping the literal leaves the column blank or zero for that branch. The field inventory (${CLAUDE_PLUGIN_ROOT}/skills/migrate-custom-reports/field_inventory.py) lists all of these per program; a row with no Blitz expression is an open defect, not a simplification. UNIONin the original de-duplicates over its whole select list, including ids andROWIDs the layout never prints. KeepUNIONwhere our branches carry an equivalent key; where they do not, add the original's key columns in an inline view and drop them in the outer select. Never switch betweenUNIONandUNION ALLby analogy with another report: decide from the original's select list, becauseUNION ALLdouble counts join fan-out that the original collapses, and a narrower key merges rows the original keeps.