Skip to content

Commit 5436edb

Browse files
author
Tamas Bolner
committed
Merge branch 'dev'
2 parents 7bf7cf9 + 4dd2fc0 commit 5436edb

File tree

11 files changed

+255
-72
lines changed

11 files changed

+255
-72
lines changed
File renamed without changes.

.vscode/c_cpp_properties.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"compilerPath": "/usr/bin/g++",
1010
"cStandard": "gnu11",
1111
"cppStandard": "gnu++11",
12-
"intelliSenseMode": "clang-x64",
12+
"intelliSenseMode": "gcc-x64",
1313
"compilerArgs": [
1414
"-lssl"
1515
]

README.md

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ foreach($result as $record) {
123123
}
124124
```
125125

126+
The returned values are always in string representation except the null, which
127+
is always returned as `null`.
128+
126129
## Example 2: Get execution stats
127130

128131
```php
@@ -163,26 +166,20 @@ $result = $connection->Query('
163166
', [ "D'artagnan", 5.3 ]);
164167
```
165168

166-
The parameter types for prepared statements have to match
167-
the column types. See the below example. Never pass `null`,
168-
`true`, `false` values as strings.
169+
In MonetDB the placeholders of prepared statements have specific types.
170+
This library auto-converts some of PHP types to the corresponding MonetDB types.
169171

170-
```php
171-
$result = $connection->Query('
172-
update
173-
"test"
174-
set
175-
"nullable_column" = ?
176-
where
177-
"bool_column" = ?
178-
and "numeric_column" > ?
179-
and "date_column" > ?
180-
and "timestamp_column" < ?
181-
', [ null, false, 5.3, "2020-12-08", new DateTime()]);
182-
```
172+
| MonetDB type | Accepted PHP types | Value examples |
173+
| --- | --- | --- |
174+
| timestamp | `string`, `DateTime` | `"2020-12-20 11:14:26.123456"` |
175+
| date | `string`, `DateTime` | `"2020-12-20"` |
176+
| boolean | `boolean`, `string`, `integer` | `true`, `false`, `"true"`, `0`, `"0"`, `1`, `"t"`, `"f"`, `"yes"`, `"no"`, `"enabled"`, `"disabled"` |
177+
| Numeric values | `integer`, `float`, `string` | `12.34`, `"12.34"` (use string for huge numbers) |
178+
| Character types | `string` | `"Hello World!"` |
179+
| Binary | `string` | `"0f44ba12"` (always interpreted as hexadecimal) |
180+
| time | `string`, `DateTime` | `"11:28"`, `"12:28:34"` |
183181

184-
While the `date` values have to be passed as normal strings, the
185-
`timestamp` type has be passed as a `DateTime` object.
182+
Always pass the null values as `null`, and not as a string.
186183

187184
## Example 4: Using escaping
188185

@@ -351,7 +348,7 @@ $result3 = $connection3->Query("...");
351348
| --- | --- |
352349
| <strong>__construct</strong> | Create a new connection to a MonetDB database. <br><br><strong>@param</strong> <em>string</em> <strong>$host</strong> : The host of the database. Use '127.0.0.1' if the DB is on the same machine.<br><strong>@param</strong> <em>int</em> <strong>$port</strong> : The port of the database. For MonetDB this is usually 50000.<br><strong>@param</strong> <em>string</em> <strong>$user</strong> : The user name.<br><strong>@param</strong> <em>string</em> <strong>$password</strong> : The password of the user.<br><strong>@param</strong> <em>string</em> <strong>$database</strong> : The name of the database to connect to. Don't forget to release and start it.<br><strong>@param</strong> <em>string</em> <strong>$saltedHashAlgo</strong> <em>= "SHA1"</em> : Optional. The preferred hash algorithm to be used for exchanging the password. It has to be supported by both the server and PHP. This is only used for the salted hashing. Another stronger algorithm is used first (usually SHA512).<br><strong>@param</strong> <em>bool</em> <strong>$syncTimeZone</strong> <em>= true</em> : If true, then tells the clients time zone offset to the server, which will convert all timestamps is case there's a difference. If false, then the timestamps will end up on the server unmodified.<br><strong>@param</strong> <em>int</em> <strong>$maxReplySize</strong> <em>= 200</em> : The maximal number of tuples returned in a response. A higher value results in smaller number of memory allocations and string operations, but also in higher memory footprint. |
353350
| <strong>Close</strong> | Close the connection |
354-
| <strong>Query</strong> | Execute an SQL query and return its response. For 'select' queries the response can be iterated using a 'foreach' statement. You can pass an array as second parameter to execute the query as prepared statement, where the array contains the parameter values. SECURITY WARNING: For prepared statements in MonetDB, the parameter values are passed in a regular 'EXECUTE' command, using escaping. Therefore the same security considerations apply here as for using the Connection->Escape(...) method. Please read the comments for that method. <br><br><strong>@param</strong> <em>string</em> <strong>$sql</strong><br><strong>@param</strong> <em>array</em> <strong>$params</strong> <em>= null</em> : An optional array for prepared statement parameters. If not provided (or null), then a normal query is executed instead of a prepared statement. The parameter values will retain their PHP type if possible. The following values won't be converted to string: null, true, false and numeric values.<br><strong>@return</strong> <em>Response</em> |
351+
| <strong>Query</strong> | Execute an SQL query and return its response. For 'select' queries the response can be iterated using a 'foreach' statement. You can pass an array as second parameter to execute the query as prepared statement, where the array contains the parameter values. SECURITY WARNING: For prepared statements in MonetDB, the parameter values are passed in a regular 'EXECUTE' command, using escaping. Therefore the same security considerations apply here as for using the Connection->Escape(...) method. Please read the comments for that method. <br><br><strong>@param</strong> <em>string</em> <strong>$sql</strong><br><strong>@param</strong> <em>array</em> <strong>$params</strong> <em>= null</em> : An optional array for prepared statement parameters. If not provided (or null), then a normal query is executed instead of a prepared statement. The parameter values will be converted to the proper MonetDB type when possible. See the relevant section of README.md about parameterized queries for more details.<br><strong>@return</strong> <em>Response</em> |
355352
| <strong>QueryFirst</strong> | Execute an SQL query and return only the first row as an associative array. If there is more data on the stream, then discard all. Returns null if the query has empty result. You can pass an array as second parameter to execute the query as prepared statement, where the array contains the parameter values. <br><br><strong>@param</strong> <em>string</em> <strong>$sql</strong><br><strong>@param</strong> <em>array</em> <strong>$params</strong> <em>= null</em> : An optional array for prepared statement parameters. If not provided (or null), then a normal query is executed instead of a prepared statement. See the 'Query' method for more information about the parameter values.<br><strong>@return</strong> <em>string[] -or- null</em> |
356353
| <strong>Command</strong> | Send a 'command' to MonetDB. Commands are used for configuring the database, for example setting the maximal response size, or for requesting unread parts of a query response ('export').<br><br><strong>@param</strong> <em>string</em> <strong>$command</strong><br><strong>@param</strong> <em>bool</em> <strong>$noResponse</strong> <em>= true</em> : If true, then returns NULL and makes no read to the underlying socket.<br><strong>@return</strong> <em>Response -or- null</em> |
357354
| <strong>Escape</strong> | Escape a string value, to be inserted into a query, inside single quotes. The following characters are escaped by this method: backslash, single quote, carriage return, line feed, tabulator, null character, CTRL+Z. As a security measure this library forces the use of multi-byte support and UTF-8 encoding, which is also used by MonetDB, avoiding the SQL-injection attacks, which play with differences between character encodings. <br><br><strong>@param</strong> <em>string</em> <strong>$value</strong><br><strong>@return</strong> <em>string</em> |

protocol_doc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ Discussed in chapter [Data response](#521-data-response---1).
509509
Error responses start with an exclamation mark `!`, followed by an error code, then a text
510510
message after a second exclamation mark. When the server returns an error message,
511511
then it clears the complete session state (forgets everything, including prepared
512-
statements and active queries).
512+
statements and active queries) and closes the connection.
513513

514514
Examples:
515515

src/Connection.php

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,9 @@ private function Write(string $msg)
325325
* @param string $sql
326326
* @param array|null $params An optional array for prepared statement parameters.
327327
* If not provided (or null), then a normal query is executed instead of
328-
* a prepared statement. The parameter values will retain their PHP type if
329-
* possible. The following values won't be converted to string: null, true, false
330-
* and numeric values.
328+
* a prepared statement. The parameter values will be converted to the proper
329+
* MonetDB type when possible. See the relevant section of README.md about
330+
* parameterized queries for more details.
331331
* @return Response
332332
*/
333333
public function Query(string $sql, array $params = null): Response
@@ -456,20 +456,42 @@ function($match) {
456456
* @param array $params
457457
*/
458458
private function WritePreparedStatement(string $sql, array $params) {
459-
$queryHash = hash('sha256', $sql);
459+
$params = array_values($params);
460460

461-
if (!isset($this->preparedStatements[$queryHash])) {
461+
if (!isset($this->preparedStatements[$sql])) {
462462
$this->Write("sPREPARE ".rtrim($sql, "\r\n\t ;")."\n;");
463463
$response = $this->inputStream->ReceiveResponse();
464464
$stats = $response->GetStatusRecords()[0];
465-
$this->preparedStatements[$queryHash] = $stats->GetPreparedStatementID();
465+
$paramTypes = [];
466+
467+
foreach($response as $row) {
468+
if ($row['column'] === null) {
469+
$paramTypes[] = $row['type'];
470+
}
471+
}
472+
473+
if (count($paramTypes) < 1) {
474+
throw new MonetException("The SQL statement has no placeholders (like '?') for parameters.");
475+
}
476+
477+
$this->preparedStatements[$sql] = [
478+
$stats->GetPreparedStatementID(),
479+
$paramTypes
480+
];
466481

467482
$response->Discard();
468483
}
469484

470-
$id = $this->preparedStatements[$queryHash];
485+
list($id, $paramTypes) = $this->preparedStatements[$sql];
486+
487+
if (count($params) != count($paramTypes)) {
488+
throw new MonetException("The number of placeholders in the SQL statement is not the same as the number of passed parameters.");
489+
}
490+
471491
$escaped = [];
472-
foreach($params as $param) {
492+
foreach($params as $index => $param) {
493+
$type = $paramTypes[$index];
494+
473495
if ($param === null) {
474496
$escaped[] = "NULL";
475497
}
@@ -479,18 +501,75 @@ private function WritePreparedStatement(string $sql, array $params) {
479501
else if ($param === false) {
480502
$escaped[] = "false";
481503
}
482-
elseif (is_numeric($param) && !is_string($param)) {
483-
$escaped[] = $param + 0;
504+
elseif (is_string($param)) {
505+
if (in_array($type, ['char', 'varchar', 'clob'])) {
506+
$escaped[] = "'".$this->Escape($param)."'";
507+
}
508+
else if ($type == "hugeint" || $type == "decimal") {
509+
$escaped[] = preg_replace('/[^0-9\.\+\-ex]/i', '', $param);
510+
}
511+
else if ($type == "timestamp") {
512+
$escaped[] = "TIMESTAMP '".$this->Escape($param)."'";
513+
}
514+
else if ($type == "int" || $type == "bigint") {
515+
$escaped[] = (string)((int)$param);
516+
}
517+
else if ($type == "double" || $type == "real") {
518+
$escaped[] = (string)((float)$param);
519+
}
520+
else if ($type == "blob") {
521+
$escaped[] = "x'".preg_replace('/[^0-9a-f]/i', '', $param)."'";
522+
}
523+
else if ($type == "boolean") {
524+
$lower = strtolower($param);
525+
if (in_array($lower, ['1', 'true', 'yes', 't', 'enabled'])) {
526+
$escaped[] = "true";
527+
} else if (in_array($lower, ['0', 'false', 'no', 'f', 'disabled'])) {
528+
$escaped[] = "false";
529+
} else {
530+
throw new MonetException("Invalid value passed for parameter '".($index + 1).
531+
"': Expected boolean, received: {$param}");
532+
}
533+
}
534+
else if ($type == "time") {
535+
$escaped[] = "time '".preg_replace('/[^0-9\:]/i', '', $param)."'";
536+
}
537+
else {
538+
$escaped[] = "'".$this->Escape($param)."'";
539+
}
540+
}
541+
elseif (is_float($param) || is_integer($param)) {
542+
if ($type == "boolean") {
543+
if ($param == 0) {
544+
$escaped[] = "false";
545+
} else {
546+
$escaped[] = "true";
547+
}
548+
} else {
549+
$escaped[] = (string)$param;
550+
}
484551
}
485552
elseif ($param instanceof DateTime) {
486-
$escaped[] = "TIMESTAMP '".$param->format("Y-m-d H:i:s.u")."'";
553+
if ($type == "date") {
554+
$escaped[] = "'".$param->format("Y-m-d")."'";
555+
} else if ($type == "timestamp") {
556+
$escaped[] = "TIMESTAMP '".$param->format("Y-m-d H:i:s.u")."'";
557+
} else if ($type == "time") {
558+
$escaped[] = "time '".$param->format("H:i:s")."'";
559+
}
487560
}
488561
else {
489-
$escaped[] = "'".$this->Escape((string)$param)."'";
562+
$gotType = gettype($param);
563+
if ($gotType == "object") {
564+
$gotType = get_class($param);
565+
}
566+
567+
throw new MonetException("Parameter ".($index + 1)." has invalid PHP type: '{$gotType}'. "
568+
."(Expected SQL type: '{$type}'.)");
490569
}
491570
}
492571

493-
$this->Write("sEXECUTE {$id}(".implode(",", $escaped).");");
572+
$this->Write("sEXECUTE {$id}(".implode(", ", $escaped).");");
494573
}
495574

496575
/**

src/Response.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717

1818
namespace MonetDB;
1919

20-
use Exception;
21-
2220
/**
2321
* This class represents a response for an SQL query
2422
* or for a command.
@@ -224,7 +222,7 @@ private function Parse() {
224222
return;
225223
}
226224
else if ($first == InputStream::MSG_QUERY) {
227-
if ($second == InputStream::Q_TABLE) {
225+
if ($second == InputStream::Q_TABLE || $second == InputStream::Q_PREPARE) {
228226
$status = new StatusRecord($second, $this->currentLine);
229227
$this->statusRecords[] = $status;
230228
$this->ignoreTuples = false;
@@ -298,14 +296,7 @@ private function Parse() {
298296

299297
continue;
300298
}
301-
else if ($second == InputStream::Q_PREPARE) {
302-
/*
303-
It returns some meaningless dataset when created. Skip that.
304-
*/
305-
$this->statusRecords[] = new StatusRecord($second, $this->currentLine);
306-
$this->Discard();
307-
return;
308-
} else {
299+
else {
309300
$this->statusRecords[] = new StatusRecord($second, $this->currentLine);
310301
continue;
311302
}

tests/dec38Test.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@ final class dec38Test extends TestCase {
88
/**
99
* @var Connection
1010
*/
11-
public $conn;
11+
public static $conn;
1212

13-
public function setUp(): void
13+
public static function setUpBeforeClass(): void
1414
{
15-
$this->conn = new Connection("127.0.0.1", 50000, "monetdb", "monetdb", "myDatabase");
15+
self::$conn = new Connection("127.0.0.1", 50000, "monetdb", "monetdb", "myDatabase");
1616
}
1717

1818
public function testBigIntTable(): void
1919
{
20-
$this->conn->Query("drop table if exists php_dec38");
21-
$res = $this->conn->Query("CREATE TABLE php_dec38 (d38_0 DECIMAL(38,0), d38_19 DECIMAL(38,19), d38_38 DECIMAL(38,38));");
20+
self::$conn->Query("drop table if exists php_dec38");
21+
$res = self::$conn->Query("CREATE TABLE php_dec38 (d38_0 DECIMAL(38,0), d38_19 DECIMAL(38,19), d38_38 DECIMAL(38,38));");
2222

2323
$this->assertCount(0, $res);
2424
}
2525

2626
public function testInsertBigInt(): void
2727
{
28-
$res = $this->conn->Query("INSERT INTO php_dec38 VALUES (12345678901234567899876543210987654321, 1234567890123456789.9876543210987654321, .12345678901234567899876543210987654321);");
28+
$res = self::$conn->Query("INSERT INTO php_dec38 VALUES (12345678901234567899876543210987654321, 1234567890123456789.9876543210987654321, .12345678901234567899876543210987654321);");
2929

3030
$this->assertCount(0, $res);
3131
}
3232

3333
public function testSelectBigInt(): void
3434
{
35-
$res = $this->conn->QueryFirst("SELECT * FROM php_dec38");
35+
$res = self::$conn->QueryFirst("SELECT * FROM php_dec38");
3636

3737
$this->assertEquals($res["d38_0"], "12345678901234567899876543210987654321");
3838
$this->assertEquals($res["d38_19"], "1234567890123456789.9876543210987654321");

tests/int128Test.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,38 @@ final class int128Test extends TestCase {
99
/**
1010
* @var Connection
1111
*/
12-
public $conn;
12+
public static $conn;
1313

14-
public function setUp(): void
14+
public static function setUpBeforeClass(): void
1515
{
16-
$this->conn = new Connection("127.0.0.1", 50000, "monetdb", "monetdb", "myDatabase");
16+
self::$conn = new Connection("127.0.0.1", 50000, "monetdb", "monetdb", "myDatabase");
1717
}
1818

1919
public function testStartTransaction(): void
2020
{
21-
$res = $this->conn->Query("START TRANSACTION");
21+
$res = self::$conn->Query("START TRANSACTION");
2222

2323
$this->assertCount(0, $res);
2424
}
2525

2626
public function testBigIntTable(): void
2727
{
28-
$this->conn->Query("drop table if exists php_int128");
29-
$res = $this->conn->Query("CREATE TABLE php_int128 (i HUGEINT);");
28+
self::$conn->Query("drop table if exists php_int128");
29+
$res = self::$conn->Query("CREATE TABLE php_int128 (i HUGEINT);");
3030

3131
$this->assertCount(0, $res);
3232
}
3333

3434
public function testInsertBigInt(): void
3535
{
36-
$res = $this->conn->Query("INSERT INTO php_int128 VALUES (123456789098765432101234567890987654321);");
36+
$res = self::$conn->Query("INSERT INTO php_int128 VALUES (123456789098765432101234567890987654321);");
3737

3838
$this->assertCount(0, $res);
3939
}
4040

4141
public function testSelectBigInt(): void
4242
{
43-
$res = $this->conn->QueryFirst("SELECT * FROM php_int128");
43+
$res = self::$conn->QueryFirst("SELECT * FROM php_int128");
4444

4545
$this->assertEquals($res["i"], "123456789098765432101234567890987654321");
4646
}

0 commit comments

Comments
 (0)