diff --git a/Cargo.lock b/Cargo.lock
index 08a2349266a..76fc6d987e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7050,6 +7050,7 @@ dependencies = [
"derive_more",
"duckdb",
"error-stack",
+ "hash-integrations-definition",
"pretty_assertions",
"proptest",
"serde_json",
diff --git a/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd b/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd
index 94890a37cc3..b2d1e6b2794 100644
--- a/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd
+++ b/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd
@@ -34,16 +34,17 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10
diff --git a/libs/@local/codec/rust/docs/dependency-diagram.mmd b/libs/@local/codec/rust/docs/dependency-diagram.mmd
index 8b9969e549a..68c09c7a0f7 100644
--- a/libs/@local/codec/rust/docs/dependency-diagram.mmd
+++ b/libs/@local/codec/rust/docs/dependency-diagram.mmd
@@ -39,16 +39,17 @@ graph TD
27[hashql-mir]
28[hashql-syntax-jexpr]
29[hash-integrations-definition]
- 30[hash-middleware]
- 31[hash-temporal-client]
- 32[error-stack]
- 33[hash-graph-benches]
- 34[hash-graph-integration]
- 35[hash-graph-test-data]
+ 30[hash-integrations-engine]
+ 31[hash-middleware]
+ 32[hash-temporal-client]
+ 33[error-stack]
+ 34[hash-graph-benches]
+ 35[hash-graph-integration]
+ 36[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 35
+ 1 -.-> 36
2 -.-> 3
2 --> 21
4 --> 12
@@ -58,8 +59,8 @@ graph TD
5 --> 9
5 --> 24
6 --> 10
- 6 -.-> 30
- 6 --> 30
+ 6 -.-> 31
+ 6 --> 31
7 --> 1
8 --> 13
9 -.-> 6
@@ -68,11 +69,11 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 31
+ 10 --> 32
11 --> 2
12 --> 10
- 13 -.-> 35
- 14 -.-> 35
+ 13 -.-> 36
+ 14 -.-> 36
15 --> 18
16 --> 2
17 -.-> 1
@@ -82,7 +83,7 @@ graph TD
19 --> 16
21 -.-> 20
21 --> 20
- 21 --> 32
+ 21 --> 33
22 -.-> 23
23 --> 25
23 --> 28
@@ -94,8 +95,9 @@ graph TD
28 --> 22
28 --> 24
29 --> 1
- 30 --> 1
+ 30 --> 29
31 --> 1
- 33 -.-> 4
- 34 -.-> 9
- 35 --> 10
+ 32 --> 1
+ 34 -.-> 4
+ 35 -.-> 9
+ 36 --> 10
diff --git a/libs/@local/codegen/docs/dependency-diagram.mmd b/libs/@local/codegen/docs/dependency-diagram.mmd
index bc363c14cae..ce79f4ce4f9 100644
--- a/libs/@local/codegen/docs/dependency-diagram.mmd
+++ b/libs/@local/codegen/docs/dependency-diagram.mmd
@@ -37,15 +37,16 @@ graph TD
25[hashql-mir]
26[hashql-syntax-jexpr]
27[hash-integrations-definition]
- 28[hash-middleware]
- 29[hash-temporal-client]
- 30[hash-graph-benches]
- 31[hash-graph-integration]
- 32[hash-graph-test-data]
+ 28[hash-integrations-engine]
+ 29[hash-middleware]
+ 30[hash-temporal-client]
+ 31[hash-graph-benches]
+ 32[hash-graph-integration]
+ 33[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 32
+ 1 -.-> 33
2 -.-> 3
4 --> 12
4 --> 17
@@ -54,8 +55,8 @@ graph TD
5 --> 9
5 --> 22
6 --> 10
- 6 -.-> 28
- 6 --> 28
+ 6 -.-> 29
+ 6 --> 29
7 --> 1
8 --> 13
9 -.-> 6
@@ -64,11 +65,11 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 29
+ 10 --> 30
11 --> 2
12 --> 10
- 13 -.-> 32
- 14 -.-> 32
+ 13 -.-> 33
+ 14 -.-> 33
15 --> 18
16 --> 2
17 -.-> 1
@@ -87,8 +88,9 @@ graph TD
26 --> 20
26 --> 22
27 --> 1
- 28 --> 1
+ 28 --> 27
29 --> 1
- 30 -.-> 4
- 31 -.-> 9
- 32 --> 10
+ 30 --> 1
+ 31 -.-> 4
+ 32 -.-> 9
+ 33 --> 10
diff --git a/libs/@local/graph/authorization/rust/docs/dependency-diagram.mmd b/libs/@local/graph/authorization/rust/docs/dependency-diagram.mmd
index f6e60ea0ade..836d869eb07 100644
--- a/libs/@local/graph/authorization/rust/docs/dependency-diagram.mmd
+++ b/libs/@local/graph/authorization/rust/docs/dependency-diagram.mmd
@@ -34,16 +34,17 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10
diff --git a/libs/@local/graph/store/rust/docs/dependency-diagram.mmd b/libs/@local/graph/store/rust/docs/dependency-diagram.mmd
index 31af0d97e9b..4bfb8e6ae34 100644
--- a/libs/@local/graph/store/rust/docs/dependency-diagram.mmd
+++ b/libs/@local/graph/store/rust/docs/dependency-diagram.mmd
@@ -34,16 +34,17 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10
diff --git a/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd b/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd
index 2218648527a..01cb9e911c5 100644
--- a/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd
+++ b/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd
@@ -34,16 +34,17 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10
diff --git a/libs/@local/graph/types/docs/dependency-diagram.mmd b/libs/@local/graph/types/docs/dependency-diagram.mmd
index c9ec59fba95..ab6c50e4554 100644
--- a/libs/@local/graph/types/docs/dependency-diagram.mmd
+++ b/libs/@local/graph/types/docs/dependency-diagram.mmd
@@ -34,16 +34,17 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10
diff --git a/libs/@local/harpc/types/docs/dependency-diagram.mmd b/libs/@local/harpc/types/docs/dependency-diagram.mmd
index 2dc6b0e75c0..c281d0dfea0 100644
--- a/libs/@local/harpc/types/docs/dependency-diagram.mmd
+++ b/libs/@local/harpc/types/docs/dependency-diagram.mmd
@@ -39,15 +39,16 @@ graph TD
27[hashql-mir]
28[hashql-syntax-jexpr]
29[hash-integrations-definition]
- 30[hash-middleware]
- 31[hash-temporal-client]
- 32[hash-graph-benches]
- 33[hash-graph-integration]
- 34[hash-graph-test-data]
+ 30[hash-integrations-engine]
+ 31[hash-middleware]
+ 32[hash-temporal-client]
+ 33[hash-graph-benches]
+ 34[hash-graph-integration]
+ 35[hash-graph-test-data]
0 --> 3
0 --> 4
1 --> 10
- 1 -.-> 34
+ 1 -.-> 35
2 --> 21
3 --> 11
3 --> 17
@@ -56,8 +57,8 @@ graph TD
4 --> 8
4 --> 24
5 --> 9
- 5 -.-> 30
- 5 --> 30
+ 5 -.-> 31
+ 5 --> 31
6 --> 1
7 --> 12
8 -.-> 5
@@ -66,11 +67,11 @@ graph TD
8 --> 13
9 --> 6
9 --> 12
- 9 --> 31
+ 9 --> 32
10 --> 2
11 --> 9
- 12 -.-> 34
- 13 -.-> 34
+ 12 -.-> 35
+ 13 -.-> 35
14 --> 18
15 --> 20
16 --> 2
@@ -94,8 +95,9 @@ graph TD
28 --> 22
28 --> 24
29 --> 1
- 30 --> 1
+ 30 --> 29
31 --> 1
- 32 -.-> 3
- 33 -.-> 8
- 34 --> 9
+ 32 --> 1
+ 33 -.-> 3
+ 34 -.-> 8
+ 35 --> 9
diff --git a/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd b/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd
index 1fdb422e981..b63354590f6 100644
--- a/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd
+++ b/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd
@@ -38,16 +38,17 @@ graph TD
26[hashql-mir]
27[hashql-syntax-jexpr]
28[hash-integrations-definition]
- 29[hash-middleware]
- 30[hash-temporal-client]
- 31[error-stack]
- 32[hash-graph-benches]
- 33[hash-graph-integration]
- 34[hash-graph-test-data]
+ 29[hash-integrations-engine]
+ 30[hash-middleware]
+ 31[hash-temporal-client]
+ 32[error-stack]
+ 33[hash-graph-benches]
+ 34[hash-graph-integration]
+ 35[hash-graph-test-data]
0 --> 3
0 --> 4
1 --> 10
- 1 -.-> 34
+ 1 -.-> 35
2 --> 20
3 --> 11
3 --> 16
@@ -56,8 +57,8 @@ graph TD
4 --> 8
4 --> 23
5 --> 9
- 5 -.-> 29
- 5 --> 29
+ 5 -.-> 30
+ 5 --> 30
6 --> 1
7 --> 12
8 -.-> 5
@@ -66,11 +67,11 @@ graph TD
8 --> 13
9 --> 6
9 --> 12
- 9 --> 30
+ 9 --> 31
10 --> 2
11 --> 9
- 12 -.-> 34
- 13 -.-> 34
+ 12 -.-> 35
+ 13 -.-> 35
14 --> 17
15 --> 2
16 -.-> 1
@@ -80,7 +81,7 @@ graph TD
18 --> 15
20 -.-> 19
20 --> 19
- 20 --> 31
+ 20 --> 32
21 -.-> 22
22 --> 24
22 --> 27
@@ -92,8 +93,9 @@ graph TD
27 --> 21
27 --> 23
28 --> 1
- 29 --> 1
+ 29 --> 28
30 --> 1
- 32 -.-> 3
- 33 -.-> 8
- 34 --> 9
+ 31 --> 1
+ 33 -.-> 3
+ 34 -.-> 8
+ 35 --> 9
diff --git a/libs/@local/integrations/definition/docs/dependency-diagram.mmd b/libs/@local/integrations/definition/docs/dependency-diagram.mmd
index cdbbb94b882..642cdc3af77 100644
--- a/libs/@local/integrations/definition/docs/dependency-diagram.mmd
+++ b/libs/@local/integrations/definition/docs/dependency-diagram.mmd
@@ -19,22 +19,24 @@ graph TD
8[harpc-wire-protocol]
9[hash-integrations-definition]
class 9 root
- 10[hash-temporal-client]
- 11[error-stack]
- 12[hash-graph-test-data]
+ 10[hash-integrations-engine]
+ 11[hash-temporal-client]
+ 12[error-stack]
+ 13[hash-graph-test-data]
0 --> 5
- 0 -.-> 12
+ 0 -.-> 13
1 -.-> 2
1 --> 8
3 --> 0
4 --> 3
4 --> 6
- 4 --> 10
+ 4 --> 11
5 --> 1
- 6 -.-> 12
+ 6 -.-> 13
8 -.-> 7
8 --> 7
- 8 --> 11
+ 8 --> 12
9 --> 0
- 10 --> 0
- 12 --> 4
+ 10 --> 9
+ 11 --> 0
+ 13 --> 4
diff --git a/libs/@local/integrations/engine/Cargo.toml b/libs/@local/integrations/engine/Cargo.toml
index 77d8d2fb781..169ce41ea2d 100644
--- a/libs/@local/integrations/engine/Cargo.toml
+++ b/libs/@local/integrations/engine/Cargo.toml
@@ -9,7 +9,8 @@ authors.workspace = true
[dependencies]
# Public workspace dependencies
-error-stack = { workspace = true, public = true, features = ["std"] }
+error-stack = { workspace = true, public = true, features = ["std"] }
+hash-integrations-definition = { workspace = true, public = true }
# Public third-party dependencies
duckdb = { workspace = true, public = true, features = ["bundled", "json"] }
diff --git a/libs/@local/integrations/engine/docs/dependency-diagram.mmd b/libs/@local/integrations/engine/docs/dependency-diagram.mmd
index 34dc22ca164..7ba7f28f333 100644
--- a/libs/@local/integrations/engine/docs/dependency-diagram.mmd
+++ b/libs/@local/integrations/engine/docs/dependency-diagram.mmd
@@ -8,7 +8,35 @@ graph TD
%% --> : Normal dependency
%% -.-> : Dev dependency
%% ---> : Build dependency
- 0[hash-integrations-engine]
- class 0 root
- 1[error-stack]
- 0 --> 1
+ 0[type-system]
+ 1[hash-codec]
+ 2[hash-codegen]
+ 3[hash-graph-authorization]
+ 4[hash-graph-store]
+ 5[hash-graph-temporal-versioning]
+ 6[hash-graph-types]
+ 7[harpc-types]
+ 8[harpc-wire-protocol]
+ 9[hash-integrations-definition]
+ 10[hash-integrations-engine]
+ class 10 root
+ 11[hash-temporal-client]
+ 12[error-stack]
+ 13[hash-graph-test-data]
+ 0 --> 5
+ 0 -.-> 13
+ 1 -.-> 2
+ 1 --> 8
+ 3 --> 0
+ 4 --> 3
+ 4 --> 6
+ 4 --> 11
+ 5 --> 1
+ 6 -.-> 13
+ 8 -.-> 7
+ 8 --> 7
+ 8 --> 12
+ 9 --> 0
+ 10 --> 9
+ 11 --> 0
+ 13 --> 4
diff --git a/libs/@local/integrations/engine/docs/task-dependencies.json b/libs/@local/integrations/engine/docs/task-dependencies.json
index 5a2d69a8b0d..e36859a5e10 100644
--- a/libs/@local/integrations/engine/docs/task-dependencies.json
+++ b/libs/@local/integrations/engine/docs/task-dependencies.json
@@ -1,7 +1,8 @@
{
"package": "hash-integrations-engine",
"dependencies": [
- "error-stack"
+ "error-stack",
+ "hash-integrations-definition"
],
"tasks": {
"doc:dependency-diagram": {
diff --git a/libs/@local/integrations/engine/src/workspace/connection.rs b/libs/@local/integrations/engine/src/workspace/connection.rs
index 9a8bafd751d..d49a5b6825a 100644
--- a/libs/@local/integrations/engine/src/workspace/connection.rs
+++ b/libs/@local/integrations/engine/src/workspace/connection.rs
@@ -6,8 +6,9 @@ use std::{
path::{Path, PathBuf},
};
-use duckdb::{Config, types::Value};
+use duckdb::{Config, Statement, params_from_iter, types::Value};
use error_stack::{Report, ResultExt as _};
+use hash_integrations_definition::Variables;
use tokio::sync::mpsc;
use crate::{
@@ -38,6 +39,7 @@ pub(super) struct Connection {
duckdb: duckdb::Connection,
files: WorkspaceFiles,
disk_limit: Option,
+ variables: Variables,
/// An exclusive lock on the workspace's lock file, held until the database is closed.
lock: File,
}
@@ -132,7 +134,7 @@ impl Connection {
}
/// Opens the database at `path` with the limits in `config` and its settings locked.
- pub(super) fn open(path: PathBuf, config: &WorkspaceConfig) -> Result> {
+ pub(super) fn open(path: PathBuf, config: WorkspaceConfig) -> Result> {
let files = WorkspaceFiles::new(path);
let lock = Self::lock(&files)?;
@@ -181,6 +183,7 @@ impl Connection {
duckdb,
files,
disk_limit: config.disk_limit,
+ variables: config.variables,
lock,
};
match opened.disk_usage().change_context(OpenError::DiskUsage)? {
@@ -207,6 +210,27 @@ impl Connection {
Select::check(sql, &serialized)
}
+ /// Returns the value of each parameter of `statement`, in the order of their indices.
+ ///
+ /// DuckDB numbers each parameter name once, and compares names without regard to ASCII case.
+ fn parameter_values(
+ &self,
+ statement: &Statement<'_>,
+ ) -> Result, Report> {
+ (1..=statement.parameter_count())
+ .map(|index| {
+ let name = statement
+ .parameter_name(index)
+ .map_err(StatementError::from_duckdb)?;
+ self.variables.get(&name).ok_or_else(|| {
+ Report::new(StatementError::UnknownParameter {
+ name: name.into_boxed_str(),
+ })
+ })
+ })
+ .collect()
+ }
+
#[tracing::instrument(level = "debug", skip_all)]
fn query(&self, sql: &str) -> Result> {
let select = self.select(sql)?;
@@ -214,7 +238,10 @@ impl Connection {
.duckdb
.prepare(select.as_str())
.map_err(StatementError::from_duckdb)?;
- let results = statement.query([]).map_err(StatementError::from_duckdb)?;
+ let values = self.parameter_values(&statement)?;
+ let results = statement
+ .query(params_from_iter(values))
+ .map_err(StatementError::from_duckdb)?;
if let Some(executed) = results.as_ref() {
for index in 0..executed.column_count() {
@@ -253,13 +280,17 @@ impl Connection {
fn create_table(&self, table: &str, sql: &str) -> Result<(), Report> {
let select = self.select(sql)?;
// `select` is one statement, so the whole text is one statement as well.
- self.duckdb
+ let mut statement = self
+ .duckdb
.prepare(&format!(
"CREATE OR REPLACE TABLE {} AS {}",
Identifier::new(table),
select.as_str()
))
- .and_then(|mut statement| statement.execute([]))
+ .map_err(StatementError::from_duckdb)?;
+ let values = self.parameter_values(&statement)?;
+ statement
+ .execute(params_from_iter(values))
.map_err(StatementError::from_duckdb)?;
self.check_disk_limit()
}
@@ -399,7 +430,7 @@ impl Connection {
/// unwinds, so nothing it holds is used after the panic.
pub(super) fn serve(
path: PathBuf,
- config: &WorkspaceConfig,
+ config: WorkspaceConfig,
ready: Reply, OpenError>,
commands: &mut mpsc::Receiver<(RequestId, Command)>,
) {
diff --git a/libs/@local/integrations/engine/src/workspace/error.rs b/libs/@local/integrations/engine/src/workspace/error.rs
index 1ab25e51fd6..591ebf2d0c9 100644
--- a/libs/@local/integrations/engine/src/workspace/error.rs
+++ b/libs/@local/integrations/engine/src/workspace/error.rs
@@ -179,6 +179,8 @@ pub enum StatementError {
StatementCount { count: usize },
#[display("the query calls table function `{name}`, which is not allowed")]
TableFunction { name: Box },
+ #[display("the query reads parameter `${name}`, which is not set")]
+ UnknownParameter { name: Box },
/// `data_type` is the column's type as the `duckdb` crate receives it, in Arrow's notation.
#[display("column `{column}` has type `{data_type}`, which the workspace cannot read")]
UnsupportedType {
diff --git a/libs/@local/integrations/engine/src/workspace/mod.rs b/libs/@local/integrations/engine/src/workspace/mod.rs
index e36fbfc278d..641528b7cf1 100644
--- a/libs/@local/integrations/engine/src/workspace/mod.rs
+++ b/libs/@local/integrations/engine/src/workspace/mod.rs
@@ -11,6 +11,7 @@ use core::{error::Error, num::NonZeroUsize};
use std::{path::PathBuf, thread};
use error_stack::{Report, ResultExt as _};
+use hash_integrations_definition::Variables;
use tokio::sync::mpsc;
pub use self::{
@@ -35,6 +36,8 @@ pub struct WorkspaceConfig {
/// The number of requests that can wait while another runs. Defaults to 64.
/// Callers wait for queue space when it is full.
pub queue_capacity: NonZeroUsize,
+ /// The values of the `$NAME` parameters that queries read.
+ pub variables: Variables,
/// The most memory DuckDB uses. A query that needs more spills to the temporary directory,
/// or fails if it cannot.
pub memory_limit: Option,
@@ -58,6 +61,7 @@ impl Default for WorkspaceConfig {
Self {
queue_capacity: NonZeroUsize::new(64)
.expect("the default queue capacity should be nonzero"),
+ variables: Variables::default(),
memory_limit: None,
temp_directory_limit: None,
disk_limit: None,
@@ -90,8 +94,8 @@ enum Command {
///
/// A dedicated thread owns the database's only connection and runs queries in the order they
/// are sent. A query is a single `SELECT` statement, and the only table functions it can call are
-/// `range`, `generate_series` and `unnest`. Queries cannot open files, load extensions or change
-/// settings.
+/// `range`, `generate_series` and `unnest`. Queries read [`WorkspaceConfig::variables`] as
+/// `$NAME` parameters, and cannot open files, load extensions or change settings.
/// [`WorkspaceConfig::queue_capacity`] bounds the queue; callers wait for space when it is full.
///
/// Only one workspace at a time can open a database. It holds a lock on `.lock` until
@@ -142,7 +146,7 @@ impl Workspace {
.name("integrations-workspace".to_owned())
.spawn(move || {
let _entered = span.entered();
- Connection::serve(path, &config, ready, &mut receiver);
+ Connection::serve(path, config, ready, &mut receiver);
})
.change_context(OpenError::Spawn)?;
@@ -182,6 +186,8 @@ impl Workspace {
/// - [`NotSelect`] if DuckDB does not read `sql` as a `SELECT` query
/// - [`StatementCount`] if `sql` is not exactly one statement
/// - [`TableFunction`] if the query calls a table function that is not allowed
+ /// - [`UnknownParameter`] if the query reads a parameter that [`WorkspaceConfig::variables`]
+ /// does not set
/// - [`UnsupportedType`] if a column has a type whose values cannot be read
/// - [`InvalidQuery`] if DuckDB cannot run the query as written
/// - [`InvalidData`] if a value in the rows causes an error
@@ -194,6 +200,7 @@ impl Workspace {
/// [`NotSelect`]: StatementError::NotSelect
/// [`StatementCount`]: StatementError::StatementCount
/// [`TableFunction`]: StatementError::TableFunction
+ /// [`UnknownParameter`]: StatementError::UnknownParameter
/// [`UnsupportedType`]: StatementError::UnsupportedType
/// [`InvalidQuery`]: StatementError::InvalidQuery
/// [`InvalidData`]: StatementError::InvalidData
@@ -216,6 +223,8 @@ impl Workspace {
/// - [`NotSelect`] if DuckDB does not read `sql` as a `SELECT` query
/// - [`StatementCount`] if `sql` is not exactly one statement
/// - [`TableFunction`] if the query calls a table function that is not allowed
+ /// - [`UnknownParameter`] if the query reads a parameter that [`WorkspaceConfig::variables`]
+ /// does not set
/// - [`InvalidQuery`] if DuckDB cannot run the query as written
/// - [`InvalidData`] if a value in the rows causes an error
/// - [`Failed`] if DuckDB fails for another reason
@@ -227,6 +236,7 @@ impl Workspace {
/// [`NotSelect`]: StatementError::NotSelect
/// [`StatementCount`]: StatementError::StatementCount
/// [`TableFunction`]: StatementError::TableFunction
+ /// [`UnknownParameter`]: StatementError::UnknownParameter
/// [`InvalidQuery`]: StatementError::InvalidQuery
/// [`InvalidData`]: StatementError::InvalidData
/// [`Failed`]: StatementError::Failed
diff --git a/libs/@local/integrations/engine/tests/workspace.rs b/libs/@local/integrations/engine/tests/workspace.rs
index 544d0963304..655580a216a 100644
--- a/libs/@local/integrations/engine/tests/workspace.rs
+++ b/libs/@local/integrations/engine/tests/workspace.rs
@@ -1,6 +1,11 @@
#![expect(missing_docs, reason = "integration tests")]
-use core::{future::poll_fn, num::NonZeroUsize, pin::Pin, task::Poll, time::Duration};
+extern crate alloc;
+
+use alloc::collections::BTreeMap;
+use core::{
+ future::poll_fn, num::NonZeroUsize, pin::Pin, str::FromStr as _, task::Poll, time::Duration,
+};
use std::{
fs,
path::{Path, PathBuf},
@@ -8,6 +13,7 @@ use std::{
use duckdb::types::{TimeUnit, Value};
use error_stack::Report;
+use hash_integrations_definition::{VariableName, Variables};
use hash_integrations_engine::{
ByteSize, DataErrorType, OpenError, QueryErrorType, SnapshotError, StatementError, Workspace,
WorkspaceConfig,
@@ -748,3 +754,76 @@ async fn disk_limit_checkpoints_before_refusing() {
"the last table should be kept"
);
}
+
+#[tokio::test]
+async fn query_binds_parameters() {
+ let directory = TempDir::new().expect("should create a directory");
+ let variables = Variables::try_from(BTreeMap::from(
+ [
+ ("MIN_SEATS", "300"),
+ ("Excluded", "'); DROP TABLE flights; --"),
+ ]
+ .map(|(name, value)| {
+ (
+ VariableName::from_str(name).expect("should be a valid variable name"),
+ value.into(),
+ )
+ }),
+ ))
+ .expect("the variables should be valid");
+ let workspace = open_with(
+ &directory,
+ WorkspaceConfig {
+ variables,
+ ..WorkspaceConfig::default()
+ },
+ )
+ .await;
+
+ workspace
+ .create_table(
+ "flights".into(),
+ "SELECT * FROM (VALUES ('BA117', 214), ('LH400', 364)) AS flights (code, seats) WHERE \
+ seats >= $min_seats::INTEGER - 100"
+ .into(),
+ )
+ .await
+ .expect("the table should be created");
+ let rows = workspace
+ .query(
+ "SELECT code, $EXCLUDED AS excluded FROM flights WHERE seats > $MIN_SEATS::INTEGER \
+ AND code <> $excluded"
+ .into(),
+ )
+ .await
+ .expect("the query should run");
+
+ assert_eq!(
+ rows.iter().collect::>(),
+ [[
+ Value::Text("LH400".to_owned()),
+ Value::Text("'); DROP TABLE flights; --".to_owned()),
+ ]],
+ "each parameter should be bound as text, matched without regard to case"
+ );
+}
+
+#[tokio::test]
+async fn query_refuses_unknown_parameter() {
+ let directory = TempDir::new().expect("should create a directory");
+ let workspace = open(&directory).await;
+
+ for (sql, name) in [("SELECT $MISSING", "MISSING"), ("SELECT ?", "1")] {
+ let report = workspace
+ .query(sql.into())
+ .await
+ .expect_err("a parameter without a value should be refused");
+ assert!(
+ matches!(
+ report.current_context(),
+ StatementError::UnknownParameter { name: found } if &**found == name
+ ),
+ "`{sql}` should be refused for parameter `{name}`, not by {report:?}"
+ );
+ }
+}
diff --git a/libs/@local/temporal-client/docs/dependency-diagram.mmd b/libs/@local/temporal-client/docs/dependency-diagram.mmd
index 2b573047cdc..e6bf5e8dfab 100644
--- a/libs/@local/temporal-client/docs/dependency-diagram.mmd
+++ b/libs/@local/temporal-client/docs/dependency-diagram.mmd
@@ -33,17 +33,18 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- class 26 root
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ class 27 root
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10
diff --git a/tests/graph/test-data/rust/docs/dependency-diagram.mmd b/tests/graph/test-data/rust/docs/dependency-diagram.mmd
index b3f0d9b9c02..05238d870ee 100644
--- a/tests/graph/test-data/rust/docs/dependency-diagram.mmd
+++ b/tests/graph/test-data/rust/docs/dependency-diagram.mmd
@@ -33,17 +33,18 @@ graph TD
22[hashql-mir]
23[hashql-syntax-jexpr]
24[hash-integrations-definition]
- 25[hash-middleware]
- 26[hash-temporal-client]
- 27[error-stack]
- 28[hash-graph-benches]
- 29[hash-graph-integration]
- 30[hash-graph-test-data]
- class 30 root
+ 25[hash-integrations-engine]
+ 26[hash-middleware]
+ 27[hash-temporal-client]
+ 28[error-stack]
+ 29[hash-graph-benches]
+ 30[hash-graph-integration]
+ 31[hash-graph-test-data]
+ class 31 root
0 --> 4
0 --> 5
1 --> 11
- 1 -.-> 30
+ 1 -.-> 31
2 -.-> 3
2 --> 17
4 --> 12
@@ -52,8 +53,8 @@ graph TD
4 --> 23
5 --> 9
6 --> 10
- 6 -.-> 25
- 6 --> 25
+ 6 -.-> 26
+ 6 --> 26
7 --> 1
8 --> 13
9 -.-> 6
@@ -62,16 +63,16 @@ graph TD
9 --> 14
10 --> 7
10 --> 13
- 10 --> 26
+ 10 --> 27
11 --> 2
12 --> 10
- 13 -.-> 30
- 14 -.-> 30
+ 13 -.-> 31
+ 14 -.-> 31
15 -.-> 1
15 --> 16
17 -.-> 16
17 --> 16
- 17 --> 27
+ 17 --> 28
18 -.-> 19
19 --> 20
19 --> 23
@@ -81,8 +82,9 @@ graph TD
22 --> 21
23 --> 18
24 --> 1
- 25 --> 1
+ 25 --> 24
26 --> 1
- 28 -.-> 4
- 29 -.-> 9
- 30 --> 10
+ 27 --> 1
+ 29 -.-> 4
+ 30 -.-> 9
+ 31 --> 10