Skip to content

Commit 79dc773

Browse files
authored
Add Spanner stale read sample. (#475)
1 parent 868a585 commit 79dc773

File tree

4 files changed

+137
-76
lines changed

4 files changed

+137
-76
lines changed

spanner/README.md

+7-30
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<img src="https://avatars2.githubusercontent.com/u/2810941?v=3&s=96" alt="Google Cloud Platform logo" title="Google Cloud Platform" align="right" height="96" width="96"/>
22

3-
# Google Cloud Spanner Node.js Samples
3+
# Cloud Spanner: Node.js Samples
44

5-
[![Build](https://storage.googleapis.com/cloud-docs-samples-badges/GoogleCloudPlatform/nodejs-docs-samples/nodejs-docs-samples-spanner.svg)]()
5+
[![Build](https://storage.googleapis.com/.svg)]()
66

77
[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.
88

@@ -18,19 +18,6 @@
1818

1919
## Setup
2020

21-
1. Read [Prerequisites][prereq] and [How to run a sample][run] first.
22-
1. Install dependencies:
23-
24-
With **npm**:
25-
26-
npm install
27-
28-
With **yarn**:
29-
30-
yarn install
31-
32-
[prereq]: ../README.md#prerequisites
33-
[run]: ../README.md#how-to-run-a-sample
3421

3522
## Samples
3623

@@ -70,10 +57,11 @@ __Usage:__ `node crud.js --help`
7057

7158
```
7259
Commands:
73-
update <instanceName> <databaseName> Modifies existing rows of data in an example Cloud Spanner table.
74-
query <instanceName> <databaseName> Executes a read-only SQL query against an example Cloud Spanner table.
75-
insert <instanceName> <databaseName> Inserts new rows of data into an example Cloud Spanner table.
76-
read <instanceName> <databaseName> Reads data in an example Cloud Spanner table.
60+
update <instanceName> <databaseName> Modifies existing rows of data in an example Cloud Spanner table.
61+
query <instanceName> <databaseName> Executes a read-only SQL query against an example Cloud Spanner table.
62+
insert <instanceName> <databaseName> Inserts new rows of data into an example Cloud Spanner table.
63+
read <instanceName> <databaseName> Reads data in an example Cloud Spanner table.
64+
read-stale <instanceName> <databaseName> Reads data in an example Cloud Spanner table.
7765
7866
Options:
7967
--help Show help [boolean]
@@ -151,14 +139,3 @@ For more information, see https://cloud.google.com/spanner/docs
151139

152140
## Running the tests
153141

154-
1. Set the **GCLOUD_PROJECT** and **GOOGLE_APPLICATION_CREDENTIALS** environment variables.
155-
156-
1. Run the tests:
157-
158-
With **npm**:
159-
160-
npm test
161-
162-
With **yarn**:
163-
164-
yarn test

spanner/crud.js

+55-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ function readData (instanceId, databaseId) {
141141
const instance = spanner.instance(instanceId);
142142
const database = instance.database(databaseId);
143143

144-
// Read rows from the Albums table
144+
// Reads rows from the Albums table
145145
const albumsTable = database.table('Albums');
146146

147147
const query = {
@@ -163,6 +163,53 @@ function readData (instanceId, databaseId) {
163163
// [END read_data]
164164
}
165165

166+
function readStaleData (instanceId, databaseId) {
167+
// [START read_stale_data]
168+
// Imports the Google Cloud client library
169+
const Spanner = require('@google-cloud/spanner');
170+
171+
// Instantiates a client
172+
const spanner = Spanner();
173+
174+
// Uncomment these lines to specify the instance and database to use
175+
// const instanceId = 'my-instance';
176+
// const databaseId = 'my-database';
177+
178+
// Gets a reference to a Cloud Spanner instance and database
179+
const instance = spanner.instance(instanceId);
180+
const database = instance.database(databaseId);
181+
182+
// Reads rows from the Albums table
183+
const albumsTable = database.table('Albums');
184+
185+
const query = {
186+
columns: ['SingerId', 'AlbumId', 'AlbumTitle', 'MarketingBudget'],
187+
keySet: {
188+
all: true
189+
}
190+
};
191+
192+
const options = {
193+
// Guarantees that all writes committed more than 10 seconds ago are visible
194+
exactStaleness: 10
195+
};
196+
197+
albumsTable.read(query, options)
198+
.then((results) => {
199+
const rows = results[0];
200+
201+
rows.forEach((row) => {
202+
const json = row.toJSON();
203+
const id = json.SingerId.value;
204+
const album = json.AlbumId.value;
205+
const title = json.AlbumTitle;
206+
const budget = json.MarketingBudget ? json.MarketingBudget.value : '';
207+
console.log(`SingerId: ${id}, AlbumId: ${album}, AlbumTitle: ${title}, MarketingBudget: ${budget}`);
208+
});
209+
});
210+
// [END read_stale_data]
211+
}
212+
166213
const cli = require(`yargs`)
167214
.demand(1)
168215
.command(
@@ -189,10 +236,17 @@ const cli = require(`yargs`)
189236
{},
190237
(opts) => readData(opts.instanceName, opts.databaseName)
191238
)
239+
.command(
240+
`read-stale <instanceName> <databaseName>`,
241+
`Reads stale data in an example Cloud Spanner table.`,
242+
{},
243+
(opts) => readStaleData(opts.instanceName, opts.databaseName)
244+
)
192245
.example(`node $0 update "my-instance" "my-database"`)
193246
.example(`node $0 query "my-instance" "my-database"`)
194247
.example(`node $0 insert "my-instance" "my-database"`)
195248
.example(`node $0 read "my-instance" "my-database"`)
249+
.example(`node $0 read-stale "my-instance" "my-database"`)
196250
.wrap(120)
197251
.recommendCommands()
198252
.epilogue(`For more information, see https://cloud.google.com/spanner/docs`);

spanner/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
},
2323
"devDependencies": {
2424
"@google-cloud/nodejs-repo-tools": "1.4.17",
25-
"ava": "0.21.0",
25+
"ava": "0.22.0",
2626
"proxyquire": "1.8.0",
27-
"sinon": "3.2.0"
27+
"sinon": "3.2.1"
2828
},
2929
"cloud-repo-tools": {
3030
"requiresKeyFile": true,

spanner/system-test/spanner.test.js

+73-43
Original file line numberDiff line numberDiff line change
@@ -53,103 +53,133 @@ test.after.always(async (t) => {
5353

5454
// create_database
5555
test.serial(`should create an example database`, async (t) => {
56-
const output = await tools.runAsync(`${schemaCmd} createDatabase "${INSTANCE_ID}" "${DATABASE_ID}"`, cwd);
57-
t.true(output.includes(`Waiting for operation on ${DATABASE_ID} to complete...`));
58-
t.true(output.includes(`Created database ${DATABASE_ID} on instance ${INSTANCE_ID}.`));
56+
const results = await tools.runAsyncWithIO(`${schemaCmd} createDatabase "${INSTANCE_ID}" "${DATABASE_ID}"`, cwd);
57+
const output = results.stdout + results.stderr;
58+
t.regex(output, new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`));
59+
t.regex(output, new RegExp(`Created database ${DATABASE_ID} on instance ${INSTANCE_ID}.`));
5960
});
6061

6162
// insert_data
6263
test.serial(`should insert rows into an example table`, async (t) => {
63-
let output = await tools.runAsync(`${crudCmd} insert ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
64-
t.true(output.includes(`Inserted data.`));
64+
const results = await tools.runAsyncWithIO(`${crudCmd} insert ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
65+
const output = results.stdout + results.stderr;
66+
t.regex(output, /Inserted data\./);
6567
});
6668

6769
// query_data
6870
test.serial(`should query an example table and return matching rows`, async (t) => {
69-
const output = await tools.runAsync(`${crudCmd} query ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
70-
t.true(output.includes(`SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go`));
71+
const results = await tools.runAsyncWithIO(`${crudCmd} query ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
72+
const output = results.stdout + results.stderr;
73+
t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go/);
7174
});
7275

7376
// read_data
7477
test.serial(`should read an example table`, async (t) => {
75-
const output = await tools.runAsync(`${crudCmd} read ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
76-
t.true(output.includes(`SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go`));
78+
const results = await tools.runAsyncWithIO(`${crudCmd} read ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
79+
const output = results.stdout + results.stderr;
80+
t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go/);
7781
});
7882

7983
// add_column
8084
test.serial(`should add a column to a table`, async (t) => {
81-
const output = await tools.runAsync(`${schemaCmd} addColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
82-
t.true(output.includes(`Waiting for operation to complete...`));
83-
t.true(output.includes(`Added the MarketingBudget column.`));
85+
const results = await tools.runAsyncWithIO(`${schemaCmd} addColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
86+
const output = results.stdout + results.stderr;
87+
t.regex(output, /Waiting for operation to complete\.\.\./);
88+
t.regex(output, /Added the MarketingBudget column\./);
8489
});
8590

8691
// update_data
8792
test.serial(`should update existing rows in an example table`, async (t) => {
88-
let output = await tools.runAsync(`${crudCmd} update ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
89-
t.true(output.includes(`Updated data.`));
93+
const results = await tools.runAsyncWithIO(`${crudCmd} update ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
94+
const output = results.stdout + results.stderr;
95+
t.regex(output, /Updated data\./);
96+
});
97+
98+
// read_stale_data
99+
test.serial(`should read stale data from an example table`, (t) => {
100+
t.plan(2);
101+
// read-stale-data reads data that is exactly 10 seconds old. So, make sure
102+
// 10 seconds have elapsed since the update_data test.
103+
return (new Promise((resolve) => setTimeout(resolve, 11000)))
104+
.then(async () => {
105+
const results = await tools.runAsyncWithIO(`${crudCmd} read-stale ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
106+
const output = results.stdout + results.stderr;
107+
t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget: 100000/);
108+
t.regex(output, /SingerId: 2, AlbumId: 2, AlbumTitle: Forever Hold your Peace, MarketingBudget: 500000/);
109+
});
90110
});
91111

92112
// query_data_with_new_column
93113
test.serial(`should query an example table with an additional column and return matching rows`, async (t) => {
94-
const output = await tools.runAsync(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
95-
t.true(output.includes(`SingerId: 1, AlbumId: 1, MarketingBudget: 100000`));
96-
t.true(output.includes(`SingerId: 2, AlbumId: 2, MarketingBudget: 500000`));
114+
const results = await tools.runAsyncWithIO(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
115+
const output = results.stdout + results.stderr;
116+
t.regex(output, /SingerId: 1, AlbumId: 1, MarketingBudget: 100000/);
117+
t.regex(output, /SingerId: 2, AlbumId: 2, MarketingBudget: 500000/);
97118
});
98119

99120
// create_index
100121
test.serial(`should create an index in an example table`, async (t) => {
101-
let output = await tools.runAsync(`${indexingCmd} createIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
102-
t.true(output.includes(`Waiting for operation to complete...`));
103-
t.true(output.includes(`Added the AlbumsByAlbumTitle index.`));
122+
const results = await tools.runAsyncWithIO(`${indexingCmd} createIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
123+
const output = results.stdout + results.stderr;
124+
t.regex(output, /Waiting for operation to complete\.\.\./);
125+
t.regex(output, /Added the AlbumsByAlbumTitle index\./);
104126
});
105127

106128
// create_storing_index
107129
test.serial(`should create a storing index in an example table`, async (t) => {
108-
const output = await tools.runAsync(`${indexingCmd} createStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
109-
t.true(output.includes(`Waiting for operation to complete...`));
110-
t.true(output.includes(`Added the AlbumsByAlbumTitle2 index.`));
130+
const results = await tools.runAsyncWithIO(`${indexingCmd} createStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
131+
const output = results.stdout + results.stderr;
132+
t.regex(output, /Waiting for operation to complete\.\.\./);
133+
t.regex(output, /Added the AlbumsByAlbumTitle2 index\./);
111134
});
112135

113136
// query_data_with_index
114137
test.serial(`should query an example table with an index and return matching rows`, async (t) => {
115-
const output = await tools.runAsync(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
116-
t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:`));
138+
const results = await tools.runAsyncWithIO(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
139+
const output = results.stdout + results.stderr;
140+
t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:/);
117141
t.false(output.includes(`AlbumId: 2, AlbumTitle: Total Junk, MarketingBudget:`));
118142
});
119143

120144
test.serial(`should respect query boundaries when querying an example table with an index`, async (t) => {
121-
const output = await tools.runAsync(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID} -s Ardvark -e Zoo`, cwd);
122-
t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:`));
123-
t.true(output.includes(`AlbumId: 2, AlbumTitle: Total Junk, MarketingBudget:`));
145+
const results = await tools.runAsyncWithIO(`${indexingCmd} queryIndex ${INSTANCE_ID} ${DATABASE_ID} -s Ardvark -e Zoo`, cwd);
146+
const output = results.stdout + results.stderr;
147+
t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go, MarketingBudget:/);
148+
t.regex(output, /AlbumId: 2, AlbumTitle: Total Junk, MarketingBudget:/);
124149
});
125150

126151
// read_data_with_index
127152
test.serial(`should read an example table with an index`, async (t) => {
128-
const output = await tools.runAsync(`${indexingCmd} readIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
129-
t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go`));
153+
const results = await tools.runAsyncWithIO(`${indexingCmd} readIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
154+
const output = results.stdout + results.stderr;
155+
t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go/);
130156
});
131157

132158
// read_data_with_storing_index
133159
test.serial(`should read an example table with a storing index`, async (t) => {
134-
const output = await tools.runAsync(`${indexingCmd} readStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
135-
t.true(output.includes(`AlbumId: 1, AlbumTitle: Go, Go, Go`));
160+
const results = await tools.runAsyncWithIO(`${indexingCmd} readStoringIndex ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
161+
const output = results.stdout + results.stderr;
162+
t.regex(output, /AlbumId: 1, AlbumTitle: Go, Go, Go/);
136163
});
137164

138165
// read_only_transaction
139166
test.serial(`should read an example table using transactions`, async (t) => {
140-
const output = await tools.runAsync(`${transactionCmd} readOnly ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
141-
t.true(output.includes(`SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go`));
142-
t.true(output.includes(`Successfully executed read-only transaction.`));
167+
const results = await tools.runAsyncWithIO(`${transactionCmd} readOnly ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
168+
const output = results.stdout + results.stderr;
169+
t.regex(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Go, Go, Go/);
170+
t.regex(output, /Successfully executed read-only transaction\./);
143171
});
144172

145173
// read_write_transaction
146174
test.serial(`should read from and write to an example table using transactions`, async (t) => {
147-
let output = await tools.runAsync(`${transactionCmd} readWrite ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
148-
t.true(output.includes(`The first album's marketing budget: 100000`));
149-
t.true(output.includes(`The second album's marketing budget: 500000`));
150-
t.true(output.includes(`Successfully executed read-write transaction to transfer 200000 from Album 2 to Album 1.`));
151-
152-
output = await tools.runAsync(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
153-
t.true(output.includes(`SingerId: 1, AlbumId: 1, MarketingBudget: 300000`));
154-
t.true(output.includes(`SingerId: 2, AlbumId: 2, MarketingBudget: 300000`));
175+
let results = await tools.runAsyncWithIO(`${transactionCmd} readWrite ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
176+
let output = results.stdout + results.stderr;
177+
t.regex(output, /The first album's marketing budget: 100000/);
178+
t.regex(output, /The second album's marketing budget: 500000/);
179+
t.regex(output, /Successfully executed read-write transaction to transfer 200000 from Album 2 to Album 1./);
180+
181+
results = await tools.runAsyncWithIO(`${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID}`, cwd);
182+
output = results.stdout + results.stderr;
183+
t.regex(output, /SingerId: 1, AlbumId: 1, MarketingBudget: 300000/);
184+
t.regex(output, /SingerId: 2, AlbumId: 2, MarketingBudget: 300000/);
155185
});

0 commit comments

Comments
 (0)