Skip to content

Commit

Permalink
feat: add connection options for emulator (#8)
Browse files Browse the repository at this point in the history
- Adds connection options for the Spanner emulator without the need
  to set the environment variable SPANNER_EMULATOR_HOST.
- Automatically creates the instance and database that is referenced
  in the connection if the connection is for the emulator. This
  removes the need to manually create the instance and database on the
  emulator before you can connect and try out simple queries.
- Adds support for DDL statements.

Fixes #6
Fixes #7
  • Loading branch information
olavloite committed Apr 16, 2021
1 parent e7c0a59 commit 776687f
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 7 deletions.
16 changes: 14 additions & 2 deletions connection.schema.json
Expand Up @@ -8,13 +8,13 @@
"minLength": 1
},
"instance": {
"title": "Cloud Spanner Instance ID",
"title": "Spanner Instance ID",
"type": "string",
"minLength": 2,
"maxLength": 64
},
"database": {
"title": "Cloud Spanner Database ID",
"title": "Spanner Database ID",
"type": "string",
"minLength": 2,
"maxLength": 30
Expand All @@ -23,6 +23,18 @@
"title": "Credentials Key File (optional)",
"$comment": "Specifying a credentials file is optional. If no file is specified, the default Google credentials on this environment will be used",
"type": "string"
},
"connectToEmulator": {
"title": "Connect to emulator",
"type": "boolean"
},
"emulatorHost": {
"title": "Emulator host",
"type": "string"
},
"emulatorPort": {
"title": "Emulator port",
"type": "string"
}
},
"required": [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -2,7 +2,7 @@
"name": "google-cloud-spanner-driver",
"displayName": "Google Cloud Spanner Driver",
"description": "Google Cloud Spanner Driver for SQLTools",
"version": "0.1.0",
"version": "0.3.0",
"engines": {
"vscode": "^1.42.0"
},
Expand Down
56 changes: 54 additions & 2 deletions src/ls/driver.ts
Expand Up @@ -18,6 +18,7 @@ import queries from './queries';
import { IConnectionDriver, MConnectionExplorer, NSDatabase, ContextValue, Arg0 } from '@sqltools/types';
import { v4 as generateId } from 'uuid';
import {Database, Spanner, SpannerOptions} from '@google-cloud/spanner';
import {grpc} from 'google-gax';
import { RunUpdateResponse } from '@google-cloud/spanner/build/src/transaction';
import { SpannerQueryParser, StatementType } from './parser';

Expand All @@ -38,12 +39,41 @@ export default class CloudSpannerDriver extends AbstractDriver<DriverLib, Driver
if (this.connection) {
return this.connection;
}
const options = {} as SpannerOptions;
let options = {} as SpannerOptions;
options.projectId = this.credentials.project;
options.keyFile = this.credentials.credentialsKeyFile;
if (this.credentials.connectToEmulator) {
options = Object.assign(options, {
servicePath: this.credentials.emulatorHost || 'localhost',
port: +(this.credentials.emulatorPort || '9010'),
sslCreds: grpc.credentials.createInsecure(),
});
}
const spanner = new Spanner(options);
const instance = spanner.instance(this.credentials.instance);
if (this.credentials.connectToEmulator) {
const [exists] = await instance.exists();
if (!exists) {
const [, operation] = await instance.create({
config: 'emulator-config',
nodes: 1,
displayName: 'Auto-created emulator instance',
});
await operation.promise();
}
}
if (this.credentials.connectToEmulator) {
// This prevents the client library from trying to initialize a session pool
// on a database that may not exist.
const database = instance.database(this.credentials.database, {min: 0});
const [exists] = await database.exists();
if (!exists) {
const [, operation] = await instance.createDatabase(this.credentials.database);
await operation.promise();
}
}
const database = instance.database(this.credentials.database);

this._databaseId = this.credentials.database;

this.connection = Promise.resolve(database);
Expand All @@ -59,7 +89,7 @@ export default class CloudSpannerDriver extends AbstractDriver<DriverLib, Driver

/**
* Executes a set of queries and/or DML statements on Cloud Spanner. Multiple statements must be
* separated by semicolons. DDL statements are currently not supported.
* separated by semicolons.
*/
public query: (typeof AbstractDriver)['prototype']['query'] = async (queries, opt = {}) => {
const db = await this.open();
Expand All @@ -75,6 +105,8 @@ export default class CloudSpannerDriver extends AbstractDriver<DriverLib, Driver
resultsAgg.push(await this.executeDml(db, sql, opt));
break;
case StatementType.DDL:
resultsAgg.push(await this.executeDdl(db, sql, opt));
break;
case StatementType.UNSPECIFIED:
throw new Error(`Unsupported statement: ${sql}`);
}
Expand Down Expand Up @@ -136,6 +168,26 @@ export default class CloudSpannerDriver extends AbstractDriver<DriverLib, Driver
};
}

/**
* Executes a statement as a DDL statement.
*/
private async executeDdl(db: Database, sql: string, opt): Promise<NSDatabase.IResult> {
const [operation] = await db.updateSchema({statements: [sql]});
await new Promise(function(resolve, reject) {
operation.on("complete", resolve);
operation.on("error", reject);
});
return {
cols: ['Result'],
connId: this.getId(),
messages: [{ date: new Date(), message: `DDL statement executed successfully`}],
results: [{Result: 'Success'}],
query: sql,
requestId: opt.requestId,
resultId: generateId(),
};
}

private mapRows(rows: any[], columns: string[]): any[] {
return rows.map((r) => {
columns.forEach((col) => {
Expand Down
7 changes: 5 additions & 2 deletions ui.schema.json
@@ -1,4 +1,7 @@
{
"ui:order": ["project", "instance", "database", "credentials"],
"credentials": { "ui:widget": "file" }
"ui:order": ["project", "instance", "database", "credentialsKeyFile", "useLocalEmulator"],
"credentialsKeyFile": { "ui:widget": "file", "ui:help": "Credentials file to use to connect to Cloud Spanner. This is only required if the connection should use other credentials than the default credentials of the environment. Ignored for emulator connections." },
"connectToEmulator": { "ui:help": "Connects to a Spanner emulator instance instead of to Google Cloud. The instance and database specified in the settings above will automatically be created on the emulator if these do not already exist. The emulator must have been started before you can connect to it." },
"emulatorHost": { "ui:help": "The host name where the emulator is running. Defaults to 'localhost', and is only required if 'Connect to emulator' is enabled and the emulator is not running on localhost." },
"emulatorPort": { "ui:help": "The port number where the emulator is running. Defaults to '9010', and is only required if 'Connect to emulator' is enabled and the emulator is not running on port 9010 (gRPC)." }
}

0 comments on commit 776687f

Please sign in to comment.