diff --git a/spanner/README.md b/spanner/README.md
index 71b177b9ce..596369ff6a 100644
--- a/spanner/README.md
+++ b/spanner/README.md
@@ -1,8 +1,8 @@
-# Google Cloud Spanner Node.js Samples
+# Cloud Spanner: Node.js Samples
-[]()
+[]()
[Cloud Spanner](https://cloud.google.com/spanner/docs/) is a fully managed, mission-critical, relational database service that offers transactional consistency at global scale, schemas, SQL (ANSI 2011 with extensions), and automatic, synchronous replication for high availability.
@@ -18,19 +18,6 @@
## Setup
-1. Read [Prerequisites][prereq] and [How to run a sample][run] first.
-1. Install dependencies:
-
- With **npm**:
-
- npm install
-
- With **yarn**:
-
- yarn install
-
-[prereq]: ../README.md#prerequisites
-[run]: ../README.md#how-to-run-a-sample
## Samples
@@ -70,10 +57,11 @@ __Usage:__ `node crud.js --help`
```
Commands:
- update Modifies existing rows of data in an example Cloud Spanner table.
- query Executes a read-only SQL query against an example Cloud Spanner table.
- insert Inserts new rows of data into an example Cloud Spanner table.
- read Reads data in an example Cloud Spanner table.
+ update Modifies existing rows of data in an example Cloud Spanner table.
+ query Executes a read-only SQL query against an example Cloud Spanner table.
+ insert Inserts new rows of data into an example Cloud Spanner table.
+ read Reads data in an example Cloud Spanner table.
+ read-stale Reads data in an example Cloud Spanner table.
Options:
--help Show help [boolean]
@@ -151,14 +139,3 @@ For more information, see https://cloud.google.com/spanner/docs
## Running the tests
-1. Set the **GCLOUD_PROJECT** and **GOOGLE_APPLICATION_CREDENTIALS** environment variables.
-
-1. Run the tests:
-
- With **npm**:
-
- npm test
-
- With **yarn**:
-
- yarn test
diff --git a/spanner/crud.js b/spanner/crud.js
index 63d51ea6fc..696c6cae14 100644
--- a/spanner/crud.js
+++ b/spanner/crud.js
@@ -141,7 +141,7 @@ function readData (instanceId, databaseId) {
const instance = spanner.instance(instanceId);
const database = instance.database(databaseId);
- // Read rows from the Albums table
+ // Reads rows from the Albums table
const albumsTable = database.table('Albums');
const query = {
@@ -163,6 +163,53 @@ function readData (instanceId, databaseId) {
// [END read_data]
}
+function readStaleData (instanceId, databaseId) {
+ // [START read_stale_data]
+ // Imports the Google Cloud client library
+ const Spanner = require('@google-cloud/spanner');
+
+ // Instantiates a client
+ const spanner = Spanner();
+
+ // Uncomment these lines to specify the instance and database to use
+ // const instanceId = 'my-instance';
+ // const databaseId = 'my-database';
+
+ // Gets a reference to a Cloud Spanner instance and database
+ const instance = spanner.instance(instanceId);
+ const database = instance.database(databaseId);
+
+ // Reads rows from the Albums table
+ const albumsTable = database.table('Albums');
+
+ const query = {
+ columns: ['SingerId', 'AlbumId', 'AlbumTitle', 'MarketingBudget'],
+ keySet: {
+ all: true
+ }
+ };
+
+ const options = {
+ // Guarantees that all writes committed more than 10 seconds ago are visible
+ exactStaleness: 10
+ };
+
+ albumsTable.read(query, options)
+ .then((results) => {
+ const rows = results[0];
+
+ rows.forEach((row) => {
+ const json = row.toJSON();
+ const id = json.SingerId.value;
+ const album = json.AlbumId.value;
+ const title = json.AlbumTitle;
+ const budget = json.MarketingBudget ? json.MarketingBudget.value : '';
+ console.log(`SingerId: ${id}, AlbumId: ${album}, AlbumTitle: ${title}, MarketingBudget: ${budget}`);
+ });
+ });
+ // [END read_stale_data]
+}
+
const cli = require(`yargs`)
.demand(1)
.command(
@@ -189,10 +236,17 @@ const cli = require(`yargs`)
{},
(opts) => readData(opts.instanceName, opts.databaseName)
)
+ .command(
+ `read-stale `,
+ `Reads stale data in an example Cloud Spanner table.`,
+ {},
+ (opts) => readStaleData(opts.instanceName, opts.databaseName)
+ )
.example(`node $0 update "my-instance" "my-database"`)
.example(`node $0 query "my-instance" "my-database"`)
.example(`node $0 insert "my-instance" "my-database"`)
.example(`node $0 read "my-instance" "my-database"`)
+ .example(`node $0 read-stale "my-instance" "my-database"`)
.wrap(120)
.recommendCommands()
.epilogue(`For more information, see https://cloud.google.com/spanner/docs`);
diff --git a/spanner/package.json b/spanner/package.json
index 1fd115fcd5..cc2c3db35f 100644
--- a/spanner/package.json
+++ b/spanner/package.json
@@ -22,9 +22,9 @@
},
"devDependencies": {
"@google-cloud/nodejs-repo-tools": "1.4.17",
- "ava": "0.21.0",
+ "ava": "0.22.0",
"proxyquire": "1.8.0",
- "sinon": "3.2.0"
+ "sinon": "3.2.1"
},
"cloud-repo-tools": {
"requiresKeyFile": true,
diff --git a/spanner/system-test/spanner.test.js b/spanner/system-test/spanner.test.js
index ddce1e9a8e..ae505e2d37 100644
--- a/spanner/system-test/spanner.test.js
+++ b/spanner/system-test/spanner.test.js
@@ -53,103 +53,133 @@ test.after.always(async (t) => {
// create_database
test.serial(`should create an example database`, async (t) => {
- const output = await tools.runAsync(`${schemaCmd} createDatabase "${INSTANCE_ID}" "${DATABASE_ID}"`, cwd);
- t.true(output.includes(`Waiting for operation on ${DATABASE_ID} to complete...`));
- t.true(output.includes(`Created database ${DATABASE_ID} on instance ${INSTANCE_ID}.`));
+ const results = await tools.runAsyncWithIO(`${schemaCmd} createDatabase "${INSTANCE_ID}" "${DATABASE_ID}"`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`));
+ t.regex(output, new RegExp(`Created database ${DATABASE_ID} on instance ${INSTANCE_ID}.`));
});
// insert_data
test.serial(`should insert rows into an example table`, async (t) => {
- let output = await tools.runAsync(`${crudCmd} insert ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`Inserted data.`));
+ const results = await tools.runAsyncWithIO(`${crudCmd} insert ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /Inserted data\./);
});
// query_data
test.serial(`should query an example table and return matching rows`, async (t) => {
- const output = await tools.runAsync(`${crudCmd} query ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go`));
+ const results = await tools.runAsyncWithIO(`${crudCmd} query ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go/);
});
// read_data
test.serial(`should read an example table`, async (t) => {
- const output = await tools.runAsync(`${crudCmd} read ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go`));
+ const results = await tools.runAsyncWithIO(`${crudCmd} read ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go/);
});
// add_column
test.serial(`should add a column to a table`, async (t) => {
- const output = await tools.runAsync(`${schemaCmd} addColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`Waiting for operation to complete...`));
- t.true(output.includes(`Added the MarketingBudget column.`));
+ const results = await tools.runAsyncWithIO(`${schemaCmd} addColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /Waiting for operation to complete\.\.\./);
+ t.regex(output, /Added the MarketingBudget column\./);
});
// update_data
test.serial(`should update existing rows in an example table`, async (t) => {
- let output = await tools.runAsync(`${crudCmd} update ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`Updated data.`));
+ const results = await tools.runAsyncWithIO(`${crudCmd} update ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /Updated data\./);
+});
+
+// read_stale_data
+test.serial(`should read stale data from an example table`, (t) => {
+ t.plan(2);
+ // read-stale-data reads data that is exactly 10 seconds old. So, make sure
+ // 10 seconds have elapsed since the update_data test.
+ return (new Promise((resolve) => setTimeout(resolve, 11000)))
+ .then(async () => {
+ const results = await tools.runAsyncWithIO(`${crudCmd} read-stale ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget: 100000/);
+ t.regex(output, /SingerId: 2, AlbumId: 2, AlbumTitle: Forever Hold your Peace, MarketingBudget: 500000/);
+ });
});
// query_data_with_new_column
test.serial(`should query an example table with an additional column and return matching rows`, async (t) => {
- const output = await tools.runAsync(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`SingerId: 1, AlbumId: 1, MarketingBudget: 100000`));
- t.true(output.includes(`SingerId: 2, AlbumId: 2, MarketingBudget: 500000`));
+ const results = await tools.runAsyncWithIO(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /SingerId: 1, AlbumId: 1, MarketingBudget: 100000/);
+ t.regex(output, /SingerId: 2, AlbumId: 2, MarketingBudget: 500000/);
});
// create_index
test.serial(`should create an index in an example table`, async (t) => {
- let output = await tools.runAsync(`${indexingCmd} createIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`Waiting for operation to complete...`));
- t.true(output.includes(`Added the AlbumsByAlbumTitle index.`));
+ const results = await tools.runAsyncWithIO(`${indexingCmd} createIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /Waiting for operation to complete\.\.\./);
+ t.regex(output, /Added the AlbumsByAlbumTitle index\./);
});
// create_storing_index
test.serial(`should create a storing index in an example table`, async (t) => {
- const output = await tools.runAsync(`${indexingCmd} createStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`Waiting for operation to complete...`));
- t.true(output.includes(`Added the AlbumsByAlbumTitle2 index.`));
+ const results = await tools.runAsyncWithIO(`${indexingCmd} createStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /Waiting for operation to complete\.\.\./);
+ t.regex(output, /Added the AlbumsByAlbumTitle2 index\./);
});
// query_data_with_index
test.serial(`should query an example table with an index and return matching rows`, async (t) => {
- const output = await tools.runAsync(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:`));
+ const results = await tools.runAsyncWithIO(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:/);
t.false(output.includes(`AlbumId: 2, AlbumTitle: Total Junk, MarketingBudget:`));
});
test.serial(`should respect query boundaries when querying an example table with an index`, async (t) => {
- const output = await tools.runAsync(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID} -s Ardvark -e Zoo`, cwd);
- t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:`));
- t.true(output.includes(`AlbumId: 2, AlbumTitle: Total Junk, MarketingBudget:`));
+ const results = await tools.runAsyncWithIO(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID} -s Ardvark -e Zoo`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:/);
+ t.regex(output, /AlbumId: 2, AlbumTitle: Total Junk, MarketingBudget:/);
});
// read_data_with_index
test.serial(`should read an example table with an index`, async (t) => {
- const output = await tools.runAsync(`${indexingCmd} readIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go`));
+ const results = await tools.runAsyncWithIO(`${indexingCmd} readIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go/);
});
// read_data_with_storing_index
test.serial(`should read an example table with a storing index`, async (t) => {
- const output = await tools.runAsync(`${indexingCmd} readStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go`));
+ const results = await tools.runAsyncWithIO(`${indexingCmd} readStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go/);
});
// read_only_transaction
test.serial(`should read an example table using transactions`, async (t) => {
- const output = await tools.runAsync(`${transactionCmd} readOnly ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go`));
- t.true(output.includes(`Successfully executed read-only transaction.`));
+ const results = await tools.runAsyncWithIO(`${transactionCmd} readOnly ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ const output = results.stdout + results.stderr;
+ t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go/);
+ t.regex(output, /Successfully executed read-only transaction\./);
});
// read_write_transaction
test.serial(`should read from and write to an example table using transactions`, async (t) => {
- let output = await tools.runAsync(`${transactionCmd} readWrite ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`The first album's marketing budget: 100000`));
- t.true(output.includes(`The second album's marketing budget: 500000`));
- t.true(output.includes(`Successfully executed read-write transaction to transfer 200000 from Album 2 to Album 1.`));
-
- output = await tools.runAsync(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
- t.true(output.includes(`SingerId: 1, AlbumId: 1, MarketingBudget: 300000`));
- t.true(output.includes(`SingerId: 2, AlbumId: 2, MarketingBudget: 300000`));
+ let results = await tools.runAsyncWithIO(`${transactionCmd} readWrite ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ let output = results.stdout + results.stderr;
+ t.regex(output, /The first album's marketing budget: 100000/);
+ t.regex(output, /The second album's marketing budget: 500000/);
+ t.regex(output, /Successfully executed read-write transaction to transfer 200000 from Album 2 to Album 1./);
+
+ results = await tools.runAsyncWithIO(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
+ output = results.stdout + results.stderr;
+ t.regex(output, /SingerId: 1, AlbumId: 1, MarketingBudget: 300000/);
+ t.regex(output, /SingerId: 2, AlbumId: 2, MarketingBudget: 300000/);
});