Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PHP Driver and Client #384

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions drivers/php/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.
*/

{
"name": "apache/age-php",
"description": "Apache AGE PHP Driver",
"type": "library",
"license": "Apache-2.0",
"autoload": {
"psr-4": {
"Apache\\AgePhp\\": "src/"
}
},
"authors": [
{
"name": "Matt Hall",
"email": "[email protected]"
}
],
"minimum-stability": "stable",
"require": {
"php": ">=8.1"
},
"require-dev": {
"phpunit/phpunit": "9.5",
"antlr/antlr4-php-runtime": "0.8"
}
}
26 changes: 26 additions & 0 deletions drivers/php/src/AgeClient/AGTypeParse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Apache\AgePhp\AgeClient;

use Apache\AgePhp\Parser\AgtypeLexer;
use Apache\AgePhp\Parser\AgtypeParser;
use Antlr\Antlr4\Runtime\CommonTokenStream;
use Antlr\Antlr4\Runtime\InputStream;
use Antlr\Antlr4\Runtime\Tree\ParseTreeWalker;
use Apache\AgePhp\Parser\CustomAgTypeListener;

class AGTypeParse
{
public static function parse(string $string): mixed
{
$charStream = InputStream::fromString($string);
$lexer = new AgtypeLexer($charStream);
$tokens = new CommonTokenStream($lexer);
$parser = new AgtypeParser($tokens);
$tree = $parser->agType();
$printer = new CustomAgTypeListener();
ParseTreeWalker::default()->walk($printer, $tree);

return $printer->getResult();
}
}
151 changes: 151 additions & 0 deletions drivers/php/src/AgeClient/AgeClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace Apache\AgePhp\AgeClient;

use Apache\AgePhp\AgeClient\Exceptions\AgeClientConnectionException;
use Apache\AgePhp\AgeClient\Exceptions\AgeClientQueryException;
use PgSql\Connection;
use PgSql\Result;

class AgeClient
{
private Connection $connection;
private ?Result $queryResult = null;
private int $storedProcedureCount = 0;

public function __construct(string $host, string $port, string $dbName, string $user, string $password)
{
$this->connection = pg_connect("host=$host port=$port dbname=$dbName user=$user password=$password");
if (!$this->connection) throw new AgeClientConnectionException('Unable to connect to Postgres Database');

pg_query($this->connection, "CREATE EXTENSION IF NOT EXISTS age;");
pg_query($this->connection, "LOAD 'age';");
pg_query($this->connection, "SET search_path = ag_catalog, '\$user', public;");
pg_query($this->connection, "GRANT USAGE ON SCHEMA ag_catalog TO $user;");
}

public function close(): void
{
pg_close($this->connection);
}

/**
* Flush outbound query data on the connection and any preparedId or queryResult
*/
public function flush(): void
{
$this->prepareId = null;
$this->queryResult = null;
pg_flush($this->connection);
}

/**
* Get the current open Postgresql connection to submit your own commands
* @return Connection
*/
public function getConnection(): Connection
{
return $this->connection;
}

public function createGraph(string $graphName): void
{
pg_query($this->connection, "SELECT create_graph('$graphName');");
}

public function dropGraph(string $graphName): void
{
pg_query($this->connection, "SELECT drop_graph('$graphName', true);");
}

/**
* Execute Postgresql and Cypher queries, refer to Apache AGE documentation for query examples
* @param string $query - Postgresql-style query
* @return AgeClient
*/
public function query(string $query): AgeClient
{
$this->flush();

$queryResult = pg_query($this->connection, $query);
if ($queryResult) $this->queryResult = $queryResult;

return $this;
}

/**
* Execute prepared cypher-specific command without to specify Postgresql SELECT or agtypes
* @param string $graphName
* @param string $columnCount - number of agtypes returned, will throw an error if incorrect
* @param string $cypherQuery - Parameterized Cypher-style query
* @param array $params - Array of parameters to substitute for the placeholders in the original prepared cypherQuery string
* @return AgeClient
*/
public function cypherQuery(string $graphName, int $columnCount, string $cypherQuery, array $params = []): AgeClient
{
$this->flush();

$jsonParams = json_encode($params);

$storedProcedureName = "cypher_stored_procedure_" . $this->storedProcedureCount++;
$statement = "
PREPARE $storedProcedureName(agtype) AS
SELECT * FROM cypher('$graphName', \$\$ $cypherQuery \$\$, \$1) as (v0 agtype";

for ($i = 1; $i < $columnCount; $i++) {
$statement .= sprintf(", v%d agtype", $i);
}

$statement .= ");
EXECUTE $storedProcedureName('$jsonParams')
";

$queryResult = pg_query($this->connection, $statement);
if ($queryResult) $this->queryResult = $queryResult;

return $this;
}

/**
* Get a row as an enumerated array with parsed AgTypes
* @param int? $row
* @return array|null
* @throws AgeClientQueryException
*/
public function fetchRow(?int $row = null): array|null
{
if (!$this->queryResult) throw new AgeClientQueryException('No result from prior query to fetch from');
$fetchResults = pg_fetch_row($this->queryResult, $row);

$parsedResults = [];
foreach ($fetchResults as $key => $column) {
$parsedResults[$key] = $column ? AGTypeParse::parse($column) : null;
}

return $parsedResults;
}

/**
* Fetches all rows as an array with parsed AgTypes
* @return array|null
* @throws AgeClientQueryException
*/
public function fetchAll(): array|null
{
if (!$this->queryResult) throw new AgeClientQueryException('No result from prior query to fetch from');
$fetchResults = pg_fetch_all($this->queryResult);

if (!$fetchResults) return null;

$parsedResults = [];
foreach ($fetchResults as $row) {
$rowData = [];
foreach ($row as $key => $column) {
$rowData[$key] = $column ? AGTypeParse::parse($column) : null;
}
$parsedResults[] = $rowData;
}

return $parsedResults;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Apache\AgePhp\AgeClient\Exceptions;

use Exception;

class AgeClientConnectionException extends Exception
{

}
10 changes: 10 additions & 0 deletions drivers/php/src/AgeClient/Exceptions/AgeClientQueryException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Apache\AgePhp\AgeClient\Exceptions;

use Exception;

class AgeClientQueryException extends Exception
{

}
116 changes: 116 additions & 0 deletions drivers/php/src/Parser/Agtype.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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.
*/

grammar Agtype;

agType
: agValue EOF
;

agValue
: value typeAnnotation?
;

value
: STRING #StringValue
| INTEGER #IntegerValue
| floatLiteral #FloatValue
| 'true' #TrueBoolean
| 'false' #FalseBoolean
| 'null' #NullValue
| obj #ObjectValue
| array #ArrayValue
;

obj
: '{' pair (',' pair)* '}'
| '{' '}'
;

pair
: STRING ':' agValue
;

array
: '[' agValue (',' agValue)* ']'
| '[' ']'
;

typeAnnotation
: '::' IDENT
;

IDENT
: [A-Z_a-z][$0-9A-Z_a-z]*
;

STRING
: '"' (ESC | SAFECODEPOINT)* '"'
;

fragment ESC
: '\\' (["\\/bfnrt] | UNICODE)
;

fragment UNICODE
: 'u' HEX HEX HEX HEX
;

fragment HEX
: [0-9a-fA-F]
;

fragment SAFECODEPOINT
: ~ ["\\\u0000-\u001F]
;

INTEGER
: '-'? INT
;

fragment INT
: '0' | [1-9] [0-9]*
;

floatLiteral
: RegularFloat
| ExponentFloat
| '-'? 'Infinity'
| 'NaN'
;

RegularFloat
: '-'? INT DECIMAL
;

ExponentFloat
: '-'? INT DECIMAL? SCIENTIFIC
;

fragment DECIMAL
: '.' [0-9]+
;

fragment SCIENTIFIC
: [Ee][+-]? [0-9]+
;

WS
: [ \t\n\r] + -> skip
;
Loading