478 lines
19 KiB
Python
478 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from typing import Union, Tuple
|
|
from uuid import uuid4
|
|
import re
|
|
import networkx as nx
|
|
import string
|
|
|
|
from Core.GlobalVariables import non_string_fields
|
|
|
|
"""
|
|
This class handles the backend stuff for the LinkScope Query Language.
|
|
"""
|
|
|
|
|
|
class LQLQueryBuilder:
|
|
|
|
QUERIES_HISTORY = {}
|
|
|
|
databaseSnapshot = None
|
|
databaseEntities = None
|
|
allCanvases = None
|
|
canvasesEntitiesDict = None
|
|
allEntityFields = None
|
|
allEntities = None
|
|
|
|
def __init__(self, mainWindow):
|
|
self.mainWindow = mainWindow
|
|
|
|
def takeSnapshot(self):
|
|
self.mainWindow.LENTDB.dbLock.acquire()
|
|
# Create a copy
|
|
self.databaseSnapshot = nx.DiGraph(self.mainWindow.LENTDB.database)
|
|
self.mainWindow.LENTDB.dbLock.release()
|
|
|
|
self.databaseEntities = set(self.databaseSnapshot.nodes)
|
|
|
|
self.allCanvases = self.getAllCanvasNames()
|
|
self.canvasesEntitiesDict = self.getCanvasesEntitiesDict(self.allCanvases)
|
|
self.allEntityFields, self.allEntities = self.getAllEntitiesAndFields()
|
|
|
|
def getAllEntitiesAndFields(self) -> (set, list):
|
|
entitiesSnapshot = {entity: self.databaseSnapshot.nodes[entity] for entity in self.databaseSnapshot.nodes
|
|
if self.databaseSnapshot.nodes[entity].get('Entity Type') != 'EntityGroup'}
|
|
entityFields = set()
|
|
for entityUID in entitiesSnapshot:
|
|
entityFields.update(entitiesSnapshot[entityUID].keys())
|
|
for field in non_string_fields:
|
|
try:
|
|
entityFields.remove(field)
|
|
except KeyError:
|
|
# This typically only happens if there are no entities in the database.
|
|
continue
|
|
return entityFields, entitiesSnapshot
|
|
|
|
def getAllCanvasNames(self) -> list:
|
|
canvasNames = list(self.mainWindow.centralWidget().tabbedPane.canvasTabs.keys())
|
|
canvasNames.append('*')
|
|
return canvasNames
|
|
|
|
def getEntitiesOnCanvas(self, canvasName: str):
|
|
try:
|
|
# Ensure that we don't have nodes here that are not present in our database snapshot
|
|
canvasNodes = set(
|
|
self.mainWindow.centralWidget().tabbedPane.canvasTabs[canvasName].scene().sceneGraph.nodes)
|
|
return canvasNodes.intersection(self.databaseEntities)
|
|
except KeyError:
|
|
return None
|
|
|
|
def getCanvasesEntitiesDict(self, allCanvasNames: list):
|
|
returnDict = {}
|
|
for canvas in allCanvasNames:
|
|
allEntitiesOnCanvas = self.getEntitiesOnCanvas(canvas)
|
|
if allEntitiesOnCanvas is not None:
|
|
returnDict[canvas] = allEntitiesOnCanvas
|
|
else:
|
|
returnDict[canvas] = set()
|
|
|
|
return returnDict
|
|
|
|
def parseSelect(self, selectClause: str, selectValue: Union[str, list]):
|
|
if selectClause == 'SELECT':
|
|
if '*' in selectValue:
|
|
# No need to remove the '*'. Could cause errors if that's a field name (even though it is bad practice).
|
|
return self.allEntityFields
|
|
return set([entityField for entityField in selectValue if entityField in self.allEntityFields])
|
|
else:
|
|
try:
|
|
clauseValue = re.compile(selectValue)
|
|
return set([entityField for entityField in self.allEntityFields if clauseValue.match(entityField)])
|
|
except re.error:
|
|
return set()
|
|
|
|
def parseSource(self, sourceClause: str, sourceValues: Union[None, list]) -> set:
|
|
"""
|
|
sourceValues:
|
|
[[("AND" | "OR" | None), ("CANVAS" | "RCANVAS"), (True | False), <User Input>], ...]
|
|
OR
|
|
None
|
|
if sourceClause == "FROMDB"
|
|
"""
|
|
if sourceClause == "FROMDB":
|
|
return self.databaseEntities
|
|
else:
|
|
resultEntitySet = set()
|
|
for sourceValue in sourceValues:
|
|
try:
|
|
if sourceValue[1] == "CANVAS":
|
|
if sourceValue[3] not in self.allCanvases:
|
|
raise ValueError('Reference to nonexistent canvas.')
|
|
matchingCanvases = [sourceValue]
|
|
else:
|
|
canvasRegex = re.compile(sourceValue[3])
|
|
matchingCanvases = [canvasMatch for canvasMatch in self.allCanvases
|
|
if canvasRegex.match(canvasMatch)]
|
|
except (ValueError, re.error):
|
|
continue
|
|
|
|
# Not the most efficient way of phrasing this, but by far the most compact and legible.
|
|
for matchingCanvas in matchingCanvases:
|
|
if sourceValue[0] == 'AND':
|
|
if sourceValue[2] is False:
|
|
resultEntitySet = self.canvasAndNot(resultEntitySet,
|
|
self.canvasesEntitiesDict[matchingCanvas])
|
|
else:
|
|
resultEntitySet = self.canvasAnd(resultEntitySet,
|
|
self.canvasesEntitiesDict[matchingCanvas])
|
|
else:
|
|
# If this is the first clause, or'ing the empty initial resultEntitySet is what we want.
|
|
if sourceValue[2] is False:
|
|
resultEntitySet = self.canvasOrNot(resultEntitySet,
|
|
self.canvasesEntitiesDict[matchingCanvas],
|
|
self.databaseEntities)
|
|
else:
|
|
resultEntitySet = self.canvasOr(resultEntitySet,
|
|
self.canvasesEntitiesDict[matchingCanvas])
|
|
|
|
return resultEntitySet
|
|
|
|
def parseConditions(self, conditionClauses: Union[None, list], entitiesPool) -> set:
|
|
"""
|
|
conditionClauses:
|
|
[[("AND" | "OR" | None), ("VC" | "GC"), (True | False), conditionValue], ...]
|
|
|
|
conditionValue:
|
|
if VC:
|
|
[("ATTRIBUTE" | "RATTRIBUTE"), <User Input>,
|
|
("EQ" | "CONTAINS" | "STARTSWITH" | "ENDSWITH" | "RMATCH"), <User Input>]
|
|
if GC:
|
|
[("CHILDOF" <ENTITY> | "DESCENDANTOF " <ENTITY> |
|
|
"PARENTOF" <ENTITY> | "ANCESTOROF " <ENTITY> |
|
|
"NUMCHILDREN" (" < " | " <= " | " > " | " >= " | " == ") <DIGITS> |
|
|
"NUMPARENTS" (" < " | " <= " | " > " | " >= " | " == ") <DIGITS> |
|
|
"CONNECTEDTO" <ENTITY> | "ISOLATED" | "ISROOT" | "ISLEAF")]
|
|
"""
|
|
|
|
self.allEntities = {uid: self.allEntities[uid] for uid in self.allEntities if uid in entitiesPool}
|
|
uidsToSelect = set()
|
|
|
|
for conditionClause in conditionClauses:
|
|
isNot = conditionClause[2]
|
|
conditionValue = conditionClause[3]
|
|
userInput1 = conditionValue[1]
|
|
userInput2 = conditionValue[3]
|
|
firstArgument = conditionValue[0]
|
|
if conditionClause[1] == "VC":
|
|
matchingFields = []
|
|
if firstArgument == "ATTRIBUTE":
|
|
if userInput1 in self.allEntityFields:
|
|
matchingFields.append(userInput1)
|
|
else:
|
|
try:
|
|
attributeRegex = re.compile(userInput1)
|
|
except re.error:
|
|
continue
|
|
for field in self.allEntityFields:
|
|
if attributeRegex.match(field):
|
|
matchingFields.append(field)
|
|
|
|
for matchingField in matchingFields:
|
|
entitiesToRemove = []
|
|
for entity in self.allEntities:
|
|
attributeKeyValue = str(self.allEntities[entity].get(matchingField))
|
|
if not self.checkVCHelper(conditionValue[2], isNot, attributeKeyValue, userInput2):
|
|
if conditionClause[0] == "AND":
|
|
entitiesToRemove.append(entity)
|
|
else:
|
|
uidsToSelect.add(entity)
|
|
for entityToRemove in entitiesToRemove:
|
|
uidsToSelect.remove(entityToRemove)
|
|
|
|
elif conditionClause[1] == "GC":
|
|
entitiesToRemove = []
|
|
for entity in self.allEntities:
|
|
if not self.checkGCHelper(firstArgument, isNot, conditionValue[1:]):
|
|
if conditionClause[0] == "AND":
|
|
entitiesToRemove.append(entity)
|
|
else:
|
|
uidsToSelect.add(entity)
|
|
for entityToRemove in entitiesToRemove:
|
|
uidsToSelect.remove(entityToRemove)
|
|
|
|
uidsToRemove = set(self.allEntities).difference(uidsToSelect)
|
|
for entity in uidsToRemove:
|
|
self.allEntities.pop(entity, None)
|
|
|
|
return uidsToSelect
|
|
|
|
def canvasOr(self, canvasSetA: set, canvasSetB: set):
|
|
return canvasSetA.union(canvasSetB)
|
|
|
|
def canvasAnd(self, canvasSetA: set, canvasSetB: set):
|
|
return canvasSetA.intersection(canvasSetB)
|
|
|
|
def canvasAndNot(self, canvasSetA: set, canvasSetB: set):
|
|
return canvasSetA.difference(canvasSetB)
|
|
|
|
def canvasOrNot(self, canvasSetA: set, canvasSetB: set, allEntitiesSet: set):
|
|
return canvasSetA.union(allEntitiesSet.difference(canvasSetB))
|
|
|
|
def checkEQ(self, valueA: str, valueB: str):
|
|
if valueA == valueB:
|
|
return True
|
|
return False
|
|
|
|
def checkContains(self, valueA: str, valueB: str):
|
|
if valueB in valueA:
|
|
return True
|
|
return False
|
|
|
|
def checkStartsWith(self, valueA: str, valueB: str):
|
|
if valueA.startswith(valueB):
|
|
return True
|
|
return False
|
|
|
|
def checkEndsWith(self, valueA: str, valueB: str):
|
|
if valueA.endswith(valueB):
|
|
return True
|
|
return False
|
|
|
|
def checkRMatch(self, valueA: str, valueB: str):
|
|
try:
|
|
valueMatch = re.compile(valueB)
|
|
if valueMatch.match(valueA):
|
|
return True
|
|
except re.error:
|
|
pass
|
|
return False
|
|
|
|
def checkVCHelper(self, checkType: str, isNot: bool, valueA: str, valueB: str):
|
|
returnVal = False
|
|
if checkType == "EQ":
|
|
returnVal = self.checkEQ(valueA, valueB)
|
|
elif checkType == "CONTAINS":
|
|
returnVal = self.checkContains(valueA, valueB)
|
|
elif checkType == "STARTSWITH":
|
|
returnVal = self.checkStartsWith(valueA, valueB)
|
|
elif checkType == "ENDSWITH":
|
|
returnVal = self.checkEndsWith(valueA, valueB)
|
|
elif checkType == "RMATCH":
|
|
returnVal = self.checkRMatch(valueA, valueB)
|
|
if isNot:
|
|
return not returnVal
|
|
return returnVal
|
|
|
|
def checkChildOf(self, valueA: str, valueB: str):
|
|
return self.databaseSnapshot.has_successor(valueA, valueB)
|
|
|
|
def checkDescendantOf(self, valueA: str, valueB: str):
|
|
try:
|
|
if valueB in nx.descendants(self.databaseSnapshot, valueA):
|
|
return True
|
|
except nx.NetworkXError:
|
|
pass
|
|
return False
|
|
|
|
def checkParentOf(self, valueA: str, valueB: str):
|
|
return self.databaseSnapshot.has_predecessor(valueA, valueB)
|
|
|
|
def checkAncestorOf(self, valueA: str, valueB: str):
|
|
try:
|
|
if valueB in nx.ancestors(self.databaseSnapshot, valueA):
|
|
return True
|
|
except nx.NetworkXError:
|
|
pass
|
|
return False
|
|
|
|
def checkNumChildren(self, valueA: str, valueB: str, valueC: int):
|
|
numChildren = len(list(self.databaseSnapshot.successors(valueA)))
|
|
returnValue = False
|
|
if valueB == "<":
|
|
if numChildren < valueC:
|
|
returnValue = True
|
|
elif valueB == "<=":
|
|
if numChildren <= valueC:
|
|
returnValue = True
|
|
elif valueB == ">":
|
|
if numChildren > valueC:
|
|
returnValue = True
|
|
elif valueB == ">=":
|
|
if numChildren >= valueC:
|
|
returnValue = True
|
|
elif valueB == "==":
|
|
if numChildren == valueC:
|
|
returnValue = True
|
|
return returnValue
|
|
|
|
def checkNumParents(self, valueA: str, valueB: str, valueC: int):
|
|
numParents = len(list(self.databaseSnapshot.predecessors(valueA)))
|
|
returnValue = False
|
|
if valueB == "<":
|
|
if numParents < valueC:
|
|
returnValue = True
|
|
elif valueB == "<=":
|
|
if numParents <= valueC:
|
|
returnValue = True
|
|
elif valueB == ">":
|
|
if numParents > valueC:
|
|
returnValue = True
|
|
elif valueB == ">=":
|
|
if numParents >= valueC:
|
|
returnValue = True
|
|
elif valueB == "==":
|
|
if numParents == valueC:
|
|
returnValue = True
|
|
return returnValue
|
|
|
|
def checkConnectedTo(self, valueA: str, valueB: str):
|
|
try:
|
|
if nx.has_path(self.databaseSnapshot, valueA, valueB):
|
|
return True
|
|
except nx.NetworkXError:
|
|
pass
|
|
return False
|
|
|
|
def checkIsolated(self, valueA: str):
|
|
try:
|
|
if valueA in self.databaseSnapshot.nodes and nx.is_isolate(self.databaseSnapshot, valueA):
|
|
return True
|
|
except nx.NetworkXError:
|
|
pass
|
|
return False
|
|
|
|
def checkIsRoot(self, valueA: str):
|
|
try:
|
|
if len(self.databaseSnapshot.in_edges(valueA)) == 0:
|
|
return True
|
|
except nx.NetworkXError:
|
|
pass
|
|
return False
|
|
|
|
def checkIsLeaf(self, valueA: str):
|
|
try:
|
|
if len(self.databaseSnapshot.out_edges(valueA)) == 0:
|
|
return True
|
|
except nx.NetworkXError:
|
|
pass
|
|
return False
|
|
|
|
def checkGCHelper(self, checkType: str, isNot: bool, args: list):
|
|
returnVal = False
|
|
if checkType == "CHILDOF":
|
|
returnVal = self.checkChildOf(*args)
|
|
elif checkType == "DESCENDANTOF":
|
|
returnVal = self.checkDescendantOf(*args)
|
|
elif checkType == "PARENTOF":
|
|
returnVal = self.checkParentOf(*args)
|
|
elif checkType == "ANCESTOROF":
|
|
returnVal = self.checkAncestorOf(*args)
|
|
elif checkType == "NUMCHILDREN":
|
|
returnVal = self.checkNumChildren(*args)
|
|
elif checkType == "NUMPARENTS":
|
|
returnVal = self.checkNumParents(*args)
|
|
elif checkType == "CONNECTEDTO":
|
|
returnVal = self.checkConnectedTo(*args)
|
|
elif checkType == "ISOLATED":
|
|
returnVal = self.checkIsolated(*args)
|
|
elif checkType == "ISROOT":
|
|
returnVal = self.checkIsRoot(*args)
|
|
elif checkType == "ISLEAF":
|
|
returnVal = self.checkIsLeaf(*args)
|
|
if isNot:
|
|
return not returnVal
|
|
return returnVal
|
|
|
|
def modifyNumify(self, valueA: str):
|
|
# Get the first number that shows up.
|
|
tempString = valueA.replace(',', '.') # Making sure that floats are expressed the right way.
|
|
count = 0
|
|
for c in tempString:
|
|
if c not in string.digits:
|
|
count += 1
|
|
else:
|
|
break
|
|
|
|
count2 = 0
|
|
for c in tempString[count:]:
|
|
if c in string.digits or c == '.':
|
|
count2 += 1
|
|
else:
|
|
break
|
|
|
|
return str(float(tempString[count:count + count2]))
|
|
|
|
def modifyUpperCase(self, valueA: str):
|
|
return valueA.upper()
|
|
|
|
def modifyLowerCase(self, valueA: str):
|
|
return valueA.lower()
|
|
|
|
def parseModify(self, resultsToModify: (set, set), modifyQueries: list) -> (set, set):
|
|
"""
|
|
modifyQueries:
|
|
[[("MODIFY" | "RMODIFY"), <User Input>, ("NUMIFY" | "UPPERCASE" | "LOWERCASE")], ...]
|
|
"""
|
|
|
|
matchingFields = resultsToModify[1]
|
|
|
|
modifiedUIDs = set()
|
|
numifiedFields = set()
|
|
|
|
for modification in modifyQueries:
|
|
userInput1 = modification[1]
|
|
modificationType = modification[2]
|
|
modifyFields = []
|
|
if modification[0] == "MODIFY":
|
|
if userInput1 in resultsToModify[1]:
|
|
modifyFields.append(userInput1)
|
|
else:
|
|
try:
|
|
userInputRegex = re.compile(userInput1)
|
|
modifyFields = [fieldMatch for fieldMatch in matchingFields if userInputRegex.match(fieldMatch)]
|
|
except (ValueError, re.error):
|
|
continue
|
|
for entity in self.allEntities:
|
|
for modifyField in modifyFields:
|
|
entityFieldValue = self.allEntities[entity].get(modifyField)
|
|
if modificationType == "UPPERCASE":
|
|
newFieldValue = self.modifyUpperCase(entityFieldValue) if entityFieldValue is not None else None
|
|
elif modificationType == "LOWERCASE":
|
|
newFieldValue = self.modifyLowerCase(entityFieldValue) if entityFieldValue is not None else None
|
|
elif modificationType == "NUMIFY":
|
|
newFieldValue = self.modifyNumify(entityFieldValue) if entityFieldValue is not None else None
|
|
numifiedFields.add(modifyField)
|
|
else:
|
|
newFieldValue = None
|
|
if newFieldValue is not None:
|
|
modifiedUIDs.add(entity)
|
|
self.allEntities[entity][modifyField] = newFieldValue
|
|
|
|
return modifiedUIDs, numifiedFields
|
|
|
|
def parseQuery(self, selectClause: str, selectValue: Union[str, list], sourceClause: str,
|
|
sourceValues: Union[None, list], conditionClauses: Union[None, list],
|
|
modifyQueries: Union[list, None] = None) -> Union[Tuple[set, Union[set, None]], None]:
|
|
|
|
if self.databaseSnapshot is None:
|
|
return None
|
|
|
|
returnValue = None
|
|
modifications = None
|
|
fieldsToSelect = self.parseSelect(selectClause, selectValue)
|
|
if fieldsToSelect:
|
|
entitiesToConsider = self.parseSource(sourceClause, sourceValues)
|
|
if entitiesToConsider:
|
|
if conditionClauses:
|
|
entitiesToConsider = self.parseConditions(conditionClauses, entitiesToConsider)
|
|
returnValue = (entitiesToConsider, fieldsToSelect)
|
|
if modifyQueries:
|
|
modifications = self.parseModify(returnValue, modifyQueries)
|
|
|
|
queryUID = str(uuid4())
|
|
self.QUERIES_HISTORY[queryUID] = (selectClause, selectValue, sourceClause, sourceValues, conditionClauses,
|
|
modifyQueries)
|
|
|
|
return returnValue, modifications # TODO: return value check
|
|
|