Skip to content

Commit 1e1cef9

Browse files
author
Annie Zhang
authored
Move __query and its dependencies to stdlib (#296)
* Move __query and its dependencies to stdlib * rename operationsToSqlTemplate to makeQueryTemplate * oops still need that export * export makeQueryTemplate
1 parent ea95f4d commit 1e1cef9

File tree

5 files changed

+449
-0
lines changed

5 files changed

+449
-0
lines changed

src/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export {default as FileAttachments, AbstractFile} from "./fileAttachment.mjs";
22
export {default as Library} from "./library.mjs";
3+
export {makeQueryTemplate} from "./table.mjs";

src/library.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import tex from "./tex.mjs";
1818
import vegalite from "./vegalite.mjs";
1919
import width from "./width.mjs";
2020
import {arquero, arrow, d3, graphviz, htl, inputs, lodash, plot, topojson} from "./dependencies.mjs";
21+
import {__query} from "./table.mjs";
2122

2223
export default Object.assign(Object.defineProperties(function Library(resolver) {
2324
const require = requirer(resolver);
@@ -45,6 +46,7 @@ export default Object.assign(Object.defineProperties(function Library(resolver)
4546
L: () => leaflet(require),
4647
mermaid: () => mermaid(require),
4748
Plot: () => require(plot.resolve()),
49+
__query: () => __query,
4850
require: () => require,
4951
resolve: () => resolve, // deprecated; use async require.resolve instead
5052
SQLite: () => SQLite(require),

src/table.mjs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
export const __query = Object.assign(
2+
// This function is used by table cells.
3+
async (source, operations, invalidation) => {
4+
const args = makeQueryTemplate(operations, await source);
5+
if (!args) return null; // the empty state
6+
return evaluateQuery(await source, args, invalidation);
7+
},
8+
{
9+
// This function is used by SQL cells.
10+
sql(source, invalidation) {
11+
return async function () {
12+
return evaluateQuery(source, arguments, invalidation);
13+
};
14+
}
15+
}
16+
);
17+
18+
async function evaluateQuery(source, args, invalidation) {
19+
if (!source) return;
20+
21+
// If this DatabaseClient supports abort and streaming, use that.
22+
if (typeof source.queryTag === "function") {
23+
const abortController = new AbortController();
24+
const options = {signal: abortController.signal};
25+
invalidation.then(() => abortController.abort("invalidated"));
26+
if (typeof source.queryStream === "function") {
27+
return accumulateQuery(
28+
source.queryStream(...source.queryTag.apply(source, args), options)
29+
);
30+
}
31+
if (typeof source.query === "function") {
32+
return source.query(...source.queryTag.apply(source, args), options);
33+
}
34+
}
35+
36+
// Otherwise, fallback to the basic sql tagged template literal.
37+
if (typeof source.sql === "function") {
38+
return source.sql.apply(source, args);
39+
}
40+
41+
// TODO: test if source is a file attachment, and support CSV etc.
42+
throw new Error("source does not implement query, queryStream, or sql");
43+
}
44+
45+
// Generator function that yields accumulated query results client.queryStream
46+
async function* accumulateQuery(queryRequest) {
47+
const queryResponse = await queryRequest;
48+
const values = [];
49+
values.done = false;
50+
values.error = null;
51+
values.schema = queryResponse.schema;
52+
try {
53+
const iterator = queryResponse.readRows();
54+
do {
55+
const result = await iterator.next();
56+
if (result.done) {
57+
values.done = true;
58+
} else {
59+
for (const value of result.value) {
60+
values.push(value);
61+
}
62+
}
63+
yield values;
64+
} while (!values.done);
65+
} catch (error) {
66+
values.error = error;
67+
yield values;
68+
}
69+
}
70+
71+
/**
72+
* Returns a SQL query in the form [[parts], ...params] where parts is an array
73+
* of sub-strings and params are the parameter values to be inserted between each
74+
* sub-string.
75+
*/
76+
export function makeQueryTemplate(operations, source) {
77+
const escaper =
78+
source && typeof source.escape === "function" ? source.escape : (i) => i;
79+
const {select, from, filter, sort, slice} = operations;
80+
if (
81+
from.table === null ||
82+
select.columns === null ||
83+
select.columns?.length === 0
84+
)
85+
return;
86+
const columns = select.columns.map((c) => `t.${escaper(c)}`);
87+
const args = [
88+
[`SELECT ${columns} FROM ${formatTable(from.table, escaper)} t`]
89+
];
90+
for (let i = 0; i < filter.length; ++i) {
91+
appendSql(i ? `\nAND ` : `\nWHERE `, args);
92+
appendWhereEntry(filter[i], args);
93+
}
94+
for (let i = 0; i < sort.length; ++i) {
95+
appendSql(i ? `, ` : `\nORDER BY `, args);
96+
appendOrderBy(sort[i], args);
97+
}
98+
if (slice.to !== null || slice.from !== null) {
99+
appendSql(
100+
`\nLIMIT ${slice.to !== null ? slice.to - (slice.from ?? 0) : 1e9}`,
101+
args
102+
);
103+
}
104+
if (slice.from !== null) {
105+
appendSql(` OFFSET ${slice.from}`, args);
106+
}
107+
return args;
108+
}
109+
110+
function formatTable(table, escaper) {
111+
if (typeof table === "object") {
112+
let from = "";
113+
if (table.database != null) from += escaper(table.database) + ".";
114+
if (table.schema != null) from += escaper(table.schema) + ".";
115+
from += escaper(table.table);
116+
return from;
117+
}
118+
return table;
119+
}
120+
121+
function appendSql(sql, args) {
122+
const strings = args[0];
123+
strings[strings.length - 1] += sql;
124+
}
125+
126+
function appendOrderBy({column, direction}, args) {
127+
appendSql(`t.${column} ${direction.toUpperCase()}`, args);
128+
}
129+
130+
function appendWhereEntry({type, operands}, args) {
131+
if (operands.length < 1) throw new Error("Invalid operand length");
132+
133+
// Unary operations
134+
if (operands.length === 1) {
135+
appendOperand(operands[0], args);
136+
switch (type) {
137+
case "n":
138+
appendSql(` IS NULL`, args);
139+
return;
140+
case "nn":
141+
appendSql(` IS NOT NULL`, args);
142+
return;
143+
default:
144+
throw new Error("Invalid filter operation");
145+
}
146+
}
147+
148+
// Binary operations
149+
if (operands.length === 2) {
150+
if (["in", "nin"].includes(type)) {
151+
// Fallthrough to next parent block.
152+
} else if (["c", "nc"].includes(type)) {
153+
// TODO: Case (in)sensitive?
154+
appendOperand(operands[0], args);
155+
switch (type) {
156+
case "c":
157+
appendSql(` LIKE `, args);
158+
break;
159+
case "nc":
160+
appendSql(` NOT LIKE `, args);
161+
break;
162+
}
163+
appendOperand(likeOperand(operands[1]), args);
164+
return;
165+
} else {
166+
appendOperand(operands[0], args);
167+
switch (type) {
168+
case "eq":
169+
appendSql(` = `, args);
170+
break;
171+
case "ne":
172+
appendSql(` <> `, args);
173+
break;
174+
case "gt":
175+
appendSql(` > `, args);
176+
break;
177+
case "lt":
178+
appendSql(` < `, args);
179+
break;
180+
case "gte":
181+
appendSql(` >= `, args);
182+
break;
183+
case "lte":
184+
appendSql(` <= `, args);
185+
break;
186+
default:
187+
throw new Error("Invalid filter operation");
188+
}
189+
appendOperand(operands[1], args);
190+
return;
191+
}
192+
}
193+
194+
// List operations
195+
appendOperand(operands[0], args);
196+
switch (type) {
197+
case "in":
198+
appendSql(` IN (`, args);
199+
break;
200+
case "nin":
201+
appendSql(` NOT IN (`, args);
202+
break;
203+
default:
204+
throw new Error("Invalid filter operation");
205+
}
206+
appendListOperands(operands.slice(1), args);
207+
appendSql(")", args);
208+
}
209+
210+
function appendOperand(o, args) {
211+
if (o.type === "column") {
212+
appendSql(`t.${o.value}`, args);
213+
} else {
214+
args.push(o.value);
215+
args[0].push("");
216+
}
217+
}
218+
219+
// TODO: Support column operands here?
220+
function appendListOperands(ops, args) {
221+
let first = true;
222+
for (const op of ops) {
223+
if (first) first = false;
224+
else appendSql(",", args);
225+
args.push(op.value);
226+
args[0].push("");
227+
}
228+
}
229+
230+
function likeOperand(operand) {
231+
return {...operand, value: `%${operand.value}%`};
232+
}
233+

test/index-test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ it("new Library returns a library with the expected keys", () => {
1616
"SQLite",
1717
"SQLiteDatabaseClient",
1818
"_",
19+
"__query",
1920
"aapl",
2021
"alphabet",
2122
"aq",

0 commit comments

Comments
 (0)