Skip to content
Snippets Groups Projects
aql.go 12.3 KiB
Newer Older
sivan's avatar
sivan committed
package convertquery
sivan's avatar
sivan committed

import (
	"encoding/json"
	"fmt"
	"strconv"
sivan's avatar
sivan committed
)

sivan's avatar
sivan committed
// Service is a model for the convertquery use case
type Service struct {
}

// NewService creates a new convertquery service
func NewService() *Service {
	return &Service{}
}

sivan's avatar
sivan committed
/*
// Query format for exporting to JSON
export type JSONFormat = {
  Return: {
    Entities: number[];
    Relations: number[];
  };
  Entities: Entity[];
  Relations: Relation[];
};
type Entity = {
  Type: string;
  Constraints: Constraint[];
};
type Relation = {
  Type: string;
  EntityFrom: number;
  EntityTo: number;
  Depth: { min: number; max: number };
  Constraints: Constraint[];
};
type Constraint = {
  Attribute: string;
  Value: string;

  // Constraint datatypes
  // text		  MatchTypes: exact/contains/startswith/endswith
  // number   MatchTypes: GT/LT/EQ
  // bool     MatchTypes: EQ/NEQ
  DataType: string;
  MatchType: string;
};

{
   "Return":{
      "Entities":[
         0
      ],
      "Relations":[
         0
      ]
   },
   "Entities":[
      {
         "Type":"Airport",
         "Constraints":[
            {
               "Attribute":"month",
               "Value":"8",
               "DataType":"text",
               "MatchType":"exact"
            }
         ]
      }
   ],
   "Relations":[
      {
         "Type":"Flight",
         "Depth":{
            "min":1,
            "max":3
         },
         "EntityFrom":0,
         "EntityTo":-1,
         "Constraints":[

         ]
      }
   ]
}

*/

sivan's avatar
sivan committed
// Constraint datatypes
// text		  MatchTypes: exact/contains/startswith/endswith
// number   MatchTypes: GT/LT/EQ
// bool     MatchTypes: EQ/NEQ

// Ranges dus tussen 10 half 5 bijv.

// Struct used for JSON conversion
type parsedJSON struct {
	Return    returnStruct
sivan's avatar
sivan committed
	Entities  []entityStruct
sivan's avatar
sivan committed
	Relations []relationStruct
}

type returnStruct struct {
	Entities  []int
	Relations []int
}

sivan's avatar
sivan committed
type entityStruct struct {
	Type        string
	Constraints []constraintStruct
sivan's avatar
sivan committed
}
type relationStruct struct {
sivan's avatar
sivan committed
	Type        string
	EntityFrom  int
	EntityTo    int
	Depth       searchDepthStruct
	Constraints []constraintStruct
sivan's avatar
sivan committed
}
type searchDepthStruct struct {
	Min int
	Max int
}

type constraintStruct struct {
sivan's avatar
sivan committed
	Attribute string
sivan's avatar
sivan committed
	Value     string
	DataType  string
	MatchType string
}

// ConvertQuery converts a json string to an AQL query
sivan's avatar
sivan committed
func (s *Service) ConvertQuery(jsonMsg *[]byte) (*string, error) {
sivan's avatar
sivan committed

	jsonStruct, err := convertJSONToStruct(jsonMsg)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	//Per node query
	//	per constraint
	//relations koppelen ze samen
	//return statement

	var result *string
	if len(jsonStruct.Return.Relations) > 0 {
		result = createEdgeQuery(jsonStruct, "e0")
	} else {

		result = createAllNodesQuery(jsonStruct.Return.Entities, jsonStruct.Entities)
	}

sivan's avatar
sivan committed
	return result, nil
}

// createAllNodesQuery creates node queries in AQL
sivan's avatar
sivan committed
func createAllNodesQuery(returnEntitiesIndices []int, entities []entityStruct) *string {
	var (
		result string
		ret    string
	)

	ret += "RETURN { \n"
sivan's avatar
sivan committed
	for _, entityIndex := range returnEntitiesIndices {
		nodeID := fmt.Sprintf("n%s", strconv.Itoa(entityIndex))
sivan's avatar
sivan committed

sivan's avatar
sivan committed
		nodeQueryString := *createNodeQuery(&entities[entityIndex], nodeID)
		ret += "\t" + nodeID + ":" + nodeID + ",\n"
sivan's avatar
sivan committed

		result += fmt.Sprintf(" \n%s", nodeQueryString)
	}
	ret = strings.TrimSuffix(ret, ",\n")
	ret += "\n}"
sivan's avatar
sivan committed

	result += "\n" + ret
sivan's avatar
sivan committed
	return &result
}

// createNodeQuery converts the node part of the json to a subquery
sivan's avatar
sivan committed
func createNodeQuery(node *entityStruct, name string) *string {
sivan's avatar
sivan committed
	/*
		LET alices = (
			FOR x IN female
			FILTER x.name == "Alice" AND x.birth_year > 1997
			RETURN x
		)

		NAAR -->

		LET {NAAM**} = (
			FOR x IN {NODETYPE}
			FILTER x.{CONSTRAINT[0]} {{CONSTRAINT[0]}.MATCHTYPE} {CONSTRAINT[0].VALUE}
			AND x.{CONSTRAINT[1]} {{CONSTRAINT[1]}.MATCHTYPE} {CONSTRAINT[1].VALUE}
			RETURN x
		)

	*/

	letStatement := fmt.Sprintf("LET %s = (\n", name)
sivan's avatar
sivan committed
	forStatement := fmt.Sprintf("\tFOR x IN %s \n", node.Type)
sivan's avatar
sivan committed

	// Generate all constraints as FILTER statements
	first := true
	filter := "\tFILTER "
sivan's avatar
sivan committed
	for _, constraint := range node.Constraints {
		constraint := createQueryConstraint(&constraint)
sivan's avatar
sivan committed

		if first {
			filter += fmt.Sprintf("\t%s ", *constraint)
			first = false
		} else {
			filter += fmt.Sprintf("AND\n\t\t%s", *constraint)
		}
	}

	returnStatement := "\n\tRETURN x\n)"

	// Concatenate all the statements
	result := letStatement + forStatement + filter + returnStatement
sivan's avatar
sivan committed
	return &result
}

/*
	{
  "Return": {
    "Entities": [
      0,
      1
    ],
    "Relations": [
      0
    ]
  },
  "Entities": [
    {
      "Type": "airports",
      "Constraints": [
        {
          "Attribute": "country",
          "Value": "USA",
          "DataType": "text",
          "MatchType": "exact"
        }
      ]
    },
    {
      "Type": "airports",
      "Constraints": [
        {
          "Attribute": "city",
          "Value": "New York",
          "DataType": "text",
          "MatchType": "exact"
        },
        {
          "Attribute": "vip",
          "Value": "true",
          "DataType": "bool",
          "MatchType": "exact"
        }
      ]
    }
  ],
  "Relations": [
    {
      "Type": "flights",
      "Depth": {
        "min": 1,
        "max": 1
      },
      "EntityFrom": 0,
      "EntityTo": 1,
      "Constraints": [
        {
          "Attribute": "Month",
          "Value": "1",
          "DataType": "number",
          "MatchType": "exact"
        },
        {
          "Attribute": "Day",
          "Value": "15",
          "DataType": "number",
          "MatchType": "exact"
        }
      ]
    }
  ]
}


	LET n0 = (
    FOR n IN airports
    FILTER n.country == "USA"
    RETURN n
)

LET n1 = (
    FOR n IN airports
    FILTER n.city == "New York" AND
            n.vip == true
    RETURN n
)

LET e0 = (
    FOR a IN n0
    FOR v, e, p IN 1..1 OUTBOUND a flights
    FILTER v in n1
    FILTER p.edges[0].Month == 1 AND
            p.edges[0].Day == 15
    RETURN { vertices: p.vertices[*], edges: p.edges[*] }
)

RETURN e0

*/
// createEdgeQuery creates an aql query for relations
func createEdgeQuery(q *parsedJSON, name string) *string {
	//Creation of n0 n1 let statements
	query := ""
	entityList := q.Entities
	for i, x := range entityList {
		query += "\n"
		n := fmt.Sprintf("n%v", i)
		query += *createNodeQuery(&x, n)
	}

	query += *createLetStatementForRelation(&q.Relations[0], name) //DIT GAAN WE EVEN NEGEREN

	//ZEER GEHARDCODED ivm demo
	// finalReturn := "\nRETURN { "

	// for x := range q.Return.Entities {
	// 	finalReturn += fmt.Sprintf("n%v, ", x)
	// }

	// finalReturn += fmt.Sprintf("e%v }", q.Return.Relations[0])
	// query += finalReturn
	query += "\nRETURN {e0:e0}"
// createLetStatementForRelation creates the relation query
func createLetStatementForRelation(relation *relationStruct, name string) *string {

	letStatement := fmt.Sprintf("\nLET %s = (\n", name)
	upperForStatement := fmt.Sprintf("\tFOR x IN n%v \n", relation.EntityFrom)
	edgeForStatement := fmt.Sprintf("\tFOR v, e, p IN %v..%v OUTBOUND x %s \n", relation.Depth.Min, relation.Depth.Max, relation.Type)
	upperFilter := fmt.Sprintf("\tFILTER v IN n%v \n", relation.EntityTo)
	// Generate all constraints as FILTER statements
	first := true
	nestedFilter := "\tFILTER "
	for _, constraint := range relation.Constraints {
		constraint := createEdgeQueryConstraint(&constraint)

		if first {
			nestedFilter += fmt.Sprintf("\t%s ", *constraint)
			first = false
		} else {
			nestedFilter += fmt.Sprintf("AND\n\t\t%s", *constraint)
		}
	}

	returnStatement := "\n\tLIMIT 100\n\tRETURN { vertices: p.vertices[*], edges: p.edges[*] }\n)"

	// Concatenate all the statements
	result := letStatement + upperForStatement + edgeForStatement + upperFilter + nestedFilter + returnStatement
	return &result
}
sivan's avatar
sivan committed
// Constraint datatypes
// text		  MatchTypes: exact/contains/startswith/endswith
// number   MatchTypes: GT/LT/EQ
// bool     MatchTypes: EQ/NEQ

// createQueryConstraint creates a sinlge line of AQL filtering/constraint
sivan's avatar
sivan committed
func createQueryConstraint(constraint *constraintStruct) *string {
sivan's avatar
sivan committed
	//FILTER x.{CONSTRAINT[0]} {{CONSTRAINT[0]}.MATCHTYPE} {CONSTRAINT[0].VALUE}
sivan's avatar
sivan committed
	//			name				match					value + dataType
sivan's avatar
sivan committed
	var (
sivan's avatar
sivan committed
		match string
		value string
		line  string
sivan's avatar
sivan committed
	)

	//Wicked switches letsgo
sivan's avatar
sivan committed
	switch constraint.DataType {
sivan's avatar
sivan committed
	case "text":
sivan's avatar
sivan committed
		value = fmt.Sprintf("\"%s\"", constraint.Value)
		switch constraint.MatchType {
sivan's avatar
sivan committed
		case "contains":
sivan's avatar
sivan committed
			match = "IN"
sivan's avatar
sivan committed
		case "startswith":
sivan's avatar
sivan committed
			match = "LIKE"
			value = fmt.Sprintf("\"%s%%\"", constraint.Value)
sivan's avatar
sivan committed
		case "endswith":
sivan's avatar
sivan committed
			match = "LIKE"
			value = fmt.Sprintf("\"_%s\"", constraint.Value)
sivan's avatar
sivan committed
		default: //exact
sivan's avatar
sivan committed
			match = "=="
sivan's avatar
sivan committed
		}
	case "number":
sivan's avatar
sivan committed
		value = constraint.Value
		switch constraint.MatchType {
sivan's avatar
sivan committed
		case "GT":
sivan's avatar
sivan committed
			match = ">"
sivan's avatar
sivan committed
		case "LT":
sivan's avatar
sivan committed
			match = "<"
sivan's avatar
sivan committed
		case "GET":
sivan's avatar
sivan committed
			match = ">="
sivan's avatar
sivan committed
		case "LET":
sivan's avatar
sivan committed
			match = "<="
sivan's avatar
sivan committed
		default: //EQ
sivan's avatar
sivan committed
			match = "=="
sivan's avatar
sivan committed
		}
	default: /*bool*/
sivan's avatar
sivan committed
		value = constraint.Value
		switch constraint.MatchType {
sivan's avatar
sivan committed
		case "NEQ":
sivan's avatar
sivan committed
			match = "!="
sivan's avatar
sivan committed
		default: //EQ
sivan's avatar
sivan committed
			match = "=="
sivan's avatar
sivan committed
		}
	}
sivan's avatar
sivan committed
	line = fmt.Sprintf("x.%s %s %s", constraint.Attribute, match, value)
sivan's avatar
sivan committed
	return &line
}

// createEdgeQueryConstraint translates the constraints of an edge query to aql
func createEdgeQueryConstraint(constraint *constraintStruct) *string {
	//FILTER x.{CONSTRAINT[0]} {{CONSTRAINT[0]}.MATCHTYPE} {CONSTRAINT[0].VALUE}
	//			name				match					value + dataType
	var (
		match string
		value string
		line  string
	)

	//Wicked switches letsgo
	switch constraint.DataType {
	case "text":
		value = fmt.Sprintf("\"%s\"", constraint.Value)
		switch constraint.MatchType {
		case "contains":
			match = "IN"
		case "startswith":
			match = "LIKE"
			value = fmt.Sprintf("\"%s%%\"", constraint.Value)
		case "endswith":
			match = "LIKE"
			value = fmt.Sprintf("\"_%s\"", constraint.Value)
		default: //exact
			match = "=="
		}
	case "number":
		value = constraint.Value
		switch constraint.MatchType {
		case "GT":
			match = ">"
		case "LT":
			match = "<"
		case "GET":
			match = ">="
		case "LET":
			match = "<="
		default: //EQ
			match = "=="
		}
	default: /*bool*/
		value = constraint.Value
		switch constraint.MatchType {
		case "NEQ":
			match = "!="
		default: //EQ
			match = "=="
		}
	}
	line = fmt.Sprintf("p.edges[*].%s ALL %s %s", constraint.Attribute, match, value)
	return &line
}

// convertJSONToStruct takes the json as byte array and converts it to a struct
sivan's avatar
sivan committed
func convertJSONToStruct(jsonMsg *[]byte) (*parsedJSON, error) {
	jsonStruct := parsedJSON{}
sivan's avatar
sivan committed
	err := json.Unmarshal(*jsonMsg, &jsonStruct)
sivan's avatar
sivan committed

	if err != nil {
		return nil, err
	}

	return &jsonStruct, nil
}

/*
	desired output

	WITH male
	FOR a1 IN female
		FILTER a1.name == "Alice"

		(FOR r1v,r1e,r1p IN 1..1 OUTBOUND a1 relation
			FILTER r1v.name == "Bob")
			OR
		(FOR r2v,r2e,r2p IN 1..1 OUTBOUND a1 relation
			FILTER r2v.name == "Martin" && r2v.hasdog == true)
																// constraints for Bob
				OR r1v.name == "Martin"
			FILTER r1e.type == "married"

			FOR r2v,r2e,r2p IN 1..1 OUTBOUND r1v relation
				FILTER r2v.name == "Figo"
				FILTER r2e.type == "has_dog"

				RETURN {a1,r1v,r2v}


	of


	LET alices = (
		FOR x IN female
			FILTER x.name == "Alice"
			RETURN x
	)

	LET bobs = (
		FOR x IN male
			FILTER x.name == "Bob"
			RETURN x
	)

	LET alices = (
		FOR alice IN alices
			FOR r1v,r1e,r1p IN 1..1 OUTBOUND alice relation
				FILTER r1v IN bobs AND r1e.type == "married"
				RETURN {alice}
	)

	LET alices = (
		FOR alice IN alices
			FOR r1v,r1e,r1p IN 1..1 OUTBOUND alice relation
				FILTER r1v IN marting AND r1e.type == "friend"
				RETURN {alice}
	)

		FOR a2 IN male
			FILTER a2.name == "Bob"

			FOR r1v,r1e,r1p IN 1..1 OUTBOUND a1 relation
				FILTER r1v._id == a2._id
				FILTER r1e.type == "married"
				RETURN {a1,a2,r1e}

*/
/*
{
	"NodeType": "female",
	"Constraints":
		{
			"name":       { "Value": "Alice", "DataType": "text",   "MatchType": "exact" },
			"birth_year": { "Value": "1997",  "DataType": "number", "MatchType": "GT"    }
		}
	}

	NAAR -->

	LET alices = (
		FOR x IN female
			FILTER x.name == "Alice" AND x.birth_year > 1997
			RETURN x
	)

*/

//Nog een manier vinden om namen te syncen over de queries heen **

// func forGlory() *constraintStruct {
// 	var yeet constraintStruct
// 	yeet = constraintStruct{
// 		ConstraintName: "name",
// 		Value:          "Alice",
// 		MatchType:      "exact",
// 		DataType:       "text",
// 	}
// 	return &yeet
// }

// func alsoForGlory() {
// 	con := forGlory()

// 	toPrint := createQueryConstraint(*con)
// 	fmt.Println(*toPrint)
// }