Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Iceberg: POC Iceberg Catalog#1059

Draft
ddecrulle wants to merge 2 commits intomainfrom
iceberg
Draft

Iceberg: POC Iceberg Catalog#1059
ddecrulle wants to merge 2 commits intomainfrom
iceberg

Conversation

Copy link
Contributor

ddecrulle commented Mar 5, 2026 *
edited by coderabbitai bot
Loading

Summary by CodeRabbit

  • New Features
    • Added Iceberg Catalog feature to browse and preview Iceberg tables across multiple catalogs and namespaces.
    • Added "Catalog Iceberg" navigation menu item to access the new catalog interface.
    • Added support for Iceberg storage configuration in deployment regions.

garronej reacted with heart emoji garronej reacted with rocket emoji
ddecrulle added 2 commits March 4, 2026 16:41
ddecrulle marked this pull request as draft March 5, 2026 15:25
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026 *
edited
Loading

Walkthrough

Walkthrough

This PR adds Iceberg catalog support through a new DuckDB-based adapter. It introduces configuration-driven catalog discovery, a state management system for browsing tables, and a dedicated UI page for exploring Iceberg tables across multiple configured catalogs.

Changes

Cohort / File(s) Summary
Dependencies & Build Configuration
web/package.json, web/scripts/update-duckdb-extensions.ts
Reorders @aws-sdk/client-sts dependency; extends DuckDB extension targets from 3 (parquet, json, httpfs) to 5 by adding iceberg and avro extensions.
Port Definitions
web/src/core/ports/IcebergApi.ts, web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
Defines IcebergApi port interface with listAllTables and fetchTablePreview operations; adds iceberg catalog configuration field to DeploymentRegion type with warehouse, endpoint, catalog, and oidcParams.
API Type Extensions
web/src/core/adapters/onyxiaApi/ApiTypes.ts, web/src/core/adapters/onyxiaApi/onyxiaApi.ts
Extends ApiTypes to support optional iceberg configuration under data.S3 entries; adds iceberg catalog normalization and mapping logic to region configuration retrieval.
Iceberg Adapter Layer
web/src/core/adapters/icebergApi/icebergApi.ts, web/src/core/adapters/icebergApi/index.ts
Introduces createDuckDbIcebergApi factory that manages Iceberg catalogs via DuckDB, handling extension installation, per-catalog secrets, catalog attachment, and error classification for table listing and preview operations.
Bootstrap & Initialization
web/src/core/bootstrap.ts
Adds Iceberg API initialization block post-core setup; manages OIDC client creation per catalog, fetches access tokens, and creates DuckDB-based Iceberg API with computed catalog configurations.
Iceberg Catalog Use Case
web/src/core/usecases/icebergCatalog/state.ts, web/src/core/usecases/icebergCatalog/thunks.ts, web/src/core/usecases/icebergCatalog/selectors.ts, web/src/core/usecases/icebergCatalog/index.ts
Defines state machine for catalog browsing with loading, catalog hierarchy, and table preview caching; implements initialize and selectTable thunks; provides memoized selectors mapping state to UI view structures.
Use Cases Integration
web/src/core/usecases/index.ts
Adds icebergCatalog as a new exported use case module.
UI Navigation & Pages
web/src/ui/App/LeftBar.tsx, web/src/ui/pages/icebergCatalog/route.ts, web/src/ui/pages/icebergCatalog/index.ts, web/src/ui/pages/index.ts
Registers icebergCatalog route at /iceberg-catalog and adds LeftBar navigation item with Storage icon; wires lazy-loaded page component with dev-mode visibility gating.
Iceberg Catalog UI Components
web/src/ui/pages/icebergCatalog/IcebergCatalogs.tsx, web/src/ui/pages/icebergCatalog/Page.tsx
Creates hierarchical catalog browser with collapsible namespaces and table rows; integrates inline data grid preview with dynamic columns/rows; wraps page with login enforcement and hooks state/thunks for table discovery and preview fetching.

Sequence Diagram

sequenceDiagram
participant User as User
participant UI as IcebergCatalog Page
participant Thunks as Thunks (initialize/selectTable)
participant IcebergAPI as IcebergApi (DuckDB)
participant DuckDB as DuckDB

User->>UI: Navigate to Iceberg Catalog
UI->>Thunks: dispatch initialize()
Thunks->>IcebergAPI: listAllTables()
IcebergAPI->>DuckDB: SELECT * FROM information_schema.tables
DuckDB-->>IcebergAPI: table rows
IcebergAPI-->>Thunks: TableEntry[] or error
Thunks->>UI: dispatch catalogsLoaded (hierarchical structure)
UI->>User: Render catalog list

User->>UI: Click table row
UI->>Thunks: dispatch selectTable({catalog, namespace, table})
Thunks->>IcebergAPI: fetchTablePreview({catalog, namespace, table, limit: 10})
IcebergAPI->>DuckDB: SELECT ... FROM catalog.namespace.table LIMIT 10
DuckDB-->>IcebergAPI: columns + rows
IcebergAPI-->>Thunks: {columns, rows} or error
Thunks->>UI: dispatch tablePreviewLoaded
UI->>User: Render inline preview grid
Loading

Estimated code review effort

4 (Complex) | ~50 minutes

Possibly related PRs

Poem

A rabbit hops through catalogs so neat,
DuckDB's iceberg, a frosty treat,
Tables and namespaces, rows on display,
Onyxia's new way to browse and play!

Pre-merge checks | 2 | 1

Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks (2 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit's high-level summary is enabled.
Title check Passed The title 'Iceberg: POC Iceberg Catalog' accurately reflects the main change in the PR, which introduces a proof-of-concept Iceberg catalog integration with DuckDB support, new UI pages, and related infrastructure.

Tip: You can configure your own custom pre-merge checks in the settings.

Finishing Touches
  • Generate docstrings (stacked PR)
  • Generate docstrings (commit on current branch)
Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch iceberg

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

sonarqubecloud bot commented Mar 5, 2026

Quality Gate failed

Failed conditions
B Reliability Rating on New Code (required >= A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

coderabbitai bot reviewed Mar 5, 2026
Copy link
Contributor

coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Nitpick comments (6)
web/src/core/adapters/icebergApi/icebergApi.ts (1)

28-30: Consider moving secretName to module scope.

Per static analysis, this function doesn't depend on any closure variables from createDuckDbIcebergApi and could be hoisted to the outer scope for clarity.

Prompt for AI Agents Verify each finding against the current code and only fix it if needed.

In `@web/src/core/adapters/icebergApi/icebergApi.ts` around lines 28 - 30, The
function secretName is defined inside createDuckDbIcebergApi but doesn't use any
closure variables; move it to module scope by hoisting the secretName function
out of createDuckDbIcebergApi so it is declared alongside other top-level
helpers in icebergApi.ts, update any internal references to still call
secretName(catalogName), and ensure imports/exports and TypeScript visibility
remain correct.
web/src/ui/App/LeftBar.tsx (1)

117-123: Missing i18n for label.

The label "Catalog Iceberg" is hardcoded. While this matches the pattern used for dataCollection (line 113), other LeftBar items use the translation function t() for localization.

Consider adding an i18n key for production readiness:

Suggested i18n integration
{
itemId: "icebergCatalog",
icon: getIconUrlByName("Storage"),
- label: "Catalog Iceberg",
+ label: t("icebergCatalog"),
link: routes.icebergCatalog().link,
availability: isDevModeEnabled ? "available" : "not visible"
},

And add "icebergCatalog" to the declareComponentKeys at the bottom of the file.

Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/ui/App/LeftBar.tsx` around lines 117 - 123, The LeftBar item with
itemId "icebergCatalog" uses a hardcoded label "Catalog Iceberg"; update it to
use the translation function (t) like other items (e.g., match the pattern used
by dataCollection) so the label becomes t("icebergCatalog"), add the
"icebergCatalog" key to the declareComponentKeys call at the bottom of the file,
and ensure any i18n resource files are updated with the new key and
translations; modify the object in LeftBar.tsx referencing itemId
"icebergCatalog" and update the declareComponentKeys list accordingly.
web/src/core/adapters/onyxiaApi/onyxiaApi.ts (1)

318-341: LGTM - follows existing patterns.

The iceberg configuration mapping correctly mirrors the S3 config normalization pattern. The IIFE structure and instanceof Array check are consistent with surrounding code (e.g., lines 221-233 for S3).

Static analysis flags nesting depth and instanceof Array, but these match established patterns in this file. Consider extracting the array normalization to a shared helper in a future refactor if the pattern proliferates.

Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/adapters/onyxiaApi/onyxiaApi.ts` around lines 318 - 341, The
iceberg mapping block (the iceberg IIFE) is fine as-is and follows the S3
normalization pattern; no functional change required--keep the existing logic in
the iceberg constant that uses icebergConfig_api and
apiTypesOidcConfigurationToOidcParams_Partial; optionally, as a follow-up
refactor, extract the repeated array-normalization IIFE into a small helper
(e.g., normalizeToArray(value)) and replace the inline IIFE in iceberg (and the
S3 block) to reduce nesting and remove repeated instanceof Array checks.
web/src/ui/pages/icebergCatalog/Page.tsx (1)

32-32: Consider i18n for hardcoded strings.

The title "Iceberg Catalog" and help content are hardcoded. If this project supports internationalization, consider using translation functions.

Prompt for AI Agents Verify each finding against the current code and only fix it if needed.

In `@web/src/ui/pages/icebergCatalog/Page.tsx` at line 32, Replace hardcoded UI
strings in the Page component by calling the project's i18n translation helper
(e.g., t or useTranslation) instead of literal text; specifically convert the
title="Iceberg Catalog" and any inline help content in the Page component (and
related JSX/props such as the help text passed to the component) to use
translation keys (e.g., t('icebergCatalog.title') and t('icebergCatalog.help'))
and ensure keys are added to the locale resource files so runtime translations
are available.
web/src/core/bootstrap.ts (1)

290-313: Consider error resilience for catalog initialization.

If OIDC initialization fails for any single catalog in Promise.all, the entire Iceberg API initialization fails. Consider whether partial initialization (some catalogs working) would be preferable, or ensure upstream guarantees prevent individual failures.

Alternative with Promise.allSettled for partial success
- const catalogs = await Promise.all(
- deploymentRegion.iceberg.map(async warehouseConfig => {
+ const catalogResults = await Promise.allSettled(
+ deploymentRegion.iceberg.map(async warehouseConfig => {
const oidc_iceberg = await createOidc({
...mergeOidcParams({
oidcParams,
oidcParams_partial: warehouseConfig.oidcParams
}),
transformBeforeRedirectForKeycloakTheme,
getCurrentLang,
autoLogin: true,
enableDebugLogs: enableOidcDebugLogs
});

return {
name: warehouseConfig.catalog,
warehouse: warehouseConfig.warehouse,
endpoint: warehouseConfig.endpoint,
getAccessToken: async (): Promise => {
if (!oidc_iceberg.isUserLoggedIn) return undefined;
return (await oidc_iceberg.getTokens()).accessToken;
}
};
})
);
+
+ const catalogs = catalogResults
+ .filter((r): r is PromiseFulfilledResult ? T : never> => r.status === "fulfilled")
+ .map(r => r.value);
+
+ if (catalogs.length === 0) {
+ break init_iceberg_api;
+ }
Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/bootstrap.ts` around lines 290 - 313, The current Promise.all
over deploymentRegion.iceberg will reject the whole catalogs initialization if
any createOidc call throws; change to per-item error handling so partial
successes are allowed: replace the Promise.all([...map...]) pattern with
Promise.allSettled or wrap each async map entry in a try/catch around
createOidc/oidc_iceberg, log the error, and return undefined/failure marker,
then filter out failed entries before assigning catalogs; reference the map over
deploymentRegion.iceberg, createOidc, oidc_iceberg, and the resulting catalogs
array when implementing this change.
web/src/core/usecases/icebergCatalog/thunks.ts (1)

22-29: Pre-index tables by catalog to avoid repeated full scans.

Line 25 filters the full allTables array once per catalog. This is O(catalogs * tables) and can get expensive on large catalogs.

Suggested refactor
- const catalogs: CatalogMetadata[] = icebergCatalogConfigs.map(config => {
+ const tableEntriesByCatalog = new Map();
+ for (const entry of allTables) {
+ const entries = tableEntriesByCatalog.get(entry.catalog) ?? [];
+ entries.push(entry);
+ tableEntriesByCatalog.set(entry.catalog, entries);
+ }
+
+ const catalogs: CatalogMetadata[] = icebergCatalogConfigs.map(config => {
const namespaceMap = new Map();

- for (const entry of allTables.filter(t => t.catalog === config.name)) {
+ for (const entry of tableEntriesByCatalog.get(config.name) ?? []) {
const tables = namespaceMap.get(entry.namespace) ?? [];
tables.push(entry.name);
namespaceMap.set(entry.namespace, tables);
}
Prompt for AI Agents ) or directly into Map> before the icebergCatalogConfigs.map, then iterate each config and pull its precomputed list to build namespaceMap without repeated full scans.">Verify each finding against the current code and only fix it if needed.

In `@web/src/core/usecases/icebergCatalog/thunks.ts` around lines 22 - 29, The
current loop in thunks.ts builds each catalog's namespaceMap by filtering
allTables for every config (see icebergCatalogConfigs, allTables, catalogs:
CatalogMetadata[], namespaceMap, entry), causing O(catalogs * tables) work; fix
it by pre-indexing allTables once into a Map keyed by catalog (e.g., MapTableEntry[]>) or directly into Map> before the
icebergCatalogConfigs.map, then iterate each config and pull its precomputed
list to build namespaceMap without repeated full scans.
Prompt for all review comments with AI agents
) or directly into Map> before the icebergCatalogConfigs.map, then iterate each config and pull its precomputed list to build namespaceMap without repeated full scans. In `@web/src/ui/App/LeftBar.tsx`: - Around line 117-123: The LeftBar item with itemId "icebergCatalog" uses a hardcoded label "Catalog Iceberg"; update it to use the translation function (t) like other items (e.g., match the pattern used by dataCollection) so the label becomes t("icebergCatalog"), add the "icebergCatalog" key to the declareComponentKeys call at the bottom of the file, and ensure any i18n resource files are updated with the new key and translations; modify the object in LeftBar.tsx referencing itemId "icebergCatalog" and update the declareComponentKeys list accordingly. In `@web/src/ui/pages/icebergCatalog/Page.tsx`: - Line 32: Replace hardcoded UI strings in the Page component by calling the project's i18n translation helper (e.g., t or useTranslation) instead of literal text; specifically convert the title="Iceberg Catalog" and any inline help content in the Page component (and related JSX/props such as the help text passed to the component) to use translation keys (e.g., t('icebergCatalog.title') and t('icebergCatalog.help')) and ensure keys are added to the locale resource files so runtime translations are available.">Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/core/adapters/icebergApi/icebergApi.ts`:
- Around line 128-130: The SELECT built for previewing tables interpolates
catalogName, namespace, and table directly into conn.query (in icebergApi.ts)
creating a potential SQL injection point; validate or escape namespace and table
(which originate from listAllTables) before use--e.g., enforce a strict
identifier regex or whitelist allowed names for namespace and table, ensure
limit is an integer, and only then interpolate with proper quoted identifiers
when calling conn.query; update the code path that constructs the query (the
conn.query call) to perform these validations/escapes first.
- Around line 45-64: The SQL built in the iceberg catalog code interpolates
unescaped values into conn.query strings (see conn.query usage and attachLines
creation around secretName, token, catalogConfig.warehouse, and
catalogConfig.endpoint); to fix, stop inserting raw values and either switch to
parameterized queries if the DuckDB/WASM client supports them or else implement
proper SQL escaping by replacing any single quote ' with two single quotes ''
for every interpolated value (token, catalogConfig.warehouse,
catalogConfig.endpoint and any value passed into secretName) before building the
query string so injected quotes cannot break or alter the SQL.
- Around line 41-52: The current loop only calls catalogConfig.getAccessToken()
once and writes it to the secret but must refresh the token before each query to
avoid expiration; modify the code that runs queries (the logic that uses
conn.query against Iceberg catalogs) to call catalogConfig.getAccessToken()
again immediately before executing any query and then re-run conn.query with the
CREATE OR REPLACE SECRET payload using secretName(catalogConfig.name) and the
fresh token (same SQL pattern shown) so the secret is updated dynamically prior
to every request.

In `@web/src/core/ports/IcebergApi.ts`:
- Around line 10-14: Update the JSDoc for the listAllTables method to reflect
its actual cross-catalog behavior: change the description from "for a given
catalog" to indicate it returns a flat list of (namespace, table) pairs across
all catalogs (equivalent to SHOW ALL TABLES). Modify the comment immediately
above the listAllTables declaration in the IcebergApi interface so it matches
the method signature and the returned type IcebergApi.ListAllTablesResult.

In `@web/src/core/usecases/icebergCatalog/state.ts`:
- Around line 83-104: The reducer tablePreviewLoaded must ignore out-of-order
responses by verifying the preview identity before applying payload: compute the
incoming preview key from the dispatched identifiers (catalog/namespace/table)
included in the action payload and compare it to the current selectedTable's key
(using previewCacheKey and state.selectedTable.{catalog,namespace,table}); if
they differ or state.selectedTable is undefined, return without mutating state;
otherwise set isLoading, columns, rows and cache as before. Ensure thunks that
fetch previews dispatch those same identifiers alongside the columns/rows so the
reducer can perform this identity check.

In `@web/src/core/usecases/icebergCatalog/thunks.ts`:
- Around line 94-98: Do not dispatch actions.tablePreviewLoaded when
result.errorCause is set; change the thunk so it only dispatches
actions.tablePreviewLoaded with result.columns/rows when result.errorCause ===
undefined, and otherwise either dispatch a dedicated error action (e.g.,
actions.tablePreviewFailed or actions.tablePreviewError) with the errorCause or
simply return/abort to avoid caching an empty preview; update the branch around
dispatch(actions.tablePreviewLoaded(...)) to check result.errorCause and handle
the error path explicitly.

In `@web/src/ui/pages/icebergCatalog/IcebergCatalogs.tsx`:
- Around line 209-226: The table row div using onClick (classNames
tableRow/tableRowSelected) is not keyboard accessible; update the element that
renders each row (where onSelectTable is invoked with catalogName,
namespaceName, table.name) to support keyboard activation by either using a
semantic interactive element (e.g., a button) or adding tabIndex={0},
role="button", aria-selected appropriately, and an onKeyDown handler that
triggers onSelectTable when Enter or Space is pressed; ensure focus styles
remain visible and that the Icon/Text children remain non-interactive to avoid
nested interactive controls.

---

Nitpick comments:
In `@web/src/core/adapters/icebergApi/icebergApi.ts`:
- Around line 28-30: The function secretName is defined inside
createDuckDbIcebergApi but doesn't use any closure variables; move it to module
scope by hoisting the secretName function out of createDuckDbIcebergApi so it is
declared alongside other top-level helpers in icebergApi.ts, update any internal
references to still call secretName(catalogName), and ensure imports/exports and
TypeScript visibility remain correct.

In `@web/src/core/adapters/onyxiaApi/onyxiaApi.ts`:
- Around line 318-341: The iceberg mapping block (the iceberg IIFE) is fine
as-is and follows the S3 normalization pattern; no functional change
required--keep the existing logic in the iceberg constant that uses
icebergConfig_api and apiTypesOidcConfigurationToOidcParams_Partial; optionally,
as a follow-up refactor, extract the repeated array-normalization IIFE into a
small helper (e.g., normalizeToArray(value)) and replace the inline IIFE in
iceberg (and the S3 block) to reduce nesting and remove repeated instanceof
Array checks.

In `@web/src/core/bootstrap.ts`:
- Around line 290-313: The current Promise.all over deploymentRegion.iceberg
will reject the whole catalogs initialization if any createOidc call throws;
change to per-item error handling so partial successes are allowed: replace the
Promise.all([...map...]) pattern with Promise.allSettled or wrap each async map
entry in a try/catch around createOidc/oidc_iceberg, log the error, and return
undefined/failure marker, then filter out failed entries before assigning
catalogs; reference the map over deploymentRegion.iceberg, createOidc,
oidc_iceberg, and the resulting catalogs array when implementing this change.

In `@web/src/core/usecases/icebergCatalog/thunks.ts`:
- Around line 22-29: The current loop in thunks.ts builds each catalog's
namespaceMap by filtering allTables for every config (see icebergCatalogConfigs,
allTables, catalogs: CatalogMetadata[], namespaceMap, entry), causing O(catalogs
* tables) work; fix it by pre-indexing allTables once into a Map keyed by
catalog (e.g., Map) or directly into MapMap> before the icebergCatalogConfigs.map, then iterate each
config and pull its precomputed list to build namespaceMap without repeated full
scans.

In `@web/src/ui/App/LeftBar.tsx`:
- Around line 117-123: The LeftBar item with itemId "icebergCatalog" uses a
hardcoded label "Catalog Iceberg"; update it to use the translation function (t)
like other items (e.g., match the pattern used by dataCollection) so the label
becomes t("icebergCatalog"), add the "icebergCatalog" key to the
declareComponentKeys call at the bottom of the file, and ensure any i18n
resource files are updated with the new key and translations; modify the object
in LeftBar.tsx referencing itemId "icebergCatalog" and update the
declareComponentKeys list accordingly.

In `@web/src/ui/pages/icebergCatalog/Page.tsx`:
- Line 32: Replace hardcoded UI strings in the Page component by calling the
project's i18n translation helper (e.g., t or useTranslation) instead of literal
text; specifically convert the title="Iceberg Catalog" and any inline help
content in the Page component (and related JSX/props such as the help text
passed to the component) to use translation keys (e.g.,
t('icebergCatalog.title') and t('icebergCatalog.help')) and ensure keys are
added to the locale resource files so runtime translations are available.

i Review info
Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4dd87a9-d377-48c9-8dc3-8225ad87a7bf

Commits

Reviewing files that changed from the base of the PR and between 6a96798 and a77cbef.

Files ignored due to path filters (3)
  • web/public/duckdb-extensions/v1.4.3/wasm_eh/avro.duckdb_extension.wasm is excluded by !**/*.wasm
  • web/public/duckdb-extensions/v1.4.3/wasm_eh/iceberg.duckdb_extension.wasm is excluded by !**/*.wasm
  • web/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
Files selected for processing (20)
  • web/package.json
  • web/scripts/update-duckdb-extensions.ts
  • web/src/core/adapters/icebergApi/icebergApi.ts
  • web/src/core/adapters/icebergApi/index.ts
  • web/src/core/adapters/onyxiaApi/ApiTypes.ts
  • web/src/core/adapters/onyxiaApi/onyxiaApi.ts
  • web/src/core/bootstrap.ts
  • web/src/core/ports/IcebergApi.ts
  • web/src/core/ports/OnyxiaApi/DeploymentRegion.ts
  • web/src/core/usecases/icebergCatalog/index.ts
  • web/src/core/usecases/icebergCatalog/selectors.ts
  • web/src/core/usecases/icebergCatalog/state.ts
  • web/src/core/usecases/icebergCatalog/thunks.ts
  • web/src/core/usecases/index.ts
  • web/src/ui/App/LeftBar.tsx
  • web/src/ui/pages/icebergCatalog/IcebergCatalogs.tsx
  • web/src/ui/pages/icebergCatalog/Page.tsx
  • web/src/ui/pages/icebergCatalog/index.ts
  • web/src/ui/pages/icebergCatalog/route.ts
  • web/src/ui/pages/index.ts

Comment on lines +41 to +52
for (const catalogConfig of catalogs) {
const token = await catalogConfig.getAccessToken();

if (token !== undefined) {
await conn.query(
[
`CREATE OR REPLACE SECRET "${secretName(catalogConfig.name)}" (`,
` TYPE iceberg,`,
` TOKEN '${token}'`,
");"
].join("\n")
);
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Major

Analysis chain

Script executed:

cd web && find . -name "icebergApi.ts" -type f

Repository: InseeFrLab/onyxia

Length of output: 105


Script executed:

cat -n web/src/core/adapters/icebergApi/icebergApi.ts | head -80

Repository: InseeFrLab/onyxia

Length of output: 3450


Script executed:

# Search for any token refresh or expiration handling in the codebase
rg -i "token.*refresh|refresh.*token|token.*expir|expir.*token" --type ts --type tsx

Repository: InseeFrLab/onyxia

Length of output: 88


Script executed:

# Look for getAccessToken implementation
rg -A 10 "getAccessToken" --type ts --type tsx

Repository: InseeFrLab/onyxia

Length of output: 88


Web query:

DuckDB WASM CREATE OR REPLACE SECRET support token refresh dynamic update

Result:

  • CREATE OR REPLACE SECRET itself is supported in DuckDB SQL, but whether it's useful in DuckDB-Wasm depends on whether the relevant secret types are available (they're registered by extensions such as httpfs/aws). (duckdb.org)
  • In official DuckDB-Wasm builds, httpfs is not available (Wasm uses a separate browser-oriented HTTP implementation), so the common secret types provided by httpfs (e.g., s3, http, huggingface, etc.) are not available out of the box in DuckDB-Wasm. (duckdb.org)

Token refresh / dynamic update

  • DuckDB's cloud-credential refresh is implemented as extension-driven secret refresh (e.g., aws supports REFRESH auto for credential refresh; httpfs has a refresh mechanism that can recreate/replace secrets when invalid). (duckdb.org)
  • For browser (Wasm) scenarios, there is no general "OAuth refresh token flow" built into DuckDB-Wasm secrets. The practical pattern is: refresh the token in JavaScript (or your host app), then re-run CREATE OR REPLACE SECRET to overwrite the stored token value.
  • To make that overwrite less error-prone / avoid embedding tokens in SQL text, DuckDB supports expressions in CREATE SECRET (e.g., set a variable, then create/replace the secret from getvariable(...); update by changing the variable and running CREATE OR REPLACE SECRET again). (duckdb.org)

Implication: if you need s3/http/bearer-token secrets with refresh in the browser, you generally must either (a) handle refresh outside DuckDB-Wasm and replace the secret manually, or (b) use a custom DuckDB-Wasm build/extension strategy that actually provides those secret types and refresh hooks (since the stock Wasm distribution doesn't include httpfs). (duckdb.org)

Citations:


Implement token refresh before queries to honor the interface contract.

The IcebergCatalogConfig.getAccessToken() interface documents that it returns "a fresh bearer token before each request", but the current implementation calls it only once during initialization. For long-running browser sessions, the token stored in the secret will expire while queries continue using the stale value, causing authentication failures.

Refresh the secrets before queries by calling getAccessToken() again and re-running CREATE OR REPLACE SECRET. DuckDB-Wasm supports this pattern for dynamic token updates.

Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/adapters/icebergApi/icebergApi.ts` around lines 41 - 52, The
current loop only calls catalogConfig.getAccessToken() once and writes it to the
secret but must refresh the token before each query to avoid expiration; modify
the code that runs queries (the logic that uses conn.query against Iceberg
catalogs) to call catalogConfig.getAccessToken() again immediately before
executing any query and then re-run conn.query with the CREATE OR REPLACE SECRET
payload using secretName(catalogConfig.name) and the fresh token (same SQL
pattern shown) so the secret is updated dynamically prior to every request.

Comment on lines +45 to +64
await conn.query(
[
`CREATE OR REPLACE SECRET "${secretName(catalogConfig.name)}" (`,
` TYPE iceberg,`,
` TOKEN '${token}'`,
");"
].join("\n")
);
}

const attachLines = [
`ATTACH '${catalogConfig.warehouse}' AS "${catalogConfig.name}" (`,
` TYPE iceberg,`,
...(token !== undefined
? [` SECRET '${secretName(catalogConfig.name)}',`]
: []),
` ENDPOINT '${catalogConfig.endpoint}'`,
");"
];
await conn.query(attachLines.join("\n"));
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Major

SQL injection risk from string interpolation.

Values are interpolated directly into SQL strings without escaping:

  • Line 49: TOKEN '${token}'
  • Line 56: ATTACH '${catalogConfig.warehouse}'
  • Line 61: ENDPOINT '${catalogConfig.endpoint}'

While these values originate from controlled sources (OIDC tokens, region config), a token or config value containing a single quote (') would break the query or enable injection. Consider escaping single quotes or using parameterized queries if DuckDB WASM supports them.

Proposed minimal escaping
+function escapeSqlString(value: string): string {
+ return value.replace(/'/g, "''");
+}
+
if (token !== undefined) {
await conn.query(
[
`CREATE OR REPLACE SECRET "${secretName(catalogConfig.name)}" (`,
` TYPE iceberg,`,
- ` TOKEN '${token}'`,
+ ` TOKEN '${escapeSqlString(token)}'`,
");"
].join("\n")
);
}
Tools
GitHub Check: SonarCloud Code Analysis

[warning] 58-58: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia&issues=AZy-m4epvjWlMhfwQLvt&open=AZy-m4epvjWlMhfwQLvt&pullRequest=1059

Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/adapters/icebergApi/icebergApi.ts` around lines 45 - 64, The SQL
built in the iceberg catalog code interpolates unescaped values into conn.query
strings (see conn.query usage and attachLines creation around secretName, token,
catalogConfig.warehouse, and catalogConfig.endpoint); to fix, stop inserting raw
values and either switch to parameterized queries if the DuckDB/WASM client
supports them or else implement proper SQL escaping by replacing any single
quote ' with two single quotes '' for every interpolated value (token,
catalogConfig.warehouse, catalogConfig.endpoint and any value passed into
secretName) before building the query string so injected quotes cannot break or
alter the SQL.

Comment on lines +128 to +130
const result = await conn.query(
`SELECT * FROM "${catalogName}"."${namespace}"."${table}" LIMIT ${limit};`
);
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Minor

SQL injection risk in table preview query.

catalogName, namespace, and table are interpolated directly into the query. While catalogName is validated against known catalogs (line 109), namespace and table come from listAllTables results which should be safe, but a defense-in-depth approach would escape or validate these identifiers.

-const result = await conn.query(
- `SELECT * FROM "${catalogName}"."${namespace}"."${table}" LIMIT ${limit};`
-);
+const escapedCatalog = catalogName.replace(/"/g, '""');
+const escapedNamespace = namespace.replace(/"/g, '""');
+const escapedTable = table.replace(/"/g, '""');
+const result = await conn.query(
+ `SELECT * FROM "${escapedCatalog}"."${escapedNamespace}"."${escapedTable}" LIMIT ${limit};`
+);
Committable suggestion

!! IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await conn.query(
`SELECT * FROM "${catalogName}"."${namespace}"."${table}" LIMIT ${limit};`
);
const escapedCatalog = catalogName.replace(/"/g, '""');
const escapedNamespace = namespace.replace(/"/g, '""');
const escapedTable = table.replace(/"/g, '""');
const result = await conn.query(
`SELECT * FROM "${escapedCatalog}"."${escapedNamespace}"."${escapedTable}" LIMIT ${limit};`
);
Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/adapters/icebergApi/icebergApi.ts` around lines 128 - 130, The
SELECT built for previewing tables interpolates catalogName, namespace, and
table directly into conn.query (in icebergApi.ts) creating a potential SQL
injection point; validate or escape namespace and table (which originate from
listAllTables) before use--e.g., enforce a strict identifier regex or whitelist
allowed names for namespace and table, ensure limit is an integer, and only then
interpolate with proper quoted identifiers when calling conn.query; update the
code path that constructs the query (the conn.query call) to perform these
validations/escapes first.

Comment on lines +10 to +14
* Returns the flat list of all (namespace, table) pairs for a given catalog.
*
* Equivalent to: SHOW ALL TABLES
*/
listAllTables: () => Promise;
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Minor

Align listAllTables docs with the actual contract.

The comment says "for a given catalog", but the method takes no catalog parameter and the rest of the flow treats it as cross-catalog. Please update the wording to avoid ambiguity.

Suggested wording change
- * Returns the flat list of all (namespace, table) pairs for a given catalog.
+ * Returns the flat list of all (catalog, namespace, table) entries across
+ * all configured catalogs.
Committable suggestion

!! IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Returns the flat list of all (namespace, table) pairs for a given catalog.
*
* Equivalent to: SHOW ALL TABLES
*/
listAllTables: () => Promise<IcebergApi.ListAllTablesResult>;
* Returns the flat list of all (catalog, namespace, table) entries across
* all configured catalogs.
*
* Equivalent to: SHOW ALL TABLES
*/
listAllTables: () => Promise<IcebergApi.ListAllTablesResult>;
Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/ports/IcebergApi.ts` around lines 10 - 14, Update the JSDoc for
the listAllTables method to reflect its actual cross-catalog behavior: change
the description from "for a given catalog" to indicate it returns a flat list of
(namespace, table) pairs across all catalogs (equivalent to SHOW ALL TABLES).
Modify the comment immediately above the listAllTables declaration in the
IcebergApi interface so it matches the method signature and the returned type
IcebergApi.ListAllTablesResult.

Comment on lines +83 to +104
tablePreviewLoaded: (
state,
{
payload
}: {
payload: {
columns: IcebergApi.Column[];
rows: Record[];
};
}
) => {
if (state.selectedTable === undefined) return;
state.selectedTable.isLoading = false;
state.selectedTable.columns = payload.columns;
state.selectedTable.rows = payload.rows;
const key = previewCacheKey(
state.selectedTable.catalog,
state.selectedTable.namespace,
state.selectedTable.table
);
state.previewCache[key] = { columns: payload.columns, rows: payload.rows };
},
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Critical

Guard tablePreviewLoaded against out-of-order responses.

tablePreviewLoaded applies payload to the currently selected table without checking request identity. If user selects A then B quickly, a late A response can overwrite B and be cached under B's key.

Proposed fix
tablePreviewLoaded: (
state,
{
payload
}: {
payload: {
+ catalog: string;
+ namespace: string;
+ table: string;
columns: IcebergApi.Column[];
rows: Record[];
};
}
) => {
if (state.selectedTable === undefined) return;
+ if (
+ state.selectedTable.catalog !== payload.catalog ||
+ state.selectedTable.namespace !== payload.namespace ||
+ state.selectedTable.table !== payload.table
+ ) {
+ return;
+ }
state.selectedTable.isLoading = false;
state.selectedTable.columns = payload.columns;
state.selectedTable.rows = payload.rows;
- const key = previewCacheKey(
- state.selectedTable.catalog,
- state.selectedTable.namespace,
- state.selectedTable.table
- );
+ const key = previewCacheKey(
+ payload.catalog,
+ payload.namespace,
+ payload.table
+ );
state.previewCache[key] = { columns: payload.columns, rows: payload.rows };
},

web/src/core/usecases/icebergCatalog/thunks.ts should dispatch these identifiers with tablePreviewLoaded.

Prompt for AI Agents Verify each finding against the current code and only fix it if needed.

In `@web/src/core/usecases/icebergCatalog/state.ts` around lines 83 - 104, The
reducer tablePreviewLoaded must ignore out-of-order responses by verifying the
preview identity before applying payload: compute the incoming preview key from
the dispatched identifiers (catalog/namespace/table) included in the action
payload and compare it to the current selectedTable's key (using previewCacheKey
and state.selectedTable.{catalog,namespace,table}); if they differ or
state.selectedTable is undefined, return without mutating state; otherwise set
isLoading, columns, rows and cache as before. Ensure thunks that fetch previews
dispatch those same identifiers alongside the columns/rows so the reducer can
perform this identity check.

Comment on lines +94 to +98
dispatch(
actions.tablePreviewLoaded({
columns: result.errorCause === undefined ? result.columns : [],
rows: result.errorCause === undefined ? result.rows : []
})
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Major

Don't convert fetch failures into cacheable empty previews.

Line 94 currently dispatches tablePreviewLoaded even on error by sending empty arrays. Since loaded previews are cached, transient/network errors become sticky empty results and prevent retry for that table.

Proposed fix
- dispatch(
- actions.tablePreviewLoaded({
- columns: result.errorCause === undefined ? result.columns : [],
- rows: result.errorCause === undefined ? result.rows : []
- })
- );
+ if (result.errorCause !== undefined) {
+ dispatch(actions.tablePreviewFailed());
+ return;
+ }
+
+ dispatch(
+ actions.tablePreviewLoaded({
+ columns: result.columns,
+ rows: result.rows
+ })
+ );
Committable suggestion

!! IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dispatch(
actions.tablePreviewLoaded({
columns: result.errorCause === undefined ? result.columns : [],
rows: result.errorCause === undefined ? result.rows : []
})
if (result.errorCause !== undefined) {
dispatch(actions.tablePreviewFailed());
return;
}
dispatch(
actions.tablePreviewLoaded({
columns: result.columns,
rows: result.rows
})
);
Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/core/usecases/icebergCatalog/thunks.ts` around lines 94 - 98, Do not
dispatch actions.tablePreviewLoaded when result.errorCause is set; change the
thunk so it only dispatches actions.tablePreviewLoaded with result.columns/rows
when result.errorCause === undefined, and otherwise either dispatch a dedicated
error action (e.g., actions.tablePreviewFailed or actions.tablePreviewError)
with the errorCause or simply return/abort to avoid caching an empty preview;
update the branch around dispatch(actions.tablePreviewLoaded(...)) to check
result.errorCause and handle the error path explicitly.

Comment on lines +209 to +226
className={cx(classes.tableRow, isSelected && classes.tableRowSelected)}
onClick={() =>
onSelectTable({
catalog: catalogName,
namespace: namespaceName,
table: table.name
})
}
>
icon={getIconUrlByName("TableChart")}
className={classes.tableIcon}
/>
{table.name}
Copy link
Contributor

coderabbitai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue | Major

Accessibility issue: table row lacks keyboard support.

The div element with onClick is not accessible via keyboard navigation. Users relying on keyboard or assistive technologies cannot select tables.

Proposed fix to add keyboard accessibility
-
- className={cx(classes.tableRow, isSelected && classes.tableRowSelected)}
- onClick={() =>
- onSelectTable({
- catalog: catalogName,
- namespace: namespaceName,
- table: table.name
- })
- }
- >
+
+ role="button"
+ tabIndex={0}
+ className={cx(classes.tableRow, isSelected && classes.tableRowSelected)}
+ onClick={() =>
+ onSelectTable({
+ catalog: catalogName,
+ namespace: namespaceName,
+ table: table.name
+ })
+ }
+ onKeyDown={e => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onSelectTable({
+ catalog: catalogName,
+ namespace: namespaceName,
+ table: table.name
+ });
+ }
+ }}
+ >
Committable suggestion

!! IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className={cx(classes.tableRow, isSelected && classes.tableRowSelected)}
onClick={() =>
onSelectTable({
catalog: catalogName,
namespace: namespaceName,
table: table.name
})
}
>
<Icon
icon={getIconUrlByName("TableChart")}
className={classes.tableIcon}
/>
<Text typo="body 1" className={classes.tableName}>
{table.name}
</Text>
</div>
<div
role="button"
tabIndex={0}
className={cx(classes.tableRow, isSelected && classes.tableRowSelected)}
onClick={() =>
onSelectTable({
catalog: catalogName,
namespace: namespaceName,
table: table.name
})
}
onKeyDown={e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onSelectTable({
catalog: catalogName,
namespace: namespaceName,
table: table.name
});
}
}}
>
<Icon
icon={getIconUrlByName("TableChart")}
className={classes.tableIcon}
/>
<Text typo="body 1" className={classes.tableName}>
{table.name}
</Text>
</div>
Tools
GitHub Check: SonarCloud Code Analysis

[warning] 209-218: Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia&issues=AZy-m4fQvjWlMhfwQLv3&open=AZy-m4fQvjWlMhfwQLv3&pullRequest=1059


[warning] 209-218: Visible, non-interactive elements with click handlers must have at least one keyboard listener.

See more on https://sonarcloud.io/project/issues?id=InseeFrLab_onyxia&issues=AZy-m4fQvjWlMhfwQLv4&open=AZy-m4fQvjWlMhfwQLv4&pullRequest=1059

Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/ui/pages/icebergCatalog/IcebergCatalogs.tsx` around lines 209 - 226,
The table row div using onClick (classNames tableRow/tableRowSelected) is not
keyboard accessible; update the element that renders each row (where
onSelectTable is invoked with catalogName, namespaceName, table.name) to support
keyboard activation by either using a semantic interactive element (e.g., a
button) or adding tabIndex={0}, role="button", aria-selected appropriately, and
an onKeyDown handler that triggers onSelectTable when Enter or Space is pressed;
ensure focus styles remain visible and that the Icon/Text children remain
non-interactive to avoid nested interactive controls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

coderabbitai[bot] coderabbitai[bot] left review comments

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant