Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime: Deprecate runtime/server/export.go – migrate frontend to use the new Export API #3419

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion runtime/queries/table_head.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type TableHead struct {
TableName string
Limit int
Result []*structpb.Struct
Schema *runtimev1.StructType
}

var _ runtime.Query = &TableHead{}
Expand Down Expand Up @@ -79,9 +80,77 @@ func (q *TableHead) Resolve(ctx context.Context, rt *runtime.Runtime, instanceID
}

q.Result = data
q.Schema = rows.Schema
return nil
}

func (q *TableHead) Export(ctx context.Context, rt *runtime.Runtime, instanceID string, w io.Writer, opts *runtime.ExportOptions) error {
return ErrExportNotSupported
olap, release, err := rt.OLAP(ctx, instanceID)
if err != nil {
return err
}
defer release()

switch olap.Dialect() {
case drivers.DialectDuckDB:
if opts.Format == runtimev1.ExportFormat_EXPORT_FORMAT_CSV || opts.Format == runtimev1.ExportFormat_EXPORT_FORMAT_PARQUET {
filename := q.TableName

limitClause := ""
if q.Limit > 0 {
limitClause = fmt.Sprintf(" LIMIT %d", q.Limit)
}

sql := fmt.Sprintf(
`SELECT * FROM %s%s`,
safeName(q.TableName),
limitClause,
)
args := []interface{}{}
if err := duckDBCopyExport(ctx, w, opts, sql, args, filename, olap, opts.Format); err != nil {
return err
}
} else {
if err := q.generalExport(ctx, rt, instanceID, w, opts, olap); err != nil {
return err
}
}
case drivers.DialectDruid:
if err := q.generalExport(ctx, rt, instanceID, w, opts, olap); err != nil {
return err
}
default:
return fmt.Errorf("not available for dialect '%s'", olap.Dialect())
}

return nil
}

func (q *TableHead) generalExport(ctx context.Context, rt *runtime.Runtime, instanceID string, w io.Writer, opts *runtime.ExportOptions, olap drivers.OLAPStore) error {
err := q.Resolve(ctx, rt, instanceID, opts.Priority)
if err != nil {
return err
}

if opts.PreWriteHook != nil {
err = opts.PreWriteHook(q.TableName)
if err != nil {
return err
}
}

meta := structTypeToMetricsViewColumn(q.Schema)

switch opts.Format {
case runtimev1.ExportFormat_EXPORT_FORMAT_UNSPECIFIED:
return fmt.Errorf("unspecified format")
case runtimev1.ExportFormat_EXPORT_FORMAT_CSV:
return writeCSV(meta, q.Result, w)
case runtimev1.ExportFormat_EXPORT_FORMAT_XLSX:
return writeXLSX(meta, q.Result, w)
case runtimev1.ExportFormat_EXPORT_FORMAT_PARQUET:
return writeParquet(meta, q.Result, w)
}

return nil
}
12 changes: 12 additions & 0 deletions runtime/server/downloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,18 @@ func (s *Server) downloadHandler(w http.ResponseWriter, req *http.Request) {
MetricsView: mv,
ResolvedMVSecurity: security,
}
case *runtimev1.Query_TableRowsRequest:
r := v.TableRowsRequest
if !auth.GetClaims(req.Context()).CanInstance(r.InstanceId, auth.ReadOLAP) {
http.Error(w, "action not allowed", http.StatusUnauthorized)
return
}

q = &queries.TableHead{
TableName: r.TableName,
Limit: int(r.Limit),
Result: nil,
}
default:
http.Error(w, fmt.Sprintf("unsupported request type: %s", reflect.TypeOf(v).Name()), http.StatusBadRequest)
return
Expand Down
74 changes: 0 additions & 74 deletions runtime/server/export.go

This file was deleted.

7 changes: 0 additions & 7 deletions runtime/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,6 @@ func (s *Server) HTTPHandler(ctx context.Context, registerAdditionalHandlers fun
panic(err)
}

// One-off REST-only path for file export
// NOTE: It's local only and we should deprecate it in favor of a cloud-friendly alternative.
err = gwMux.HandlePath("GET", "/v1/instances/{instance_id}/table/{table_name}/export/{format}", auth.GatewayMiddleware(s.aud, s.ExportTable))
if err != nil {
panic(err)
}

// Call callback to register additional paths
// NOTE: This is so ugly, but not worth refactoring it properly right now.
httpMux := http.NewServeMux()
Expand Down
30 changes: 22 additions & 8 deletions web-common/src/features/models/workspace/ModelWorkspaceCTAs.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import CaretDownIcon from "@rilldata/web-common/components/icons/CaretDownIcon.svelte";
import Forward from "@rilldata/web-common/components/icons/Forward.svelte";
import { Menu, MenuItem } from "@rilldata/web-common/components/menu";
import { createExportTableMutation } from "@rilldata/web-common/features/models/workspace/export-table";
import type { V1Resource } from "@rilldata/web-common/runtime-client";
import { RuntimeUrl } from "@rilldata/web-local/lib/application-state-stores/initialize-node-store-contexts";
import { V1ExportFormat } from "@rilldata/web-common/runtime-client";

import ResponsiveButtonText from "@rilldata/web-common/components/panel/ResponsiveButtonText.svelte";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
Expand All @@ -24,11 +25,16 @@

export let collapse = false;

const onExport = async (exportExtension: "csv" | "parquet") => {
// TODO: how do we handle errors ?
window.open(
`${RuntimeUrl}/v1/instances/${$runtime.instanceId}/table/${modelName}/export/${exportExtension}`
);
const exportModelMutation = createExportTableMutation();

const onExport = async (format: V1ExportFormat) => {
return $exportModelMutation.mutateAsync({
data: {
instanceId: $runtime.instanceId,
format,
tableName: modelName,
},
});
};
</script>

Expand Down Expand Up @@ -66,19 +72,27 @@
<MenuItem
on:select={() => {
toggleFloatingElement();
onExport("parquet");
onExport(V1ExportFormat.EXPORT_FORMAT_PARQUET);
}}
>
Export as Parquet
</MenuItem>
<MenuItem
on:select={() => {
toggleFloatingElement();
onExport("csv");
onExport(V1ExportFormat.EXPORT_FORMAT_CSV);
}}
>
Export as CSV
</MenuItem>
<MenuItem
on:select={() => {
toggleFloatingElement();
onExport(V1ExportFormat.EXPORT_FORMAT_XLSX);
}}
>
Export as XLSX
</MenuItem>
</Menu>
</WithTogglableFloatingElement>
<TooltipContent slot="tooltip-content">
Expand Down
60 changes: 60 additions & 0 deletions web-common/src/features/models/workspace/export-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
createQueryServiceExport,
RpcStatus,
V1ExportFormat,
} from "@rilldata/web-common/runtime-client";
import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
import { createMutation, CreateMutationOptions } from "@tanstack/svelte-query";
import type { MutationFunction } from "@tanstack/svelte-query";
import { get } from "svelte/store";

export type ExportTableRequest = {
instanceId: string;
format: V1ExportFormat;
tableName: string;
};

export function createExportTableMutation<
TError = { response: { data: RpcStatus } },
TContext = unknown
>(options?: {
mutation?: CreateMutationOptions<
Awaited<Promise<void>>,
TError,
{ data: ExportTableRequest },
TContext
>;
}) {
const { mutation: mutationOptions } = options ?? {};
const exporter = createQueryServiceExport();

const mutationFn: MutationFunction<
Awaited<Promise<void>>,
{ data: ExportTableRequest }
> = async (props) => {
const { data } = props ?? {};
if (!data.instanceId) throw new Error("Missing instanceId");

const exportResp = await get(exporter).mutateAsync({
instanceId: data.instanceId,
data: {
format: data.format,
query: {
tableRowsRequest: {
instanceId: data.instanceId,
tableName: data.tableName,
},
},
},
});
const downloadUrl = `${get(runtime).host}${exportResp.downloadUrlPath}`;
window.open(downloadUrl, "_self");
};

return createMutation<
Awaited<Promise<void>>,
TError,
{ data: ExportTableRequest },
TContext
>(mutationFn, mutationOptions);
}