From e1dd26392f2854ffa7f8ca8924ff264aa8e570f1 Mon Sep 17 00:00:00 2001
From: Dennis Collaris <d.a.c.collaris@uu.nl>
Date: Thu, 24 Oct 2024 12:46:10 +0000
Subject: [PATCH] feat: query builder or union setting

---
 apps/web/src/app/App.tsx                      |  4 +-
 libs/shared/lib/components/pills/Pill.tsx     |  3 +-
 .../lib/components/pills/PillHandle.tsx       | 20 +++-
 libs/shared/lib/data-access/api/eventBus.tsx  | 11 ++-
 libs/shared/lib/data-access/store/hooks.ts    |  3 +
 .../data-access/store/querybuilderSlice.ts    | 23 ++++-
 .../entitypill/QueryEntityPill.tsx            | 59 +++++++++---
 .../relationpill/QueryRelationPill.tsx        | 93 ++++++++++++-------
 .../pills/pillattributes/PillAttributes.tsx   |  3 +
 .../pillattributes/PillAttributesItem.tsx     | 15 ++-
 .../querybuilder/query-utils/query2backend.ts | 28 ++++--
 11 files changed, 201 insertions(+), 61 deletions(-)

diff --git a/apps/web/src/app/App.tsx b/apps/web/src/app/App.tsx
index 17355b185..215d4ec57 100644
--- a/apps/web/src/app/App.tsx
+++ b/apps/web/src/app/App.tsx
@@ -6,6 +6,7 @@ import {
   useQuerybuilderGraph,
   useQuerybuilderSettings,
   useSessionCache,
+  useQuerybuilderUnionTypes,
 } from '@graphpolaris/shared/lib/data-access';
 import { addError, setCurrentTheme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { resetGraphQueryResults, queryingBackend } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
@@ -38,6 +39,7 @@ export function App(props: App) {
   const dispatch = useAppDispatch();
   const queryBuilderSettings = useQuerybuilderSettings();
   const [monitoringOpen, setMonitoringOpen] = useState<boolean>(false);
+  const unionTypes = useQuerybuilderUnionTypes();
 
   const runQuery = () => {
     if (session?.currentSaveState && query) {
@@ -45,7 +47,7 @@ export function App(props: App) {
         dispatch(resetGraphQueryResults());
       } else {
         dispatch(queryingBackend());
-        wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml));
+        wsQueryRequest(Query2BackendQuery(session.currentSaveState, query, queryBuilderSettings, ml, unionTypes));
       }
     }
   };
diff --git a/libs/shared/lib/components/pills/Pill.tsx b/libs/shared/lib/components/pills/Pill.tsx
index 0164732da..079931625 100644
--- a/libs/shared/lib/components/pills/Pill.tsx
+++ b/libs/shared/lib/components/pills/Pill.tsx
@@ -1,8 +1,9 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext, useState, useMemo } from 'react';
 import { pillWidth, pillHeight, pillXPadding, pillInnerMargin, topLineHeight, pillBorderWidth, pillAttributesPadding } from './pill.const';
 import { Position } from 'reactflow';
 import { PillHandle } from './PillHandle';
 import { PillContext } from './PillContext';
+import { QueryUnionType } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 
 export type PillI = {
   onHovered?: (hovered: boolean) => void;
diff --git a/libs/shared/lib/components/pills/PillHandle.tsx b/libs/shared/lib/components/pills/PillHandle.tsx
index 88aa24aaa..50bf2ea20 100644
--- a/libs/shared/lib/components/pills/PillHandle.tsx
+++ b/libs/shared/lib/components/pills/PillHandle.tsx
@@ -10,14 +10,14 @@ export const PillHandle = (props: {
   children?: React.ReactNode;
   className?: string;
   position: Position;
-  type: 'square' | 'arrowUp' | 'arrowDown';
+  type: 'square' | 'diamond' | 'arrowUp' | 'arrowDown';
   mr?: number;
   outerSize?: number;
   innerSize?: number;
 }) => {
   const pillContext = useContext(PillContext);
-  const outerSize = props.outerSize || (props.type === 'square' ? 6 : 4);
-  const innerSize = props.innerSize || (props.type === 'square' ? 4 : 6);
+  const outerSize = props.outerSize || (['square', 'diamond'].includes(props.type) ? 6 : 4);
+  const innerSize = props.innerSize || (['square', 'diamond'].includes(props.type) ? 4 : 6);
 
   const style: React.CSSProperties = {
     width: outerSize * 2,
@@ -49,6 +49,20 @@ export const PillHandle = (props: {
           <rect x="0.5" y="0.5" width="7" height="7" className={props.className} />
         </svg>
       );
+    if (props.type === 'diamond')
+      return (
+        <svg className={'absolute pointer-events-none'} style={innerStyle} width="8" height="8" viewBox="0 0 8 8" fill="none">
+          <rect
+            x="0.5"
+            y="0.5"
+            width="7"
+            height="7"
+            className={props.className}
+            style={{ transformOrigin: 'center center' }}
+            transform="rotate(45)"
+          />
+        </svg>
+      );
     if (props.type === 'arrowUp')
       return (
         <svg width="14" height="7" viewBox="0 0 14 7" fill="none" className={'absolute pointer-events-none'} style={innerStyle}>
diff --git a/libs/shared/lib/data-access/api/eventBus.tsx b/libs/shared/lib/data-access/api/eventBus.tsx
index d783b723d..bc471755d 100644
--- a/libs/shared/lib/data-access/api/eventBus.tsx
+++ b/libs/shared/lib/data-access/api/eventBus.tsx
@@ -14,11 +14,17 @@ import {
   wsSchemaSubscription,
   useQuerybuilderAttributesShown,
   wsSchemaStatsRequest,
+  useQuerybuilderUnionTypes,
 } from '@graphpolaris/shared/lib/data-access';
 import { Broker, wsQuerySubscription, wsQueryTranslationSubscription } from '@graphpolaris/shared/lib/data-access/broker';
 import { addInfo } from '@graphpolaris/shared/lib/data-access/store/configSlice';
 import { allMLTypes, LinkPredictionInstance, setMLResult } from '@graphpolaris/shared/lib/data-access/store/mlSlice';
-import { QueryBuilderText, setQueryText, setQuerybuilderNodes } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
+import {
+  QueryBuilderText,
+  queryUnionTypes,
+  setQueryText,
+  setQuerybuilderNodes,
+} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import { useEffect } from 'react';
 import {
   SaveStateI,
@@ -64,6 +70,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
   const mlHash = useMLEnabledHash();
   const visState = useVisualization();
   const queryBuilderSettings = useQuerybuilderSettings();
+  const unionTypes = useQuerybuilderUnionTypes();
 
   function loadSaveState(saveStateID: string | undefined, saveStates: Record<string, SaveStateI>) {
     if (saveStateID && saveStates && saveStateID in saveStates) {
@@ -282,7 +289,7 @@ export const EventBus = (props: { onRunQuery: Function; onAuthorized: Function }
       //   body: { type: 'query_builder_state', status: '', value: queryBuilder },
       // });
     }
-  }, [queryHash, mlHash, queryBuilderSettings]);
+  }, [queryHash, mlHash, queryBuilderSettings, unionTypes]);
 
   return <div className="hide"></div>;
 };
diff --git a/libs/shared/lib/data-access/store/hooks.ts b/libs/shared/lib/data-access/store/hooks.ts
index 9e7feb3fe..16fc370b5 100644
--- a/libs/shared/lib/data-access/store/hooks.ts
+++ b/libs/shared/lib/data-access/store/hooks.ts
@@ -20,6 +20,8 @@ import {
   queryBuilderState,
   selectQuerybuilderGraph,
   selectQuerybuilderHash,
+  queryUnionTypes,
+  QueryUnionType
 } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import { activeSaveState, activeSaveStateAuthorization, SessionCacheI, sessionCacheState } from './sessionSlice';
 import { AuthSliceState, authState } from './authSlice';
@@ -67,6 +69,7 @@ export const useQuerybuilderHash: () => string = () => useAppSelector(selectQuer
 export const useQuerybuilderSettings: () => QueryBuilderSettings = () => useAppSelector(queryBuilderSettingsState);
 export const useQuerybuilder: () => QueryBuilderState = () => useAppSelector(queryBuilderState);
 export const useQuerybuilderAttributesShown: () => QueryGraphEdgeHandle[] = () => useAppSelector(queryBuilderAttributesShown);
+export const useQuerybuilderUnionTypes: () => {[nodeId: string]: QueryUnionType} = () => useAppSelector(queryUnionTypes);
 
 // Overall Configuration of the app
 export const useConfig: () => ConfigStateI = () => useAppSelector(configState);
diff --git a/libs/shared/lib/data-access/store/querybuilderSlice.ts b/libs/shared/lib/data-access/store/querybuilderSlice.ts
index df417947a..ce77ca763 100644
--- a/libs/shared/lib/data-access/store/querybuilderSlice.ts
+++ b/libs/shared/lib/data-access/store/querybuilderSlice.ts
@@ -20,6 +20,11 @@ export type QueryBuilderText = {
   result: string;
 };
 
+export enum QueryUnionType {
+  AND = 'And',
+  OR = 'Or',
+}
+
 export type QueryBuilderAttributeBeingShown = {};
 
 export type QueryBuilderState = {
@@ -28,6 +33,7 @@ export type QueryBuilderState = {
   settings: QueryBuilderSettings;
   queryTranslation: QueryBuilderText;
   attributesBeingShown: QueryGraphEdgeHandle[];
+  unionTypes: { [nodeId: string]: QueryUnionType };
 };
 
 // Define the initial state using that type
@@ -45,6 +51,7 @@ export const initialState: QueryBuilderState = {
     result: '',
   },
   attributesBeingShown: [],
+  unionTypes: {},
   // schemaLayout: 'Graphology_noverlap',
 } as QueryBuilderState;
 
@@ -83,6 +90,9 @@ export const querybuilderSlice = createSlice({
         state.attributesBeingShown.splice(existing, 1);
       }
     },
+    setQueryUnionType: (state: QueryBuilderState, action: PayloadAction<{ nodeId: string; unionType: QueryUnionType }>) => {
+      state.unionTypes[action.payload.nodeId] = action.payload.unionType;
+    },
   },
 });
 
@@ -140,7 +150,16 @@ export const selectQuerybuilderHash = (state: RootState): string => {
 //   state.schema.schemaLayout;
 
 export default querybuilderSlice.reducer;
-export const { setQuerybuilderGraph, clearQB, setQuerybuilderSettings, setQuerybuilderNodes, setQueryText, attributeShownToggle } =
-  querybuilderSlice.actions;
+export const {
+  setQuerybuilderGraph,
+  clearQB,
+  setQuerybuilderSettings,
+  setQuerybuilderNodes,
+  setQueryText,
+  attributeShownToggle,
+  setQueryUnionType,
+} = querybuilderSlice.actions;
 
 export const queryBuilderAttributesShown = (state: RootState) => state.querybuilder.attributesBeingShown;
+
+export const queryUnionTypes = (state: RootState) => state.querybuilder.unionTypes;
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
index 0aa50ebba..f3900d7a4 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx
@@ -1,4 +1,16 @@
-import { useQuerybuilderAttributesShown, useQuerybuilderGraph, useQuerybuilderHash } from '@graphpolaris/shared/lib/data-access';
+import {
+  useQuerybuilderAttributesShown,
+  useQuerybuilderGraph,
+  useQuerybuilderHash,
+  useQuerybuilderUnionTypes,
+} from '@graphpolaris/shared/lib/data-access';
+import {
+  setQuerybuilderGraphology,
+  toQuerybuilderGraphology,
+  attributeShownToggle,
+  setQueryUnionType,
+  QueryUnionType,
+} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { Button } from '@graphpolaris/shared/lib/components/buttons';
 import { Icon } from '@graphpolaris/shared/lib/components/icon';
@@ -9,13 +21,8 @@ import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../m
 import { PillAttributes } from '../../pillattributes/PillAttributes';
 import { DropdownTrigger, DropdownContainer, DropdownItemContainer, DropdownItem } from '@graphpolaris/shared/lib/components/dropdowns';
 import { PopoverContext } from '@graphpolaris/shared/lib/components/layout/Popover';
-import {
-  toQuerybuilderGraphology,
-  setQuerybuilderGraphology,
-  attributeShownToggle,
-} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
-import { isEqual } from 'lodash-es';
 import { useDispatch } from 'react-redux';
+import { isEqual } from 'lodash-es';
 
 /**
  * Component to render an entity flow element
@@ -68,6 +75,12 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
     dispatch(attributeShownToggle(attribute.handleData));
   }
 
+  const unionType = useQuerybuilderUnionTypes()[node.id];
+
+  function setUnionType(unionType: QueryUnionType) {
+    dispatch(setQueryUnionType({ nodeId: node.id, unionType: unionType }));
+  }
+
   const attributesBeingShown = useQuerybuilderAttributesShown();
   function isAttributeAdded(attribute: NodeAttribute): boolean {
     return attributesBeingShown.some((x) => isEqual(x, attribute.handleData));
@@ -93,7 +106,7 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
 
               <DropdownItemContainer>
                 <PopoverContext.Consumer>
-                  {(popover) => (
+                  {(popover) => [
                     <DropdownItem
                       value={'Add/remove attribute'}
                       onClick={(e) => {
@@ -128,8 +141,27 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
                           </DropdownItem>
                         )),
                       ]}
-                    />
-                  )}
+                    />,
+                    <DropdownItem
+                      value="Union type"
+                      onClick={(e) => {
+                        popover?.setOpen(false);
+                        setOpenDropdown(false);
+                      }}
+                      submenu={[
+                        <DropdownItem
+                          value="AND"
+                          onClick={(_) => setUnionType(QueryUnionType.AND)}
+                          selected={unionType != QueryUnionType.OR} // Also selected when null
+                        />,
+                        <DropdownItem
+                          value="OR"
+                          onClick={(_) => setUnionType(QueryUnionType.OR)}
+                          selected={unionType == QueryUnionType.OR}
+                        />,
+                      ]}
+                    />,
+                  ]}
                 </PopoverContext.Consumer>
                 <DropdownItem value="Remove" className="text-danger" onClick={(e) => removeNode()} />
               </DropdownItemContainer>
@@ -156,7 +188,12 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => {
         }
       >
         {data?.attributes && (
-          <PillAttributes node={node} attributes={data.attributes} attributeEdges={attributeEdges.map((edge) => edge?.attributes)} />
+          <PillAttributes
+            node={node}
+            attributes={data.attributes}
+            attributeEdges={attributeEdges.map((edge) => edge?.attributes)}
+            unionType={unionType}
+          />
         )}
       </EntityPill>
     </div>
diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx
index adc55c386..f97bbda5d 100644
--- a/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx
+++ b/libs/shared/lib/querybuilder/pills/customFlowPills/relationpill/QueryRelationPill.tsx
@@ -1,10 +1,26 @@
 import { memo, useState, useMemo, useEffect } from 'react';
 import { NodeAttribute, RelationNodeAttributes, SchemaReactflowRelationNode, toHandleId } from '../../../model';
-import { useAppDispatch, useQuerybuilderGraph, useQuerybuilderSettings } from '@graphpolaris/shared/lib/data-access';
+import {
+  useAppDispatch,
+  useQuerybuilderGraph,
+  useQuerybuilderSettings,
+  useQuerybuilderUnionTypes,
+} from '@graphpolaris/shared/lib/data-access';
 import { addWarning } from '@graphpolaris/shared/lib/data-access/store/configSlice';
-import { setQuerybuilderGraphology, toQuerybuilderGraphology, attributeShownToggle } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
+import {
+  setQuerybuilderGraphology,
+  toQuerybuilderGraphology,
+  attributeShownToggle,
+} from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 import { PillAttributes } from '../../pillattributes/PillAttributes';
-import { Button, DropdownContainer, DropdownTrigger, RelationPill, DropdownItemContainer, DropdownItem } from '@graphpolaris/shared/lib/components';
+import {
+  Button,
+  DropdownContainer,
+  DropdownTrigger,
+  RelationPill,
+  DropdownItemContainer,
+  DropdownItem,
+} from '@graphpolaris/shared/lib/components';
 import { Icon } from '@graphpolaris/shared/lib/components/icon';
 import { TextInput } from '@graphpolaris/shared/lib/components/inputs';
 import { PopoverContext } from '@graphpolaris/shared/lib/components/layout/Popover';
@@ -82,9 +98,11 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => {
     dispatch(attributeShownToggle(attribute.handleData));
   }
 
+  const unionType = useQuerybuilderUnionTypes()[node.id];
+
   const attributesBeingShown = useQuerybuilderAttributesShown();
   function isAttributeAdded(attribute: NodeAttribute): boolean {
-    return attributesBeingShown.some((x) => isEqual(x, attribute.handleData))
+    return attributesBeingShown.some((x) => isEqual(x, attribute.handleData));
   }
 
   // TODO: must do this once design is chosen
@@ -105,7 +123,7 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => {
         title={
           <div className="flex flex-row w-full">
             <span className="flex-grow text-justify truncate">{data?.name}</span>
-            
+
             <DropdownContainer>
               <DropdownTrigger size="md">
                 <Button
@@ -119,34 +137,46 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => {
 
               <DropdownItemContainer>
                 <PopoverContext.Consumer>
-                  {popover => <DropdownItem value={'Add/remove attribute'} onClick={(e) => {
-                    popover?.setOpen(false);
-                    setOpenDropdown(false);
-                  }} submenu={
-                    [
-                      <TextInput 
-                        type={'text'} 
-                        placeholder="Filter" 
-                        size="xs" 
-                        className="mb-1 min-w-40 rounded-sm" 
-                        value={filter}
-                        onClick={(e) => e.stopPropagation()}
-                        onChange={(v) => setFilter(v)} />,
-
-                      filteredAttributes.map(attr => 
-                        <DropdownItem
-                          key={attr.handleData.attributeName + attr.handleData.nodeId}
-                          value={attr.handleData.attributeName ?? ''} 
-                          selected={isAttributeAdded(attr)}
-                          onClick={(_) => addAttribute(attr)}>
-                            <Icon component={attr?.handleData?.attributeDimension != null ? IconMap[attr.handleData.attributeDimension] : undefined} className="ms-2 float-end" size={16} />
-                        </DropdownItem>
-                      )
-                    ]
-                  } />}
+                  {(popover) => (
+                    <DropdownItem
+                      value={'Add/remove attribute'}
+                      onClick={(e) => {
+                        popover?.setOpen(false);
+                        setOpenDropdown(false);
+                      }}
+                      submenu={[
+                        <TextInput
+                          type={'text'}
+                          placeholder="Filter"
+                          size="xs"
+                          className="mb-1 min-w-40 rounded-sm"
+                          value={filter}
+                          onClick={(e) => e.stopPropagation()}
+                          onChange={(v) => setFilter(v)}
+                        />,
+
+                        filteredAttributes.map((attr) => (
+                          <DropdownItem
+                            key={attr.handleData.attributeName + attr.handleData.nodeId}
+                            value={attr.handleData.attributeName ?? ''}
+                            selected={isAttributeAdded(attr)}
+                            onClick={(_) => addAttribute(attr)}
+                          >
+                            <Icon
+                              component={
+                                attr?.handleData?.attributeDimension != null ? IconMap[attr.handleData.attributeDimension] : undefined
+                              }
+                              className="ms-2 float-end"
+                              size={16}
+                            />
+                          </DropdownItem>
+                        )),
+                      ]}
+                    />
+                  )}
                 </PopoverContext.Consumer>
+                <DropdownItem value="Union type" submenu={[<DropdownItem value="AND" selected />, <DropdownItem value="OR" />]} />
                 <DropdownItem value="Remove" className="text-danger" onClick={(e) => removeNode()} />
-                
               </DropdownItemContainer>
             </DropdownContainer>
           </div>
@@ -176,6 +206,7 @@ export const QueryRelationPill = memo((node: SchemaReactflowRelationNode) => {
           attributes={data?.attributes || []}
           attributeEdges={attributeEdges.map((edge) => edge?.attributes)}
           mr={-pillWidth * 0.05}
+          unionType={unionType}
         />
       </RelationPill>
     </div>
diff --git a/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx
index 9f9b1c7be..28e63fefb 100644
--- a/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx
+++ b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributes.tsx
@@ -3,6 +3,7 @@ import { NodeAttribute, QueryGraphEdges, SchemaReactflowEntityNode, SchemaReactf
 import { useAppDispatch, useQuerybuilderAttributesShown } from '../../..';
 import { isEqual } from 'lodash-es';
 import { PillAttributesItem } from './PillAttributesItem';
+import { QueryUnionType } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 
 type PillAttributesProps = {
   node: SchemaReactflowEntityNode | SchemaReactflowRelationNode;
@@ -10,6 +11,7 @@ type PillAttributesProps = {
   attributeEdges: (QueryGraphEdges | undefined)[];
   mr?: number;
   className?: string;
+  unionType?: QueryUnionType;
 };
 
 type IconMapType = {
@@ -50,6 +52,7 @@ export const PillAttributes = (props: PillAttributesProps) => {
                   ? IconMap[props.attributes[i].handleData.attributeDimension || 0]
                   : 'icon-[ic--outline-question-mark]'
               }
+              unionType={props.unionType}
             />
           );
         })}
diff --git a/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx
index 7052033b5..5fdb64a10 100644
--- a/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx
+++ b/libs/shared/lib/querybuilder/pills/pillattributes/PillAttributesItem.tsx
@@ -11,6 +11,7 @@ import { PillHandle } from '@graphpolaris/shared/lib/components/pills/PillHandle
 import { pillAttributesPadding } from '@graphpolaris/shared/lib/components/pills/pill.const';
 import { Button } from '../../..';
 import { QueryBuilderDispatcherContext } from '../../panel/QueryBuilderDispatcher';
+import { QueryUnionType } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice';
 
 type PillAttributesItemProps = {
   attribute: NodeAttribute;
@@ -18,6 +19,7 @@ type PillAttributesItemProps = {
   className?: string;
   mr?: number;
   icon: string | undefined;
+  unionType?: QueryUnionType;
 };
 
 export const PillAttributesItem = (props: PillAttributesItemProps) => {
@@ -30,6 +32,17 @@ export const PillAttributesItem = (props: PillAttributesItemProps) => {
   const handleId = toHandleId(handleDataFromReactflowToDataId(props.node, props.attribute));
   const handleType = 'source';
 
+  function shapeForType(unionType: QueryUnionType | undefined) {
+    if (unionType == null) return 'square';
+
+    switch (unionType) {
+      case QueryUnionType.AND:
+        return 'square';
+      case QueryUnionType.OR:
+        return 'diamond';
+    }
+  }
+
   return (
     <div className="px-2 py-1 bg-secondary-100 flex justify-between items-center">
       <p className="truncate text-[0.6rem]">{props.attribute.handleData.attributeName}</p>
@@ -57,7 +70,7 @@ export const PillAttributesItem = (props: PillAttributesItemProps) => {
         handleTop="auto"
         position={Position.Right}
         className={`stroke-white${props.className ? ` ${props.className}` : ''}`}
-        type="square"
+        type={shapeForType(props.unionType)}
       >
         <Handle
           id={handleId}
diff --git a/libs/shared/lib/querybuilder/query-utils/query2backend.ts b/libs/shared/lib/querybuilder/query-utils/query2backend.ts
index b498011a8..875783278 100644
--- a/libs/shared/lib/querybuilder/query-utils/query2backend.ts
+++ b/libs/shared/lib/querybuilder/query-utils/query2backend.ts
@@ -8,7 +8,7 @@ import Graph from 'graphology';
 import { allSimplePaths } from 'graphology-simple-path';
 import { AllLogicStatement, ReferenceStatement } from '../model/logic/general';
 import { ML, MLTypes, mlDefaultState } from '../../data-access/store/mlSlice';
-import { QueryBuilderSettings } from '../../data-access/store/querybuilderSlice';
+import { QueryBuilderSettings, QueryUnionType } from '../../data-access/store/querybuilderSlice';
 
 // export type QueryI {
 
@@ -148,12 +148,22 @@ export function calculateQueryLogic(
   return ret as AllLogicStatement;
 }
 
-function queryLogicUnion(graphLogicChunks: AllLogicStatement[]): AllLogicStatement | undefined {
+function queryLogicUnion(
+  nodes: SerializedNode<LogicNodeAttributes>[],
+  graph: QueryMultiGraph,
+  logics: SerializedNode<LogicNodeAttributes>[],
+  unionTypes: { [node_id: string]: QueryUnionType },
+): AllLogicStatement | undefined {
+  let graphLogicChunks = nodes.map((node) => calculateQueryLogic(node, graph, logics));
+
+  if (graphLogicChunks.length === 0) return undefined;
   if (graphLogicChunks.length === 1) return graphLogicChunks[0];
-  else if (graphLogicChunks.length > 1) {
-    return ['And', graphLogicChunks[0], queryLogicUnion(graphLogicChunks.slice(1)) || '0'];
-  }
-  return undefined;
+
+  const constraintNodeId = nodes[0].key;
+  const entityNodeId = graph.edges.filter((x) => x.target == constraintNodeId)[0].source;
+  const unionType = unionTypes[entityNodeId] || QueryUnionType.AND;
+
+  return [unionType, graphLogicChunks[0], queryLogicUnion(nodes.slice(1), graph, logics, unionTypes) || '0'];
 }
 
 /**
@@ -165,6 +175,7 @@ export function Query2BackendQuery(
   graph: QueryMultiGraph,
   settings: QueryBuilderSettings,
   ml: ML = mlDefaultState,
+  unionTypes: { [node_id: string]: QueryUnionType },
 ): BackendQueryFormat {
   let query: BackendQueryFormat = {
     saveStateID: saveStateID,
@@ -205,7 +216,7 @@ export function Query2BackendQuery(
       });
     });
 
-    return Query2BackendQuery(saveStateID, graphologyQuery.export(), settings, ml);
+    return Query2BackendQuery(saveStateID, graphologyQuery.export(), settings, ml, unionTypes);
   }
   // Chunk extraction: traverse graph to find all paths of logic between relations and entities
   let graphSequenceChunks: QueryGraphNodes[][] = [];
@@ -240,8 +251,7 @@ export function Query2BackendQuery(
   let logicsRightHandleFinal = logics.filter((n) => {
     return !graph.edges.some((e) => e.source === n.key);
   });
-  let graphLogicChunks = logicsRightHandleFinal.map((node) => calculateQueryLogic(node, graph, logics));
-  query.logic = queryLogicUnion(graphLogicChunks);
+  query.logic = queryLogicUnion(logicsRightHandleFinal, graph, logics, unionTypes);
 
   if (!graphSequenceChunks || graphSequenceChunks.length === 0 || graphSequenceChunks?.[0].length === 0) return query;
 
-- 
GitLab