Skip to content

Commit

Permalink
test: add Knex tests (#1947)
Browse files Browse the repository at this point in the history
* test: add Knex tests

* test: fix the timezone to UTC

* fix: make sure dates are shown as dates
  • Loading branch information
olavloite authored Jun 12, 2024
1 parent ab31da1 commit ec04265
Show file tree
Hide file tree
Showing 3 changed files with 443 additions and 26 deletions.
5 changes: 4 additions & 1 deletion samples/nodejs/knex/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# PGAdapter Spanner and Knex.js

PGAdapter has experimental support for [Knex.js](https://knexjs.org/) with the standard Node.js `pg`
PGAdapter can be used with [Knex.js](https://knexjs.org/) with the standard Node.js `pg`
driver. This sample application shows how to connect to PGAdapter with Knex, and how to execute
queries and transactions on Cloud Spanner.

Expand All @@ -15,3 +15,6 @@ npm start

PGAdapter and the emulator are started in a Docker test container by the sample application.
Docker is therefore required to be installed on your system to run this sample.

It is recommended to run PGAdapter as a side-car container in production. See
https://cloud.google.com/spanner/docs/pgadapter-start#run-pgadapter for more information.
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,28 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import com.google.cloud.ByteArray;
import com.google.cloud.Date;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.pgadapter.AbstractMockServerTest;
import com.google.cloud.spanner.pgadapter.metadata.OptionsMetadata;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ListValue;
import com.google.protobuf.Value;
import com.google.spanner.v1.BeginTransactionRequest;
import com.google.spanner.v1.CommitRequest;
import com.google.spanner.v1.ExecuteSqlRequest;
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
import com.google.spanner.v1.ResultSet;
import com.google.spanner.v1.ResultSetMetadata;
import com.google.spanner.v1.ResultSetStats;
import com.google.spanner.v1.RollbackRequest;
import com.google.spanner.v1.TypeCode;
import io.grpc.Status;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.BeforeClass;
Expand Down Expand Up @@ -84,6 +93,297 @@ public void testSelect1() throws Exception {

@Test
public void testSelectUser() throws Exception {
String sql = setupSelectUserResult();

String output = runTest("testSelectUser", getHost(), pgServer.getLocalPort());

assertEquals("{ id: '1', name: 'User 1' }\n", output);

List<ExecuteSqlRequest> executeSqlRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(sql))
.collect(Collectors.toList());
// Knex uses the extended protocol and describes the statement first.
assertEquals(2, executeSqlRequests.size());
ExecuteSqlRequest planRequest = executeSqlRequests.get(0);
assertEquals(QueryMode.PLAN, planRequest.getQueryMode());
assertTrue(planRequest.getTransaction().hasSingleUse());
assertTrue(planRequest.getTransaction().getSingleUse().hasReadOnly());
ExecuteSqlRequest executeRequest = executeSqlRequests.get(1);
assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
assertTrue(executeRequest.getTransaction().hasSingleUse());
assertTrue(executeRequest.getTransaction().getSingleUse().hasReadOnly());
}

@Test
public void testSelectAllTypes() throws Exception {
String sql = "select * from \"all_types\" where \"col_bigint\" = $1 limit $2";
ResultSet resultSet = createAllTypesResultSet("");

ResultSet metadataResultSet =
ResultSet.newBuilder()
.setMetadata(
resultSet
.getMetadata()
.toBuilder()
.setUndeclaredParameters(
createParameterTypesMetadata(
ImmutableList.of(TypeCode.INT64, TypeCode.INT64))
.getUndeclaredParameters())
.build())
.build();
mockSpanner.putStatementResult(StatementResult.query(Statement.of(sql), metadataResultSet));
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(sql).bind("p1").to(1L).bind("p2").to(1L).build(), resultSet));

String output = runTest("testSelectAllTypes", getHost(), pgServer.getLocalPort());

assertEquals(
"{\n"
+ " col_bigint: '1',\n"
+ " col_bool: true,\n"
+ " col_bytea: <Buffer 74 65 73 74>,\n"
+ " col_float4: 3.14,\n"
+ " col_float8: 3.14,\n"
+ " col_int: '100',\n"
+ " col_numeric: '6.626',\n"
+ " col_timestamptz: 2022-02-16T13:18:02.123Z,\n"
+ " col_date: '2022-03-29',\n"
+ " col_varchar: 'test',\n"
+ " col_jsonb: { key: 'value' },\n"
+ " col_array_bigint: [ '1', null, '2' ],\n"
+ " col_array_bool: [ true, null, false ],\n"
+ " col_array_bytea: [ <Buffer 62 79 74 65 73 31>, null, <Buffer 62 79 74 65 73 32> ],\n"
+ " col_array_float4: [ 3.14, null, -99.99 ],\n"
+ " col_array_float8: [ 3.14, null, -99.99 ],\n"
+ " col_array_int: [ '-100', null, '-200' ],\n"
+ " col_array_numeric: [ 6.626, null, -3.14 ],\n"
+ " col_array_timestamptz: [ 2022-02-16T16:18:02.123Z, null, 2000-01-01T00:00:00.000Z ],\n"
+ " col_array_date: '{\"2023-02-20\",NULL,\"2000-01-01\"}',\n"
+ " col_array_varchar: [ 'string1', null, 'string2' ],\n"
+ " col_array_jsonb: [ { key: 'value1' }, null, { key: 'value2' } ]\n"
+ "}\n",
output);

List<ExecuteSqlRequest> executeSqlRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(sql))
.collect(Collectors.toList());
// Knex uses the extended protocol and describes the statement first.
assertEquals(2, executeSqlRequests.size());
ExecuteSqlRequest planRequest = executeSqlRequests.get(0);
assertEquals(QueryMode.PLAN, planRequest.getQueryMode());
assertTrue(planRequest.getTransaction().hasSingleUse());
assertTrue(planRequest.getTransaction().getSingleUse().hasReadOnly());
ExecuteSqlRequest executeRequest = executeSqlRequests.get(1);
assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
assertTrue(executeRequest.getTransaction().hasSingleUse());
assertTrue(executeRequest.getTransaction().getSingleUse().hasReadOnly());
}

@Test
public void testInsertAllTypes() throws IOException, InterruptedException {
String sql =
"insert into \"all_types\" "
+ "(\"col_bigint\", \"col_bool\", \"col_bytea\", \"col_date\", \"col_float4\", \"col_float8\", \"col_int\", \"col_jsonb\", \"col_numeric\", \"col_timestamptz\", \"col_varchar\") "
+ "values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)";
mockSpanner.putStatementResult(
StatementResult.query(
Statement.of(sql),
ResultSet.newBuilder()
.setMetadata(
createParameterTypesMetadata(
ImmutableList.of(
TypeCode.INT64,
TypeCode.BOOL,
TypeCode.BYTES,
TypeCode.DATE,
TypeCode.FLOAT32,
TypeCode.FLOAT64,
TypeCode.INT64,
TypeCode.JSON,
TypeCode.NUMERIC,
TypeCode.TIMESTAMP,
TypeCode.STRING)))
.setStats(ResultSetStats.getDefaultInstance())
.build()));
mockSpanner.putStatementResult(
StatementResult.update(
Statement.newBuilder(sql)
.bind("p1")
.to(1L)
.bind("p2")
.to(true)
.bind("p3")
.to(ByteArray.copyFrom("some random string".getBytes(StandardCharsets.UTF_8)))
.bind("p4")
.to(Date.parseDate("2024-06-10"))
.bind("p5")
.to(3.14f)
.bind("p6")
.to(3.14d)
.bind("p7")
.to(100L)
.bind("p8")
.to(com.google.cloud.spanner.Value.pgJsonb("{\"key\":\"value\"}"))
.bind("p9")
.to(com.google.cloud.spanner.Value.pgNumeric("6.626"))
.bind("p10")
.to(Timestamp.parseTimestamp("2022-07-22T18:15:42.011000000Z"))
.bind("p11")
.to("some random string")
.build(),
1L));

String output = runTest("testInsertAllTypes", getHost(), pgServer.getLocalPort());

assertTrue(output, output.contains("rowCount: 1"));
}

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

String output = runTest("testReadWriteTransaction", getHost(), pgServer.getLocalPort());

assertTrue(output, output.contains("{ id: '1', name: 'User 1' }"));
assertTrue(output, output.contains("rowCount: 1,"));

List<ExecuteSqlRequest> selectRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(sql))
.collect(Collectors.toList());
assertEquals(2, selectRequests.size());
ExecuteSqlRequest analyzeSelectRequest = selectRequests.get(0);
assertEquals(QueryMode.PLAN, analyzeSelectRequest.getQueryMode());
assertTrue(analyzeSelectRequest.hasTransaction());
assertTrue(analyzeSelectRequest.getTransaction().hasBegin());
assertTrue(analyzeSelectRequest.getTransaction().getBegin().hasReadWrite());

ExecuteSqlRequest executeSelectRequest = selectRequests.get(1);
assertTrue(executeSelectRequest.hasTransaction());
assertTrue(executeSelectRequest.getTransaction().hasId());

List<ExecuteSqlRequest> insertRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(insertSql))
.collect(Collectors.toList());
assertEquals(2, insertRequests.size());
ExecuteSqlRequest analyzeInsertRequest = insertRequests.get(0);
assertEquals(QueryMode.PLAN, analyzeInsertRequest.getQueryMode());
assertTrue(analyzeInsertRequest.hasTransaction());
assertTrue(analyzeInsertRequest.getTransaction().hasId());

ExecuteSqlRequest executeInsertRequest = insertRequests.get(1);
assertTrue(executeInsertRequest.hasTransaction());
assertTrue(executeInsertRequest.getTransaction().hasId());

assertEquals(1, mockSpanner.countRequestsOfType(CommitRequest.class));
assertEquals(0, mockSpanner.countRequestsOfType(RollbackRequest.class));
}

@Test
public void testReadWriteTransactionError() throws IOException, InterruptedException {
String sql = setupSelectUserResult();
String insertSql = "insert into \"users\" (\"id\", \"value\") values ($1, $2)";
mockSpanner.putStatementResult(
StatementResult.query(
Statement.of(insertSql),
ResultSet.newBuilder()
.setMetadata(
createParameterTypesMetadata(ImmutableList.of(TypeCode.INT64, TypeCode.STRING)))
.setStats(ResultSetStats.getDefaultInstance())
.build()));
mockSpanner.putStatementResult(
StatementResult.exception(
Statement.newBuilder(insertSql).bind("p1").to(1L).bind("p2").to("One").build(),
Status.ALREADY_EXISTS.asRuntimeException()));

String output = runTest("testReadWriteTransactionError", getHost(), pgServer.getLocalPort());

assertEquals(
"Transaction error: error: insert into \"users\" (\"id\", \"value\") values ($1, $2) - "
+ "com.google.api.gax.rpc.AlreadyExistsException: io.grpc.StatusRuntimeException: ALREADY_EXISTS\n"
+ "{ id: '1', name: 'User 1' }\n",
output);

List<ExecuteSqlRequest> insertRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(insertSql))
.collect(Collectors.toList());
assertEquals(2, insertRequests.size());
ExecuteSqlRequest analyzeInsertRequest = insertRequests.get(0);
assertEquals(QueryMode.PLAN, analyzeInsertRequest.getQueryMode());
assertTrue(analyzeInsertRequest.hasTransaction());
assertTrue(analyzeInsertRequest.getTransaction().hasBegin());
assertTrue(analyzeInsertRequest.getTransaction().getBegin().hasReadWrite());

ExecuteSqlRequest executeInsertRequest = insertRequests.get(1);
assertTrue(executeInsertRequest.hasTransaction());
assertTrue(executeInsertRequest.getTransaction().hasId());

List<ExecuteSqlRequest> selectRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(sql))
.collect(Collectors.toList());
assertEquals(2, selectRequests.size());
ExecuteSqlRequest analyzeSelectRequest = selectRequests.get(0);
assertEquals(QueryMode.PLAN, analyzeSelectRequest.getQueryMode());
assertTrue(analyzeSelectRequest.hasTransaction());
assertTrue(analyzeSelectRequest.getTransaction().hasSingleUse());
assertTrue(analyzeSelectRequest.getTransaction().getSingleUse().hasReadOnly());

ExecuteSqlRequest executeSelectRequest = selectRequests.get(1);
assertTrue(executeSelectRequest.hasTransaction());
assertTrue(executeSelectRequest.getTransaction().hasSingleUse());
assertTrue(executeSelectRequest.getTransaction().getSingleUse().hasReadOnly());

assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
assertEquals(1, mockSpanner.countRequestsOfType(RollbackRequest.class));
}

@Test
public void testReadOnlyTransaction() throws IOException, InterruptedException {
String sql = setupSelectUserResult();

String output = runTest("testReadOnlyTransaction", getHost(), pgServer.getLocalPort());

assertEquals("{ id: '1', name: 'User 1' }\n" + "{ id: '1', name: 'User 1' }\n", output);

assertEquals(1, mockSpanner.countRequestsOfType(BeginTransactionRequest.class));
BeginTransactionRequest beginRequest =
mockSpanner.getRequestsOfType(BeginTransactionRequest.class).get(0);
assertTrue(beginRequest.getOptions().hasReadOnly());

List<ExecuteSqlRequest> selectRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(sql))
.collect(Collectors.toList());
assertEquals(3, selectRequests.size());
ExecuteSqlRequest analyzeSelectRequest = selectRequests.get(0);
assertEquals(QueryMode.PLAN, analyzeSelectRequest.getQueryMode());
for (ExecuteSqlRequest request : selectRequests) {
assertTrue(request.getTransaction().hasId());
}

assertEquals(0, mockSpanner.countRequestsOfType(CommitRequest.class));
assertEquals(0, mockSpanner.countRequestsOfType(RollbackRequest.class));
}

static String setupSelectUserResult() {
String sql = "select * from \"users\" where \"id\" = $1 limit $2";
ResultSetMetadata metadata =
createMetadata(
Expand All @@ -107,25 +407,7 @@ public void testSelectUser() throws Exception {
.addValues(Value.newBuilder().setStringValue("User 1"))
.build())
.build()));

String output = runTest("testSelectUser", getHost(), pgServer.getLocalPort());

assertEquals("{ id: '1', name: 'User 1' }\n", output);

List<ExecuteSqlRequest> executeSqlRequests =
mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).stream()
.filter(request -> request.getSql().equals(sql))
.collect(Collectors.toList());
// Knex uses the extended protocol and describes the statement first.
assertEquals(2, executeSqlRequests.size());
ExecuteSqlRequest planRequest = executeSqlRequests.get(0);
assertEquals(QueryMode.PLAN, planRequest.getQueryMode());
assertTrue(planRequest.getTransaction().hasSingleUse());
assertTrue(planRequest.getTransaction().getSingleUse().hasReadOnly());
ExecuteSqlRequest executeRequest = executeSqlRequests.get(1);
assertEquals(QueryMode.NORMAL, executeRequest.getQueryMode());
assertTrue(executeRequest.getTransaction().hasSingleUse());
assertTrue(executeRequest.getTransaction().getSingleUse().hasReadOnly());
return sql;
}

static String runTest(String testName, String host, int port)
Expand Down
Loading

0 comments on commit ec04265

Please sign in to comment.