diff --git a/packages/graph-explorer/src/core/ConfigurationProvider/types.ts b/packages/graph-explorer/src/core/ConfigurationProvider/types.ts index 2c940d88e..d1d9a41ec 100644 --- a/packages/graph-explorer/src/core/ConfigurationProvider/types.ts +++ b/packages/graph-explorer/src/core/ConfigurationProvider/types.ts @@ -98,6 +98,19 @@ export type PrefixTypeConfig = { __inferred?: boolean; }; +/** Creates a PrefixTypeConfig from plain strings. */ +export function createPrefixTypeConfig(options: { + prefix: string; + uri: string; + inferred?: boolean; +}): PrefixTypeConfig { + return { + prefix: options.prefix as RdfPrefix, + uri: options.uri as IriNamespace, + __inferred: options.inferred, + }; +} + /** * Represents a connection between node labels via an edge type. * Used by Schema Explorer to visualize relationships between node types. diff --git a/packages/graph-explorer/src/core/StateProvider/schema.test.ts b/packages/graph-explorer/src/core/StateProvider/schema.test.ts index 5fdb559dc..903ad2631 100644 --- a/packages/graph-explorer/src/core/StateProvider/schema.test.ts +++ b/packages/graph-explorer/src/core/StateProvider/schema.test.ts @@ -1,5 +1,6 @@ // @vitest-environment happy-dom import { createArray, createRandomName } from "@shared/utils/testing"; +import { act } from "@testing-library/react"; import { useAtomValue } from "jotai"; import type { IriNamespace, RdfPrefix } from "@/utils/rdf"; @@ -14,7 +15,6 @@ import { LABELS } from "@/utils"; import { createRandomEdge, createRandomEdgeConnection, - createRandomEntities, createRandomRawConfiguration, createRandomSchema, createRandomVertex, @@ -24,12 +24,12 @@ import { renderHookWithState, } from "@/utils/testing"; -import type { - EdgeTypeConfig, - PrefixTypeConfig, - VertexTypeConfig, +import { + createPrefixTypeConfig, + type EdgeTypeConfig, + type PrefixTypeConfig, + type VertexTypeConfig, } from "../ConfigurationProvider"; - import { createEdge, createEdgeType, @@ -43,12 +43,12 @@ import { mapVertexToTypeConfigs, maybeActiveSchemaAtom, type SchemaStorageModel, - shouldUpdateSchemaFromEntities, updateSchemaFromEntities, useActiveSchema, useGraphSchema, useHasActiveSchema, useMaybeActiveSchema, + useUpdateSchemaFromEntities, } from "./schema"; describe("schema", () => { @@ -264,6 +264,136 @@ describe("schema", () => { const prefixes = result.prefixes?.map(p => p.prefix); expect(prefixes).toContain("country"); }); + + it("should add all types from a multi-label vertex", () => { + const schema: SchemaStorageModel = { + vertices: [], + edges: [], + }; + + const vertex = createVertex({ + id: "1", + types: ["Person", "Employee"], + attributes: { name: "Alice" }, + }); + + const result = updateSchemaFromEntities({ vertices: [vertex] }, schema); + + expect(result.vertices).toHaveLength(2); + expect(result.vertices.map(v => v.type)).toStrictEqual([ + createVertexType("Person"), + createVertexType("Employee"), + ]); + expect(result.vertices[0].attributes).toStrictEqual([ + { name: "name", dataType: "String" }, + ]); + expect(result.vertices[1].attributes).toStrictEqual([ + { name: "name", dataType: "String" }, + ]); + }); + + it("should not regenerate prefixes for existing schema types", () => { + const schema: SchemaStorageModel = { + vertices: [ + { + type: createVertexType("http://example.com/ontology#Person"), + attributes: [ + { name: "http://example.com/ontology#name", dataType: "String" }, + ], + }, + ], + edges: [], + prefixes: [ + createPrefixTypeConfig({ + prefix: "ontology", + uri: "http://example.com/ontology#", + inferred: true, + }), + ], + }; + + const vertex = createVertex({ + id: "1", + types: ["http://example.com/ontology#Person"], + attributes: { + "http://example.com/ontology#name": "Alice", + }, + }); + + const result = updateSchemaFromEntities({ vertices: [vertex] }, schema); + + expect(result.prefixes).toBe(schema.prefixes); + }); + + it("should generate prefixes for new attribute namespaces on existing types", () => { + const schema: SchemaStorageModel = { + vertices: [ + { + type: createVertexType("http://example.com/ontology#Person"), + attributes: [ + { name: "http://example.com/ontology#name", dataType: "String" }, + ], + }, + ], + edges: [], + prefixes: [ + createPrefixTypeConfig({ + prefix: "ontology", + uri: "http://example.com/ontology#", + inferred: true, + }), + ], + }; + + const vertex = createVertex({ + id: "1", + types: ["http://example.com/ontology#Person"], + attributes: { + "http://new.example.com/props#age": 30, + }, + }); + + const result = updateSchemaFromEntities({ vertices: [vertex] }, schema); + + const newPrefixes = result.prefixes!.filter( + p => !schema.prefixes!.includes(p), + ); + expect(newPrefixes).toHaveLength(1); + expect(newPrefixes[0].prefix).toBe("props"); + }); + + it("should generate prefixes for new types but not existing ones", () => { + const schema: SchemaStorageModel = { + vertices: [ + { + type: createVertexType("http://example.com/ontology#Person"), + attributes: [], + }, + ], + edges: [], + prefixes: [ + createPrefixTypeConfig({ + prefix: "ontology", + uri: "http://example.com/ontology#", + inferred: true, + }), + ], + }; + + const vertex = createVertex({ + id: "http://example.com/resource/1", + types: ["http://example.com/classes#Employee"], + attributes: {}, + }); + + const result = updateSchemaFromEntities({ vertices: [vertex] }, schema); + + const newPrefixes = result.prefixes!.filter( + p => !schema.prefixes!.includes(p), + ); + expect(newPrefixes.length).toBeGreaterThan(0); + expect(newPrefixes.every(p => p.__inferred)).toBe(true); + }); }); describe("generateSchemaPrefixes", () => { @@ -282,24 +412,24 @@ describe("schema", () => { const result = generateSchemaPrefixes(iris, []); expect(result).toEqual([ - { - prefix: "vertex" as RdfPrefix, - uri: "http://abcdefg.com/vertex#" as IriNamespace, - __inferred: true, - }, - { - prefix: "edge" as RdfPrefix, - uri: "http://abcdefg.com/edge#" as IriNamespace, - __inferred: true, - }, + createPrefixTypeConfig({ + prefix: "vertex", + uri: "http://abcdefg.com/vertex#", + inferred: true, + }), + createPrefixTypeConfig({ + prefix: "edge", + uri: "http://abcdefg.com/edge#", + inferred: true, + }), ] satisfies PrefixTypeConfig[]); }); it("should not regenerate prefixes already covered by existing ones", () => { - const existingPrefix: PrefixTypeConfig = { - prefix: "custom" as RdfPrefix, - uri: "http://custom.example.com/" as IriNamespace, - }; + const existingPrefix = createPrefixTypeConfig({ + prefix: "custom", + uri: "http://custom.example.com/", + }); const result = generateSchemaPrefixes( new Set(["http://custom.example.com/Thing"]), @@ -308,73 +438,33 @@ describe("schema", () => { expect(result).toStrictEqual([]); }); - }); - - describe("shouldUpdateSchemaFromEntities", () => { - it("should return false when no entities are provided", () => { - const result = shouldUpdateSchemaFromEntities( - { vertices: [], edges: [] }, - createRandomSchema(), - ); - expect(result).toBeFalsy(); - }); - it("should return true when entities are provided", () => { - const entities = createRandomEntities(); - const result = shouldUpdateSchemaFromEntities( - entities, - createRandomSchema(), - ); - expect(result).toBeTruthy(); - }); + it("should append next numeral when prefix name collides with existing deduplicated prefixes", () => { + const existingPrefixes = [ + createPrefixTypeConfig({ + prefix: "country", + uri: "http://data.example.org/country/", + inferred: true, + }), + createPrefixTypeConfig({ + prefix: "country2", + uri: "http://stats.example.org/country/", + inferred: true, + }), + ]; - it("should return false when the vertex has an existing type", () => { - const schema = createRandomSchema(); - const vertex = createRandomVertex(); - vertex.type = schema.vertices[0].type; - vertex.attributes = schema.vertices[0].attributes.reduce((acc, attr) => { - acc[attr.name] = createRandomName("value"); - return acc; - }, {} as EntityProperties); - const result = shouldUpdateSchemaFromEntities( - { - vertices: [vertex], - edges: [], - }, - schema, + const result = generateSchemaPrefixes( + new Set(["http://geo.example.org/country/France"]), + existingPrefixes, ); - expect(result).toBeFalsy(); - }); - it("should return false when the edge is an existing type", () => { - const schema = createRandomSchema(); - const source = createRandomVertex(); - const target = createRandomVertex(); - source.type = schema.vertices[0].type; - source.attributes = schema.vertices[0].attributes.reduce((acc, attr) => { - acc[attr.name] = createRandomName("value"); - return acc; - }, {} as EntityProperties); - target.type = schema.vertices[1].type; - target.attributes = schema.vertices[1].attributes.reduce((acc, attr) => { - acc[attr.name] = createRandomName("value"); - return acc; - }, {} as EntityProperties); - const edge = createRandomEdge(source, target); - edge.type = schema.edges[0].type; - edge.attributes = schema.edges[0].attributes.reduce((acc, attr) => { - acc[attr.name] = createRandomName("value"); - return acc; - }, {} as EntityProperties); - - const result = shouldUpdateSchemaFromEntities( - { - vertices: [source, target], - edges: [edge], - }, - schema, - ); - expect(result).toBeFalsy(); + expect(result).toStrictEqual([ + createPrefixTypeConfig({ + prefix: "country3", + uri: "http://geo.example.org/country/", + inferred: true, + }), + ]); }); }); }); @@ -463,6 +553,33 @@ describe("referential integrity", () => { expect(result.edges[0]).not.toBe(schema.edges[0]); expect(result.edges[0].attributes).not.toBe(schema.edges[0].attributes); }); + + it("should preserve vertices array reference when only edges change", () => { + const schema = createRandomSchema(); + const edge = createRandomEdge(); + + const result = updateSchemaFromEntities({ edges: [edge] }, schema); + + expect(result.vertices).toBe(schema.vertices); + }); + + it("should preserve edges array reference when only vertices change", () => { + const schema = createRandomSchema(); + const vertex = createRandomVertex(); + + const result = updateSchemaFromEntities({ vertices: [vertex] }, schema); + + expect(result.edges).toBe(schema.edges); + }); + + it("should preserve prefixes reference when no new prefixes needed", () => { + const schema = createRandomSchema(); + const vertex = createRandomVertex(); + + const result = updateSchemaFromEntities({ vertices: [vertex] }, schema); + + expect(result.prefixes).toBe(schema.prefixes); + }); }); describe("useGraphSchema edgeConnections", () => { @@ -560,6 +677,60 @@ describe("useGraphSchema edgeConnections", () => { }); }); +describe("useUpdateSchemaFromEntities", () => { + it("should not churn schemaAtom when active config has no schema entry", () => { + const state = new DbState().withNoActiveSchema(); + + const { result } = renderHookWithState( + () => ({ + updateSchema: useUpdateSchemaFromEntities(), + schemaMap: useAtomValue(schemaAtom), + }), + state, + ); + + const schemaMapBefore = result.current.schemaMap; + + act(() => { + result.current.updateSchema({ + vertices: [createRandomVertex()], + }); + }); + + expect(result.current.schemaMap).toBe(schemaMapBefore); + }); + + it("should not churn schemaAtom when entities already match schema", () => { + const state = new DbState(); + const existingVertex = createRandomVertex(); + existingVertex.type = state.activeSchema.vertices[0].type; + existingVertex.types = [state.activeSchema.vertices[0].type]; + existingVertex.attributes = + state.activeSchema.vertices[0].attributes.reduce((acc, attr) => { + acc[attr.name] = createRandomName("value"); + return acc; + }, {} as EntityProperties); + + const { result } = renderHookWithState( + () => ({ + updateSchema: useUpdateSchemaFromEntities(), + schemaMap: useAtomValue(schemaAtom), + }), + state, + ); + + const schemaMapBefore = result.current.schemaMap; + + act(() => { + result.current.updateSchema({ + vertices: [existingVertex], + }); + }); + + expect(result.current.schemaMap).toBe(schemaMapBefore); + }); +}); + describe("useHasActiveSchema", () => { test("should return false when schema has no lastUpdate", () => { const state = new DbState(); @@ -738,11 +909,11 @@ describe("backward compatibility: legacy __matches on prefixes", () => { // New prefix should be generated expect(result).toStrictEqual([ - { - prefix: "vertex" as RdfPrefix, - uri: "http://newdomain.com/vertex#" as IriNamespace, - __inferred: true, - }, + createPrefixTypeConfig({ + prefix: "vertex", + uri: "http://newdomain.com/vertex#", + inferred: true, + }), ]); }); @@ -791,10 +962,12 @@ describe("backward compatibility: legacy __matches on prefixes", () => { expect(result.prefixes?.[0]).toBe(legacyPrefix); // New prefix should be appended for the new namespace expect(result.prefixes).toHaveLength(2); - expect(result.prefixes?.[1]).toStrictEqual({ - prefix: "vertex" as RdfPrefix, - uri: "http://new.example.com/vertex#" as IriNamespace, - __inferred: true, - }); + expect(result.prefixes?.[1]).toStrictEqual( + createPrefixTypeConfig({ + prefix: "vertex", + uri: "http://new.example.com/vertex#", + inferred: true, + }), + ); }); }); diff --git a/packages/graph-explorer/src/core/StateProvider/schema.ts b/packages/graph-explorer/src/core/StateProvider/schema.ts index 2cbd59101..da163051d 100644 --- a/packages/graph-explorer/src/core/StateProvider/schema.ts +++ b/packages/graph-explorer/src/core/StateProvider/schema.ts @@ -29,7 +29,7 @@ import { type VertexType, } from "@/core"; import { logger } from "@/utils"; -import { generatePrefixes, PrefixLookup, splitIri } from "@/utils/rdf"; +import { generatePrefixes, PrefixLookup } from "@/utils/rdf"; /** * Persisted schema state for a database connection. @@ -252,21 +252,23 @@ export const activeSchemaSelector = atom( return; } set(schemaAtom, prevSchemaMap => { - const updatedSchemaMap = new Map(prevSchemaMap); - const prev = updatedSchemaMap.get(schemaId); + const prev = prevSchemaMap.get(schemaId); const newValue = typeof update === "function" ? update(prev) : update; - // Handle reset value or undefined + if (newValue === prev) { + return prevSchemaMap; + } + + const updatedSchemaMap = new Map(prevSchemaMap); + if (newValue === RESET || !newValue) { + if (!prev) { + return prevSchemaMap; + } updatedSchemaMap.delete(schemaId); return updatedSchemaMap; } - if (newValue === prev) { - return prevSchemaMap; - } - - // Update the map updatedSchemaMap.set(schemaId, newValue); return updatedSchemaMap; @@ -274,7 +276,7 @@ export const activeSchemaSelector = atom( }, ); -/** Updates the schema based on the given nodes and edges. */ +/** Updates the schema based on the given nodes and edges. Preserves referential equality at every level when nothing changes. */ export function updateSchemaFromEntities( entities: Partial, schema: SchemaStorageModel, @@ -286,131 +288,217 @@ export function updateSchemaFromEntities( return schema; } - const newVertexConfigs = vertices.flatMap(mapVertexToTypeConfigs); - const newEdgeConfigs = edges.map(mapEdgeToTypeConfig); - - const mergedVertices = merge(schema.vertices, newVertexConfigs); - const mergedEdges = merge(schema.edges, newEdgeConfigs); + const { configs: mergedVertices, newIris: vertexIris } = mergeVertices( + schema.vertices, + vertices, + ); + const { configs: mergedEdges, newIris: edgeIris } = mergeEdges( + schema.edges, + edges, + ); - // Generate new prefixes for the schema changes and resource IRIs const existingPrefixes = schema.prefixes ?? []; - const entityUris = getEntityUris(entities); - const schemaUris = getSchemaUris({ - vertices: mergedVertices, - edges: mergedEdges, - }); - const newPrefixes = generateSchemaPrefixes( - entityUris.union(schemaUris), + const mergedPrefixes = mergePrefixes( existingPrefixes, + entities, + vertexIris, + edgeIris, ); if ( mergedVertices === schema.vertices && mergedEdges === schema.edges && - newPrefixes.length === 0 + mergedPrefixes === existingPrefixes ) { + logger.debug("Schema already up to date with given entities"); return schema; } - const newSchema = { + const result = { ...schema, vertices: mergedVertices, edges: mergedEdges, prefixes: - newPrefixes.length > 0 - ? [...existingPrefixes, ...newPrefixes] - : schema.prefixes, + mergedPrefixes !== existingPrefixes ? mergedPrefixes : schema.prefixes, }; - logger.debug("Updated schema:", { newSchema, prevSchema: schema }); - return newSchema; + logger.debug("Updated schema from entities", result); + return result; } -/** Merges new node or edge configs in to a set of existing node or edge configs. */ -function merge( - existing: T[], - newConfigs: T[], -): T[] { - const configMap = new Map(existing.map(vt => [vt.type, vt])); +type MergeResult = { + configs: T[]; + newIris: Set; +}; + +/** Merges new vertex entities into existing vertex type configs. */ +function mergeVertices( + existing: VertexTypeConfig[], + vertices: Vertex[], +): MergeResult { + if (!vertices.length) { + return { configs: existing, newIris: new Set() }; + } + + const byType = new Map(existing.map(v => [v.type, v])); + const newIris = new Set(); let hasChanges = false; - for (const newConfig of newConfigs) { - const existingConfig = configMap.get(newConfig.type); + for (const vertex of vertices) { + const attrs = attributesFromProperties(vertex.attributes); + for (const type of vertex.types) { + const existingConfig = byType.get(type); + if (!existingConfig) { + logger.debug("Discovered new vertex type:", type); + byType.set(type, { type, attributes: attrs }); + newIris.add(type); + for (const attr of attrs) { + newIris.add(attr.name); + } + hasChanges = true; + } else { + const mergedAttrs = mergeAttributesFromProperties( + existingConfig.attributes, + vertex.attributes, + ); + if (mergedAttrs !== existingConfig.attributes) { + logger.debug("Discovered new attributes for vertex type:", type); + byType.set(type, { ...existingConfig, attributes: mergedAttrs }); + // Only the newly added attributes need IRI scanning + for ( + let i = existingConfig.attributes.length; + i < mergedAttrs.length; + i++ + ) { + newIris.add(mergedAttrs[i].name); + } + hasChanges = true; + } + } + } + } + + return { + configs: hasChanges ? Array.from(byType.values()) : existing, + newIris, + }; +} + +/** Merges new edge entities into existing edge type configs. */ +function mergeEdges( + existing: EdgeTypeConfig[], + edges: Edge[], +): MergeResult { + if (!edges.length) { + return { configs: existing, newIris: new Set() }; + } + + const byType = new Map(existing.map(e => [e.type, e])); + const newIris = new Set(); + let hasChanges = false; + + for (const edge of edges) { + const existingConfig = byType.get(edge.type); if (!existingConfig) { - configMap.set(newConfig.type, newConfig); + logger.debug("Discovered new edge type:", edge.type); + byType.set(edge.type, { + type: edge.type, + attributes: attributesFromProperties(edge.attributes), + }); + newIris.add(edge.type); hasChanges = true; } else { - const mergedAttributes = mergeAttributes( + const mergedAttrs = mergeAttributesFromProperties( existingConfig.attributes, - newConfig.attributes, + edge.attributes, ); - if (mergedAttributes === existingConfig.attributes) { - continue; + if (mergedAttrs !== existingConfig.attributes) { + logger.debug("Discovered new attributes for edge type:", edge.type); + byType.set(edge.type, { ...existingConfig, attributes: mergedAttrs }); + hasChanges = true; } - configMap.set(newConfig.type, { - ...existingConfig, - attributes: mergedAttributes, - }); - hasChanges = true; } } - // Return original array if nothing changed - return hasChanges ? Array.from(configMap.values()) : existing; + return { + configs: hasChanges ? Array.from(byType.values()) : existing, + newIris, + }; } -export function mergeAttributes( +/** Generates and merges new RDF prefixes from entity IRIs and newly-discovered schema IRIs. */ +function mergePrefixes( + existing: PrefixTypeConfig[], + entities: Partial, + vertexIris: Set, + edgeIris: Set, +): PrefixTypeConfig[] { + const iris = new Set(vertexIris); + + for (const iri of edgeIris) { + iris.add(iri); + } + for (const v of entities.vertices ?? []) { + iris.add(String(v.id)); + } + for (const e of entities.edges ?? []) { + iris.add(String(e.id)); + } + + const newPrefixes = generateSchemaPrefixes(iris, existing); + if (newPrefixes.length === 0) { + return existing; + } + + logger.debug( + "Discovered new prefixes:", + newPrefixes.map(p => p.prefix), + ); + return [...existing, ...newPrefixes]; +} + +/** Merges entity properties into an existing attribute list. Preserves existing dataType on conflicts. */ +function mergeAttributesFromProperties( existing: AttributeConfig[], - newAttributes: AttributeConfig[], + properties: EntityProperties, ): AttributeConfig[] { - const attrMap = new Map(existing.map(attr => [attr.name, attr])); - let hasChanges = false; + const existingNames = new Set(existing.map(a => a.name)); + const newAttrs: AttributeConfig[] = []; - for (const newAttr of newAttributes) { - const existingAttr = attrMap.get(newAttr.name); - if (!existingAttr) { - attrMap.set(newAttr.name, newAttr); - hasChanges = true; - } else if ( - existingAttr.name === newAttr.name && - existingAttr.dataType !== newAttr.dataType - ) { - continue; - } else { - // Check if merge would actually change anything - const merged = { ...existingAttr, ...newAttr }; - if ( - merged.name === existingAttr.name && - merged.dataType === existingAttr.dataType - ) { - continue; - } - attrMap.set(newAttr.name, merged); - hasChanges = true; + for (const name of Object.keys(properties)) { + if (!existingNames.has(name)) { + newAttrs.push({ name, dataType: detectDataType(properties[name]) }); } } - // Return original array if nothing changed - return hasChanges ? Array.from(attrMap.values()) : existing; + if (newAttrs.length === 0) { + return existing; + } + + return [...existing, ...newAttrs]; +} + +/** Converts entity properties to an attribute config array. */ +function attributesFromProperties( + properties: EntityProperties, +): AttributeConfig[] { + return Object.entries(properties).map(([name, value]) => ({ + name, + dataType: detectDataType(value), + })); } export function mapVertexToTypeConfigs(vertex: Vertex): VertexTypeConfig[] { return vertex.types.map(type => ({ type, - attributes: Object.entries(vertex.attributes).map(([name, value]) => ({ - name, - dataType: detectDataType(value), - })), + attributes: attributesFromProperties(vertex.attributes), })); } export function mapEdgeToTypeConfig(edge: Edge): EdgeTypeConfig { return { type: edge.type, - attributes: Object.entries(edge.attributes).map(([name, value]) => ({ - name, - dataType: detectDataType(value), - })), + attributes: attributesFromProperties(edge.attributes), }; } @@ -450,8 +538,6 @@ export function generateSchemaPrefixes( return []; } - logger.debug("Generated new prefixes:", newPrefixes); - return newPrefixes; } @@ -462,28 +548,14 @@ export function getSchemaUris(schema: { }) { const result = new Set(); - schema.vertices.forEach(v => { + for (const v of schema.vertices) { result.add(v.type); - v.attributes.forEach(attr => { + for (const attr of v.attributes) { result.add(attr.name); - }); - }); - schema.edges.forEach(e => { - result.add(e.type); - }); - - return result; -} - -/** Collects IDs from entities. */ -function getEntityUris(entities: Partial) { - const result = new Set(); - - for (const v of entities.vertices ?? []) { - result.add(String(v.id)); + } } - for (const e of entities.edges ?? []) { - result.add(String(e.id)); + for (const e of schema.edges) { + result.add(e.type); } return result; @@ -492,24 +564,13 @@ function getEntityUris(entities: Partial) { /** Updates the schema with any new vertex or edge types, any new attributes, and updates the generated prefixes for sparql connections. */ export function useUpdateSchemaFromEntities() { return useAtomCallback( - useCallback((get, set, entities: Partial) => { + useCallback((_get, set, entities: Partial) => { const vertices = entities.vertices ?? []; const edges = entities.edges ?? []; - const activeSchema = get(activeSchemaSelector); if (vertices.length === 0 && edges.length === 0) { return; } - if (!activeSchema) { - return; - } - if ( - !shouldUpdateSchemaFromEntities(entities, activeSchema) && - !hasNewPrefixNamespaces(entities, get(prefixesAtom)) - ) { - logger.debug("Schema is already up to date with the given entities"); - return; - } - logger.debug("Updating schema from entities"); + set(activeSchemaSelector, prev => { if (!prev) { return prev; @@ -523,85 +584,3 @@ export function useUpdateSchemaFromEntities() { export type UpdateSchemaHandler = ReturnType< typeof useUpdateSchemaFromEntities >; - -/** Attempts to efficiently detect if the schema should be updated. */ -export function shouldUpdateSchemaFromEntities( - entities: Partial, - schema: SchemaStorageModel, -) { - const vertices = entities.vertices ?? []; - const edges = entities.edges ?? []; - if (vertices.length > 0) { - // Check if the vertex types and attributes are the same - const fromEntities = getUniqueTypesAndAttributes(vertices); - const fromSchema = getUniqueTypesAndAttributes(schema.vertices); - - if (!fromSchema.isSupersetOf(fromEntities)) { - logger.debug( - "Found new vertex types or attributes:", - fromEntities.difference(fromSchema), - ); - return true; - } - } - - if (edges.length > 0) { - // Check if the edge types and attributes are the same - const fromEntities = getUniqueTypesAndAttributes(edges); - const fromSchema = getUniqueTypesAndAttributes(schema.edges); - - if (!fromSchema.isSupersetOf(fromEntities)) { - logger.debug( - "Found new edge types or attributes:", - fromEntities.difference(fromSchema), - ); - return true; - } - } - - return false; -} - -/** Checks if any entity IDs have namespaces not yet covered by existing prefixes. */ -function hasNewPrefixNamespaces( - entities: Partial, - prefixes: PrefixLookup, -) { - for (const v of entities.vertices ?? []) { - const parts = splitIri(String(v.id)); - if (parts && !prefixes.findPrefix(parts.namespace)) { - return true; - } - } - for (const e of entities.edges ?? []) { - const parts = splitIri(String(e.id)); - if (parts && !prefixes.findPrefix(parts.namespace)) { - return true; - } - } - return false; -} - -/** - * Creates a set of unique types and attribute names as a set of strings in order to be used for comparisons. - * - * The entries in the set will be in the format of `vertexType.attributeName` or `edgeType.attributeName`. - */ -function getUniqueTypesAndAttributes( - entities: (Vertex | Edge | VertexTypeConfig | EdgeTypeConfig)[], -) { - return new Set( - entities.flatMap(e => { - return [ - e.type, - ...getAttributeNames(e.attributes).map(a => `${e.type}.${a}`), - ]; - }), - ); -} - -function getAttributeNames(attributes: EntityProperties | AttributeConfig[]) { - return Array.isArray(attributes) - ? attributes.map(a => a.name) - : Object.keys(attributes); -}