From fec988e26152dcfe046e6fd1a4377be59438ffb9 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:41:34 -0500 Subject: [PATCH 01/19] Iceberg pt1 --- .../icons/connectors/ApacheIceberg.svelte | 32 ++++ .../icons/connectors/ApacheIceberg.svg | 28 ++++ .../icons/connectors/ApacheIcebergIcon.svelte | 45 ++++++ .../icons/connectors/ApacheIcebergIcon.svg | 59 ++++++++ .../connectors/connector-icon-mapping.ts | 2 + .../sources/modal/connector-schemas.ts | 2 + .../src/features/sources/modal/constants.ts | 1 + .../src/features/sources/modal/icons.ts | 2 + .../src/features/sources/sourceUtils.ts | 30 ++++ .../templates/ConnectionTypeSelector.svelte | 10 +- .../src/features/templates/schemas/iceberg.ts | 139 ++++++++++++++++++ 11 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 web-common/src/components/icons/connectors/ApacheIceberg.svelte create mode 100644 web-common/src/components/icons/connectors/ApacheIceberg.svg create mode 100644 web-common/src/components/icons/connectors/ApacheIcebergIcon.svelte create mode 100644 web-common/src/components/icons/connectors/ApacheIcebergIcon.svg create mode 100644 web-common/src/features/templates/schemas/iceberg.ts diff --git a/web-common/src/components/icons/connectors/ApacheIceberg.svelte b/web-common/src/components/icons/connectors/ApacheIceberg.svelte new file mode 100644 index 00000000000..d784012a892 --- /dev/null +++ b/web-common/src/components/icons/connectors/ApacheIceberg.svelte @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web-common/src/components/icons/connectors/ApacheIceberg.svg b/web-common/src/components/icons/connectors/ApacheIceberg.svg new file mode 100644 index 00000000000..50296a4f1cb --- /dev/null +++ b/web-common/src/components/icons/connectors/ApacheIceberg.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web-common/src/components/icons/connectors/ApacheIcebergIcon.svelte b/web-common/src/components/icons/connectors/ApacheIcebergIcon.svelte new file mode 100644 index 00000000000..5f8a539cbb1 --- /dev/null +++ b/web-common/src/components/icons/connectors/ApacheIcebergIcon.svelte @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/web-common/src/components/icons/connectors/ApacheIcebergIcon.svg b/web-common/src/components/icons/connectors/ApacheIcebergIcon.svg new file mode 100644 index 00000000000..7c1984e23ad --- /dev/null +++ b/web-common/src/components/icons/connectors/ApacheIcebergIcon.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web-common/src/features/connectors/connector-icon-mapping.ts b/web-common/src/features/connectors/connector-icon-mapping.ts index 30ad7744eae..7545a2431f7 100644 --- a/web-common/src/features/connectors/connector-icon-mapping.ts +++ b/web-common/src/features/connectors/connector-icon-mapping.ts @@ -3,6 +3,7 @@ import ApacheDruidIcon from "../../components/icons/connectors/ApacheDruidIcon.s import ApachePinotIcon from "../../components/icons/connectors/ApachePinotIcon.svelte"; import ClickHouseIcon from "../../components/icons/connectors/ClickHouseIcon.svelte"; import ClickHouseCloudIcon from "../../components/icons/connectors/ClickHouseCloudIcon.svelte"; +import ApacheIcebergIcon from "../../components/icons/connectors/ApacheIcebergIcon.svelte"; import DuckDbIcon from "../../components/icons/connectors/DuckDBIcon.svelte"; import GoogleBigQueryIcon from "../../components/icons/connectors/GoogleBigQueryIcon.svelte"; import AthenaIcon from "../../components/icons/connectors/AthenaIcon.svelte"; @@ -22,6 +23,7 @@ export const connectorIconMapping = { motherduck: MotherDuckIcon, druid: ApacheDruidIcon, duckdb: DuckDbIcon, + iceberg: ApacheIcebergIcon, mysql: MySqlIcon, pinot: ApachePinotIcon, postgres: PostgresIcon, diff --git a/web-common/src/features/sources/modal/connector-schemas.ts b/web-common/src/features/sources/modal/connector-schemas.ts index f82b0968bff..f4011da018d 100644 --- a/web-common/src/features/sources/modal/connector-schemas.ts +++ b/web-common/src/features/sources/modal/connector-schemas.ts @@ -16,6 +16,7 @@ import { sqliteSchema } from "../../templates/schemas/sqlite"; import { localFileSchema } from "../../templates/schemas/local_file"; import { duckdbSchema } from "../../templates/schemas/duckdb"; import { httpsSchema } from "../../templates/schemas/https"; +import { icebergSchema } from "../../templates/schemas/iceberg"; import { motherduckSchema } from "../../templates/schemas/motherduck"; import { druidSchema } from "../../templates/schemas/druid"; import { pinotSchema } from "../../templates/schemas/pinot"; @@ -42,6 +43,7 @@ export const multiStepFormSchemas: Record = { https: httpsSchema, s3: s3Schema, gcs: gcsSchema, + iceberg: icebergSchema, azure: azureSchema, }; diff --git a/web-common/src/features/sources/modal/constants.ts b/web-common/src/features/sources/modal/constants.ts index b46ef041f11..f63e5f5bae1 100644 --- a/web-common/src/features/sources/modal/constants.ts +++ b/web-common/src/features/sources/modal/constants.ts @@ -85,6 +85,7 @@ export const SOURCES = [ "azure", "bigquery", "gcs", + "iceberg", "mysql", "postgres", "redshift", diff --git a/web-common/src/features/sources/modal/icons.ts b/web-common/src/features/sources/modal/icons.ts index aaec1e3525f..c4982d4528b 100644 --- a/web-common/src/features/sources/modal/icons.ts +++ b/web-common/src/features/sources/modal/icons.ts @@ -7,6 +7,7 @@ import ApachePinot from "../../../components/icons/connectors/ApachePinot.svelte import ClickHouse from "../../../components/icons/connectors/ClickHouse.svelte"; import DuckDB from "../../../components/icons/connectors/DuckDB.svelte"; import GoogleBigQuery from "../../../components/icons/connectors/GoogleBigQuery.svelte"; +import ApacheIceberg from "../../../components/icons/connectors/ApacheIceberg.svelte"; import GoogleCloudStorage from "../../../components/icons/connectors/GoogleCloudStorage.svelte"; import Https from "../../../components/icons/connectors/HTTPS.svelte"; import LocalFile from "../../../components/icons/connectors/LocalFile.svelte"; @@ -20,6 +21,7 @@ import StarRocks from "../../../components/icons/connectors/StarRocks.svelte"; export const ICONS = { gcs: GoogleCloudStorage, + iceberg: ApacheIceberg, s3: AmazonS3, azure: MicrosoftAzureBlobStorage, bigquery: GoogleBigQuery, diff --git a/web-common/src/features/sources/sourceUtils.ts b/web-common/src/features/sources/sourceUtils.ts index 5332ab67a08..3a5ffb27aa1 100644 --- a/web-common/src/features/sources/sourceUtils.ts +++ b/web-common/src/features/sources/sourceUtils.ts @@ -269,6 +269,36 @@ export function maybeRewriteToDuckDb( delete formValues.table; break; + case "iceberg": { + connectorCopy.name = "duckdb"; + + // Determine which path field has a value + const icebergPath = (formValues.gcs_path || + formValues.s3_path || + formValues.azure_path || + formValues.local_path) as string; + const storageType = formValues.storage_type as string; + + // Set create_secrets_from_connectors for cloud storage backends + if (storageType && storageType !== "local") { + formValues.create_secrets_from_connectors = storageType; + } + + // Build iceberg_scan SQL + formValues.sql = `SELECT *\nFROM iceberg_scan('${icebergPath}',\n allow_moved_paths = true)`; + + // Clean up intermediate fields + delete formValues.storage_type; + delete formValues.gcs_path; + delete formValues.s3_path; + delete formValues.azure_path; + delete formValues.local_path; + delete formValues.gcs_info; + delete formValues.s3_info; + delete formValues.azure_info; + + break; + } } return [connectorCopy, formValues]; diff --git a/web-common/src/features/templates/ConnectionTypeSelector.svelte b/web-common/src/features/templates/ConnectionTypeSelector.svelte index a5659c9c047..4824cab9f26 100644 --- a/web-common/src/features/templates/ConnectionTypeSelector.svelte +++ b/web-common/src/features/templates/ConnectionTypeSelector.svelte @@ -1,7 +1,7 @@ + + + + + + + + + + + + diff --git a/web-common/src/components/icons/connectors/MicrosoftAzureBlobStorageIcon.svelte b/web-common/src/components/icons/connectors/MicrosoftAzureBlobStorageIcon.svelte new file mode 100644 index 00000000000..4084a10287a --- /dev/null +++ b/web-common/src/components/icons/connectors/MicrosoftAzureBlobStorageIcon.svelte @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/web-common/src/components/icons/connectors/logo-icon-Azure.svg b/web-common/src/components/icons/connectors/logo-icon-Azure.svg new file mode 100644 index 00000000000..bf22a2b7951 --- /dev/null +++ b/web-common/src/components/icons/connectors/logo-icon-Azure.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web-common/src/features/connectors/connector-icon-mapping.ts b/web-common/src/features/connectors/connector-icon-mapping.ts index 7545a2431f7..3c8b2a96338 100644 --- a/web-common/src/features/connectors/connector-icon-mapping.ts +++ b/web-common/src/features/connectors/connector-icon-mapping.ts @@ -6,6 +6,8 @@ import ClickHouseCloudIcon from "../../components/icons/connectors/ClickHouseClo import ApacheIcebergIcon from "../../components/icons/connectors/ApacheIcebergIcon.svelte"; import DuckDbIcon from "../../components/icons/connectors/DuckDBIcon.svelte"; import GoogleBigQueryIcon from "../../components/icons/connectors/GoogleBigQueryIcon.svelte"; +import GoogleCloudStorageIcon from "../../components/icons/connectors/GoogleCloudStorageIcon.svelte"; +import MicrosoftAzureBlobStorageIcon from "../../components/icons/connectors/MicrosoftAzureBlobStorageIcon.svelte"; import AthenaIcon from "../../components/icons/connectors/AthenaIcon.svelte"; import PostgresIcon from "../../components/icons/connectors/PostgresIcon.svelte"; import MySqlIcon from "../../components/icons/connectors/MySqlIcon.svelte"; @@ -17,12 +19,14 @@ import StarRocksIcon from "../../components/icons/connectors/StarRocksIcon.svelt export const connectorIconMapping = { athena: AthenaIcon, + azure: MicrosoftAzureBlobStorageIcon, bigquery: GoogleBigQueryIcon, clickhouse: ClickHouseIcon, clickhousecloud: ClickHouseCloudIcon, motherduck: MotherDuckIcon, druid: ApacheDruidIcon, duckdb: DuckDbIcon, + gcs: GoogleCloudStorageIcon, iceberg: ApacheIcebergIcon, mysql: MySqlIcon, pinot: ApachePinotIcon, diff --git a/web-common/src/features/sources/modal/AddDataForm.svelte b/web-common/src/features/sources/modal/AddDataForm.svelte index 729768b0c16..478547ec7c3 100644 --- a/web-common/src/features/sources/modal/AddDataForm.svelte +++ b/web-common/src/features/sources/modal/AddDataForm.svelte @@ -27,6 +27,8 @@ isVisibleForValues, } from "../../templates/schema-utils"; import { runtimeServiceGetFile } from "@rilldata/web-common/runtime-client"; + import { fileArtifacts } from "@rilldata/web-common/features/entity-management/file-artifacts"; + import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; import { ICONS } from "./icons"; export let connector: V1ConnectorDriver; @@ -102,6 +104,26 @@ const connectorSchema = getConnectorSchema(schemaName); + // For Iceberg: check which cloud storage connectors exist and disable unavailable options + $: disabledOptions = (() => { + if (schemaName !== "iceberg") return {}; + const existingConnectors = new Set( + fileArtifacts.getNamesForKind(ResourceKind.Connector), + ); + const disabled: Record = {}; + const connectorMap: Record = { + gcs: "GCS", + s3: "S3", + azure: "Azure", + }; + for (const [key, label] of Object.entries(connectorMap)) { + if (!existingConnectors.has(key)) { + disabled[key] = `Create a ${label} connector first`; + } + } + return disabled; + })(); + // Capture .env blob ONCE on mount for consistent conflict detection in YAML preview. // This prevents the preview from updating when Test and Connect writes to .env. // Use null to indicate "not yet loaded" vs "" for "loaded but empty" @@ -318,6 +340,7 @@ {onStringInputChange} {handleFileUpload} iconMap={ICONS} + {disabledOptions} /> {:else} diff --git a/web-common/src/features/templates/ConnectionTypeSelector.svelte b/web-common/src/features/templates/ConnectionTypeSelector.svelte index 4824cab9f26..9c417be5717 100644 --- a/web-common/src/features/templates/ConnectionTypeSelector.svelte +++ b/web-common/src/features/templates/ConnectionTypeSelector.svelte @@ -3,6 +3,9 @@ import * as Select from "@rilldata/web-common/components/select"; import { Cloud, HardDrive, Play, Server, Sparkles } from "lucide-svelte"; import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte"; + import GoogleCloudStorageIcon from "@rilldata/web-common/components/icons/connectors/GoogleCloudStorageIcon.svelte"; + import AmazonS3Icon from "@rilldata/web-common/components/icons/connectors/AmazonS3Icon.svelte"; + import MicrosoftAzureBlobStorageIcon from "@rilldata/web-common/components/icons/connectors/MicrosoftAzureBlobStorageIcon.svelte"; import type { ComponentType, SvelteComponent } from "svelte"; type ConnectionOption = { @@ -16,6 +19,9 @@ export let label: string = ""; export let onChange: (value: string) => void = () => {}; + // Map of option value → disabled reason message. Options in this map are grayed out. + export let disabledOptions: Record = {}; + // Icon and color maps for rich select display. // Defaults support ClickHouse and DuckDB deployment types; override via props for other connectors. export let iconMap: Record> = { @@ -24,9 +30,9 @@ "self-managed": Server, "self-hosted": Server, "rill-managed": Sparkles, - gcs: Cloud, - s3: Cloud, - azure: Cloud, + gcs: GoogleCloudStorageIcon, + s3: AmazonS3Icon, + azure: MicrosoftAzureBlobStorageIcon, local: HardDrive, }; @@ -36,10 +42,10 @@ "self-managed": { bg: "bg-purple-100", text: "text-purple-600" }, "self-hosted": { bg: "bg-purple-100", text: "text-purple-600" }, "rill-managed": { bg: "bg-blue-100", text: "text-blue-600" }, - gcs: { bg: "bg-blue-100", text: "text-blue-600" }, - s3: { bg: "bg-orange-100", text: "text-orange-600" }, - azure: { bg: "bg-sky-100", text: "text-sky-600" }, - local: { bg: "bg-gray-100", text: "text-gray-600" }, + gcs: { bg: "", text: "" }, + s3: { bg: "", text: "" }, + azure: { bg: "", text: "" }, + local: { bg: "", text: "text-gray-600" }, }; function getIcon(optionValue: string): ComponentType { @@ -112,7 +118,12 @@ {#each options as option (option.value)} {@const Icon = getIcon(option.value)} {@const colors = getColors(option.value)} - + {@const disabledReason = disabledOptions[option.value]} +
{option.label} - {#if option.description} + {#if disabledReason} + {disabledReason} + {:else if option.description} {option.description} {/if}
diff --git a/web-common/src/features/templates/JSONSchemaFormRenderer.svelte b/web-common/src/features/templates/JSONSchemaFormRenderer.svelte index e69bec568bb..88854d9ce40 100644 --- a/web-common/src/features/templates/JSONSchemaFormRenderer.svelte +++ b/web-common/src/features/templates/JSONSchemaFormRenderer.svelte @@ -26,6 +26,9 @@ // Icon mapping for select options export let iconMap: Record> = {}; + // Map of option value → disabled reason for rich select options + export let disabledOptions: Record = {}; + // Use `any` for form values since field types are determined by JSON schema at runtime type FormData = Record; @@ -410,6 +413,7 @@ handleSelectChange(key, newValue)} /> diff --git a/web-common/src/features/templates/schemas/iceberg.ts b/web-common/src/features/templates/schemas/iceberg.ts index b4c9feec846..f390d8bd9c9 100644 --- a/web-common/src/features/templates/schemas/iceberg.ts +++ b/web-common/src/features/templates/schemas/iceberg.ts @@ -10,21 +10,21 @@ export const icebergSchema: MultiStepFormSchema = { storage_type: { type: "string", title: "Storage backend", - enum: ["gcs", "s3", "azure", "local"], - default: "gcs", + enum: ["local", "gcs", "s3", "azure"], + default: "local", "x-display": "select", "x-select-style": "rich", "x-enum-labels": [ + "Local", "Google Cloud Storage", "Amazon S3", "Azure Blob Storage", - "Local", ], "x-enum-descriptions": [ + "Read Iceberg tables from a local directory", "Read Iceberg tables from a GCS bucket", "Read Iceberg tables from an S3 bucket", "Read Iceberg tables from Azure Blob Storage", - "Read Iceberg tables from a local directory", ], "x-ui-only": true, "x-grouped-fields": { @@ -38,8 +38,6 @@ export const icebergSchema: MultiStepFormSchema = { gcs_info: { type: "boolean", title: "GCS Connector Required", - description: - "Requires a configured GCS connector for authentication. If you haven't set one up yet, go back and create a GCS connector first.", default: true, "x-informational": true, "x-ui-only": true, @@ -51,8 +49,7 @@ export const icebergSchema: MultiStepFormSchema = { description: "GCS path to the Iceberg table directory", pattern: "^gs://[^/]+(/.*)?$", errorMessage: { - pattern: - "Must be a GCS URI (e.g. gs://bucket/path/to/iceberg_table)", + pattern: "Must be a GCS URI (e.g. gs://bucket/path/to/iceberg_table)", }, "x-placeholder": "gs://bucket/path/to/iceberg_table", "x-step": "source", @@ -60,8 +57,6 @@ export const icebergSchema: MultiStepFormSchema = { s3_info: { type: "boolean", title: "S3 Connector Required", - description: - "Requires a configured S3 connector for authentication. If you haven't set one up yet, go back and create an S3 connector first.", default: true, "x-informational": true, "x-ui-only": true, @@ -73,8 +68,7 @@ export const icebergSchema: MultiStepFormSchema = { description: "S3 path to the Iceberg table directory", pattern: "^s3://[^/]+(/.*)?$", errorMessage: { - pattern: - "Must be an S3 URI (e.g. s3://bucket/path/to/iceberg_table)", + pattern: "Must be an S3 URI (e.g. s3://bucket/path/to/iceberg_table)", }, "x-placeholder": "s3://bucket/path/to/iceberg_table", "x-step": "source", @@ -82,8 +76,6 @@ export const icebergSchema: MultiStepFormSchema = { azure_info: { type: "boolean", title: "Azure Connector Required", - description: - "Requires a configured Azure connector for authentication. If you haven't set one up yet, go back and create an Azure connector first.", default: true, "x-informational": true, "x-ui-only": true, From 686c8f7b6889697afdcd5d9ce32e8b30fa082342 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:59:10 -0500 Subject: [PATCH 03/19] remove raw svg --- .../icons/connectors/ApacheIceberg.svg | 26 -------- .../icons/connectors/ApacheIcebergIcon.svg | 59 ------------------- .../icons/connectors/logo-icon-Azure.svg | 9 --- 3 files changed, 94 deletions(-) delete mode 100644 web-common/src/components/icons/connectors/ApacheIceberg.svg delete mode 100644 web-common/src/components/icons/connectors/ApacheIcebergIcon.svg delete mode 100644 web-common/src/components/icons/connectors/logo-icon-Azure.svg diff --git a/web-common/src/components/icons/connectors/ApacheIceberg.svg b/web-common/src/components/icons/connectors/ApacheIceberg.svg deleted file mode 100644 index 251a15d5cdc..00000000000 --- a/web-common/src/components/icons/connectors/ApacheIceberg.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web-common/src/components/icons/connectors/ApacheIcebergIcon.svg b/web-common/src/components/icons/connectors/ApacheIcebergIcon.svg deleted file mode 100644 index 7c1984e23ad..00000000000 --- a/web-common/src/components/icons/connectors/ApacheIcebergIcon.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web-common/src/components/icons/connectors/logo-icon-Azure.svg b/web-common/src/components/icons/connectors/logo-icon-Azure.svg deleted file mode 100644 index bf22a2b7951..00000000000 --- a/web-common/src/components/icons/connectors/logo-icon-Azure.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - From d100f577e275fd00758bc5280458fc3ad2a0cce9 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:33:45 -0500 Subject: [PATCH 04/19] adding required parameters for managed vs direct Iceberg Connections --- .../src/features/sources/sourceUtils.ts | 9 ++ .../src/features/templates/schemas/iceberg.ts | 142 ++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/web-common/src/features/sources/sourceUtils.ts b/web-common/src/features/sources/sourceUtils.ts index 3a5ffb27aa1..7c77aec07ea 100644 --- a/web-common/src/features/sources/sourceUtils.ts +++ b/web-common/src/features/sources/sourceUtils.ts @@ -11,6 +11,7 @@ import { getSchemaSecretKeys, getSchemaStringKeys, } from "../templates/schema-utils"; +import { deepEqual } from "vega-lite"; // Helper text that we put at the top of every Model YAML file function sourceModelFileTop(driverName: string) { @@ -296,6 +297,14 @@ export function maybeRewriteToDuckDb( delete formValues.gcs_info; delete formValues.s3_info; delete formValues.azure_info; + delete formValues.connection_mode; + delete formValues.catalog_type; + delete formValues.postgres_info; + delete formValues.postgres_table; + delete formValues.mysql_info; + delete formValues.mysql_table; + delete formValues.rest_info; + delete formValues.rest_uri; break; } diff --git a/web-common/src/features/templates/schemas/iceberg.ts b/web-common/src/features/templates/schemas/iceberg.ts index f390d8bd9c9..5f705198adf 100644 --- a/web-common/src/features/templates/schemas/iceberg.ts +++ b/web-common/src/features/templates/schemas/iceberg.ts @@ -7,6 +7,124 @@ export const icebergSchema: MultiStepFormSchema = { "x-category": "fileStore", "x-form-height": "tall", properties: { + connection_mode: { + type: "string", + title: "Connection method", + enum: ["catalog", "direct"], + default: "catalog", + "x-display": "tabs", + "x-enum-labels": ["Managed Table (Catalog)", "Direct Table Path"], + "x-ui-only": true, + "x-tab-group": { + catalog: [ + "catalog_type", + "postgres_info", + "postgres_table", + "mysql_info", + "mysql_table", + "rest_info", + "rest_uri", + "storage_type", + "gcs_info", + "gcs_path", + "s3_info", + "s3_path", + "azure_path", + "azure_info", + "local_path", + "name", + ], + direct: [ + "storage_type", + "gcs_info", + "gcs_path", + "s3_info", + "s3_path", + "azure_path", + "azure_info", + "local_path", + "name", + ], + }, + }, + catalog_type: { + type: "string", + title: "Catalog Location", + enum: ["postgres", "mysql", "rest"], + default: "postgres", + "x-display": "select", + "x-select-style": "rich", + "x-enum-labels": ["PostgreSQL", "MySQL", "Rest API"], + "x-enum-descriptions": [ + "Read Iceberg catalog from Postgres", + "Read Iceberg catalog from mysql", + "Read Iceberg catalog from Rest", + ], + "x-ui-only": true, + "x-grouped-fields": { + postgres: ["postgres_info", "postgres_table"], + mysql: ["mysql_info", "mysql_table"], + rest: ["rest_info", "rest_uri"], + }, + "x-step": "source", + }, + postgres_info: { + type: "boolean", + title: "PostGreSQL Connector Required", + default: true, + "x-informational": true, + "x-ui-only": true, + "x-step": "source", + }, + postgres_table: { + type: "string", + title: "Iceberg catalog table", + description: "Postgres table to the Iceberg catalog", + pattern: "", + errorMessage: { + pattern: "Must be a valid table", + }, + "x-placeholder": "db.table", + "x-step": "source", + }, + mysql_info: { + type: "boolean", + title: "MySQL Connector Required", + default: true, + "x-informational": true, + "x-ui-only": true, + "x-step": "source", + }, + mysql_table: { + type: "string", + title: "Iceberg catalog table", + description: "MySQL table to the Iceberg catalog", + pattern: "", + errorMessage: { + pattern: "Must be a valid table", + }, + "x-placeholder": "db.table", + "x-step": "source", + }, + rest_info: { + type: "boolean", + title: "HTTPS Connector Required", + default: true, + "x-informational": true, + "x-ui-only": true, + "x-step": "source", + }, + rest_uri: { + type: "string", + title: "Iceberg catalog table", + description: "REST URi to the Iceberg catalog", + pattern: "", + errorMessage: { + pattern: "Must be a valid URI", + }, + "x-placeholder": "https://endpoint.com/api", + "x-step": "source", + }, storage_type: { type: "string", title: "Storage backend", @@ -110,7 +228,31 @@ export const icebergSchema: MultiStepFormSchema = { }, }, required: ["name"], + oneOf: [ + { + title: "Managed Table (Catalog)", + required: ["catalog"], + not: {}, + }, + { + title: "Direct Table Access", + required: ["direct"], + not: {}, + }, + ], allOf: [ + { + if: { properties: { catalog_type: { const: "postgres" } } }, + then: { required: ["postgres_table"] }, + }, + { + if: { properties: { catalog_type: { const: "mysql" } } }, + then: { required: ["mysql_table"] }, + }, + { + if: { properties: { catalog_type: { const: "rest" } } }, + then: { required: ["rest_uri"] }, + }, { if: { properties: { storage_type: { const: "gcs" } } }, then: { required: ["gcs_path"] }, From 0ec16df50870c95616c989892ae62874ef19e1c1 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Sat, 21 Feb 2026 19:17:28 -0700 Subject: [PATCH 05/19] reverting to simple, due to nuances that is difficult to solve on our connectors --- runtime/drivers/duckdb/duckdb.go | 2 + .../src/features/sources/sourceUtils.ts | 30 ++-- .../templates/GroupedFieldsRenderer.svelte | 44 ++++- .../templates/JSONSchemaFormRenderer.svelte | 124 ++++++++++++-- .../src/features/templates/schemas/iceberg.ts | 158 ++---------------- 5 files changed, 188 insertions(+), 170 deletions(-) diff --git a/runtime/drivers/duckdb/duckdb.go b/runtime/drivers/duckdb/duckdb.go index e0b20da013d..b60fa148836 100644 --- a/runtime/drivers/duckdb/duckdb.go +++ b/runtime/drivers/duckdb/duckdb.go @@ -529,12 +529,14 @@ func (c *connection) reopenDB(ctx context.Context) error { "INSTALL 'parquet'", "INSTALL 'httpfs'", "INSTALL 'spatial'", + "INSTALL 'iceberg'", "LOAD 'json'", "LOAD 'sqlite'", "LOAD 'icu'", "LOAD 'parquet'", "LOAD 'httpfs'", "LOAD 'spatial'", + "LOAD 'iceberg'", "SET GLOBAL timezone='UTC'", "SET GLOBAL old_implicit_casting = true", // Implicit Cast to VARCHAR ) diff --git a/web-common/src/features/sources/sourceUtils.ts b/web-common/src/features/sources/sourceUtils.ts index 7c77aec07ea..07f2bb51510 100644 --- a/web-common/src/features/sources/sourceUtils.ts +++ b/web-common/src/features/sources/sourceUtils.ts @@ -285,8 +285,24 @@ export function maybeRewriteToDuckDb( formValues.create_secrets_from_connectors = storageType; } - // Build iceberg_scan SQL - formValues.sql = `SELECT *\nFROM iceberg_scan('${icebergPath}',\n allow_moved_paths = true)`; + // Build iceberg_scan parameter list + const scanParams: string[] = []; + + const allowMovedPaths = formValues.allow_moved_paths; + if (allowMovedPaths !== undefined && allowMovedPaths !== "") { + scanParams.push(`allow_moved_paths = ${allowMovedPaths}`); + } + + const icebergVersion = formValues.version as string; + if (icebergVersion?.trim()) { + scanParams.push(`version = '${icebergVersion.trim()}'`); + } + + const paramsStr = scanParams.length + ? `,\n ${scanParams.join(",\n ")}` + : ""; + + formValues.sql = `SELECT *\nFROM iceberg_scan('${icebergPath}'${paramsStr})`; // Clean up intermediate fields delete formValues.storage_type; @@ -297,14 +313,8 @@ export function maybeRewriteToDuckDb( delete formValues.gcs_info; delete formValues.s3_info; delete formValues.azure_info; - delete formValues.connection_mode; - delete formValues.catalog_type; - delete formValues.postgres_info; - delete formValues.postgres_table; - delete formValues.mysql_info; - delete formValues.mysql_table; - delete formValues.rest_info; - delete formValues.rest_uri; + delete formValues.allow_moved_paths; + delete formValues.version; break; } diff --git a/web-common/src/features/templates/GroupedFieldsRenderer.svelte b/web-common/src/features/templates/GroupedFieldsRenderer.svelte index 75bf75a2a72..58aab537f29 100644 --- a/web-common/src/features/templates/GroupedFieldsRenderer.svelte +++ b/web-common/src/features/templates/GroupedFieldsRenderer.svelte @@ -4,9 +4,11 @@ import { TabsContent } from "@rilldata/web-common/components/tabs"; import SchemaField from "./SchemaField.svelte"; import type { JSONSchemaField } from "./schemas/types"; + import ConnectionTypeSelector from "./ConnectionTypeSelector.svelte"; import { type EnumOption, isRadioEnum, + isRichSelectEnum, isSelectEnum, isTabsEnum, radioOptions, @@ -46,6 +48,19 @@ includeIcons?: boolean, ) => EnumOption[]; + // Props for rich select support (nested ConnectionTypeSelector) + export let disabledOptions: Record = {}; + export let groupedFieldsMap: Map> = + new Map(); + export let getGroupedFieldsForOption: ( + controllerKey: string, + optionValue: string | number | boolean, + ) => FieldEntry[] = () => []; + export let handleSelectChange: ( + key: string, + newValue: string, + ) => void = () => {}; + function getSelectOptions(prop: JSONSchemaField) { return buildEnumOptions(prop, true, true); } @@ -53,7 +68,34 @@ {#each fields as [childKey, childProp] (childKey)}
- {#if isTabsEnum(childProp)} + {#if isRichSelectEnum(childProp)} + {@const richOptions = getSelectOptions(childProp)} + handleSelectChange(childKey, newValue)} + /> + {#if groupedFieldsMap.get(childKey)} + + {/if} + {:else if isTabsEnum(childProp)} {@const childOptions = tabOptions(childProp)} {#if childProp.title}
diff --git a/web-common/src/features/templates/JSONSchemaFormRenderer.svelte b/web-common/src/features/templates/JSONSchemaFormRenderer.svelte index 88854d9ce40..49ef887733c 100644 --- a/web-common/src/features/templates/JSONSchemaFormRenderer.svelte +++ b/web-common/src/features/templates/JSONSchemaFormRenderer.svelte @@ -59,10 +59,15 @@ $: tabGroupedFields = schema ? buildTabGroupedFields(schema, stepFilter) : new Map>(); - $: groupedChildKeys = new Set([ - ...Array.from(groupedFields.values()).flatMap((group) => + // Keys that are children of x-grouped-fields only (rendered by GroupedFieldsRenderer). + // Used to filter these out of tab content so they don't render twice. + $: groupedFieldChildKeys = new Set( + Array.from(groupedFields.values()).flatMap((group) => Object.values(group).flat(), ), + ); + $: groupedChildKeys = new Set([ + ...groupedFieldChildKeys, ...Array.from(tabGroupedFields.values()).flatMap((group) => Object.values(group).flat(), ), @@ -429,6 +434,10 @@ {getTabFieldsForOption} {tabGroupedFields} buildEnumOptions={buildEnumOptionsWithIconMap} + {disabledOptions} + groupedFieldsMap={groupedFields} + {getGroupedFieldsForOption} + {handleSelectChange} /> {/if}
@@ -458,6 +467,10 @@ {getTabFieldsForOption} {tabGroupedFields} buildEnumOptions={buildEnumOptionsWithIconMap} + {disabledOptions} + groupedFieldsMap={groupedFields} + {getGroupedFieldsForOption} + {handleSelectChange} /> {/if}
@@ -484,6 +497,10 @@ {getTabFieldsForOption} {tabGroupedFields} buildEnumOptions={buildEnumOptionsWithIconMap} + {disabledOptions} + groupedFieldsMap={groupedFields} + {getGroupedFieldsForOption} + {handleSelectChange} /> {/if} @@ -500,23 +517,96 @@ {#if tabGroupedFields.get(key)} {#each getTabFieldsForOption(key, option.value) as [childKey, childProp] (childKey)} + {#if groupedFieldChildKeys.has(childKey)} + + {:else}
- + {#if isRichSelectEnum(childProp)} + {@const childOptions = getSelectOptions(childProp)} + + handleSelectChange(childKey, newValue)} + /> + {#if groupedFields.get(childKey)} + + {/if} + {:else if isSelectEnum(childProp)} + {@const childSelectOptions = getSelectOptions(childProp)} + - handleSelectChange(childKey, newValue)} - /> - {#if groupedFields.get(childKey)} - + {/if} + {:else if isSelectEnum(childProp)} + {@const childSelectOptions = + getSelectOptions(childProp)} +