Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions libs/@local/integrations/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ authors.workspace = true
# Public workspace dependencies
error-stack = { workspace = true, public = true, features = ["std"] }
hash-integrations-definition = { workspace = true, public = true }
type-system = { workspace = true, public = true }

# Public third-party dependencies
duckdb = { workspace = true, public = true, features = ["bundled", "json"] }
Expand All @@ -23,10 +24,12 @@ serde_json = { workspace = true }
simple-mermaid = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tracing = { workspace = true, features = ["attributes"] }
uuid = { workspace = true, features = ["v5"] }

[dev-dependencies]
pretty_assertions = { workspace = true }
proptest = { workspace = true, features = ["attr-macro", "std"] }
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt", "time"] }

Expand Down
2 changes: 2 additions & 0 deletions libs/@local/integrations/engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
Runs an integration's pipelines in DuckDB.

Each run gets a `Workspace`, a DuckDB database file in which the run's queries execute. A query is a single `SELECT`, and cannot open files, load extensions or change settings.

`derive_entity_id` derives stable entity IDs from an integration namespace, entity type and source record ID using UUID v5.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"package": "hash-integrations-engine",
"dependencies": [
"error-stack",
"hash-integrations-definition"
"hash-integrations-definition",
"type-system"
],
"tasks": {
"doc:dependency-diagram": {
Expand Down
24 changes: 24 additions & 0 deletions libs/@local/integrations/engine/src/entity_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use type_system::{
knowledge::entity::id::{EntityId, EntityUuid},
ontology::id::VersionedUrl,
principal::actor_group::WebId,
};
use uuid::Uuid;

const NAMESPACE: Uuid = Uuid::from_u128(0xD6E2_C7A1_F84B_4E3A_9C0D_5B7F_1E3A_2D4C);

/// Derives a live entity ID from the integration namespace, entity type, and source record ID.
#[must_use]
pub fn derive_entity_id(
web_id: WebId,
namespace: &str,
entity_type: &VersionedUrl,
source_id: &str,
) -> EntityId {
let name = format!("{namespace}::{entity_type}::{source_id}");
EntityId {
web_id,
entity_uuid: EntityUuid::new(Uuid::new_v5(&NAMESPACE, name.as_bytes())),
draft_id: None,
}
}
2 changes: 2 additions & 0 deletions libs/@local/integrations/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
extern crate alloc;

mod byte_size;
mod entity_id;
mod sql;
mod variant_name;
mod workspace;

pub use self::{
byte_size::ByteSize,
entity_id::derive_entity_id,
workspace::{
CloseError, DataErrorType, DiskUsageError, OpenError, QueryErrorType, Rows, Setting,
SnapshotError, StatementError, Workspace, WorkspaceConfig,
Expand Down
46 changes: 46 additions & 0 deletions libs/@local/integrations/engine/tests/entity_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Deterministic entity ID derivation.

use hash_integrations_engine::derive_entity_id;
use pretty_assertions::assert_eq;
use serde::Deserialize;
use type_system::{
knowledge::entity::id::EntityUuid, ontology::id::VersionedUrl, principal::actor_group::WebId,
};
use uuid::Uuid;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Case {
ns: String,
entity_type: VersionedUrl,
entity_id: String,
uuid: EntityUuid,
composite: String,
note: String,
}

#[test]
fn derive_entity_id_recorded_vectors() {
let web_id = WebId::new(Uuid::from_u128(0xA2C8_E6C7_485D_4B54_A248_53C9_B9B0_14A2));
let cases: Vec<Case> =
serde_json::from_str(include_str!("fixtures/entity-ids-typescript.json"))
.expect("entity ID fixtures should be valid");
for case in cases {
let entity_id = derive_entity_id(web_id, &case.ns, &case.entity_type, &case.entity_id);
assert_eq!(
entity_id.entity_uuid, case.uuid,
"entity UUID should match the recorded value: {}",
case.note
);
assert_eq!(
entity_id.to_string(),
case.composite.replace("web-fixture", &web_id.to_string()),
"composite ID should match the recorded value: {}",
case.note
);
assert!(
entity_id.draft_id.is_none(),
"derived entity ID should identify a live entity"
);
}
}
5 changes: 5 additions & 0 deletions libs/@local/integrations/engine/tests/fixtures/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Entity ID fixtures

`entity-ids-typescript.json` contains the proof of concept's [recorded UUID vectors](https://github.com/hashintel/labs/blob/40a8d14535cee4020c2cd24c7e513bc209b59e19/pocs/integrations_rs/tests/golden/uuids.json). The two numeric source IDs are stored as text to match the derivation function's input.

`web-fixture` is replaced with a valid web UUID when testing composite entity IDs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "M-0001",
"note": "plain string id",
"uuid": "fdb92954-9bd5-5558-ae43-40437c693f27",
"composite": "web-fixture~fdb92954-9bd5-5558-ae43-40437c693f27"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "1",
"note": "numeric ID represented as text",
"uuid": "2f37a211-5787-5474-83b3-e3d7cf6bb83a",
"composite": "web-fixture~2f37a211-5787-5474-83b3-e3d7cf6bb83a"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "1.5",
"note": "decimal ID represented as text",
"uuid": "42a6299f-2d8d-5b73-b11f-64badff46a94",
"composite": "web-fixture~42a6299f-2d8d-5b73-b11f-64badff46a94"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "Grüße-仓库",
"note": "unicode id, hashed as UTF-8 bytes",
"uuid": "5aec7f53-3989-5d44-ace4-a78b860193a5",
"composite": "web-fixture~5aec7f53-3989-5d44-ace4-a78b860193a5"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "a::b",
"note": "id containing the :: separator",
"uuid": "b0ed6b55-ea24-512b-97fe-e17e1c7f6861",
"composite": "web-fixture~b0ed6b55-ea24-512b-97fe-e17e1c7f6861"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "A|B",
"note": "composite-joined id",
"uuid": "1ecf2406-4eba-5b0e-93f2-1a08389594d7",
"composite": "web-fixture~1ecf2406-4eba-5b0e-93f2-1a08389594d7"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/stored-at/v/1",
"entityId": "https://x/@t/types/entity-type/material/v/1::M-0001::https://x/@t/types/entity-type/plant/v/1::P-0100",
"note": "link uuid: entityId is sourceType::sourceId::targetType::targetId",
"uuid": "25bbb6e3-3e0a-53c2-a2f3-b8b83f7ff314",
"composite": "web-fixture~25bbb6e3-3e0a-53c2-a2f3-b8b83f7ff314"
},
{
"ns": "conn-a",
"entityType": "https://x/@t/types/entity-type/material/v/1",
"entityId": "",
"note": "empty-string id",
"uuid": "54b1ee67-5978-54fd-81ca-18d4ed3cdb2e",
"composite": "web-fixture~54b1ee67-5978-54fd-81ca-18d4ed3cdb2e"
}
]
Loading