Skip to content

Commit 777b635

Browse files
Filmbostock
andauthored
accept bare URL definitions for sql tables (#1098)
* accept bare URL definitions for sql tables: --- sql: adresses: https://static.data.gouv.fr/resources/bureaux-de-vote-et-adresses-de-leurs-electeurs/20230626-135723/table-adresses-reu.parquet quakes: https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv --- ```sql echo SELECT COUNT() FROM quakes; ``` closes #1071 * checkpoint url-based sources * simplify * remove live example * VIEW instead of TABLE * Update docs/sql.md --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent a020a7b commit 777b635

File tree

6 files changed

+67
-39
lines changed

6 files changed

+67
-39
lines changed

docs/sql.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ sql:
1616
---
1717
```
1818

19+
To load externally-hosted data, you can use a full URL:
20+
21+
```yaml
22+
---
23+
sql:
24+
quakes: https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv
25+
---
26+
```
27+
28+
<div class="tip">For performance and reliability, we recommend using local files rather than loading data from external servers at runtime. If needed, you can use a <a href="./loaders">data loader</a> to take a snapshot of a remote data during build.</div>
29+
1930
## SQL code blocks
2031

2132
To run SQL queries, create a SQL fenced code block (<code>```sql</code>). For example, to query the first 10 rows from the `gaia` table:

src/client/preview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function open({hash, eval: compile} = {}) {
8989
registerTable(name, null);
9090
}
9191
for (const table of message.tables.added) {
92-
registerTable(table.name, FileAttachment(table.path));
92+
registerTable(table.name, !/^(\w+:|#)/.test(table.path) ? FileAttachment(table.path) : table.path);
9393
}
9494
}
9595
if (message.tables.removed.length || message.tables.added.length) {

src/client/stdlib/duckdb.js

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -181,31 +181,33 @@ Object.defineProperty(DuckDBClient.prototype, "dialect", {
181181

182182
async function insertSource(database, name, source) {
183183
source = await source;
184-
if (isFileAttachment(source)) {
185-
// bare file
186-
await insertFile(database, name, source);
187-
} else if (isArrowTable(source)) {
188-
// bare arrow table
189-
await insertArrowTable(database, name, source);
190-
} else if (Array.isArray(source)) {
191-
// bare array of objects
192-
await insertArray(database, name, source);
193-
} else if (isArqueroTable(source)) {
194-
await insertArqueroTable(database, name, source);
195-
} else if ("data" in source) {
196-
// data + options
197-
const {data, ...options} = source;
198-
if (isArrowTable(data)) {
199-
await insertArrowTable(database, name, data, options);
200-
} else {
201-
await insertArray(database, name, data, options);
184+
if (isFileAttachment(source)) return insertFile(database, name, source);
185+
if (isArrowTable(source)) return insertArrowTable(database, name, source);
186+
if (Array.isArray(source)) return insertArray(database, name, source);
187+
if (isArqueroTable(source)) return insertArqueroTable(database, name, source);
188+
if (typeof source === "string") return insertUrl(database, name, source);
189+
if (source && typeof source === "object") {
190+
if ("data" in source) {
191+
// data + options
192+
const {data, ...options} = source;
193+
if (isArrowTable(data)) return insertArrowTable(database, name, data, options);
194+
return insertArray(database, name, data, options);
202195
}
203-
} else if ("file" in source) {
204-
// file + options
205-
const {file, ...options} = source;
206-
await insertFile(database, name, file, options);
207-
} else {
208-
throw new Error(`invalid source: ${source}`);
196+
if ("file" in source) {
197+
// file + options
198+
const {file, ...options} = source;
199+
return insertFile(database, name, file, options);
200+
}
201+
}
202+
throw new Error(`invalid source: ${source}`);
203+
}
204+
205+
async function insertUrl(database, name, url) {
206+
const connection = await database.connect();
207+
try {
208+
await connection.query(`CREATE VIEW '${name}' AS FROM '${url}'`);
209+
} finally {
210+
await connection.close();
209211
}
210212
}
211213

src/javascript/transpile.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {join} from "node:path/posix";
22
import type {CallExpression, Node} from "acorn";
33
import type {ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier} from "acorn";
44
import {simple} from "acorn-walk";
5-
import {isPathImport, relativePath, resolvePath} from "../path.js";
5+
import {isPathImport, relativePath, resolvePath, resolveRelativePath} from "../path.js";
66
import {getModuleResolver} from "../resolvers.js";
77
import {Sourcemap} from "../sourcemap.js";
88
import type {FileExpression} from "./files.js";
@@ -106,7 +106,7 @@ export async function transpileModule(
106106
function rewriteFileExpressions(output: Sourcemap, files: FileExpression[], path: string): void {
107107
for (const {name, node} of files) {
108108
const source = node.arguments[0];
109-
const resolved = relativePath(path, resolvePath(path, name));
109+
const resolved = resolveRelativePath(path, name);
110110
output.replaceLeft(source.start, source.end, JSON.stringify(resolved));
111111
}
112112
}

src/path.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,10 @@ export function isPathImport(specifier: string): boolean {
6565
export function isAssetPath(specifier: string): boolean {
6666
return !/^(\w+:|#)/.test(specifier);
6767
}
68+
69+
/**
70+
* Returns the relative path to the specified target from the given source.
71+
*/
72+
export function resolveRelativePath(source: string, target: string): string {
73+
return relativePath(source, resolvePath(source, target));
74+
}

src/render.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {transpileJavaScript} from "./javascript/transpile.js";
88
import type {MarkdownPage} from "./markdown.js";
99
import type {PageLink} from "./pager.js";
1010
import {findLink, normalizePath} from "./pager.js";
11-
import {relativePath, resolvePath} from "./path.js";
11+
import {isAssetPath, relativePath, resolvePath, resolveRelativePath} from "./path.js";
1212
import type {Resolvers} from "./resolvers.js";
1313
import {getResolvers} from "./resolvers.js";
1414
import {rollupClient} from "./rollup.js";
@@ -64,21 +64,15 @@ import ${preview || page.code.length ? `{${preview ? "open, " : ""}define} from
6464
: ""
6565
}${data?.sql ? `\nimport {registerTable} from ${JSON.stringify(resolveImport("npm:@observablehq/duckdb"))};` : ""}${
6666
files.size
67-
? `\n${renderFiles(
67+
? `\n${registerFiles(
6868
files,
6969
resolveFile,
7070
preview
7171
? (name) => loaders.getSourceLastModified(resolvePath(path, name))
7272
: (name) => loaders.getOutputLastModified(resolvePath(path, name))
7373
)}`
7474
: ""
75-
}${
76-
data?.sql
77-
? `\n${Object.entries<string>(data.sql)
78-
.map(([name, source]) => `registerTable(${JSON.stringify(name)}, FileAttachment(${JSON.stringify(source)}));`)
79-
.join("\n")}`
80-
: ""
81-
}
75+
}${data?.sql ? `\n${registerTables(data.sql, options)}` : ""}
8276
${preview ? `\nopen({hash: ${JSON.stringify(resolvers.hash)}, eval: (body) => eval(body)});\n` : ""}${page.code
8377
.map(({node, id}) => `\n${transpileJavaScript(node, {id, path, resolveImport})}`)
8478
.join("")}`)}
@@ -92,18 +86,32 @@ ${html.unsafe(rewriteHtml(page.body, resolvers))}</main>${renderFooter(page.foot
9286
`);
9387
}
9488

95-
function renderFiles(
89+
function registerTables(sql: Record<string, any>, options: RenderOptions): string {
90+
return Object.entries(sql)
91+
.map(([name, source]) => registerTable(name, source, options))
92+
.join("\n");
93+
}
94+
95+
function registerTable(name: string, source: any, {path}: RenderOptions): string {
96+
return `registerTable(${JSON.stringify(name)}, ${
97+
isAssetPath(source)
98+
? `FileAttachment(${JSON.stringify(resolveRelativePath(path, source))})`
99+
: JSON.stringify(source)
100+
});`;
101+
}
102+
103+
function registerFiles(
96104
files: Iterable<string>,
97105
resolve: (name: string) => string,
98106
getLastModified: (name: string) => number | undefined
99107
): string {
100108
return Array.from(files)
101109
.sort()
102-
.map((f) => renderFile(f, resolve, getLastModified))
110+
.map((f) => registerFile(f, resolve, getLastModified))
103111
.join("");
104112
}
105113

106-
function renderFile(
114+
function registerFile(
107115
name: string,
108116
resolve: (name: string) => string,
109117
getLastModified: (name: string) => number | undefined

0 commit comments

Comments
 (0)