Skip to content

Commit 6d471e0

Browse files
authored
Merge pull request #718 from SlideRuleEarth/carlos-dev4
Fix support for geo parquet file exports #717
2 parents 216645d + dfdd58d commit 6d471e0

File tree

4 files changed

+79
-36
lines changed

4 files changed

+79
-36
lines changed

web-client/src/components/SrExportDialog.vue

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { useToast } from 'primevue/usetoast';
5151
import { db } from '@/db/SlideRuleDb';
5252
import { createDuckDbClient } from '@/utils/SrDuckDb';
5353
import { useSrParquetCfgStore } from '@/stores/srParquetCfgStore';
54-
import {getFetchUrlAndOptions} from "@/utils/fetchUtils";
54+
import {getArrowFetchUrlAndOptions} from "@/utils/fetchUtils";
5555
import { exportCsvStreamed, getWritableFileStream } from "@/utils/SrParquetUtils";
5656
5757
@@ -212,43 +212,85 @@ async function exportParquet(fileName: string): Promise<boolean> {
212212
}
213213
214214
async function exportGeoParquet(fileName: string) : Promise<boolean> {
215-
const { url, options } = await getFetchUrlAndOptions(props.reqId,true);
215+
// Check if file already has geo metadata
216+
const duckDbClient = await createDuckDbClient();
217+
const metadata = await duckDbClient.getAllParquetMetadata(fileName);
216218
217-
const wb = await getWritableFileStream(`${fileName}_GEO.parquet`, 'application/octet-stream');
218-
if (!wb) return false;
219+
const hasGeoMetadata = metadata && 'geo' in metadata;
219220
220-
try {
221-
console.log('Fetching GeoParquet export:', url, options);
222-
const response = await fetch(url, {
223-
method: 'POST',
224-
body: options.body,
225-
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
226-
});
221+
if (hasGeoMetadata) {
222+
// File already has geo metadata, export as-is using exportParquet pattern
223+
console.log('File already has geo metadata, exporting as-is');
224+
const opfsRoot = await navigator.storage.getDirectory();
225+
const directoryHandle = await opfsRoot.getDirectoryHandle('SlideRule');
226+
const fileHandle = await directoryHandle.getFileHandle(fileName);
227+
const srcFile = await fileHandle.getFile();
227228
228-
if (!response.ok || !response.body) throw new Error(`Export failed with status ${response.status}`);
229+
const wb = await getWritableFileStream(fileName, 'application/octet-stream');
230+
if (!wb) return false;
229231
230-
if (wb.writable) {
231-
// 🚀 Fast path: let the streams connect directly
232-
await response.body.pipeTo(wb.writable);
233-
} else {
234-
// fallback: manual loop (same as version 1)
235-
// exportGeoParquet manual loop branch
236-
const reader = response.body.getReader();
237-
const writer = wb.getWriter();
238-
while (true) {
239-
const { done, value } = await reader.read();
240-
if (done) break;
241-
if (value) {
242-
await writer.ready;
243-
await writer.write(value.slice());
232+
try {
233+
if (wb.writable && typeof srcFile.stream === 'function') {
234+
await srcFile.stream().pipeTo(wb.writable);
235+
} else {
236+
const reader = srcFile.stream().getReader();
237+
const writer = wb.getWriter();
238+
while (true) {
239+
const { done, value } = await reader.read();
240+
if (done) break;
241+
if (value) {
242+
await writer.ready;
243+
await writer.write(value.slice());
244+
}
244245
}
246+
await writer.close();
245247
}
246-
await writer.close();
248+
return true;
249+
} catch (err) {
250+
console.error('Stream copy failed:', err);
251+
try { await wb.abort(err); } catch {}
252+
return false;
253+
}
254+
} else {
255+
// No geo metadata, fetch from server with as_geo: true
256+
console.log('No geo metadata found, fetching GeoParquet from server');
257+
const { url, options } = await getArrowFetchUrlAndOptions(props.reqId, true);
258+
259+
const baseFileName = fileName.replace(/\.parquet$/i, '');
260+
const wb = await getWritableFileStream(`${baseFileName}_GEO.parquet`, 'application/octet-stream');
261+
if (!wb) return false;
262+
263+
try {
264+
console.log('Fetching GeoParquet export:', url, options);
265+
const response = await fetch(url, {
266+
method: 'POST',
267+
body: options.body,
268+
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
269+
});
270+
271+
if (!response.ok || !response.body) throw new Error(`Export failed with status ${response.status}`);
272+
273+
if (wb.writable) {
274+
await response.body.pipeTo(wb.writable);
275+
} else {
276+
const reader = response.body.getReader();
277+
const writer = wb.getWriter();
278+
while (true) {
279+
const { done, value } = await reader.read();
280+
if (done) break;
281+
if (value) {
282+
await writer.ready;
283+
await writer.write(value.slice());
284+
}
285+
}
286+
await writer.close();
287+
}
288+
return true;
289+
} catch (err) {
290+
console.error('Failed to write GeoParquet file:', err);
291+
try { await wb.abort(err); } catch {}
292+
return false;
247293
}
248-
return true;
249-
} catch (err) {
250-
await wb.abort(err);
251-
return false;
252294
}
253295
}
254296

web-client/src/components/SrImportParquetFile.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ const customUploader = async (event: any) => {
154154
upload_progress.value = 90;
155155
156156
const metadata = await duckDbClient.getAllParquetMetadata(opfsFile.name);
157-
157+
console.log('SrImportParquetFile Extracted metadata:', metadata);
158158
// Metadata validations that DELETE the file on failure:
159159
if (!metadata || !('sliderule' in metadata)) {
160160
toast.add({ severity: 'error', summary: 'Invalid File Format', detail: 'SlideRule metadata missing.', life: 5000 });

web-client/src/utils/fetchUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function forceGeoParquet(inputReqParms: any): any {
2727
return inputReqParms;
2828
}
2929

30-
export async function getFetchUrlAndOptions(
30+
export async function getArrowFetchUrlAndOptions(
3131
reqid: number,
3232
forceAsGeo: boolean = false
3333
): Promise<{ url: string; options: RequestInit }> {
@@ -40,9 +40,9 @@ export async function getFetchUrlAndOptions(
4040
parm = forceGeoParquet(parm);
4141
}
4242
const host = sysConfigStore.getOrganization() && (sysConfigStore.getOrganization() + '.' + sysConfigStore.getDomain()) || sysConfigStore.getDomain();
43-
const api_path = `source/${api}`;
43+
const api_path = `arrow/${api}`;
4444
const url = 'https://' + host + '/' + api_path;
45-
//console.log('source url:', url);
45+
//console.log('getArrowFetchUrlAndOptions source url:', url);
4646
// Setup Request Options
4747
let body = null;
4848
const options: RequestInit = {

web-client/tests/unit/SrExportDialog.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ vi.mock('@/utils/SrDuckDb', () => ({
5050
getTotalRowCount: duckRowCount,
5151
queryForColNames: duckCols,
5252
query: duckQuery,
53+
getAllParquetMetadata: vi.fn().mockResolvedValue(undefined),
5354
}),
5455
}));
5556

@@ -61,7 +62,7 @@ vi.mock('@/stores/srParquetCfgStore', () => ({
6162
}));
6263

6364
vi.mock('@/utils/fetchUtils', () => ({
64-
getFetchUrlAndOptions: vi.fn().mockResolvedValue({
65+
getArrowFetchUrlAndOptions: vi.fn().mockResolvedValue({
6566
url: '/fake/export',
6667
options: { body: JSON.stringify({ ok: true }), headers: {} },
6768
}),

0 commit comments

Comments
 (0)