OData V2: Export and Update
OData V2 Excel Export and Update: Design and Implementation Plan¶
This document explains what is being implemented to bring the existing OData V4 deep download (Excel export) feature to OData V2, why certain design decisions were made, and how the OData V2 mass update will be implemented next.
The implementation follows UI5 APIs and patterns for sap.ui.model.odata.v2.ODataModel and sap.ui.model.odata.v2.ODataListBinding. Differences from OData V4 are explicitly called out.
Goals¶
- Achieve feature parity with the V4 deep download:
- Entity graph traversal by navigation properties (recursive up to a configurable level).
- Optional deep export of siblings and nested entities.
- Column selection and ordering, optional inclusion of key properties.
- Events to hook into data processing and the final file export.
- Keep a consistent API surface for component consumers regardless of OData version.
Key Classes and Responsibilities¶
controller/download/SpreadsheetDownload:- Orchestrates fetching data and shaping it into an entity tree using
OData.getODataEntitiesRecursiveandDataAssigner. - Delegates file creation to
SpreadsheetGenerator. controller/download/SpreadsheetGenerator:- Builds the workbook and triggers the download.
- Fires
beforeDownloadFileExportevent for last‑mile customization. controller/odata/ODataV2:- OData V2 specific data access, binding creation, pagination, and metadata resolution.
- Converts V4
$expandobject shape to V2 comma‑separated$expandstring. controller/odata/MetadataHandlerV2:- Reads V2 metamodel, resolves entity types, keys, labels, and builds a recursive entity graph for
expand. controller/download/DataAssignerandcontroller/Util:- Normalize and assign raw results to the entity graph structure expected by the generator.
End‑to‑End Flow (V2)¶
- Resolve entity graph and expand
SpreadsheetDownload.fetchDatacallsODataV2.getODataEntitiesRecursive(entityType, deepLevel)to obtainmainEntityand anexpandsobject produced byMetadataHandlerV2.- Create a list binding with expand
ODataV2.getBindingFromBinding(binding, expands)returns a newODataListBindingfor the same path with a V2‑compatibleexpandparameter (comma‑separated paths, e.g.Orders,Orders/Items).- Fetch data (with pagination fallback)
ODataV2.fetchBatch(customBinding, batchSize)performs amodel.read(path, { urlParameters })and maps results into context‑like objects so the existingUtil.extractObjectspipeline works the same way for V2 and V4.- For future large datasets,
_fetchAllDataV2supports$inlinecount,$skip,$topbased chunking. - Assign data and generate workbook
DataAssignerattaches$XYZEntity,$XYZData, and flattened columns as in V4. ThenSpreadsheetGeneratorcreates the workbook and triggersXLSX.writeFileafter firingbeforeDownloadFileExport.
Notable Differences vs V4 and How They’re Addressed¶
- Expand format:
- V4 uses nested
$expandobjects on the binding. V2 requires a comma‑separated string of navigation paths.ODataV2._convertExpandToV2Formatconverts the nested object intoprop,prop/subProp,.... - Binding API:
- V4 uses
bindList(path, ..., { $$updateGroupId, $count })andrequestContexts. V2 usesODataModel.readfor data retrieval; the implementation maps results into lightweight context‑like objects to keep the rest of the pipeline unchanged. - Count and pagination:
- V4 can
requestContextsand read$countfrom the header context. V2 uses$inlinecount=allpagesand$skip/$top._fetchAllDataV2is prepared for this. - Key extraction:
- A basic
_extractKeyexists as a fallback. Production code will rely onMetadataHandlerV2.getKeyListto construct proper key predicates for context paths when needed.
UI5 APIs used (V2)¶
sap.ui.model.odata.v2.ODataModel.read(sPath, mParameters)withurlParameters: { $expand, $inlinecount, $skip, $top }.sap.ui.model.odata.v2.ODataModel.bindList(path, context, sorters, filters, parameters)for creating a list binding withexpand.sap.ui.model.odata.v2.ODataMetaModelfor entity type and label metadata.
Error Handling¶
ODataV2.checkForErrorsinspects thesubmitChangesbatch response; whenshowBackendErrorMessagesis enabled, messages are shown via the UI5MessageManagerwrapper.
Current Implementation Status¶
- Implemented for export (deep download):
- Expand conversion and list binding creation in
ODataV2.getBindingFromBinding. - Data fetching via
ODataV2.fetchBatchwith$expandand$inlinecount, auto-switching to paginated reads ($skip/$top) when needed. - Data shaping and workbook generation via the existing
SpreadsheetDownloadandSpreadsheetGeneratorpipeline. - To be improved next:
- Replace
_extractKeyfallback with metadata‑based key predicate construction for logging.
OData V2 Update: Implementation Plan¶
Objective: Make UPDATE work with V2 similar to V4’s updateAsync, honoring the same UpdateConfig (columns, fullUpdate, continueOnError).
- Derive keys and target path
- Use
MetadataHandlerV2.getKeys(binding, payload)(orgetKeyList(odataEntityType)) to extract key values from the import row. - Use
ODataModel.createKey(entitySetName, keys)to construct the key predicate and derive the absolute path to the entity. - Update path and batching
- Directly call
model.update(sPath, payload, { merge: !fullUpdate })per entity, wheresPathis built viaODataModel.createKey(entitySetName, keys). - All updates are collected and sent using a single
submitChangescall as part of the existing pipeline. - Draft compatibility
- If the service uses Draft (FE V2 pattern), detect draft vs active based on
IsActiveEntityin payload or metadata. When updating a draft, includeIsActiveEntity=falsein the key predicate or path when applicable. Where available, useDraftControllerto activate (already implemented inwaitForDraft). - Respect
UpdateConfig fullUpdate: boolean: send full payload viamodel.update(..., { merge: false }); otherwise send only changed, configured fields viamerge: true.columns: string[]: if provided and not empty, restrict updates to these properties.- Error handling and continue‑on‑error
- Collect errors from the batch response in
checkForErrors(already implemented) and use the existing message handler to display backend messages when enabled. - Performance
- Group updates into a single batch with
submitChangeswhen possible (ensureuseBatchis enabled on the model). For very large updates, chunk requests to avoid payload limits.
API and Events¶
- The same component settings and events used for V4 apply to V2, notably:
deepDownloadConfigfor export (columns, deepExport, deepLevel, filename, addKeysToExport, showOptions).beforeDownloadFileProcessingandbeforeDownloadFileExportevents.updateConfigfor UPDATE behavior in future V2 implementation.
What changed (August 2025)¶
- Implemented V2 UPDATE via
ODataV2.updateAsyncusing metadata-based keys (MetadataHandlerV2.getKeys) andODataModel.createKey, honoringUpdateConfig(fullUpdate,columns). - Enhanced expand conversion to support deep nesting by flattening nested objects into comma-separated V2
$expandpaths. - Improved V2 export to switch automatically to paginated reads for large datasets.
- Fixed typings in
MetadataHandlerV2(ODataMetaModel) and minor readability refactors in V2 handler.
What changed (March 2026)¶
- Draft prefetch for V2 UPDATE: Created
ODataV2RequestObjectsmirroring V4'sODataV4RequestObjectspattern. Dual-fetches active and draft entities usingmodel.read()with filters, matches spreadsheet rows to backend entities, validates draft state, and reports not-found/mismatch errors. - Draft-aware
updateAsync: Checks prefetched entity forHasDraftEntity/IsActiveEntitystatus. When targeting a draft entity, includesIsActiveEntity=falsein the key predicate. ExistingwaitForDraft()withDraftController.activateDraftEntity()handles post-update activation. - Deep export
getLabelListfix: Stored MetaModel reference onMetadataHandlerV2(lazily cached) sogetLabelListcan resolve entity types without a binding parameter during recursive sibling sheet generation. - Restored
precision,scale,nullablein V2 metadata: Re-added metadata property extraction that was accidentally dropped during branch development. Required for null/empty marker support and decimal validation. - Replaced
console.logwith SAPLogAPI and removed dead_extractKey()fallback method.
What changed (June 2026)¶
- Debug instrumentation across the V2 path: Added lazy support-info dumps (
Log.debug(..., () => logger.returnObject(obj))) at the V2 decision points — entity-graph resolution and association ends (MetadataHandlerV2), expand conversion, fetch parameters/response and pagination, and the update key/payload (ODataV2), plus active/draft reads and entity matching (ODataV2RequestObjects). Standardized component tags (SpreadsheetUpload: ODataV2/ODataV2RequestObjects/MetadataHandlerV2), fixed a copy-pastedODataV4tag insideODataV2, and converted the remaining strayconsole.*calls to theLogAPI. - Fixed deep-export nested expand:
MetadataHandlerV2._findEntitiesByNavigationPropertyresolved associations againstrootEntityinstead of the entity currently being traversed, so navigation properties beyond the first hop (e.g.Orders/Items,Orders/Shipping) silently failed to resolve and were dropped from the export. It now passes the currententity, so the graph traverses fully up todeepLevel. - Fixed the mass-update example wiring:
ordersv2fe'smassUpdate()did not pass atableId; on an Object Page with more than one table this aborts initialization (Found more than one table on Object Page) before the dialog opens, soupdateAsyncwas never reached. Added the ItemstableId. - wdi5 failure diagnostics: on a failed test, the config dumps the
SpreadsheetUploadlog buffer + browser console and saves a screenshot underexamples/reports/errorShots/. UI5DEBUG-level logging is opt-in viaWDI5_LOG_LEVEL=DEBUG(default off, to keep CI fast); the fixes above were found this way. See Debugging an OData V2 issue below. - Fixed V4 deep export leaking collection properties: the no-columns export path included every
$kind:'Property'field, including the draft-internalDraftMessagescollection present in newer Fiori Elements metadata (UI5 ≥ 1.120). That extra column was rejected asColumnNotFoundon re-upload and blocked the whole update (the active entity was never changed). The export now skips$isCollectionproperties, so the download → upload round-trip is consistent across UI5 versions. (This was the cause of theordersv4fe1.120/1.136 CI failures.) - Excel-serial date recovery in the parser: when a date/time-typed column arrives as a numeric cell (date-cell typing lost in a round-trip, or produced by another tool), the parser previously fed the serial to
new Date()and produced 1970-based values. A new pure, unit-testedexcelSerialToDatehelper is now used forEdm.Date/Edm.DateTimeOffset/Edm.TimeOfDaywhen the cell is numeric and not date-typed. Proper date cells and non-numeric values are unchanged. checkForErrorsrobustness (V2): previously inspected only__batchResponses[0].response, missing errors in batched create/update operations (nested in__changeResponses) and in later batch parts. A pure, unit-testedhasV2BatchErrorhelper now scans every batch part and changeset sub-response for an HTTP error (≥ 400).- Update value-change detection (parity with V4):
ODataV2.updateAsyncnow skips columns whose value already matches the prefetched backend entity (unlessfullUpdate), instead of sending every configured column. Date-typed values are still sent (formats differ), so the date round-trip is unaffected. - CI stabilization: a merge conflict with
mainmakes GitHub skip thepull_requestwdi5 workflow entirely (no run is created), so mergingmaininto the branch is required to unblock it. Flaky busy-overlay (sap-ui-blocklayer-popup) click intercepts under parallel load were addressed withspecFileRetries(deferred) and lowermaxInstancesinwdio-base.conf.js.
Delete (V2 and V4)¶
- V2: Use
ODataModel.remove(sPath, { success, error })withsPathbuilt fromcreateKey(entitySetName, keys). Batch usingsubmitChanges()ifuseBatchis enabled. Keys can be derived fromMetadataHandlerV2.getKeys(binding, payload). - V4: Acquire the context to delete (e.g., via
requestContextsor from matched contexts) and callcontext.delete($$groupId). For FE draft scenarios, ensure correct active/draft context before delete. Integrate with the same progress/batch flow as UPDATE.
Limitations and Next Steps¶
- Expand conversion supports arbitrary depth via chained paths (e.g.,
A,B,B/C,B/C/D). - Introduce automatic switching to paginated reads when result size exceeds a threshold.
- DELETE / UPSERT are advertised by the
Actionenum but not implemented in either V2 or V4 (callOdataonly dispatches CREATE/UPDATE). See the Delete (V2 and V4) notes above for the intended approach. - Component-driven V2 draft activation (
activateDraft: true) relies onODataV2.waitForDraft, which carries a known caveat in Object Page tables (hasDraftcan stay true) and is not yet covered by an e2e test — the update spec activates via the FE Save button instead.
Quirks and Gotchas (OData V2 specifics)¶
These are the V2-specific traps that cost the most time. Most stem from V2 being a different — and, in the example apps, generated — protocol than V4.
- The example V2 service is synthetic (cov2ap). The CAP server exposes one V4/CDS service and serves V2 through the
@cap-js-community/odata-v2-adapter(cov2ap). The V2$metadata, entity-set names, key predicates and draft fields are translated, not hand-written — so inspect what the V2 layer actually emits at runtime rather than assuming it matches V4. In tests, draft state is even set via the V4 endpoint (draftEdit) and read back over V2. - The V2 metamodel is XML-derived and loosely typed. Vocabulary annotations are plain object fields (
property['sap:label'],property['com.sap.vocabularies.UI.v1.Hidden'].Bool,property['com.sap.vocabularies.Common.v1.FieldControl'].EnumMember); entity types exposekey.propertyRef[]andnavigationProperty[]. Most access goes throughas any. Dump the resolved entity type with debug logging before relying on a field's shape. getODataAssociationEndis entity-relative. Resolve a navigation property against the entity that owns it —metaModel.getODataAssociationEnd(entityType, navProp.name)— not against the root entity. The wrong entity returnsnulland the branch is silently dropped (this caused the nested-expand bug fixed in June 2026).- Expand is a string, not a tree. V4 uses nested
$expandobjects; V2 wants a comma-separated path string (Orders,Orders/Items,Orders/Shipping).ODataV2._convertExpandToV2Formatflattens the V4-style nested object into that string. - Reads, not contexts. V2 has no
requestContexts/$countheader context.ODataV2.fetchBatchusesmodel.read(path, { urlParameters: { $expand, $inlinecount } })and wraps raw results into context-like{ getObject, getPath, data }objects so the shared download pipeline (Util.extractObjects→DataAssigner→SpreadsheetGenerator) stays version-agnostic. Large result sets auto-switch to$skip/$toppagination. - Draft is runtime data, not metadata.
IsActiveEntity/HasDraftEntity/HasActiveEntityappear only in read results (from cov2ap's draft enablement). On UPDATE, when the matched entity is a draft, the key predicate must includeIsActiveEntity=false;waitForDraft()then activates viaDraftController.activateDraftEntity(). - Known open issue — date/time round-trip. On UPDATE,
Edm.DateTime/Edm.Time/Edm.DateTimeOffsetvalues can be sent back wrong (epoch-like). Not yet root-caused (export write vs. spreadsheet re-serialization vs. parser on re-upload).
Debugging an OData V2 issue¶
- Turn on debug logging. Set
debug: truein the importercomponentData, or append?sap-ui-logLevel=DEBUGto the app URL. Either flips the component intoLog.setLevel(DEBUG)+Log.logSupportInfo(true), which activates the lazy object dumps (() => logger.returnObject(...)). - Read the dumps in the browser console or via
sap.base.Log.getLogEntries(). Filter by tag —SpreadsheetUpload: ODataV2,ODataV2RequestObjects,MetadataHandlerV2. You'll see the resolved entity graph, each association resolution, the generated expand string, thereadURL parameters + raw response, the active/draft matches, and the final update key + payload. - In wdi5 runs, a failed test auto-dumps the
SpreadsheetUploadlog buffer + browser console and saves a screenshot underexamples/reports/errorShots/(setWDI5_LOG_LEVEL=ERRORto quieten). - Probe the V2 wire directly with
examples/test/http/create-order-item-v2-null-tests.http(REST Client) to see exactly what cov2ap accepts/returns for$expand,$filterand draft reads, decoupled from the UI5 layer. - Build/run note: the component is transpiled from
src/on the fly — keeppackages/ui5-cc-spreadsheetimporter/distempty (see the repoCLAUDE.md). When serving the example apps viacds watch, note thatcds-plugin-ui5expects each app'sdist/to exist, so either build the apps or disable that plugin (CI removes it).