Skip to content

Commit bf7a957

Browse files
Merge pull request #19 from WikibaseSolutions/automatic-identifier-generation
Add automatic identifier generation
2 parents dcf03db + 982b835 commit bf7a957

File tree

7 files changed

+159
-12
lines changed

7 files changed

+159
-12
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
},
3636
"require": {
3737
"php": ">=7.4",
38-
"ext-ctype": "*"
38+
"ext-ctype": "*",
39+
"ext-openssl": "*"
3940
},
4041
"require-dev": {
4142
"phpunit/phpunit": "~9.0",

src/Patterns/Node.php

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
namespace WikibaseSolutions\CypherDSL\Patterns;
2323

2424
use InvalidArgumentException;
25+
use WikibaseSolutions\CypherDSL\Property;
2526
use WikibaseSolutions\CypherDSL\PropertyMap;
2627
use WikibaseSolutions\CypherDSL\Traits\EscapeTrait;
2728
use WikibaseSolutions\CypherDSL\Traits\NodeTypeTrait;
@@ -127,6 +128,33 @@ public function named($variable): self
127128
return $this;
128129
}
129130

131+
/**
132+
* Returns the name of this node. This function automatically generates a name if the node does not have a
133+
* name yet.
134+
*
135+
* @return Variable|null The name of this node, or NULL if this node does not have a name
136+
*/
137+
public function getName(): ?Variable
138+
{
139+
if (!isset($this->variable)) {
140+
$this->named(new Variable());
141+
}
142+
143+
return $this->variable;
144+
}
145+
146+
/**
147+
* Alias of Node::named().
148+
*
149+
* @param $variable
150+
* @return $this
151+
* @see Node::named()
152+
*/
153+
public function setName($variable): self
154+
{
155+
return $this->named($variable);
156+
}
157+
130158
/**
131159
* @param string $label
132160
* @return Node
@@ -139,13 +167,15 @@ public function labeled(string $label): self
139167
}
140168

141169
/**
142-
* Returns the name of this node.
170+
* Returns the property of the given name for this node. For instance, if this node is "(foo:PERSON)", a function call
171+
* like $node->property("bar") would yield "foo.bar".
143172
*
144-
* @return Variable|null The name of this node, or NULL if this node does not have a name
173+
* @param string $property
174+
* @return Property
145175
*/
146-
public function getName(): ?Variable
176+
public function property(string $property): Property
147177
{
148-
return $this->variable;
178+
return new Property($this->getName(), $property);
149179
}
150180

151181
/**

src/Query.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
use WikibaseSolutions\CypherDSL\Patterns\Node;
4646
use WikibaseSolutions\CypherDSL\Patterns\Path;
4747
use WikibaseSolutions\CypherDSL\Traits\EscapeTrait;
48+
use WikibaseSolutions\CypherDSL\Traits\IdentifierGenerationTrait;
4849
use WikibaseSolutions\CypherDSL\Types\AnyType;
4950
use WikibaseSolutions\CypherDSL\Types\CompositeTypes\ListType;
5051
use WikibaseSolutions\CypherDSL\Types\CompositeTypes\MapType;
@@ -119,10 +120,10 @@ public static function relationship(StructuralType $a, StructuralType $b, array
119120
/**
120121
* Creates a variable.
121122
*
122-
* @param string $variable
123+
* @param string $variable The name of the variable; leave empty to automatically generate a variable name
123124
* @return Variable
124125
*/
125-
public static function variable(string $variable): Variable
126+
public static function variable(?string $variable = null): Variable
126127
{
127128
return new Variable($variable);
128129
}
@@ -270,6 +271,10 @@ public function returning($expressions, bool $distinct = false): self
270271
throw new TypeError("\$expressions should only consist of AnyType objects");
271272
}
272273

274+
if ($expression instanceof Node) {
275+
$expression = $expression->getName();
276+
}
277+
273278
$alias = is_integer($maybeAlias) ? "" : $maybeAlias;
274279
$returnClause->addColumn($expression, $alias);
275280
}
@@ -564,8 +569,8 @@ public function where(AnyType $expression): self
564569
/**
565570
* Creates the WITH clause.
566571
*
567-
* @param AnyType[]|AnyType $expressions The entries to add; if the array-key is
568-
* non-numerical, it is used as the alias
572+
* @param AnyType[]|AnyType $expressions The entries to add; if the array-key is non-numerical, it is used as the alias
573+
*
569574
*
570575
* @return Query
571576
* @see https://neo4j.com/docs/cypher-manual/current/clauses/with/
@@ -584,6 +589,10 @@ public function with($expressions): self
584589
throw new TypeError("\$expressions should only consist of AnyType objects");
585590
}
586591

592+
if ($expression instanceof Node) {
593+
$expression = $expression->getName();
594+
}
595+
587596
$alias = is_integer($maybeAlias) ? "" : $maybeAlias;
588597
$withClause->addEntry($expression, $alias);
589598
}

src/Variable.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class Variable implements
6969
StringType,
7070
TimeType
7171
{
72+
public const AUTOMATIC_VARIABLE_LENGTH = 32;
73+
7274
use EscapeTrait;
7375
use DateTrait;
7476
use DateTimeTrait;
@@ -93,8 +95,12 @@ class Variable implements
9395
*
9496
* @param string $variable The variable
9597
*/
96-
public function __construct(string $variable)
98+
public function __construct(?string $variable = null)
9799
{
100+
if ($variable === null) {
101+
$variable = $this->generateUUID(self::AUTOMATIC_VARIABLE_LENGTH);
102+
}
103+
98104
$this->variable = $variable;
99105
}
100106

@@ -139,4 +145,18 @@ public function toQuery(): string
139145
{
140146
return $this->escape($this->variable);
141147
}
148+
149+
/**
150+
* Generates a unique random identifier.
151+
*
152+
* @note It is not entirely guaranteed that this function gives a truly unique identifier. However, because the
153+
* number of possible IDs is so huge, it should not be a problem.
154+
*
155+
* @param int $length
156+
* @return string
157+
*/
158+
private static function generateUUID(int $length): string
159+
{
160+
return substr(bin2hex(openssl_random_pseudo_bytes(ceil($length / 2))), 0, $length);
161+
}
142162
}

tests/Unit/Patterns/NodeTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,21 @@ public function testAddingProperties()
192192
$this->assertSame("({foo: 'baz', baz: 'bar', qux: 'baz'})", $node->toQuery());
193193
}
194194

195+
public function testPropertyWithName()
196+
{
197+
$node = new Node();
198+
$node->named('example');
199+
200+
$this->assertSame('example.foo', $node->property('foo')->toQuery());
201+
}
202+
203+
public function testPropertyWithoutName()
204+
{
205+
$node = new Node();
206+
207+
$this->assertMatchesRegularExpression("/^[0-9a-f]+\.foo$/", $node->property('foo')->toQuery());
208+
}
209+
195210
public function provideOnlyLabelData(): array
196211
{
197212
return [

tests/Unit/QueryTest.php

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ public function testVariable()
9292
$this->assertInstanceOf(Variable::class, Query::variable("foo"));
9393
}
9494

95+
public function testVariableEmpty()
96+
{
97+
$this->assertInstanceOf(Variable::class, Query::variable());
98+
99+
$this->assertMatchesRegularExpression('/[0-9a-f]+/', Query::variable()->toQuery());
100+
}
101+
95102
public function testParameter()
96103
{
97104
$this->assertInstanceOf(Parameter::class, Query::parameter("foo"));
@@ -228,6 +235,22 @@ public function testReturning()
228235
$this->assertSame("RETURN (m:Movie) AS n", $statement);
229236
}
230237

238+
public function testReturningWithNode()
239+
{
240+
$node = Query::node("m");
241+
242+
$statement = (new Query())->returning($node)->build();
243+
244+
$this->assertMatchesRegularExpression("/(RETURN [0-9a-f]+)/", $statement);
245+
246+
$node = Query::node("m");
247+
$node->named('example');
248+
249+
$statement = (new Query())->returning($node)->build();
250+
251+
$this->assertSame('RETURN example', $statement);
252+
}
253+
231254
public function testCreate()
232255
{
233256
$m = $this->getQueryConvertableMock(PathType::class, "(m:Movie)->(b)");
@@ -379,6 +402,22 @@ public function testWith()
379402
$this->assertSame("WITH a < b AS foobar", $statement);
380403
}
381404

405+
public function testWithWithNode()
406+
{
407+
$node = Query::node('m');
408+
409+
$statement = (new Query())->with($node)->build();
410+
411+
$this->assertMatchesRegularExpression("/(WITH [0-9a-f]+)/", $statement);
412+
413+
$node = Query::node("m");
414+
$node->named('example');
415+
416+
$statement = (new Query())->with($node)->build();
417+
418+
$this->assertSame('WITH example', $statement);
419+
}
420+
382421
public function testCallProcedure()
383422
{
384423
$procedure = "apoc.json";
@@ -421,8 +460,10 @@ public function testBuild()
421460

422461
$this->assertSame("WITH foobar WHERE foobar", $statement);
423462

424-
$pathMock = $this->getQueryConvertableMock(Path::class, "(a)->(b)");
425463
$nodeMock = $this->getQueryConvertableMock(Node::class, "(a)");
464+
$nodeMock->method('getName')->willReturn($this->getQueryConvertableMock(Variable::class, 'a'));
465+
466+
$pathMock = $this->getQueryConvertableMock(Path::class, "(a)->(b)");
426467
$numeralMock = $this->getQueryConvertableMock(NumeralType::class, "12");
427468
$booleanMock = $this->getQueryConvertableMock(BooleanType::class, "a > b");
428469
$propertyMock = $this->getQueryConvertableMock(Property::class, "a.b");
@@ -444,7 +485,7 @@ public function testBuild()
444485
->with(["#" => $nodeMock])
445486
->build();
446487

447-
$this->assertSame("MATCH (a)->(b), (a) RETURN (a) AS `#` CREATE (a)->(b), (a) CREATE (a)->(b) DELETE (a), (a) DETACH DELETE (a), (a) LIMIT 12 MERGE (a) OPTIONAL MATCH (a), (a) ORDER BY a.b, a.b DESCENDING REMOVE a.b WHERE a > b WITH (a) AS `#`", $statement);
488+
$this->assertSame("MATCH (a)->(b), (a) RETURN a AS `#` CREATE (a)->(b), (a) CREATE (a)->(b) DELETE (a), (a) DETACH DELETE (a), (a) LIMIT 12 MERGE (a) OPTIONAL MATCH (a), (a) ORDER BY a.b, a.b DESCENDING REMOVE a.b WHERE a > b WITH a AS `#`", $statement);
448489
}
449490

450491
public function testBuildEmpty()
@@ -841,6 +882,26 @@ public function testWikiExamples()
841882
$this->assertSame("((nineties.released >= 1990) AND (nineties IS NOT NULL))", $expression->toQuery());
842883
}
843884

885+
public function testAutomaticIdentifierGeneration()
886+
{
887+
$node = Query::node();
888+
889+
$this->assertMatchesRegularExpression('/[0-9a-f]+\.foo/', $node->property('foo')->toQuery());
890+
891+
$node->named('foo');
892+
893+
$this->assertSame('foo.bar', $node->property('bar')->toQuery());
894+
895+
$node = Query::node();
896+
$statement = Query::new()->match($node)->returning($node)->build();
897+
898+
$this->assertMatchesRegularExpression('/MATCH \([0-9a-f]+\) RETURN [0-9a-f]+/', $statement);
899+
900+
$node = Query::node();
901+
902+
$this->assertInstanceOf(Variable::class, $node->getName());
903+
}
904+
844905
public function provideLiteralData(): array
845906
{
846907
return [

tests/Unit/VariableTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ public function testToQuery(string $variable, string $expected)
4242
$this->assertSame($expected, $variable->toQuery());
4343
}
4444

45+
public function testEmptyConstructor()
46+
{
47+
$variable = new Variable();
48+
49+
$this->assertMatchesRegularExpression('/[0-9a-f]+/', $variable->toQuery());
50+
51+
$variable = new Variable(null);
52+
53+
$this->assertMatchesRegularExpression('/[0-9a-f]+/', $variable->toQuery());
54+
}
55+
4556
/**
4657
* @dataProvider providePropertyData
4758
* @param string $variable

0 commit comments

Comments
 (0)