Skip to content

Commit dfc9f11

Browse files
committed
OK
1 parent aad05cb commit dfc9f11

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

Motely.DB.Browser/Motely.DB.Browser.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@
77
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
88
</PropertyGroup>
99

10+
<ItemGroup>
11+
<Content Include="duckdb-reader.js">
12+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
13+
<!-- If we want it packaged cleanly for NuGet distribution later: -->
14+
<!-- <Pack>true</Pack> -->
15+
<!-- <PackagePath>contentFiles\any\any\wwwroot\</PackagePath> -->
16+
</Content>
17+
</ItemGroup>
18+
1019
</Project>

Motely.DB.Browser/duckdb-reader.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// duckdb-lake.js — DuckDB WASM bridge for Avalonia Browser interop
2+
// Loaded lazily by the C# [JSImport] interop layer.
3+
// This runs OUTSIDE the .NET WASM — it's pure browser JS using the @duckdb/duckdb-wasm npm CDN bundle.
4+
5+
let db = null;
6+
let conn = null;
7+
8+
/**
9+
* Initialize DuckDB WASM with httpfs for remote Parquet querying.
10+
* Called once from C# via [JSImport].
11+
* @returns {Promise<boolean>} true if initialization succeeded
12+
*/
13+
globalThis.duckLakeInit = async function () {
14+
if (db !== null) return true;
15+
16+
try {
17+
// Import DuckDB WASM from CDN (jsdelivr serves the npm package)
18+
const DUCKDB_CDN = 'https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist';
19+
20+
const duckdb = await import(`${DUCKDB_CDN}/duckdb-browser-blocking.mjs`);
21+
22+
const MANUAL_BUNDLES = {
23+
mvp: {
24+
mainModule: `${DUCKDB_CDN}/duckdb-mvp.wasm`,
25+
mainWorker: `${DUCKDB_CDN}/duckdb-browser-mvp.worker.js`,
26+
},
27+
eh: {
28+
mainModule: `${DUCKDB_CDN}/duckdb-eh.wasm`,
29+
mainWorker: `${DUCKDB_CDN}/duckdb-browser-eh.worker.js`,
30+
},
31+
};
32+
33+
// Use the official selectBundle helper to automatically choose MVP vs EH
34+
const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
35+
36+
const worker = new Worker(bundle.mainWorker);
37+
const logger = new duckdb.ConsoleLogger();
38+
db = new duckdb.AsyncDuckDB(logger, worker);
39+
await db.instantiate(bundle.mainModule, bundle.mainWorker);
40+
41+
conn = await db.connect();
42+
43+
// Load httpfs for remote Parquet access
44+
await conn.query("INSTALL httpfs; LOAD httpfs;");
45+
46+
console.log('[DuckLake] DuckDB WASM initialized with httpfs');
47+
return true;
48+
} catch (err) {
49+
console.error('[DuckLake] Init failed:', err);
50+
db = null;
51+
conn = null;
52+
return false;
53+
}
54+
};
55+
56+
/**
57+
* Configure S3/R2 credentials for remote lake access.
58+
* @param {string} region - AWS region or 'auto' for R2
59+
* @param {string} endpoint - Custom S3 endpoint (e.g. Cloudflare R2 URL)
60+
* @param {string} accessKeyId - Access key (optional, empty for public buckets)
61+
* @param {string} secretAccessKey - Secret key (optional, empty for public buckets)
62+
* @returns {Promise<boolean>}
63+
*/
64+
globalThis.duckLakeConfigureS3 = async function (region, endpoint, accessKeyId, secretAccessKey) {
65+
if (!conn) return false;
66+
try {
67+
const statements = [];
68+
if (region) statements.push(`SET s3_region='${region}';`);
69+
if (endpoint) statements.push(`SET s3_endpoint='${endpoint}';`);
70+
if (accessKeyId) statements.push(`SET s3_access_key_id='${accessKeyId}';`);
71+
if (secretAccessKey) statements.push(`SET s3_secret_access_key='${secretAccessKey}';`);
72+
// For public R2 buckets, disable signing
73+
if (!accessKeyId) statements.push(`SET s3_url_style='path';`);
74+
75+
for (const sql of statements) {
76+
await conn.query(sql);
77+
}
78+
console.log('[DuckLake] S3/R2 configured');
79+
return true;
80+
} catch (err) {
81+
console.error('[DuckLake] S3 config failed:', err);
82+
return false;
83+
}
84+
};
85+
86+
/**
87+
* Execute a SQL query against DuckDB WASM and return results as JSON.
88+
* @param {string} sql - SQL query to execute
89+
* @returns {Promise<string>} JSON string of results: { columns: string[], rows: any[][] }
90+
*/
91+
globalThis.duckLakeQuery = async function (sql) {
92+
if (!conn) return JSON.stringify({ error: 'Not initialized', columns: [], rows: [] });
93+
try {
94+
const result = await conn.query(sql);
95+
const columns = result.schema.fields.map(f => f.name);
96+
const rows = result.toArray().map(row => {
97+
const obj = row.toJSON();
98+
return columns.map(c => obj[c]);
99+
});
100+
return JSON.stringify({ columns, rows });
101+
} catch (err) {
102+
console.error('[DuckLake] Query error:', err);
103+
return JSON.stringify({ error: err.message, columns: [], rows: [] });
104+
}
105+
};
106+
107+
/**
108+
* Query a remote Parquet file directly. Convenience wrapper.
109+
* @param {string} parquetUrl - Full HTTP(S) URL to the .parquet file
110+
* @param {string} sqlWhere - Optional WHERE clause (without the "WHERE" keyword)
111+
* @param {number} limit - Max rows to return (default 1000)
112+
* @returns {Promise<string>} JSON results
113+
*/
114+
globalThis.duckLakeQueryParquet = async function (parquetUrl, sqlWhere, limit) {
115+
const whereClause = sqlWhere ? ` WHERE ${sqlWhere}` : '';
116+
const limitClause = limit > 0 ? ` LIMIT ${limit}` : ' LIMIT 1000';
117+
const sql = `SELECT * FROM read_parquet('${parquetUrl}')${whereClause} ORDER BY score DESC${limitClause}`;
118+
return await globalThis.duckLakeQuery(sql);
119+
};
120+
121+
/**
122+
* Get the count of rows in a remote Parquet file.
123+
* @param {string} parquetUrl
124+
* @returns {Promise<number>}
125+
*/
126+
globalThis.duckLakeCountParquet = async function (parquetUrl) {
127+
const result = await globalThis.duckLakeQuery(`SELECT COUNT(*) as cnt FROM read_parquet('${parquetUrl}')`);
128+
const parsed = JSON.parse(result);
129+
if (parsed.rows && parsed.rows.length > 0) return parsed.rows[0][0];
130+
return 0;
131+
};

0 commit comments

Comments
 (0)