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 Platform logo -# Google Cloud Spanner Node.js Samples +# Cloud Spanner: Node.js Samples -[![Build](https://storage.googleapis.com/cloud-docs-samples-badges/GoogleCloudPlatform/nodejs-docs-samples/nodejs-docs-samples-spanner.svg)]() +[![Build](https://storage.googleapis.com/.svg)]() [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/); });