Skip to content
Snippets Groups Projects
convertQuery_test.go 30.9 KiB
Newer Older
LoLo5689's avatar
LoLo5689 committed
/*
This program has been developed by students from the bachelor Computer Science at Utrecht University within the Software Project course.
© Copyright Utrecht University (Department of Information and Computing Sciences)
*/

	"git.science.uu.nl/graphpolaris/query-conversion/entity"
	"github.com/stretchr/testify/assert"
)

LoLo5689's avatar
LoLo5689 committed
/*
Tests an empty query
	t: *testing.T, makes go recognise this as a test
*/
func TestEmptyQueryConversion(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [],
			"relations": []
		},
		"entities": [],
		"relations": [],
		"groupBys": [],
		"filters": [],
	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `
LET nodes = first(RETURN UNION_DISTINCT([],[]))
LET edges = first(RETURN UNION_DISTINCT([],[]))
RETURN {"vertices":nodes, "edges":edges }`
	assert.Equal(t, correctConvertedResult, *convertedResult)
}
LoLo5689's avatar
LoLo5689 committed

/*
Tests multiple entity types
	t: *testing.T, makes go recognise this as a test
*/
func TestMultipleEntityTypes(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"databaseName": "test",
		"return": {
			{
				"type": "kamerleden",
				"ID": 0
			},
			{
				"type": "partijen",
				"ID": 1
			}
			{
				"ID": 0
				"type": "lid_van",
				"depth": {
					"min": 1,
					"max": 1
				},
				"fromType": "entity",
				"fromID": 0,
				"toType": "entity",
				"toID": 1
			}
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "partij",
				"value": "GL",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
				"fromType": "entity",
				"fromID": 1,
				"toType": "relation",
				"toID": 0,
				"attribute": "zetels",
				"value": "6",
				"dataType": "int",
				"matchType": "GT",
				"inType": "",
				"inID": -1
			}
		"limit": 5000,
		"modifiers": []

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `WITH partijenLET n0 = (FOR x IN kamerleden FILTER x.partij == "GL" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x lid_van OPTIONS { uniqueEdges: "path" }FILTER v.zetels > 6 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`

	//cleanedResult := strings.ReplaceAll(correctConvertedResult, "\n", "")
	//cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	convertedCleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	convertedCleanedResult = strings.ReplaceAll(convertedCleanedResult, "\t", "")

	assert.Equal(t, correctConvertedResult, convertedCleanedResult)
LoLo5689's avatar
LoLo5689 committed
/*
Tests a query with one attribute
	t: *testing.T, makes go recognise this as a test
*/
func TestEntityOneAttributeQuery(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": []
		},
		"entities": [
			{
				"type": "airports",
		"filters": [
				{
					"ID": 0,
					"fromType": "entity",
					"fromID": 0,
					"toType": "",
					"toID": -1,
					"attribute": "state",
					"value": "HI",
					"dataType": "string",
					"matchType": "exact",
					"inType": "",
					"inID": -1
				}
		],
	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET nodes = first(RETURN UNION_DISTINCT(n0,[],[]))LET edges = first(RETURN UNION_DISTINCT([],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

LoLo5689's avatar
LoLo5689 committed
/*
Test a relation with a constraint
	t: *testing.T, makes go recognise this as a test
*/
func TestRelationWithConstraint(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
Fjodor's avatar
 
Fjodor committed
						"dataType": "string",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"FromType": "entity",
				"fromID": 0,
				"ToType": "",
				"toID": -1,
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "Day",
				"value": "15",
				"dataType": "int",
				"matchType": "EQ",
				"inType": "",
				"inID": -1
			}
		],
	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

LoLo5689's avatar
LoLo5689 committed
/*
Tests the count modifier
	t: *testing.T, makes go recognise this as a test
*/
func TestModifierCountEntity(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": []
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "state",
				"value": "HI",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			}
		],
		"limit": 5000,
		"modifiers": [
			{
				"type": "COUNT",
				"selectedType": "entity",
				"id": 0,
				"attributeIndex": -1
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)RETURN LENGTH (n0)`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
LoLo5689's avatar
LoLo5689 committed

/*
Tests the count modifer with an attribute
	t: *testing.T, makes go recognise this as a test
*/
func TestModifierCountEntityAttribute(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": []
		},
		"entities": [
			{
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "",
				"toID": -1,
				"attribute": "state",
				"value": "HI",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			}
		],
		"limit": 5000,
		"modifiers": [
			{
				"type": "SUM",
				"selectedType": "entity",
				"id": 0,
				"attributeIndex": 0
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)RETURN SUM (n0[*].state)`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
LoLo5689's avatar
LoLo5689 committed

/*
Tests the count modifier on a relation
	t: *testing.T, makes go recognise this as a test
*/
func TestModifierCountRelation(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"fromType": "entity",
				"fromID": 0,
				"toType": "",
				"toID": -1
			}
		],
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "state",
				"value": "HI",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			},
			{
				"ID": 1,
				"fromType": "relation",
				"fromID": 0,
				"toType": "entity",
				"toID": 0,
				"attribute": "Day",
				"value": "15",
				"dataType": "int",
				"matchType": "EQ",
				"inType": "",
				"inID": -1
			}
		],
		"limit": 5000,
		"modifiers": [
			{
				"type": "COUNT",
				"selectedType": "relation",
				"id": 0,
				"attributeIndex": -1
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 RETURN DISTINCT p )RETURN LENGTH (unique(r0[*].edges[**]))`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
LoLo5689's avatar
LoLo5689 committed

/*
Tests the count modifier with an entity swap
	t: *testing.T, makes go recognise this as a test
*/
func TestModifierCountEntitySwap(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"databaseName": "TweedeKamer",
		"return": {
			{
				"ID": 0,
				"type": "partijen"
			},
			{
				"ID": 1,
				"type": "kamerleden"
			}
			{
				"ID": 0,
				"type": "lid_van",
				"depth": {
					"min": 1,
					"max": 1
				},
				"fromType": "entity",
				"fromID": 1,
				"toType": "entity",
				"toID": 0
			}
		"groupBys": [],
		"filters": [],
		"limit": 5000,
		"modifiers": [
			{
				"type": "COUNT",
				"selectedType": "entity",
				"selectedTypeId": 1,
				"attributeIndex": -1
			}

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)
	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `WITH partijenLET n1 = (FOR x IN kamerleden RETURN x)LET r0 = (FOR x IN n1 FOR v, e, p IN 1..1 OUTBOUND x lid_van OPTIONS { uniqueEdges: "path" }RETURN DISTINCT p )RETURN LENGTH (unique(r0[*].vertices[0]))`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}
LoLo5689's avatar
LoLo5689 committed

/*
Tests the count modifier on a relation and attribute
	t: *testing.T, makes go recognise this as a test
*/
func TestModifierCountRelationAttribute(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"fromType": "entity",
				"fromID": 0,
				"toType": "",
				"toID": -1
			}
		],
		"groupBys": [
	
		],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "state",
				"value": "HI",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			},
			{
				"ID": 1,
				"fromType": "relation",
				"fromID": 0,
				"toType": "",
				"toID": -1,
				"attribute": "Day",
				"value": "15",
				"dataType": "int",
				"matchType": "EQ",
				"inType": "",
				"inID": -1
			}
		],
		"limit": 5000,
		"modifiers": [
			{
				"type": "AVG",
				"selectedType": "relation",
				"id": 0,
				"attributeIndex": 0
			}
		]
	}`)

	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n0 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER p.edges[*].Day ALL == 15 RETURN DISTINCT p )RETURN AVG (r0[*].edges[**].Day)`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

LoLo5689's avatar
LoLo5689 committed
/*
Tests a relation with an in out constraint
	t: *testing.T, makes go recognise this as a test
*/
func TestRelationWithInOutConstraint(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0,
				1
			],
			"relations": [
				0
			]
		},
		"entities": [
			{
				"type": "airports",
				"constraints": [
					{
						"attribute": "city",
						"value": "San Francisco",
Fjodor's avatar
 
Fjodor committed
						"dataType": "string",
				"type": "airports",
				"constraints": [
					{
						"attribute": "state",
						"value": "HI",
Fjodor's avatar
 
Fjodor committed
						"dataType": "string",
						"matchType": "exact"
					}
				]
			}
		],
		"relations": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 3
				},
				"fromType": "entity",
				"fromID": 1,
				"toType": "entity",
				"toID": 0
		],
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "city",
				"value": "San Francisco",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			}, 
			{
				"ID": 1,
				"fromType": "entity",
				"fromID": 1,
				"toType": "relation",
				"toID": 0,
				"attribute": "state",
				"value": "HI",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			},        {
				"ID": 2,
				"fromType": "relation",
				"fromID": 0,
				"toType": "",
				"toID": -1,
				"attribute": "Day",
				"value": "15",
				"dataType": "int",
				"matchType": "EQ",
				"inType": "",
				"inID": -1
			}
	
	
	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n1 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n1 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

LoLo5689's avatar
LoLo5689 committed
/*
Tests two relations
	t: *testing.T, makes go recognise this as a test
*/
func TestTwoRelations(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
		"return": {
			"entities": [
				0,
				1,
				2
			],
			"relations": [
				0,
				1
			]
		},
		"entities": [
			{
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 3
				},
				"fromType": "entity",
				"fromID": 2,
				"toType": "entity",
				"toID": 1
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"fromType": "entity",
				"fromID": 0,
				"toType": "",
				"toID": -1
		],
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 1,
				"attribute": "city",
				"value": "New York",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			}, 
			{
				"ID": 1,
				"fromType": "entity",
				"fromID": 1,
				"toType": "relation",
				"toID": 0,
				"attribute": "city",
				"value": "San Francisco",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			},
			{
				"ID": 2,
				"fromType": "entity",
				"fromID": 2,
				"toType": "relation",
				"toID": 0,
				"attribute": "state",
				"value": "HI",
				"dataType": "string",
				"matchType": "exact",
				"inType": "",
				"inID": -1
			},
			{
				"ID": 3,
				"fromType": "relation",
				"fromID": 0,
				"toType": "",
				"toID": -1,
				"attribute": "Day",
				"value": "15",
				"dataType": "int",
				"matchType": "EQ",
				"inType": "",
				"inID": -1
			}
	
	
	// Unmarshall the incoming message into an IncomingJSONQuery object
	var JSONQuery entity.IncomingQueryJSON
	json.Unmarshal(query, &JSONQuery)

	convertedResult, err := service.ConvertQuery(&JSONQuery)

	// Assert that there is no error
	assert.NoError(t, err)

	// Assert that the result and the expected result are the same
	correctConvertedResult := `LET n2 = (FOR x IN airports FILTER x.state == "HI" RETURN x)LET r0 = (FOR x IN n2 FOR v, e, p IN 1..3 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }FILTER v.city == "San Francisco" FILTER p.edges[*].Day ALL == 15 LIMIT 5000 RETURN DISTINCT p )LET n0 = (FOR x IN airports FILTER x.city == "New York" RETURN x)LET r1 = (FOR x IN n0 FOR v, e, p IN 1..1 OUTBOUND x flights OPTIONS { uniqueEdges: "path" }LIMIT 5000 RETURN DISTINCT p )LET nodes = first(RETURN UNION_DISTINCT(flatten(r0[**].vertices), flatten(r1[**].vertices), [],[]))LET edges = first(RETURN UNION_DISTINCT(flatten(r0[**].edges), flatten(r1[**].edges), [],[]))RETURN {"vertices":nodes, "edges":edges }`
	cleanedResult := strings.ReplaceAll(*convertedResult, "\n", "")
	cleanedResult = strings.ReplaceAll(cleanedResult, "\t", "")
	assert.Equal(t, correctConvertedResult, cleanedResult)
}

LoLo5689's avatar
LoLo5689 committed
/*
Tests a relation with only a to node
	t: *testing.T, makes go recognise this as a test
*/
func TestRelationWithOnlyToNode(t *testing.T) {
	// Setup for test
	// Create query conversion service
	service := NewService()

	query := []byte(`{
				0
			]
		},
		"entities": [
			{
				"ID": 0,
				"type": "airports"
			}
		],
		"relations": [
			{
				"ID": 0,
				"type": "flights",
				"depth": {
					"min": 1,
					"max": 1
				},
				"fromType": "",
				"fromID": -1,
				"toType": "entity",
				"toID": 0
			}
		],
		"groupBys": [],
		"filters": [
			{
				"ID": 0,
				"fromType": "entity",
				"fromID": 0,
				"toType": "relation",
				"toID": 0,
				"attribute": "city",
				"value": "San Francisco",
				"dataType": "string",