1
0
mirror of https://github.com/danog/postgres.git synced 2024-11-27 04:24:45 +01:00

Cast scalars and parse arrays

This commit is contained in:
Aaron Piotrowski 2017-12-17 15:51:40 -06:00
parent b644e8cc7f
commit 40c704faf6
No known key found for this signature in database
GPG Key ID: ADD1EF783EDE9EEB
5 changed files with 257 additions and 17 deletions

View File

@ -0,0 +1,97 @@
<?php
namespace Amp\Postgres\Internal;
use Amp\Postgres\ParseException;
class ArrayParser {
/**
* @param string $data
* @param callable|null $cast
* @param string $delimiter
*
* @return array Parsed column data.
*
* @throws \Amp\Postgres\ParseException
*/
public function parse(string $data, callable $cast = null, string $delimiter = ','): array {
$parser = $this->parser($data, $cast, $delimiter);
$data = \iterator_to_array($parser);
if ($parser->getReturn() !== '') {
throw new ParseException("Data left in buffer after parsing");
}
return $data;
}
private function parser(string $data, callable $cast = null, string $delimiter = ','): \Generator {
$data = \trim($data);
if ($data[0] !== '{' || \substr($data, -1) !== '}') {
throw new ParseException("Missing opening or closing brackets");
}
$data = \ltrim(\substr($data, 1));
do {
if ($data === '') {
throw new ParseException("Missing closing bracket");
}
if ($data[0] === '{') { // Array
$parser = $this->parser($data, $cast, $delimiter);
yield \iterator_to_array($parser);
list($data, $end) = $this->trim($parser->getReturn(), 0);
continue;
}
if ($data[0] === '"') { // Quoted value
$position = 1;
do {
$position = \strpos($data, '"', $position);
if ($position === false) {
throw new ParseException("Could not find matching quote in quoted value");
}
} while ($data[$position - 1] === '\\' && ++$position); // Check for escaped "
$yield = \str_replace('\\"', '"', \substr($data, 1, $position - 1));
list($data, $end) = $this->trim($data, $position + 1);
} else { // Unquoted value
$position = 0;
while (isset($data[$position]) && $data[$position] !== $delimiter && $data[$position] !== '}') {
++$position;
}
$yield = \trim(\substr($data, 0, $position));
list($data, $end) = $this->trim($data, $position);
}
if (\strcasecmp($yield, "NULL") === 0) {
yield null;
} elseif ($cast) {
yield $cast($yield);
} else {
yield $yield;
}
} while ($end !== '}');
return $data;
}
private function trim(string $data, int $position): array {
do {
if (!isset($data[$position])) {
throw new ParseException("Unexpected end of data");
}
$end = $data[$position];
} while (\ltrim($end) === '' && isset($data[++$position]));
$data = \ltrim(\substr($data, $position + 1));
return [$data, $end];
}
}

10
lib/ParseException.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Amp\Postgres;
class ParseException extends FailureException {
public function __construct(string $message = '') {
$message = "Parse error while splitting array" . (($message === '') ? '' : ": " . $message);
parent::__construct($message);
}
}

View File

@ -18,11 +18,24 @@ class PgSqlResultSet implements ResultSet {
/** @var int Next row fetch type. */
private $type = self::FETCH_ASSOC;
/** @var int[] */
private $fieldTypes = [];
/** @var \Amp\Postgres\Internal\ArrayParser */
private $parser;
/**
* @param resource $handle PostgreSQL result resource.
*/
public function __construct($handle) {
$this->handle = $handle;
$numFields = \pg_num_fields($this->handle);
for ($i = 0; $i < $numFields; ++$i) {
$this->fieldTypes[] = \pg_field_type_oid($this->handle, $i);
}
$this->parser = new Internal\ArrayParser;
}
/**
@ -58,19 +71,7 @@ class PgSqlResultSet implements ResultSet {
throw new \Error("No more rows remain in the result set");
}
switch ($this->type) {
case self::FETCH_ASSOC:
$result = \pg_fetch_array($this->handle, null, \PGSQL_ASSOC);
break;
case self::FETCH_ARRAY:
$result = \pg_fetch_array($this->handle, null, \PGSQL_NUM);
break;
case self::FETCH_OBJECT:
$result = \pg_fetch_object($this->handle);
break;
default:
throw new \Error("Invalid result fetch type");
}
if ($result === false) {
$message = \pg_result_error($this->handle);
@ -78,7 +79,134 @@ class PgSqlResultSet implements ResultSet {
throw new FailureException($message);
}
// See https://github.com/postgres/postgres/blob/REL_10_STABLE/src/include/catalog/pg_type.h for OID types.
$column = 0;
foreach ($result as $key => $value) {
if ($value === null) {
++$column;
continue;
}
switch ($this->fieldTypes[$column]) {
case 16: // bool
$result[$key] = $value === 't';
break;
case 20: // int8
case 21: // int2
case 23: // int4
case 26: // oid
case 27: // tid
case 28: // xid
$result[$key] = (int) $result[$key];
break;
case 700: // real
case 701: // double-precision
$result[$key] = (float) $result[$key];
break;
case 1000: // boolean[]
$result[$key] = $this->parser->parse($result[$key], function (string $value): bool {
return $value === 't';
});
break;
case 1005: // int2[]
case 1007: // int4[]
case 1010: // tid[]
case 1011: // xid[]
case 1016: // int8[]
case 1028: // oid[]
$result[$key] = $this->parser->parse($result[$key], function (string $value): int {
return (int) $value;
});
break;
case 1021: // real[]
case 1022: // double-precision[]
$result[$key] = $this->parser->parse($result[$key], function (string $value): float {
return (float) $value;
});
break;
case 1020: // box[] (semi-colon delimited)
$result[$key] = $this->parser->parse($result[$key], null, ';');
break;
case 199: // json[]
case 629: // line[]
case 651: // cidr[]
case 719: // circle[]
case 775: // macaddr8[]
case 791: // money[]
case 1001: // bytea[]
case 1002: // char[]
case 1003: // name[]
case 1006: // int2vector[]
case 1008: // regproc[]
case 1009: // text[]
case 1013: // oidvector[]
case 1014: // bpchar[]
case 1015: // varchar[]
case 1019: // path[]
case 1023: // abstime[]
case 1024: // realtime[]
case 1025: // tinterval[]
case 1027: // polygon[]
case 1034: // aclitem[]
case 1040: // macaddr[]
case 1041: // inet[]
case 1115: // timestamp[]
case 1182: // date[]
case 1183: // time[]
case 1185: // timestampz[]
case 1187: // interval[]
case 1231: // numeric[]
case 1263: // cstring[]
case 1270: // timetz[]
case 1561: // bit
case 1563: // varbit[]
case 2201: // refcursor[]
case 2207: // regprocedure[]
case 2208: // regoper[]
case 2209: // regoperator[]
case 2210: // regclass[]
case 2211: // regtype[]
case 2949: // txid_snapshot[]
case 2951: // uuid[]
case 3221: // pg_lsn[]
case 3643: // tsvector[]
case 3644: // gtsvector[]
case 3645: // tsquery[]
case 3735: // regconfig[]
case 3770: // regdictionary[]
case 3807: // jsonb[]
case 3905: // int4range[]
case 3907: // numrange[]
case 3909: // tsrange[]
case 3911: // tstzrange[]
case 3913: // daterange[]
case 3927: // int8range[]
case 4097: // regrole[]
case 4090: // regnamespace[]
$result[$key] = $this->parser->parse($result[$key]);
break;
}
++$column;
}
switch ($this->type) {
case self::FETCH_ASSOC:
return $this->currentRow = $result;
case self::FETCH_ARRAY:
return $this->currentRow = \array_values($result);
case self::FETCH_OBJECT:
return $this->currentRow = (object) $result;
default:
throw new \Error("Invalid result fetch type");
}
}
/**

View File

@ -24,6 +24,7 @@ class PqBufferedResultSet implements ResultSet {
*/
public function __construct(pq\Result $result) {
$this->result = $result;
$this->result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY;
}
/**

View File

@ -34,6 +34,7 @@ class PqUnbufferedResultSet implements ResultSet, Operation {
$this->producer = new Producer(static function (callable $emit) use ($queue, $result, $fetch) {
try {
do {
$result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY;
$next = $fetch(); // Request next result before current is consumed.
yield $emit($result);
$result = yield $next;
@ -68,13 +69,16 @@ class PqUnbufferedResultSet implements ResultSet, Operation {
return $this->currentRow;
}
/** @var \pq\Result $result */
$result = $this->producer->getCurrent();
switch ($this->type) {
case self::FETCH_ASSOC:
return $this->currentRow = $this->producer->getCurrent()->fetchRow(pq\Result::FETCH_ASSOC);
return $this->currentRow = $result->fetchRow(pq\Result::FETCH_ASSOC);
case self::FETCH_ARRAY:
return $this->currentRow = $this->producer->getCurrent()->fetchRow(pq\Result::FETCH_ARRAY);
return $this->currentRow = $result->fetchRow(pq\Result::FETCH_ARRAY);
case self::FETCH_OBJECT:
return $this->currentRow = $this->producer->getCurrent()->fetchRow(pq\Result::FETCH_OBJECT);
return $this->currentRow = $result->fetchRow(pq\Result::FETCH_OBJECT);
default:
throw new \Error("Invalid result fetch type");
}