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:
parent
b644e8cc7f
commit
40c704faf6
97
lib/Internal/ArrayParser.php
Normal file
97
lib/Internal/ArrayParser.php
Normal 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
10
lib/ParseException.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -18,11 +18,24 @@ class PgSqlResultSet implements ResultSet {
|
|||||||
/** @var int Next row fetch type. */
|
/** @var int Next row fetch type. */
|
||||||
private $type = self::FETCH_ASSOC;
|
private $type = self::FETCH_ASSOC;
|
||||||
|
|
||||||
|
/** @var int[] */
|
||||||
|
private $fieldTypes = [];
|
||||||
|
|
||||||
|
/** @var \Amp\Postgres\Internal\ArrayParser */
|
||||||
|
private $parser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param resource $handle PostgreSQL result resource.
|
* @param resource $handle PostgreSQL result resource.
|
||||||
*/
|
*/
|
||||||
public function __construct($handle) {
|
public function __construct($handle) {
|
||||||
$this->handle = $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");
|
throw new \Error("No more rows remain in the result set");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ($this->type) {
|
$result = \pg_fetch_array($this->handle, null, \PGSQL_ASSOC);
|
||||||
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) {
|
if ($result === false) {
|
||||||
$message = \pg_result_error($this->handle);
|
$message = \pg_result_error($this->handle);
|
||||||
@ -78,7 +79,134 @@ class PgSqlResultSet implements ResultSet {
|
|||||||
throw new FailureException($message);
|
throw new FailureException($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->currentRow = $result;
|
// 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,6 +24,7 @@ class PqBufferedResultSet implements ResultSet {
|
|||||||
*/
|
*/
|
||||||
public function __construct(pq\Result $result) {
|
public function __construct(pq\Result $result) {
|
||||||
$this->result = $result;
|
$this->result = $result;
|
||||||
|
$this->result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +34,7 @@ class PqUnbufferedResultSet implements ResultSet, Operation {
|
|||||||
$this->producer = new Producer(static function (callable $emit) use ($queue, $result, $fetch) {
|
$this->producer = new Producer(static function (callable $emit) use ($queue, $result, $fetch) {
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
|
$result->autoConvert = pq\Result::CONV_SCALAR | pq\Result::CONV_ARRAY;
|
||||||
$next = $fetch(); // Request next result before current is consumed.
|
$next = $fetch(); // Request next result before current is consumed.
|
||||||
yield $emit($result);
|
yield $emit($result);
|
||||||
$result = yield $next;
|
$result = yield $next;
|
||||||
@ -68,13 +69,16 @@ class PqUnbufferedResultSet implements ResultSet, Operation {
|
|||||||
return $this->currentRow;
|
return $this->currentRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @var \pq\Result $result */
|
||||||
|
$result = $this->producer->getCurrent();
|
||||||
|
|
||||||
switch ($this->type) {
|
switch ($this->type) {
|
||||||
case self::FETCH_ASSOC:
|
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:
|
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:
|
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:
|
default:
|
||||||
throw new \Error("Invalid result fetch type");
|
throw new \Error("Invalid result fetch type");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user