diff --git a/apps/roam/package.json b/apps/roam/package.json index df76bd551..1ca5da66b 100644 --- a/apps/roam/package.json +++ b/apps/roam/package.json @@ -68,7 +68,7 @@ "react-draggable": "4.4.5", "react-in-viewport": "1.0.0-alpha.20", "react-vertical-timeline-component": "3.5.2", - "roamjs-components": "0.85.4", + "roamjs-components": "0.85.6", "tldraw": "2.3.0", "use-sync-external-store": "1.5.0", "xregexp": "^5.0.0", diff --git a/apps/roam/src/utils/createReifiedBlock.ts b/apps/roam/src/utils/createReifiedBlock.ts index 7673edb50..621a20021 100644 --- a/apps/roam/src/utils/createReifiedBlock.ts +++ b/apps/roam/src/utils/createReifiedBlock.ts @@ -5,10 +5,19 @@ import { getSetting } from "~/utils/extensionSettings"; export const DISCOURSE_GRAPH_PROP_NAME = "discourse-graph"; +const SANE_ROLE_NAME_RE = new RegExp(/^[\w\-]*$/); + const strictQueryForReifiedBlocks = async ( parameterUids: Record, ): Promise => { const paramsAsSeq = Object.entries(parameterUids); + // validate parameter names + if ( + Object.keys(parameterUids).filter((k) => !k.match(SANE_ROLE_NAME_RE)).length + ) + throw new Error( + `invalid parameter names in ${Object.keys(parameterUids).join(", ")}`, + ); const query = `[:find ?u ?d :in $ ${paramsAsSeq.map(([k]) => "?" + k).join(" ")} :where [?s :block/uid ?u] [?s :block/props ?p] [(get ?p :${DISCOURSE_GRAPH_PROP_NAME}) ?d] @@ -72,7 +81,7 @@ const createReifiedBlock = async ({ const RELATION_PAGE_TITLE = "roam/js/discourse-graph/relations"; let relationPageUid: string | undefined = undefined; -const getRelationPageUid = async (): Promise => { +const getOrCreateRelationPageUid = async (): Promise => { if (relationPageUid === undefined) { relationPageUid = getPageUidByPageTitle(RELATION_PAGE_TITLE); if (relationPageUid === "") { @@ -82,8 +91,16 @@ const getRelationPageUid = async (): Promise => { return relationPageUid; }; +export const getExistingRelationPageUid = (): string | undefined => { + if (relationPageUid === undefined) { + const uid = getPageUidByPageTitle(RELATION_PAGE_TITLE); + if (uid !== "") relationPageUid = uid; + } + return relationPageUid; +}; + export const countReifiedRelations = async (): Promise => { - const pageUid = await getRelationPageUid(); + const pageUid = getExistingRelationPageUid(); if (pageUid === undefined) return 0; const r = await window.roamAlphaAPI.data.async.q( `[:find (count ?c) :where [?p :block/children ?c] [?p :block/uid "${pageUid}"]]`, @@ -103,7 +120,7 @@ export const createReifiedRelation = async ({ const authorized = getSetting("use-reified-relations"); if (authorized) { return await createReifiedBlock({ - destinationBlockUid: await getRelationPageUid(), + destinationBlockUid: await getOrCreateRelationPageUid(), schemaUid: relationBlockUid, parameterUids: { sourceUid, diff --git a/apps/roam/src/utils/fireQuery.ts b/apps/roam/src/utils/fireQuery.ts index 3098e0585..81ce1991f 100644 --- a/apps/roam/src/utils/fireQuery.ts +++ b/apps/roam/src/utils/fireQuery.ts @@ -1,8 +1,8 @@ import conditionToDatalog from "./conditionToDatalog"; import type { PullBlock, - DatalogAndClause, DatalogClause, + DatalogAndClause, } from "roamjs-components/types"; import compileDatalog from "./compileDatalog"; import { getNodeEnv } from "roamjs-components/util/env"; @@ -65,9 +65,9 @@ const firstVariable = ( }; const optimizeQuery = ( - clauses: (DatalogClause | DatalogAndClause)[], + clauses: DatalogClause[], capturedVariables: Set, -): (DatalogClause | DatalogAndClause)[] => { +): DatalogClause[] => { const marked = clauses.map(() => false); const orderedClauses: (DatalogClause | DatalogAndClause)[] = []; const variablesByIndex: Record> = {}; @@ -107,7 +107,8 @@ const optimizeQuery = ( if (Array.from(allVars).every((v) => capturedVariables.has(v))) { score = 10; } else { - score = 100002; + // downgrade disjunction and negation + score = c.type === "and-clause" ? 100002 : 100006; } } else if (c.type === "not-join-clause" || c.type === "or-join-clause") { if (c.variables.every((v) => capturedVariables.has(v.value))) { @@ -125,7 +126,8 @@ const optimizeQuery = ( (a) => a.type !== "variable" || capturedVariables.has(a.value), ) ) { - score = 1000; + // equality is almost as good as a binding + c.type == "pred-expr" && c.pred == "=" ? (score = 5) : (score = 1000); } else { score = 100004; } @@ -155,6 +157,16 @@ const optimizeQuery = ( bestClause.arguments .filter((v) => v.type === "variable") .forEach((v) => capturedVariables.add(v.value)); + } else if (bestClause.type === "fn-expr") { + // A function expression acts as biding a variable to a unique function value + if ( + bestClause.arguments.filter( + (a) => a.type === "variable" && !capturedVariables.has(a.value), + ).length === 0 && + bestClause.binding.type === "bind-scalar" && + bestClause.binding.variable.type === "variable" + ) + capturedVariables.add(bestClause.binding.variable.value); } } return orderedClauses; @@ -197,7 +209,7 @@ export const getDatalogQuery = ({ const whereClauses = optimizeQuery( getWhereClauses({ conditions, returnNode }), new Set([]), - ) as DatalogClause[]; + ); const defaultSelections: { mapper: PredefinedSelection["mapper"]; diff --git a/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts b/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts index 0f54e9f1c..fafb2bdcb 100644 --- a/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts +++ b/apps/roam/src/utils/registerDiscourseDatalogTranslators.ts @@ -1,5 +1,5 @@ import getPageTitlesStartingWithPrefix from "roamjs-components/queries/getPageTitlesStartingWithPrefix"; -import { DatalogAndClause, DatalogClause } from "roamjs-components/types"; +import { DatalogClause } from "roamjs-components/types"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import getSubTree from "roamjs-components/util/getSubTree"; @@ -19,13 +19,50 @@ import replaceDatalogVariables from "./replaceDatalogVariables"; import parseQuery from "./parseQuery"; import { fireQuerySync, getWhereClauses } from "./fireQuery"; import { toVar } from "./compileDatalog"; +import { getSetting } from "./extensionSettings"; +import { getExistingRelationPageUid } from "./createReifiedBlock"; const hasTag = (node: DiscourseNode): node is DiscourseNode & { tag: string } => !!node.tag; -const collectVariables = ( - clauses: (DatalogClause | DatalogAndClause)[], -): Set => +const enum ValueType { + "variable", + "nodeType", + "uid", + "title", +} + +const singleOrClause = ( + clauses: DatalogClause[], + variables?: string[], +): DatalogClause | null => { + if (clauses.length === 0) return null; + if (clauses.length === 1) return clauses[0]; + return variables && variables.length + ? { + type: "or-join-clause", + variables: variables.map((v: string) => ({ + type: "variable", + value: v, + })), + clauses: clauses, + } + : { + type: "or-clause", + clauses: clauses, + }; +}; + +const singleAndClause = (clauses: DatalogClause[]): DatalogClause | null => { + if (clauses.length === 0) return null; + if (clauses.length === 1) return clauses[0]; + return { + type: "and-clause", + clauses: clauses, + }; +}; + +const collectVariables = (clauses: DatalogClause[]): Set => new Set( clauses.flatMap((c) => { switch (c.type) { @@ -372,217 +409,572 @@ const registerDiscourseDatalogTranslators = () => { registerDatalogTranslator({ key: label, callback: ({ source, target, uid }) => { + const useReifiedRelations = getSetting( + "use-reified-relations", + false, + ); + const relationPageUid = getExistingRelationPageUid(); + const filteredRelations = getFilteredRelations({ discourseRelations, label, source, target, }); - if (!filteredRelations.length) return []; - const andParts = filteredRelations.map( - ({ - triples, - forward, - source: relationSource, - destination: relationTarget, - }) => { - const sourceTriple = triples.find((t) => t[2] === "source"); - const targetTriple = triples.find( - (t) => t[2] === "destination" || t[2] === "target", + if (!filteredRelations.length && !ANY_RELATION_REGEX.test(label)) + return []; + const typeOfValue = (value: string): ValueType => { + const possibleNodeType = nodeTypeByLabel[value?.toLowerCase()]; + if (possibleNodeType) { + return ValueType.nodeType; + } else if ( + window.roamAlphaAPI.pull("[:db/id]", [":block/uid", value]) + ) { + return ValueType.uid; + } else if ( + value?.toLowerCase() !== "node" && + !!window.roamAlphaAPI.pull("[:db/id]", [":node/title", value]) + ) { + return ValueType.title; + } else { + return ValueType.variable; + } + }; + const computeEdgeTriple = ({ + variable, + value, + nodeType, + valueType, + }: { + variable: string; + value: string; + nodeType: string; + valueType?: ValueType; + }): DatalogClause[] => { + valueType = valueType || typeOfValue(value); + switch (valueType) { + case ValueType.nodeType: + return conditionToDatalog({ + uid, + not: false, + source: variable, + relation: "is a", + target: nodeTypeByLabel[value?.toLowerCase()], + type: "clause", + }); + case ValueType.uid: + return [ + { + type: "data-pattern", + arguments: [ + { type: "variable", value: variable }, + { type: "constant", value: ":block/uid" }, + { type: "constant", value: `"${value}"` }, + ], + }, + ]; + case ValueType.title: + return conditionToDatalog({ + uid, + not: false, + source: variable, + relation: "has title", + target: value, + type: "clause", + }); + case ValueType.variable: + return conditionToDatalog({ + uid, + not: false, + source: variable, + relation: "is a", + target: nodeType, + type: "clause", + }); + } + }; + + if (useReifiedRelations && relationPageUid !== undefined) { + const relClauseBasis: DatalogClause[] = [ + { + type: "data-pattern", + arguments: [ + { type: "variable", value: "relpage" }, + { type: "constant", value: ":block/uid" }, + { type: "constant", value: `"${relationPageUid}"` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: "rel" }, + { type: "constant", value: ":block/page" }, + { type: "variable", value: "relpage" }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: "rel" }, + { type: "constant", value: ":block/props" }, + { type: "variable", value: "relprops" }, + ], + }, + { + type: "fn-expr", + fn: "get", + arguments: [ + { type: "variable", value: "relprops" }, + { type: "constant", value: `:discourse-graph` }, + ], + binding: { + type: "bind-scalar", + variable: { type: "variable", value: "reldata" }, + }, + }, + { + type: "fn-expr", + fn: "get", + arguments: [ + { type: "variable", value: "reldata" }, + { type: "constant", value: ":sourceUid" }, + ], + binding: { + type: "bind-scalar", + variable: { type: "variable", value: "relSource" }, + }, + }, + { + type: "fn-expr", + fn: "get", + arguments: [ + { type: "variable", value: "reldata" }, + { type: "constant", value: ":destinationUid" }, + ], + binding: { + type: "bind-scalar", + variable: { type: "variable", value: "relTarget" }, + }, + }, + { + type: "fn-expr", + fn: "get", + arguments: [ + { type: "variable", value: "reldata" }, + { type: "constant", value: ":hasSchema" }, + ], + binding: { + type: "bind-scalar", + variable: { type: "variable", value: "relSchema" }, + }, + }, + ]; + const typeOfSource = typeOfValue(source); + const typeOfTarget = typeOfValue(target); + const clauses: DatalogClause[] = [...relClauseBasis]; + + if ( + !( + typeOfSource <= ValueType.nodeType || + typeOfTarget <= ValueType.nodeType + ) + ) { + console.error( + `One of source: ${source} or target: ${target} should be a variable (or node type)`, ); - if (!sourceTriple || !targetTriple) return []; - const computeEdgeTriple = ({ - nodeType, - value, - triple, - }: { - nodeType: string; - value: string; - triple: readonly [string, string, string]; - }): DatalogClause[] => { - const possibleNodeType = nodeTypeByLabel[value?.toLowerCase()]; - if (possibleNodeType) { - return conditionToDatalog({ - uid, - not: false, - target: possibleNodeType, - relation: "is a", - source: triple[0], - type: "clause", - }); - } else if ( - !!window.roamAlphaAPI.pull("[:db/id]", [":block/uid", value]) - ) { - return [ - { - type: "data-pattern", - arguments: [ - { type: "variable", value: triple[0] }, - { type: "constant", value: ":block/uid" }, - { type: "constant", value: `"${value}"` }, - ], - }, - ]; - } else if ( - value?.toLowerCase() !== "node" && - !!window.roamAlphaAPI.pull("[:db/id]", [":node/title", value]) - ) { - return conditionToDatalog({ - uid, - not: false, - target: value, - relation: "has title", - source: triple[0], - type: "clause", - }); - } else { - return conditionToDatalog({ - uid, - not: false, - target: nodeType, - relation: "is a", - source: triple[0], - type: "clause", - }); - } - }; - const edgeTriples = forward - ? computeEdgeTriple({ + } + const forwardClauses: DatalogClause[] = []; + const reverseClauses: DatalogClause[] = []; + const sourceTriples = filteredRelations + .map((r) => r.triples.find((t) => t[2] === "source")) + .filter((x) => x !== undefined); + const targetTriples = filteredRelations + .map((r) => + r.triples.find( + (t) => t[2] === "destination" || t[2] === "target", + ), + ) + .filter((x) => x !== undefined); + if (typeOfSource === ValueType.uid) { + if (sourceTriples.length) + clauses.push( + ...computeEdgeTriple({ + variable: sourceTriples[0][0], value: source, - triple: sourceTriple, - nodeType: relationSource, - }) - .concat( - computeEdgeTriple({ - value: target, - triple: targetTriple, - nodeType: relationTarget, - }), - ) - .concat([ - { - type: "data-pattern", - arguments: [ - { type: "variable", value: sourceTriple[0] }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${source}-uid` }, - ], - }, - { - type: "data-pattern", - arguments: [ - { type: "variable", value: source }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${source}-uid` }, - ], - }, - { - type: "data-pattern", - arguments: [ - { type: "variable", value: targetTriple[0] }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${target}-uid` }, - ], - }, - { - type: "data-pattern", - arguments: [ - { type: "variable", value: target }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${target}-uid` }, - ], - }, - ]) - : computeEdgeTriple({ + valueType: ValueType.uid, + nodeType: "Any", + }), + ); + else console.error("Cannot find the source triple"); + forwardClauses.push({ + type: "pred-expr", + pred: "=", + arguments: [ + { type: "variable", value: "relSource" }, + { type: "constant", value: `"${source}"` }, + ], + }); + reverseClauses.push({ + type: "pred-expr", + pred: "=", + arguments: [ + { type: "variable", value: "relTarget" }, + { type: "constant", value: `"${source}"` }, + ], + }); + } else { + const sourceVarName = + typeOfSource === ValueType.title ? "sourcePage" : source; + switch (typeOfSource) { + case ValueType.title: + clauses.push( + ...computeEdgeTriple({ + variable: sourceVarName, + value: source, + valueType: ValueType.title, + nodeType: "Any", + }), + ); + break; + case ValueType.nodeType: + // not sure about using the source as both variable and value but works + clauses.push( + ...computeEdgeTriple({ + variable: sourceVarName, // equal to source in this case... + value: source, + valueType: ValueType.nodeType, + nodeType: source, + }), + ); + break; + case ValueType.variable: + // we used to put the nodeType in the or-clause, but I think with reified triples it's unneeded. + break; + default: + console.error("Wrong typeOfSource value"); + } + + forwardClauses.push({ + type: "data-pattern", + arguments: [ + { type: "variable", value: sourceVarName }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: "relSource" }, + ], + }); + reverseClauses.push({ + type: "data-pattern", + arguments: [ + { type: "variable", value: sourceVarName }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: "relTarget" }, + ], + }); + } + if (typeOfTarget === ValueType.uid) { + if (targetTriples.length) + clauses.push( + ...computeEdgeTriple({ + variable: targetTriples[0][0], + valueType: ValueType.uid, value: target, - triple: sourceTriple, - nodeType: relationSource, - }) - .concat( - computeEdgeTriple({ - value: source, - triple: targetTriple, - nodeType: relationTarget, - }), - ) - .concat([ - { - type: "data-pattern", - arguments: [ - { type: "variable", value: targetTriple[0] }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${source}-uid` }, - ], - }, - { - type: "data-pattern", - arguments: [ - { type: "variable", value: source }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${source}-uid` }, - ], - }, - { - type: "data-pattern", - arguments: [ - { type: "variable", value: sourceTriple[0] }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${target}-uid` }, - ], - }, - { - type: "data-pattern", - arguments: [ - { type: "variable", value: target }, - { type: "constant", value: ":block/uid" }, - { type: "variable", value: `${target}-uid` }, - ], - }, - ]); - const subQuery = triples - .filter((t) => t !== sourceTriple && t !== targetTriple) - .flatMap(([src, rel, tar]) => - conditionToDatalog({ - source: src, - relation: rel, - target: tar, - not: false, - uid, - type: "clause", + nodeType: "Any", }), ); - return replaceDatalogVariables( - [ - { from: source, to: source }, - { from: target, to: target }, - { from: true, to: (v) => `${uid}-${v}` }, + else console.error("Cannot find the target triple"); + forwardClauses.push({ + type: "pred-expr", + pred: "=", + arguments: [ + { type: "variable", value: "relTarget" }, + { type: "constant", value: `"${target}"` }, + ], + }); + reverseClauses.push({ + type: "pred-expr", + pred: "=", + arguments: [ + { type: "variable", value: "relSource" }, + { type: "constant", value: `"${target}"` }, + ], + }); + } else { + const targetVarName = + typeOfTarget === ValueType.title ? "targetPage" : target; + switch (typeOfTarget) { + case ValueType.title: + clauses.push( + ...computeEdgeTriple({ + variable: targetVarName, + value: target, + valueType: ValueType.title, + nodeType: "Any", + }), + ); + break; + case ValueType.nodeType: + // not sure about using the source as both variable and value but works + clauses.push( + ...computeEdgeTriple({ + variable: targetVarName, // equal to target in this case... + value: target, + valueType: ValueType.nodeType, + nodeType: target, + }), + ); + break; + case ValueType.variable: + // we used to put the nodeType in the or-clause, but I think with reified triples it's unneeded. + break; + default: + console.error("Wrong typeOfTarget value"); + } + + forwardClauses.push({ + type: "data-pattern", + arguments: [ + { type: "variable", value: targetVarName }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: "relTarget" }, ], - edgeTriples.concat(subQuery), + }); + reverseClauses.push({ + type: "data-pattern", + arguments: [ + { type: "variable", value: targetVarName }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: "relSource" }, + ], + }); + } + + if (ANY_RELATION_REGEX.test(label)) { + clauses.push({ + type: "or-clause", + clauses: [ + singleAndClause(forwardClauses)!, + singleAndClause(reverseClauses)!, + ], + }); + } else { + let forwardRelationIds = filteredRelations + .filter((r) => r.forward) + .map((r) => r.id); + forwardRelationIds = [...new Set(forwardRelationIds)]; + const forwardRelationClauses = forwardRelationIds.map( + (id) => + ({ + type: "pred-expr", + pred: "=", + arguments: [ + { type: "variable", value: "relSchema" }, + { type: "constant", value: `"${id}"` }, + ], + }) as DatalogClause, ); - }, - ); - if (andParts.length === 1) return andParts[0]; + let reverseRelationIds = filteredRelations + .filter((r) => !r.forward) + .map((r) => r.id); + reverseRelationIds = [...new Set(reverseRelationIds)]; + const reverseRelationClauses = reverseRelationIds.map( + (id) => + ({ + type: "pred-expr", + pred: "=", + arguments: [ + { type: "variable", value: "relSchema" }, + { type: "constant", value: `"${id}"` }, + ], + }) as DatalogClause, + ); + if ( + forwardRelationClauses.length && + reverseRelationClauses.length + ) { + const relClauses = [ + singleAndClause([ + ...forwardClauses, + singleOrClause(forwardRelationClauses)!, + ])!, + singleAndClause([ + ...reverseClauses, + singleOrClause(reverseRelationClauses)!, + ])!, + ]; + clauses.push(singleOrClause(relClauses)!); + } else if (forwardRelationClauses.length) { + clauses.push( + ...forwardClauses, + singleOrClause(forwardRelationClauses)!, + ); + } else if (reverseRelationClauses.length) { + clauses.push( + ...reverseClauses, + singleOrClause(reverseRelationClauses)!, + ); + } + } + return replaceDatalogVariables( + [ + { from: source, to: source }, + { from: target, to: target }, + { from: true, to: (v) => `${uid}-${v}` }, + ], + clauses, + ); + } else { + // Pattern-based search + const andParts = filteredRelations.map( + ({ + triples, + forward, + source: relationSource, + destination: relationTarget, + }) => { + const sourceTriple = triples.find((t) => t[2] === "source"); + const targetTriple = triples.find( + (t) => t[2] === "destination" || t[2] === "target", + ); + if (!sourceTriple || !targetTriple) return []; + + const edgeTriples = forward + ? computeEdgeTriple({ + value: source, + variable: sourceTriple[0], + nodeType: relationSource, + }) + .concat( + computeEdgeTriple({ + value: target, + variable: targetTriple[0], + nodeType: relationTarget, + }), + ) + .concat([ + { + type: "data-pattern", + arguments: [ + { type: "variable", value: sourceTriple[0] }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${source}-uid` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: source }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${source}-uid` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: targetTriple[0] }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${target}-uid` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: target }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${target}-uid` }, + ], + }, + ]) + : computeEdgeTriple({ + variable: sourceTriple[0], + value: target, + nodeType: relationSource, + }) + .concat( + computeEdgeTriple({ + variable: targetTriple[0], + value: source, + nodeType: relationTarget, + }), + ) + .concat([ + { + type: "data-pattern", + arguments: [ + { type: "variable", value: targetTriple[0] }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${source}-uid` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: source }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${source}-uid` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: sourceTriple[0] }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${target}-uid` }, + ], + }, + { + type: "data-pattern", + arguments: [ + { type: "variable", value: target }, + { type: "constant", value: ":block/uid" }, + { type: "variable", value: `${target}-uid` }, + ], + }, + ]); + const subQuery = triples + .filter((t) => t !== sourceTriple && t !== targetTriple) + .flatMap(([src, rel, tar]) => + conditionToDatalog({ + source: src, + relation: rel, + target: tar, + not: false, + uid, + type: "clause", + }), + ); + return replaceDatalogVariables( + [ + { from: source, to: source }, + { from: target, to: target }, + { from: true, to: (v) => `${uid}-${v}` }, + ], + edgeTriples.concat(subQuery), + ); + }, + ); + if (andParts.length === 1) return andParts[0]; - const orJoinedVars = collectVariables(andParts[0]); - andParts.slice(1).forEach((a) => { - const freeVars = collectVariables(a); - Array.from(orJoinedVars).forEach((v) => { - if (!freeVars.has(v)) orJoinedVars.delete(v); + const orJoinedVars = collectVariables(andParts[0]); + andParts.slice(1).forEach((a) => { + const freeVars = collectVariables(a); + Array.from(orJoinedVars).forEach((v) => { + if (!freeVars.has(v)) orJoinedVars.delete(v); + }); }); - }); - return [ - { - type: "or-join-clause", - variables: Array.from(orJoinedVars).map((v) => ({ - type: "variable", - value: v, - })), - clauses: andParts.map((a) => ({ - type: "and-clause", - clauses: a, - })), - }, - ]; + return [ + { + type: "or-join-clause", + variables: Array.from(orJoinedVars).map((v) => ({ + type: "variable", + value: v, + })), + clauses: andParts.map((a) => ({ + type: "and-clause", + clauses: a, + })), + }, + ]; + } }, targetOptions: () => { const allRelations = discourseRelations.flatMap((dr) => [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 31da40a9e..d2e66963a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,8 +279,8 @@ importers: specifier: 3.5.2 version: 3.5.2(react@18.2.0) roamjs-components: - specifier: 0.85.4 - version: 0.85.4(8b55b1f14ffbf718c26dbfa3a20c2d6b) + specifier: 0.85.6 + version: 0.85.6(8b55b1f14ffbf718c26dbfa3a20c2d6b) tldraw: specifier: 2.3.0 version: 2.3.0(patch_hash=53157a9866fb748b22a548ab8d9aca2a557f98c0487b201b01f0a4ca89c71708)(@types/react-dom@18.2.17)(@types/react@18.2.21)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -7963,8 +7963,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - roamjs-components@0.85.4: - resolution: {integrity: sha512-1jKcPg3HXrX3+D6XEjf21IZVQ7IMPfdJHr3zffVX88zQLS4VmqeF0hcv95oAnts4NRRjrWu/kyG3apXwq6BFHw==} + roamjs-components@0.85.6: + resolution: {integrity: sha512-uE4QcwmbBZbLuq5fuYV8iSywDAb+VYw9IyS1rfiweUQxO+BttyAYNhGoCN/O2k3Bow0dhnFKTzPkFLElW6htmg==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} hasBin: true peerDependencies: @@ -18126,7 +18126,7 @@ snapshots: dependencies: glob: 7.2.3 - roamjs-components@0.85.4(8b55b1f14ffbf718c26dbfa3a20c2d6b): + roamjs-components@0.85.6(8b55b1f14ffbf718c26dbfa3a20c2d6b): dependencies: '@blueprintjs/core': 3.50.4(patch_hash=51c5847e0a73a1be0cc263036ff64d8fada46f3b65831ed938dbca5eecf3edc0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@blueprintjs/datetime': 3.23.14(react-dom@18.2.0(react@18.2.0))(react@18.2.0)