Skip to content

Commit

Permalink
feat: support PHP PDO (#2004)
Browse files Browse the repository at this point in the history
* feat: support PHP PDO

* fix: heading one level down
  • Loading branch information
olavloite committed Jun 27, 2024
1 parent 98b0da3 commit 0a95c45
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ PGAdapter can be used with the following drivers and clients:
1. `psycopg3`: Version 3.1.x and higher are supported. See [psycopg3 support](docs/psycopg3.md) for more details.
1. `node-postgres`: Version 8.8.0 and higher are supported. See [node-postgres support](docs/node-postgres.md) for more details.
1. `npgsql`: Version 6.0.x and higher have experimental support. See [npgsql support](docs/npgsql.md) for more details.
1. `PDO_PGSQL`: The PHP PDO driver has experimental support. See [PHP PDO](docs/pdo.md) for more details.
1. `postgres_fdw`: The PostgreSQL foreign data wrapper has experimental support. See [Foreign Data Wrapper sample](samples/foreign-data-wrapper) for more details.

## ORMs, Frameworks and Tools
Expand Down
125 changes: 125 additions & 0 deletions docs/pdo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# PGAdapter - PHP PDO Connection Options

PGAdapter has experimental support for the [PHP PDO_PGSQL driver](https://www.php.net/manual/en/ref.pdo-pgsql.php).

## Sample Application

See this [sample application using PHP PDO](../samples/php/pdo) for a PHP sample application that
embeds and starts PGAdapter and the Spanner emulator automatically, and then connects to PGAdapter
using `PDO`.

## Usage

First start PGAdapter:

```shell
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
docker pull gcr.io/cloud-spanner-pg-adapter/pgadapter
docker run \
-d -p 5432:5432 \
-v ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS}:ro \
-e GOOGLE_APPLICATION_CREDENTIALS \
gcr.io/cloud-spanner-pg-adapter/pgadapter \
-p my-project -i my-instance \
-x
```

Then connect to PGAdapter using TCP like this:

```php
// Connect to PGAdapter using the PostgreSQL PDO driver.
$dsn = "pgsql:host=localhost;port=5432;dbname=test";
$connection = new PDO($dsn);

// Execute a query on Spanner through PGAdapter.
$statement = $connection->query("SELECT 'Hello World!' as hello");
$rows = $statement->fetchAll();

echo sprintf("Greeting from Cloud Spanner PostgreSQL: %s\n", $rows[0][0]);
```

You can also connect to PGAdapter using Unix Domain Sockets if PGAdapter is running on the same host
as the client application, or the `/tmp` directory in the Docker container has been mapped to a
directory on the local machine:

```shell
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
docker pull gcr.io/cloud-spanner-pg-adapter/pgadapter
docker run \
-d -p 5432:5432 \
-v /tmp:/tmp
-v ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS}:ro \
-e GOOGLE_APPLICATION_CREDENTIALS \
gcr.io/cloud-spanner-pg-adapter/pgadapter \
-p my-project -i my-instance \
-x
```

```php
// Connect to PGAdapter using the PostgreSQL PDO driver.
// Connect to host '/tmp' to use Unix Domain Sockets.
$dsn = "pgsql:host=/tmp;port=5432;dbname=test";
$connection = new PDO($dsn);

// Execute a query on Spanner through PGAdapter.
$statement = $connection->query("SELECT 'Hello World!' as hello");
$rows = $statement->fetchAll();

echo sprintf("Greeting from Cloud Spanner PostgreSQL: %s\n", $rows[0][0]);
```

## Running PGAdapter

This example uses the pre-built Docker image to run PGAdapter.
See [README](../README.md) for more possibilities on how to run PGAdapter.


## Performance Considerations

The following will give you the best possible performance when using PHP PDO with PGAdapter.

### Parameterized Queries
Use parameterized queries to reduce the number of times that Spanner has to parse the query. Spanner
caches the query execution plan based on the SQL string. Using parameterized queries allows Spanner
to re-use the query execution plan for different query parameter values, as the SQL string remains
the same.

Example:

```php
$connection = new PDO($dsn);
$statement = $connection->prepare("SELECT * FROM my_table WHERE my_col=:param_name");
$statement->execute(["param_name" => "my-value"]);
$rows = $statement->fetchAll();
```

### Unix Domain Sockets
Use Unix Domain Socket connections for the lowest possible latency when PGAdapter and the client
application are running on the same host.

### Batching
Use the batching options that are available as SQL commands in PGAdapter to batch DDL or DML
statements. PGAdapter will combine DML and DDL statements that are executed in a batch into a single
request on Cloud Spanner. This can significantly reduce the overhead of executing multiple DML or
DDL statements.

Use the `START BATCH DML`, `START BATCH DDL`, and `RUN BATCH` SQL statements to create batches.

Example for DML statements:

```php
$connection->exec("START BATCH DML");
$statement = $connection->prepare("insert into my_table (id, value) values (:id, :value)");
$statement->execute(["id" => 1, "value" => "One"]);
$statement->execute(["id" => 2, "value" => "Two"]);
$connection->exec("RUN BATCH");
```

Example for DDL statements:

```php
$connection->exec("START BATCH DDL");
$connection->exec("CREATE TABLE my_table (id bigint primary key, value varchar)");
$connection->exec("CREATE INDEX my_index ON my_table (value)");
$connection->exec("RUN BATCH");
```
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
import com.google.protobuf.ByteString;
import com.google.protobuf.ListValue;
import com.google.protobuf.Value;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest;
import com.google.spanner.v1.BeginTransactionRequest;
import com.google.spanner.v1.CommitRequest;
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ExecuteSqlRequest;
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
import com.google.spanner.v1.Mutation;
Expand Down Expand Up @@ -68,7 +70,9 @@ public static void installDependencies() throws Exception {

static String execute(String method) throws Exception {
return run(
new String[] {"php", "pdo_test.php", method, String.valueOf(pgServer.getLocalPort())},
new String[] {
"php", "pdo_test.php", method, "/tmp", String.valueOf(pgServer.getLocalPort())
},
DIRECTORY_NAME);
}

Expand Down Expand Up @@ -783,6 +787,48 @@ public void testReadOnlyTransaction() throws Exception {
assertEquals(0, mockSpanner.countRequestsOfType(RollbackRequest.class));
}

@Test
public void testBatchDml() throws Exception {
String sql = "insert into my_table (id, value) values ($1, $2)";
mockSpanner.putStatementResult(
StatementResult.query(
Statement.of(sql),
ResultSet.newBuilder()
.setMetadata(
createParameterTypesMetadata(ImmutableList.of(TypeCode.INT64, TypeCode.STRING)))
.setStats(ResultSetStats.getDefaultInstance())
.build()));
mockSpanner.putStatementResult(
StatementResult.update(
Statement.newBuilder(sql).bind("p1").to(1L).bind("p2").to("One").build(), 1L));
mockSpanner.putStatementResult(
StatementResult.update(
Statement.newBuilder(sql).bind("p1").to(2L).bind("p2").to("Two").build(), 1L));

String actualOutput = execute("batch_dml");
String expectedOutput = "Inserted two rows\n";
assertEquals(expectedOutput, actualOutput);

assertEquals(1, mockSpanner.countRequestsOfType(ExecuteBatchDmlRequest.class));
ExecuteBatchDmlRequest request =
mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class).get(0);
assertEquals(2, request.getStatementsCount());
}

@Test
public void testBatchDdl() throws Exception {
addDdlResponseToSpannerAdmin();

String actualOutput = execute("batch_ddl");
String expectedOutput = "Created a table and an index in one batch\n";
assertEquals(expectedOutput, actualOutput);

assertEquals(1, mockDatabaseAdmin.getRequests().size());
assertEquals(
2,
((UpdateDatabaseDdlRequest) mockDatabaseAdmin.getRequests().get(0)).getStatementsCount());
}

private static String getInsertAllTypesSql() {
return "INSERT INTO all_types\n"
+ " (col_bigint, col_bool, col_bytea, col_float4, col_float8, col_int,\n"
Expand Down
33 changes: 31 additions & 2 deletions src/test/php/pdo/pdo_test.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<?php

$method = $argv[1];
$port = $argv[2];
$dsn = sprintf("pgsql:host=localhost;port=%s;dbname=test", $port);
$host = $argv[2];
$port = $argv[3];
$dsn = sprintf("pgsql:host=%s;port=%s;dbname=test", $host, $port);
$method($dsn);

function select1($dsn): void
Expand Down Expand Up @@ -269,3 +270,31 @@ function bind_insert_statement($id, $statement): void
$statement->bindValue(10, "test_string");
$statement->bindValue(11, '{"key": "value"}');
}

function batch_dml($dsn): void
{
$connection = new PDO($dsn);
$connection->exec("START BATCH DML");
$statement = $connection->prepare("insert into my_table (id, value) values (:id, :value)");
$statement->execute(["id" => 1, "value" => "One"]);
$statement->execute(["id" => 2, "value" => "Two"]);
$connection->exec("RUN BATCH");

print("Inserted two rows\n");

$statement = null;
$connection = null;
}

function batch_ddl($dsn): void
{
$connection = new PDO($dsn);
$connection->exec("START BATCH DDL");
$connection->exec("CREATE TABLE my_table (id bigint primary key, value varchar)");
$connection->exec("CREATE INDEX my_index ON my_table (value)");
$connection->exec("RUN BATCH");

print("Created a table and an index in one batch\n");

$connection = null;
}

0 comments on commit 0a95c45

Please sign in to comment.