Skip to content

Commit dbf96f3

Browse files
authored
Merge pull request #50 from microsoft/multi_statement_support
Multi statement support
2 parents 5227c37 + 3b9d8ea commit dbf96f3

File tree

16 files changed

+1109
-138
lines changed

16 files changed

+1109
-138
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,33 @@ WITH 1 AS x RETURN x UNION ALL WITH 1 AS x RETURN x
202202
// [{ x: 1 }, { x: 1 }]
203203
```
204204

205+
#### Multi-Statement Queries
206+
207+
Multiple statements can be separated by semicolons. Only `CREATE VIRTUAL` and `DELETE VIRTUAL` statements may appear before the last statement. The last statement can be any valid query.
208+
209+
```cypher
210+
CREATE VIRTUAL (:Person) AS {
211+
UNWIND [{id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}] AS r
212+
RETURN r.id AS id, r.name AS name
213+
};
214+
CREATE VIRTUAL (:Person)-[:KNOWS]-(:Person) AS {
215+
UNWIND [{left_id: 1, right_id: 2}] AS r
216+
RETURN r.left_id AS left_id, r.right_id AS right_id
217+
};
218+
MATCH (a:Person)-[:KNOWS]->(b:Person)
219+
RETURN a.name AS from, b.name AS to
220+
```
221+
222+
The `Runner` also exposes a `metadata` property with counts of virtual nodes/relationships created and deleted:
223+
224+
```javascript
225+
const runner = new FlowQuery("CREATE VIRTUAL (:X) AS { RETURN 1 AS id }; MATCH (n:X) RETURN n");
226+
await runner.run();
227+
console.log(runner.metadata);
228+
// { virtual_nodes_created: 1, virtual_relationships_created: 0,
229+
// virtual_nodes_deleted: 0, virtual_relationships_deleted: 0 }
230+
```
231+
205232
### WHERE Clause
206233

207234
Filters rows based on conditions. Supports the following operators:
@@ -621,6 +648,7 @@ RETURN f.name, f.description, f.category
621648
│ LOAD JSON FROM url [HEADERS {...}] [POST {...}] AS alias │
622649
│ CALL func() [YIELD field, ...] │
623650
│ query1 UNION [ALL] query2 │
651+
│ stmt1; stmt2; ... stmtN -- multi-statement │
624652
│ LIMIT n │
625653
├─────────────────────────────────────────────────────────────┤
626654
│ GRAPH OPERATIONS │

docs/index.html

Lines changed: 207 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,221 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<script src="flowquery.min.js"></script>
7-
<title>FlowQuery</title>
8-
<style>
9-
body {
10-
display: flex;
11-
flex-direction: column;
12-
align-items: center;
13-
justify-content: center;
14-
height: 100vh;
15-
margin: 0; /* Prevent unexpected scroll issues */
16-
padding: 0;
17-
box-sizing: border-box;
18-
}
19-
20-
#input {
21-
margin-top: 5px;
22-
width: 100%;
23-
height: 300px;
24-
font-family: monospace;
25-
resize: none;
26-
box-sizing: border-box; /* Include padding/border in width/height */
27-
}
28-
29-
#output {
30-
margin-top: 5px;
31-
width: 100%;
32-
flex-grow: 1;
33-
overflow-y: auto;
34-
font-family: monospace;
35-
box-sizing: border-box;
36-
max-height: calc(100vh - 300px - 5px); /* Prevent excessive growth */
37-
}
38-
</style>
39-
<script>
40-
function createTable(results) {
41-
const table = document.createElement("table");
42-
43-
if (results.length === 0) return table;
44-
45-
const headerRow = document.createElement("tr");
46-
Object.entries(results[0]).forEach(([key, _]) => {
47-
const th = document.createElement("th");
48-
th.style.textAlign = "left";
49-
th.textContent = key;
50-
headerRow.appendChild(th);
51-
});
52-
53-
table.appendChild(headerRow);
54-
55-
results.forEach(row => {
56-
const tr = document.createElement("tr");
57-
Object.entries(row).forEach(([_, value]) => {
58-
const td = document.createElement("td");
59-
if (typeof value === "object" || Array.isArray(value)) {
60-
td.textContent = JSON.stringify(value);
61-
} else {
62-
td.textContent = value;
63-
}
64-
tr.appendChild(td);
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<script src="flowquery.min.js"></script>
7+
<title>FlowQuery</title>
8+
<style>
9+
body {
10+
display: flex;
11+
flex-direction: column;
12+
align-items: center;
13+
justify-content: center;
14+
height: 100vh;
15+
margin: 0;
16+
padding: 0;
17+
box-sizing: border-box;
18+
}
19+
20+
#toolbar {
21+
width: 100%;
22+
display: flex;
23+
gap: 5px;
24+
padding: 5px 0;
25+
box-sizing: border-box;
26+
}
27+
28+
#toolbar button {
29+
font-family: monospace;
30+
cursor: pointer;
31+
}
32+
33+
#share-link {
34+
flex-grow: 1;
35+
font-family: monospace;
36+
font-size: 12px;
37+
display: none;
38+
}
39+
40+
#input {
41+
margin-top: 5px;
42+
width: 100%;
43+
height: 300px;
44+
font-family: monospace;
45+
resize: none;
46+
box-sizing: border-box;
47+
}
48+
49+
#output {
50+
margin-top: 5px;
51+
width: 100%;
52+
flex-grow: 1;
53+
overflow-y: auto;
54+
font-family: monospace;
55+
box-sizing: border-box;
56+
max-height: calc(100vh - 340px);
57+
}
58+
59+
#metadata {
60+
width: 100%;
61+
font-family: monospace;
62+
font-size: 12px;
63+
color: #555;
64+
padding: 4px 0;
65+
box-sizing: border-box;
66+
}
67+
68+
#metadata span {
69+
margin-right: 12px;
70+
}
71+
</style>
72+
<script>
73+
function createTable(results) {
74+
const table = document.createElement("table");
75+
76+
if (results.length === 0) return table;
77+
78+
const headerRow = document.createElement("tr");
79+
Object.entries(results[0]).forEach(([key, _]) => {
80+
const th = document.createElement("th");
81+
th.style.textAlign = "left";
82+
th.textContent = key;
83+
headerRow.appendChild(th);
6584
});
66-
table.appendChild(tr);
67-
});
6885

69-
return table;
70-
}
71-
72-
function run() {
73-
const input = document.getElementById("input").value;
74-
const output = document.getElementById("output");
75-
output.innerHTML = "";
76-
try {
77-
const flowquery = new FlowQuery(input);
78-
flowquery.run().then(() => {
79-
const table = createTable(flowquery.results);
80-
output.appendChild(table);
81-
}).catch(e => {
86+
table.appendChild(headerRow);
87+
88+
results.forEach((row) => {
89+
const tr = document.createElement("tr");
90+
Object.entries(row).forEach(([_, value]) => {
91+
const td = document.createElement("td");
92+
if (typeof value === "object" || Array.isArray(value)) {
93+
td.textContent = JSON.stringify(value);
94+
} else {
95+
td.textContent = value;
96+
}
97+
tr.appendChild(td);
98+
});
99+
table.appendChild(tr);
100+
});
101+
102+
return table;
103+
}
104+
105+
function displayMetadata(metadata) {
106+
const el = document.getElementById("metadata");
107+
el.innerHTML = "";
108+
if (!metadata) return;
109+
Object.entries(metadata).forEach(([key, value]) => {
110+
const span = document.createElement("span");
111+
span.textContent = key + ": " + value;
112+
el.appendChild(span);
113+
});
114+
}
115+
116+
function run() {
117+
const input = document.getElementById("input").value;
118+
const output = document.getElementById("output");
119+
output.innerHTML = "";
120+
displayMetadata(null);
121+
try {
122+
const flowquery = new FlowQuery(input);
123+
flowquery
124+
.run()
125+
.then(() => {
126+
const table = createTable(flowquery.results);
127+
output.appendChild(table);
128+
displayMetadata(flowquery.metadata);
129+
})
130+
.catch((e) => {
131+
console.error(e);
132+
output.innerHTML = e.message;
133+
});
134+
} catch (e) {
82135
console.error(e);
83136
output.innerHTML = e.message;
84-
});
85-
} catch (e) {
86-
console.error(e);
87-
output.innerHTML = e.message;
88-
return;
137+
return;
138+
}
139+
}
140+
141+
// --- Compression / Decompression using DecompressionStream API ---
142+
143+
async function compressString(str) {
144+
const encoder = new TextEncoder();
145+
const stream = new Blob([encoder.encode(str)])
146+
.stream()
147+
.pipeThrough(new CompressionStream("deflate-raw"));
148+
const compressedBlob = await new Response(stream).blob();
149+
const buffer = await compressedBlob.arrayBuffer();
150+
// Base64url encode (URL-safe, no padding)
151+
const bytes = new Uint8Array(buffer);
152+
let binary = "";
153+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
154+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
155+
}
156+
157+
async function decompressString(compressed) {
158+
// Base64url decode
159+
let base64 = compressed.replace(/-/g, "+").replace(/_/g, "/");
160+
while (base64.length % 4) base64 += "=";
161+
const binary = atob(base64);
162+
const bytes = new Uint8Array(binary.length);
163+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
164+
165+
const stream = new Blob([bytes])
166+
.stream()
167+
.pipeThrough(new DecompressionStream("deflate-raw"));
168+
const decompressedBlob = await new Response(stream).blob();
169+
return await decompressedBlob.text();
170+
}
171+
172+
async function share() {
173+
const input = document.getElementById("input").value;
174+
if (!input.trim()) return;
175+
const compressed = await compressString(input);
176+
const url = window.location.origin + window.location.pathname + "?" + compressed;
177+
const linkEl = document.getElementById("share-link");
178+
linkEl.value = url;
179+
linkEl.style.display = "block";
180+
linkEl.select();
181+
navigator.clipboard.writeText(url).catch(() => {});
89182
}
90-
}
91-
</script>
92-
</head>
183+
184+
// --- Auto-load from URL on page load ---
185+
186+
window.addEventListener("DOMContentLoaded", async () => {
187+
const query = window.location.search;
188+
if (query && query.length > 1) {
189+
const compressed = query.substring(1); // strip leading '?'
190+
try {
191+
const statement = await decompressString(compressed);
192+
document.getElementById("input").value = statement;
193+
run();
194+
} catch (e) {
195+
console.error("Failed to decompress URL query:", e);
196+
}
197+
}
198+
});
199+
</script>
200+
</head>
93201
<body>
94202
<textarea
95203
id="input"
96204
rows="10"
97205
placeholder="Type your FlowQuery statement here and press Shift+Enter to run it."
98-
onkeydown="if (event.key === 'Enter' && event.shiftKey) {
99-
run();
100-
event.preventDefault();
101-
}"
206+
onkeydown="
207+
if (event.key === 'Enter' && event.shiftKey) {
208+
run();
209+
event.preventDefault();
210+
}
211+
"
102212
></textarea>
213+
<div id="toolbar">
214+
<button onclick="run()">Run (Shift+Enter)</button>
215+
<button onclick="share()">Share</button>
216+
<input id="share-link" type="text" readonly onclick="this.select()" />
217+
</div>
218+
<div id="metadata"></div>
103219
<div id="output"></div>
104220
</body>
105-
</html>
221+
</html>

flowquery-py/src/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88

99
from .compute.flowquery import FlowQuery
10-
from .compute.runner import Runner
10+
from .compute.runner import Runner, RunnerMetadata
1111
from .io.command_line import CommandLine
1212
from .parsing.functions.aggregate_function import AggregateFunction
1313
from .parsing.functions.async_function import AsyncFunction
@@ -24,6 +24,7 @@
2424
__all__ = [
2525
"FlowQuery",
2626
"Runner",
27+
"RunnerMetadata",
2728
"CommandLine",
2829
"Parser",
2930
"Function",

0 commit comments

Comments
 (0)