blob: 87cace007cec879977a77fc0c0d4cd645793c967 [file] [log] [blame]
////
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
////
== Proposal 3 - Removing the Need for Closures/Lambda in Gremlin
=== Status
This proposal has been accepted through a post to the TinkerPop Dev List and is ready to begin implementaion.
=== Motivation
There are a number of useful operations that Gremlin users often wish to
perform that are not provided today in the form of traversal steps or
predicates (P/TextP). For historical reasons these functions were
omitted and users were able to accomplish these tasks by specifying
anonymous code blocks or closures to perform these tasks. For example,
below is an example of how you can achieve a case-insensitive search for
any cities that contain Miami”.
....
g.V().hasLabel('city').
has('name',filter{it.get().toLowerCase().contains('Miami'.toLowerCase())})
....
While this is just one example of how closures are used, they are a
powerful fallback mechanism in Gremlin to handle use cases where there
is no functionality within the Gremlin language to meet the
requirements. However, for a variety of reasons such as security and
performance, many/most remote providers of TinkerPop do not allow users
to execute closures as part of a query. This leaves users with a
problem, as the mechanism provided to solve these sorts of use cases is
not allowed. Examples of some commonly requested functionality that
cannot be accomplished without the use of closures would be:
[cols=",,",options="header",]
|===
|String Functions |List Functions |Date Functions
|asString |reverse |dateAdd
|concat |remove |dateDiff
|length |indexOf |asDate
|split |product |
|substring |all |
|rTrim |any |
|lTrim |none |
|trim |concat |
|replace |length |
|reverse |intersect |
|toUpper |difference |
|toLower |union |
|===
=== Considerations
* Adding full support for traversals as parameters to predicates would
simplify the syntax of these examples. However, this is a known issue
with Gremlin and none of the proposed options below are blocked by it,
nor do they exacerbated this issue any further. As such, the
ramifications of that change are not covered by this proposal.
=== Proposed Options and Recommendation
==== Option 1 (Recommended)
Create a new Gremlin step for each of the desired functions a user is
looking to perform. Each step encapsulates a set of functionality that
customers are looking to achieve. While certain steps may be reused
across input types (e.g. `reverse()` for both list and string inputs)
the behaviors of each step is well-defined for a given input type.
*Example*: Find me all `city` nodes with a `name` starting with `miami`,
ignoring case?
`g.V().hasLabel('city').where(values('name').toLower(), eq('miami'))`
===== Pros:
* Most similar to current Gremlin patterns for adding steps
* Feel the most like Gremlin when writing the
===== Cons:
* Would result in a potentially large number of steps being added to the
language, hindering discoverability
* Adds complexity to creating and maintaining all the current GLVs due
to the number of new steps.
==== Option 2
Create a single Gremlin step that take the operation as a parameter and
uses that parameter to mutate the behavior to achieve the desired
functionality.
*Example*: : Find me all `city` nodes with a `name` starting with
`miami`, ignoring case?
`g.V().hasLabel('city').where(F.apply(Func.toLower, values('name')), eq('miami'))`
===== Pros:
* Single step simplifies adding new operations across GLVs
===== Cons:
* Introduces a novel concept, a function, to the Gremlin language
* No set signature for `F.apply` as it will differ per operation
==== Option 3
Create a new type of predicate in Gremlin that specifies the operation
and returns the correct output. This would be a new paradigm within
Gremlin, as it extends predicates to return non-boolean results.
*Example*:: Find me all `city` nodes with a `name` starting with
`miami`, ignoring case?
`g.V().hasLabel('city').where(SP.toLower(values('name')).is('miami'))`
===== Pros:
* Single predicate simplifies adding new operations as they are now a
token and not a new step that needs to be propagated across all the GLVs
===== Cons:
* New paradigm in Gremlin that further blurs the line between predicates
and steps
* Signature differs for each predicate operation
* Signatures of common steps needs to change to include options like
`where(Predicate)`
=== Proposed Syntax
<<string-function-syntax>>
<<list-function-syntax>>
<<date-function-syntax>>
=== Examples
==== String Examples
===== String Example 1 (SE1)
I want to find the offices, by name, where the name does not have a "-"
as the third character of the string
(https://stackoverflow.com/questions/56115935/gremlin-is-there-a-way-to-find-the-character-based-on-the-index-of-a-string[here])
`g.V().hasLabel('office').where(__.values('name').substr(2, 1)).is(neq('-'))) `
===== String Example 2 (SE2)
I would like to trim out the "Mbit/s" from the string
(https://stackoverflow.com/questions/45365726/im-unable-to-substring-values-that-i-get-by-running-a-gremlin-query-ive-been[here])
`g.V('Service').has('serviceId','ETHA12819844').out('AssociatedToService').`
`value("bandwidth").replace("Mbit/s", "")`
===== String Example 3 (SE3)
I am trying to add a new vertex which should be labeled like an existing
vertex but with some prefix attached
(https://stackoverflow.com/questions/61106927/concatenate-gremlin-graphtraversal-result-with-string[here])
....
`g.V(3).as('a').addV(constant("").concat("prefix_", select('a').label())`
....
===== String Example 4 (SE4)
Find all products that start with the same case-insensitive prefix. +
e.g. Given the following products:
[cols=",",options="header",]
|===
|id |product_name
|1 |PROD-123
|2 |PROD-234
|3 |TEST-1234
|4 |GAMMA-1234
|5 |PR-123
|===
We should return:
[cols=",",options="header",]
|===
|id |product_name
|1 |PROD-123
|2 |PROD-234
|===
....
g.V().hasLabel('Product').has('product_name').as('product1').
V().hasLabel('Product').has('product_name'`).`
where(__.is(select('product1').toLower())`.values('product_name').substring(0, 5)).
select('product1')
....
===== String Example 5 (SE5)
Perform case-insensitive search
....
g.V().hasLabel('Product').where(values('product_name').toLower(), eq('foo'))
....
===== String Example 6 (SE6)
Applying functions to returning values, in this case return the `age`
and a lower cased version of `name`
`g.V().hasLabel('person').valueMap('age', 'name').by().by(toLower())`
===== String Example 7 (SE7)
Concatenating values on the return, in this case return a concatenated
name
`g.V().hasLabel('person').project('age', 'name').` `by('age').`
`by(values('first_name').concat(" ").concat(values('last_name'))`
==== List Examples
===== List Example 1 (LE1)
Given a list of people, return the list of `age`s if everyone’s `age` >
18
`g.V().hasLabel('person').values('age').fold().where(all(gt(18)))`
===== List Example 2 (LE2)
Given a set of vertices, return the list of vertices if anyone’s `age` >
18
`g.V([1,2,3,4]).fold().where(any(values('age').is(gt(18))))`
===== List Example 3 (LE3)
Given a list, find the index of the first occurrence of `Dave`
`g.V().hasLabel('person').fold().indexOf(has('name', 'Dave'))` `==> 12`
`g.inject(['Dave', 'Kelvin', 'Stephen']).indexOf(constant('Dave'))`
`==> 0`
===== List Example 4 (LE4)
Given a list of people, remove any person with a name of `Dave`
`g.V().hasLabel('person').fold().remove(has('name', 'Dave'))`
`==> [‘Kelvin’, Stephen’]`
`g.inject(['Dave', 'Kelvin', 'Stephen']).remove(constant('Dave'))`
`==> [‘Kelvin’, Stephen’]`
`g.inject(['Dave', 'Kelvin', 'Stephen']).remove(constant(['Dave', 'Stephen'))`
`==> ['Kelvin']`
==== Date Examples
===== Date Example 1 (DE1)
Given a transaction, find me all other transactions within 7 days prior
`g.V('transaction1').values('date').dateAdd(DT.Days, -7).as('purchase_date').V().hasLabel('transaction').where(gt('purchase_date')).by('date').by()`
===== Date Example 2 (DE2)
Given two transactions, find me the difference in the dates
`g.V('transaction1').values('date').dateDiff(DT.Days, V('transaction2').values('date').asDate())`
===== Date Example 3 (DE3)
Given a static value, return me the value as a date
`g.inject('1900-01-01').asDate()`
===== Date Example 4 (DE4)
Find the difference between a transaction and the first of the year
`g.V('transaction1').values('date').dateDiff(DT.Days, inject(datetime('2023-01-01'))`
== String Manipulation functions in TinkerPop [[string-function-syntax]]
One of the common gaps that user's find when using Gremlin is that there
is a lack of string manipulation capabilities within the language
itself. This requires that users use closures to handle many common
string manipulation options that users want to do on data in the graph.
This is a problem for many users as many of the providers prevent the
use of arbitrary closures due to the security risks so for these users
there is no way to manipulate strings directly.
=== Proposal
The proposal here is to add a set of steps to handle common string
manipulation requests from users, the details for each are discussed
below:
* <<asString, asString()>>
* <<concat, concat()>>
* <<length, length()>>
* <<split, split()>>
* <<substring, substring()>>
* <<rTrim, rTrim()>>
* <<lTrim, lTrim()>>
* <<trim, trim()>>
* <<replace, replace()>>
* <<reverse, reverse()>>
* <<toUpper, toUpper()>>
* <<toLower, toLower()>>
=== Gremlin Language Variant Function Names
[cols=",,,,,",options="header",]
|===
|Groovy |Java |Python |JavaScript |.NET |Go
|asString() |asString() |as_string() |asString() |AsString() |AsString()
|concat() |concat() |concat() |concat() |Concat() |Concat()
|length() |length() |length() |length() |Length() |Length()
|split() |split() |split() |split() |Split() |Split()
|substring() |substring() |substring() |substring() |Substring()
|Substring()
|rTrim() |rTrim() |rtrim() |rTrim() |RTrim() |RTrim()
|lTrim() |lTrim() |ltrim() |lTrim() |LTrim() |LTrim()
|trim() |trim() |trim() |trim() |Trim() |Trim()
|replace() |replace() |replace() |replace() |Replace() |Replace()
|reverse() |reverse() |reverse() |reverse() |Reverse() |Reverse()
|toUpper() |toUpper() |to_upper() |toUpper() |ToUpper() |ToUpper()
|toLower() |toLower() |to_lower() |toLower() |ToLower() |ToLower()
|===
'''''
== Function Definitions
=== `asString()` [[asString]]
Returns the value of the incoming traverser as a string
==== Signature(s)
`asString()`
`asString(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
Any data type allowed by TinkerPop
==== Expected Output
A String value representing the string value of the traverser being
passed in as shown below:
[cols=",,",options="header",]
|===
|Incoming Datatype |Example Query |Example Output
|Integer |`g.inject(29).asString()` |29
|Float |`g.inject(29.0).asString()` |29.0
|String |`g.inject('foo').asString()` |foo
|UUID |`g.inject(UUID.randomUUID()).asString()`
|47557eed-04e7-4aa4-89eb-9689d26fe94a
|Map
|`g.inject([["id": 1], ["id": 2, "something":"anything"]]).asString()`
|[[id:1], [id:2, something:anything]]
|Date |`g.inject(datetime()).asString()` |Sun Nov 04 00:00:00 UTC 2018
|List |`g.inject([1,2,3]).asString()` |[1, 2, 3]
|List (Local Scope) |`g.inject([1,2,3]).asString(local)` |["1", "2",
"3"]
|Vertex |`g.V(1).asString()` |v[1]
|Edge |`g.E(7).asString()` |e[7][1-knows->2]
|Property |`g.V(1).properties('age').asString()` |vp[age->29]
|null |`g.V().group().by('foo').select(keys).asString()` |null
|===
'''''
=== `concat()` [[concat]]
Concatenates one or more strings together
==== Signature(s)
`concat(String...)`
`concat(Traversal)`
==== Parameters
* `String...` - One or more String values to concatenate to the input
string
* `Traversal` - A traversal value to concatenate
==== Allowed incoming traverser types
String data types. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A String value representing the concatenation of all the incoming traverser and input values
....
g.inject('this').concat('is', 'a', 'test')
==>thisisatest
g.V(1).values('first_name').concat(' ').concat(V(1).values('last_name')
==>John Doe
g.inject('this', 'is', 'a', 'test').concat(' inserted')
==>this inserted
==>is inserted
==>a inserted
==>test inserted
g.inject('John').concat(' ').concat(V(1).values('last_name'))
==>John Doe
....
*Note* `concat()` may also be extended to handle concatenating list
values together but that is out of scope for this change.
'''''
=== `length()` [[length]]
Returns the length of the input string
==== Signature(s)
`length()`
`length(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A Long value representing the number of items in an array or the number
of characters in a string
....
g.inject('this').length()
==>4
g.inject('this').length(local)
==>4
....
*Note*:While this is similar to `count(local)` they are not the same.
`count(local)` treats the input by calculating the count of the items
stored within the traversal. `length()` treats the input as an array and
provides the length of that array.
[cols=",,,",options="header",]
|===
|Input Datatype |Example traversal |count(local) |length()
|Integer |`g.inject(29)` |1 |IllegalArgumentException
|Float |`g.inject(29.0)` |1 |IllegalArgumentException
|String |`g.inject('foo')` |1 |3
|UUID |`g.inject(UUID.randomUUID())` |1 |IllegalArgumentException
|Map |`g.inject(["id": 2, "something":"anything"]])` |1
|IllegalArgumentException
|Date |`g.inject(datetime())` |1 |IllegalArgumentException
|List |`g.inject([1,2,3])` |3 |3
|Vertex |`g.V(1)` |1 |IllegalArgumentException
|Edge |`g.E(7)` |1 |IllegalArgumentException
|Property |`g.V(1).properties('age')` |1 |IllegalArgumentException
|null |`g.V().group().by('foo').select(keys)` |0
|IllegalArgumentException
|===
'''''
=== `split()` [[split]]
Returns a list of strings created by splitting the input string around
the matches of the given delimiter.
==== Signature(s)
`split(String)`
`split(Scope, String)`
==== Parameters
* String - The delimiter character(s) to split the input string* *
==== Allowed inputs
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
An array of strings split around the delimiter character(s)
....
g.inject('this').split('h')
==>[t, is]
g.inject('one,two').split(',')
==>[one, two]
g.inject('axxb').split('x')
==>[a, b]
g.inject('axybxc').split('xy')
==>[a, bxc]
g.inject(['this', 'that']).split('h')
==>[[t, is], [t, at]]
....
'''''
=== `substring()` [[substring]]
returns a substring of the original string with the length specified,
uses a 0-based start
==== Signature(s)
`substring(Long, Long)`
`substring(Long)`
`substring(Scope, Long, Long)`
`substring(Scope, Long)`
==== Parameters
* Long - The start index, 0 based. If the value is negative then the
start location will be the end of the string and it will go the
specified number of characters from the end of the string.
* Long - The number of characters to return. Optional - if not provided
then all remaining characters will be returned
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A String value containing the number of characters specified beginning
at the start location. If the start location plus the length specified
is greater than or equal to the input length, the result will contain
the entire string.
....
g.inject('this').substring(0, 1)
==>t
g.inject('this').substring(2)
==>is
g.inject('this').substring(2, 5)
==>is
g.inject('this').substring(-1)
==>s
g.inject(['this', 'is', 'a', 'test']).substring(local, 2)
==>[is, '' ,'' , 'st']
....
'''''
=== `rTrim()` [[rTrim]]
Returns a string with trailing whitespace removed
*Note*: Whitespace characters are defined as space/tab/line feed/line
tabulation/form feed/carriage return.
==== Signature(s)
`rTrim()`
`rTrim(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A string value with trailing whitespace removed
....
g.inject('this ').rTrim()
==>this
g.inject(['this ', 'that ']).rTrim(local)
==>[this, that]
....
'''''
=== `lTrim()` [[lTrim]]
Returns a string with leading whitespace removed
*Note*: Whitespace characters are defined as space/tab/line feed/line
tabulation/form feed/carriage return.
==== Signature(s)
`lTrim()`
`lTrim(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A string value with leading whitespace removed
....
g.inject(' this').lTrim()
==>this
g.inject([' this', ' that']).lTrim(local)
==>[this, that]
....
'''''
=== `trim()` [[trim]]
Returns a string with leading and trailing whitespace removed
*Note*: Whitespace characters are defined as space/tab/line feed/line
tabulation/form feed/carriage return.
==== Signature(s)
`trim()`
`trim(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A string value with leading and trailing whitespace removed
....
g.inject(' this ').trim()
==>this
g.inject([' this ', ' that ']).trim()
==>[this, that]
....
'''''
=== `replace()` [[replace]]
Returns a string with the specified characters in the original string
replaced with the new characters
==== Signature(s)
`replace(String, String)`
`replace(Scope, String, String)`
==== Parameters
* String - The character(s) to be replaced
* String - The character(s) to replace with
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A string
....
g.inject('this').replace('t', 'x)
==>xhis
g.inject('this').replace('x', 't')
==>this
g.inject('this').replace('is', 'was')
==>thwas
g.inject(['this', 'that']).replace('th', 'was')
==>[wasis, wasat]
....
'''''
=== `reverse()` [[reverse]]
Reverses the current string
==== Signature(s)
`reverse()`
`reverse(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A String value representing the reversed version of the incoming string
....
g.inject('this').reverse()
==>siht
g.inject(['this', 'that']).reverse(local)
==>[siht, taht]
....
*Note* `reverse()` may also be extended to handle concatenating list
values together but that is out of scope for this change.
'''''
=== `toUpper()` [[toUpper]]
Returns an upper case string representation.
*Note*: All case conversions will be done via the mappings specified for
Unicode (https://www.unicode.org/reports/tr44/#Casemapping[found here])
==== Signature(s)
`toUpper()`
`toUpper(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A string
....
g.inject('this').toUpper()
==>THIS
g.inject(['this', 'that']).toUpper()
==>[THIS, THAT]
....
'''''
=== `toLower()` [[toLower]]
Returns an lower case string representation
*Note*: All case conversions will be done via the mappings specified for
Unicode (https://www.unicode.org/reports/tr44/#Casemapping[found here])
==== Signature(s)
`toLower()`
`toLower(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
String data types or array, if local scope is used. If a non-string
traverser, or the list containing non-string values, is passed in then
an `IllegalArgumentException` will be thrown
==== Expected Output
A string
....
g.inject('THIS').toLower()
==>this
g.inject(['THIS', 'THAT']).toLower()
==>[this, that]
....
== List Manipulation functions in TinkerPop [[list-function-syntax]]
One of the common gaps that user's find when using Gremlin is that there
is a lack of list manipulation capabilities within the language itself.
This requires that users use closures to handle many common manipulation
options that users want to do on data in the graph. This is a problem
for many users as many of the providers prevent the use of arbitrary
closures due to the security risks so for these users there is no way to
manipulate strings directly.
=== Proposal
The proposal here is to add a set of steps to handle common list
manipulation requests from users, the details for each are discussed
below:
* <<length_list, length()>>
* <<reverse_list, reverse()>>
* <<remove_list, remove()>>
* <<indexOf_list, indexOf()>>
* <<product_list, product()>>
* <<all_list, all()>>
* <<any_list, any()>>
* <<none_list, none()>>
* <<concat_list, concat()>>
* <<intersect_list, intersect()>>
* <<union_list, union()>>
* <<difference_list, difference()>>
* <<disjunct_list, disjunct()>>
* <<conjoin_list, conjoin()>>
=== Gremlin Language Variant Function Names
[cols=",,,,,",options="header",]
|===
|Groovy |Java |Python |JavaScript |.NET |Go
|length() |length() |length() |length() |Length() |Length()
|reverse() |reverse() |reverse() |reverse() |Reverse() |Reverse()
|remove() |remove() |remove() |remove() |Remove() |Remove()
|indexOf() |indexOf() |index_of() |indexOf() |IndexOf() |IndexOf()
|product() |product() |product() |product() |Product() |Product()
|all() |all() |all() |all() |All() |All()
|any() |any() |any() |any() |Any() |Any()
|none() |none() |none() |none() |None() |None()
|concat() |concat() |concat() |concat() |Concat() |Concat()
|intersect() |intersect() |intersect() |intersect() |Intersect()
|Intersect()
|union() |union() |union() |union() |Union() |Union()
|difference() |difference() |difference() |difference() |Difference()
|Difference()
|disjunct() |disjunct() |disjunct() |disjunct() |Disjunct()
|Disjunct()
|conjoin() |conjoin() |conjoin() |conjoin() |Conjoin() |Conjoin()
|===
'''''
== Function Definitions
=== `length()` [[length_list]]
Returns the length of a list in the incoming traverser
==== Signature(s)
`length()`
==== Parameters
None
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
A Long value representing the number of items in an array or the number
of characters in a string
....
g.inject([1, 2]).length()
==>2
....
=== `reverse()` [[reverse_list]]
Returns the value of the incoming list in reverse order
==== Signature(s)
`reverse()`
==== Parameters
None
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array in reverse order.
....
g.inject([1,2]).reverse()
==>[2, 1]
....
=== `remove()` [[remove_list]]
Removes the first element from the incoming list where the value equals
the specified value
==== Signature(s)
`remove(value)`
`remove(Traversal)`
==== Parameters
* value - The value to remove
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array value representing the new list
....
g.inject([1,2]).remove(1)
==>[2]
....
=== `indexOf()` [[indexOf_list]]
Returns the first occurrence of the `value` in the incoming array
==== Signature(s)
`indexOf(value)`
`indexOf(Traversal)`
==== Parameters
* value - The value to locate
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
A long representing the index of the first occurrence of the value
(zero-based). If the values does not exist then `null` is returned
....
g.inject([1,2]).indexOf(1)
==>0
....
=== `product()` [[product_list]]
Returns the cartesian product of two lists
==== Signature(s)
`product(value)`
`product(Traversal)`
==== Parameters
* value - An array
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
A set of values where each value contains the cartesian product of two
lists
....
g.inject([1,2]).product([3,4])
==>[[1,3], [1,4], [2,3], [2,4]]
....
=== `any()` [[any_list]]
Allows the traverser to continue if any items in the array pass the
supplied predicate.
==== Signature(s)
`any(Predicate)`
==== Parameters
* Predicate - The predicate to use to test the values in the array
==== Allowed incoming traverser types
Array data types. All other types will be filtered out.
==== Expected Output
The arrays which have any item pass the predicate.
....
g.inject([1,2]).any(P.eq(1))
==>[1,2]
....
=== `all()` [[all_list]]
Allows the traverser to continue if all items in the array pass the
supplied predicate.
==== Signature(s)
`all(Predicate)`
==== Parameters
* Predicate - The predicate to use to test the values in the array
==== Allowed incoming traverser types
Array data types. All other types will be filtered out.
==== Expected Output
The arrays which have all items pass the predicate.
....
g.inject([1,2]).all(P.gt(0))
==>[1,2]
....
=== `none()` [[none_list]]
Returns true if no items in the array `value` exist in the input
==== Signature(s)
`none(value)`
`none(Traversal)`
==== Parameters
* value - An array of the items to check in the incoming list
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
True if no values from one list are in the other, False otherwise
....
g.inject([1,2]).none([1])
==>false
g.inject([1,2]).none([1, 3])
==>false
g.inject([1,2]).none([3])
==>true
....
=== `concat()` [[concat_list]]
Returns the concatenation of the incoming array and the traversal or
array value passed as a parameter. This will return all values,
including duplicates.
==== Signature(s)
`concat(value)`
`concat(Traversal)`
==== Parameters
* value - An array of the items to check in the incoming list
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array containing the values of the concatenation of the two lists
....
g.inject([1,2]).concat([3])
==>[1, 2, 3]
g.inject([1,2]).concat([1, 4])
==>[1, 2, 1, 4]
g.V().has('age', 29).values('age').dedup().fold().concat(V().has('age', 30).values('age').dedup().fold())
==>[29, 30]
....
=== `union()` [[union_list]]
Returns the union of the incoming array and the traversal or array value
passed as a parameter. This will return an array of unique values
==== Signature(s)
`union(value)`
`union(Traversal)`
==== Parameters
* value - An array of the items to check in the incoming list
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array containing the unique values of the union of the two lists
....
g.inject([1,2]).union([1])
==>[1, 2]
g.inject([1,2]).union([1, 4])
==>[1, 2, 4]
g.V().has('age', 29).values('age').dedup().fold().union(V().has('age', 30).values('age').dedup().fold())
==>[29, 30]
....
=== `intersect()` [[intersect_list]]
Returns the intersection of the incoming array and the traversal or
array value passed as a parameter. This will return an array of unique
values
==== Signature(s)
`intersect(value)`
`intersect(Traversal)`
==== Parameters
* value - An array of the items to check in the incoming list
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array containing the unique values of the intersection of the two
lists
....
g.inject([1,2]).intersect([1])
==>[1]
g.inject([1,2]).intersect([1, 2, 3])
==>[1, 2]
g.V().has('age', 29).values('age').dedup().fold().intersect(V().has('age', 30).values('age').dedup().fold())
==>[]
....
=== `difference()` [[difference_list]]
Returns the difference of the incoming array and the traversal or array
value passed as a parameter. This will return an array of unique values
==== Signature(s)
`difference(value)`
`difference(Traversal)`
==== Parameters
* value - An array of the items to check in the incoming list
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array containing the different values of the intersection of the two
lists
....
g.inject([1,2]).difference([1])
==>[2]
g.inject([1,2]).difference([1, 2, 3])
==>[3]
g.V().has('age', 29).values('age').dedup().fold().difference(V().has('age', 30).values('age').dedup().fold())
==>[29, 30]
....
=== `disjunct()` [[disjunct_list]]
Returns the disjunct set of the incoming array and the traversal or array
value passed as a parameter. This will return an array of unique values
==== Signature(s)
`disjunct(value)`
`disjunct(Traversal)`
==== Parameters
* value - An array of the items to check in the incoming list
==== Allowed incoming traverser types
Array data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
An array containing the different values of the intersection of the two
lists
....
g.inject([1,2]).disjunct([1])
==>[2]
g.inject([1,2,4]).disjunct([1, 2, 3])
==>[3, 4]
....
=== `conjoin()` [[conjoin_list]]
Returns the join of the incoming array and delimiter passed as a
parameter. This will return a String of the values joined together with
the delimiter.
==== Signature(s)
`conjoin(delimiter)`
==== Parameters
* delimiter - A string delimiter used to join the values
==== Allowed incoming traverser types
String data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
A string of the values joined together with the delimiter
....
g.inject([1,2]).conjoin("5")
==>152
g.inject([1,2,3]).conjoin(";")
==>1;2;3
....
== Date Manipulation functions in TinkerPop [[date-function-syntax]]
One of the common gaps that user's find when using Gremlin is that there
is a lack of date manipulation capabilities within the language itself.
This requires that users use closures to handle many common manipulation
options that users want to do on data in the graph. This is a problem
for many users as many of the providers prevent the use of arbitrary
closures due to the security risks so for these users there is no way to
manipulate strings directly.
=== Proposal
The proposal here is to add a set of steps to handle common datetime
manipulation requests from users, the details for each are discussed
below:
* <<asDate, asDate()>>
* <<dateAdd, dateAdd()>>
* <<dateDiff, dateDiff()>>
=== Gremlin Language Variant Function Names
[cols=",,,,,",options="header",]
|===
|Groovy |Java |Python |JavaScript |.NET |Go
|asDate() |asDate() |as_date() |asDate() |AsDate() |AsDate()
|dateAdd() |dateAdd() |date_add() |dateAdd() |DateAdd() |DateAdd()
|dateDiff() |dateDiff() |date_diff() |dateDiff() |DateDiff() |DateDiff()
|===
== Function Definitions
=== `asDate()` [[asDate]]
Returns the value of the incoming traverser as an ISO-8601 date
==== Signature(s)
`asDate()`
`asDate(Scope)`
==== Parameters
* Scope - Scope Enum
==== Allowed incoming traverser types
Any data type that can be parsed into an ISO-8601 date. If an
unsupported types is passed in then an `IllegalArgumentException` will
be thrown
==== Expected Output
A Date value representing the ISO-8601 value of the traverser being
passed in as shown below:
[cols=",,",options="header",]
|===
|Incoming Datatype |Example Query |Example Output
|Integer |`g.inject(0).asDate()` |1900-01-01T00:00:00Z
|Float |`g.inject(29.0).asDate()` |1900-01-01T00:00:00Z
|String |`g.inject('1/1/1900').asDate()` |1900-01-01T00:00:00Z
|UUID |`g.inject(UUID.randomUUID()).asDate()`
|`IllegalArgumentException`
|Map
|`g.inject([["id": 1], ["id": 2, "something":"anything"]]).asDate()`
|`IllegalArgumentException`]
|Datetime |`g.inject(datetime()).asDate()` |Sun Nov 04 00:00:00 UTC 2018
|List |`g.inject([1,2,3]).asDate()` |`IllegalArgumentException`
|List (Local Scope) |`g.inject([1,2,3]).asDate(local)`
|`IllegalArgumentException`
|Vertex |`g.V(1).asDate()` |`IllegalArgumentException`
|Edge |`g.E(7).asDate()` |`IllegalArgumentException`
|Property |`g.V(1).properties('age').asDate()`
|`IllegalArgumentException`
|null |`g.V().group().by('foo').select(keys).asDate()`
|`IllegalArgumentException`
|===
=== `dateAdd()` [[dateAdd]]
Returns the value with the addition of the `value` number of units as
specified by the `DateToken`
==== Signature(s)
`dateAdd(DateToken, value)`
`dateAdd(Scope, DateToken, value))`
==== Parameters
* DateToken - DateToken Enum
* value - The number of units, specified by the Datetime Token, to add to
the incoming values
==== Allowed incoming traverser types
Datetime data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
A Datetime with the value added.
....
g.inject(datetime()).dateAdd(DT.days, 7)
==> 2018-03-22
g.inject(datetime()).dateAdd(DT.days, -7)
==> 2018-03-8
g.inject([datetime(), datetime()]).dateAdd(local, DT.days, 7)
==> [2018-03-22, 2018-03-22]
....
=== `dateDiff()` [[dateDiff]]
Returns the difference between two Datetimes in epoch time
==== Signature(s)
`dateDiff(value)`
`dateDiff(Traversal)`
`dateDiff(Scope, value))`
==== Parameters
* value - The Datetime to find the difference from
==== Allowed incoming traverser types
Datetime data types. If non-array data types are passed in then an
`IllegalArgumentException` will be thrown
==== Expected Output
The epoch time difference between the two values
....
g.inject(datetime()).dateDiff(datetime().dateAdd(DT.days, 7))
==> 604800
g.inject(datetime()).dateDiff(datetime().dateAdd(DT.days, 7))
==> -604800
g.inject([datetime(), datetime()]).dateDiff(local, DT.days, 7)
==> [604800, 604800]
....