Skip to content

Commit 2e92beb

Browse files
feat: add dialect connection property (#710)
* feat: add dialect connection property Adds a `dialect` connection property that determines which dialect is used for any new database that is created by this connection. This property can also be set in combination with the `auto_config_emulator` connection property to determine the dialect of the database that should be created automatically on the Emulator. * chore: address review comments Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 40a2c6c commit 2e92beb

File tree

6 files changed

+125
-10
lines changed

6 files changed

+125
-10
lines changed

conn.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,9 +666,13 @@ func (c *conn) execDDL(ctx context.Context, statements ...spanner.Statement) (dr
666666
ddlStatements[i] = s.SQL
667667
}
668668
if c.parser.IsCreateDatabaseStatement(ddlStatements[0]) {
669+
dialect := c.parser.Dialect
670+
if d := propertyDialect.GetValueOrDefault(c.state); d != adminpb.DatabaseDialect_DATABASE_DIALECT_UNSPECIFIED {
671+
dialect = d
672+
}
669673
op, err := c.adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
670674
CreateStatement: ddlStatements[0],
671-
DatabaseDialect: c.parser.Dialect,
675+
DatabaseDialect: dialect,
672676
Parent: c.instance,
673677
ExtraStatements: ddlStatements[1:],
674678
})

conn_with_mockserver_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,55 @@ func TestCreateDatabase(t *testing.T) {
749749
}
750750
}
751751

752+
func TestCreateDatabaseWithDialect(t *testing.T) {
753+
t.Parallel()
754+
755+
ctx := context.Background()
756+
db, server, teardown := setupTestDBConnection(t)
757+
defer teardown()
758+
759+
var expectedResponse = &databasepb.Database{}
760+
anyMsg, _ := anypb.New(expectedResponse)
761+
server.TestDatabaseAdmin.SetResps([]proto.Message{
762+
&longrunningpb.Operation{
763+
Done: true,
764+
Result: &longrunningpb.Operation_Response{Response: anyMsg},
765+
Name: "test-operation",
766+
},
767+
})
768+
769+
conn, err := db.Conn(ctx)
770+
if err != nil {
771+
t.Fatal(err)
772+
}
773+
defer silentClose(conn)
774+
775+
if _, err := conn.ExecContext(ctx, "set dialect = 'postgresql'"); err != nil {
776+
t.Fatalf("failed to set dialect: %v", err)
777+
}
778+
if _, err = conn.ExecContext(ctx, `create database "foo"`); err != nil {
779+
t.Fatalf("failed to execute CREATE DATABASE: %v", err)
780+
}
781+
782+
requests := server.TestDatabaseAdmin.Reqs()
783+
if g, w := len(requests), 1; g != w {
784+
t.Fatalf("requests count mismatch\nGot: %v\nWant: %v", g, w)
785+
}
786+
if req, ok := requests[0].(*databasepb.CreateDatabaseRequest); ok {
787+
if g, w := req.Parent, "projects/p/instances/i"; g != w {
788+
t.Fatalf("parent mismatch\n Got: %v\nWant: %v", g, w)
789+
}
790+
if g, w := req.CreateStatement, `create database "foo"`; g != w {
791+
t.Fatalf("CREATE statement mismatch\n Got: %v\nWant: %v", g, w)
792+
}
793+
if g, w := req.DatabaseDialect, databasepb.DatabaseDialect_POSTGRESQL; g != w {
794+
t.Fatalf("dialect mismatch\n Got: %v\nWant: %v", g, w)
795+
}
796+
} else {
797+
t.Fatalf("request type mismatch, got %v", requests[0])
798+
}
799+
}
800+
752801
func TestCreateDatabaseWithExtraStatements(t *testing.T) {
753802
t.Parallel()
754803

connection_properties.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
"cloud.google.com/go/spanner"
24+
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
2425
"cloud.google.com/go/spanner/apiv1/spannerpb"
2526
"github.com/googleapis/go-sql-spanner/connectionstate"
2627
"github.com/googleapis/go-sql-spanner/parser"
@@ -65,6 +66,20 @@ var propertyConnectionStateType = createConnectionProperty(
6566
return parseConnectionStateType(value)
6667
},
6768
)
69+
var propertyDialect = createConnectionProperty(
70+
"dialect",
71+
"The default dialect to use when creating new databases using this connection. "+
72+
"You do not need to set this property for any existing database. The driver will "+
73+
"automatically detect the dialect of the database that you connect to. The value of "+
74+
"this property is only used when the connection is used to create a new database. "+
75+
"If the value of this property is unspecified, then the new database will be created "+
76+
"with the same dialect as the current database.",
77+
databasepb.DatabaseDialect_DATABASE_DIALECT_UNSPECIFIED,
78+
false,
79+
nil,
80+
connectionstate.ContextUser,
81+
parseDatabaseDialect,
82+
)
6883
var propertyAutoConfigEmulator = createConnectionProperty(
6984
"auto_config_emulator",
7085
"Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). "+

driver.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,7 @@ func createConnector(d *Driver, connectorConfig ConnectorConfig) (*connector, er
683683
internaloption.SkipDialSettingsValidation(),
684684
}
685685
opts = append(emulatorOpts, opts...)
686-
if err := autoConfigEmulator(context.Background(), connectorConfig.Host, connectorConfig.Project, connectorConfig.Instance, connectorConfig.Database, opts); err != nil {
686+
if err := autoConfigEmulator(context.Background(), connectorConfig.Host, connectorConfig.Project, connectorConfig.Instance, connectorConfig.Database, propertyDialect.GetValueOrDefault(state), opts); err != nil {
687687
return nil, err
688688
}
689689
}
@@ -1548,6 +1548,19 @@ func parseRpcPriority(val string) (spannerpb.RequestOptions_Priority, error) {
15481548
return spannerpb.RequestOptions_PRIORITY_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid or unsupported priority: %v", val)
15491549
}
15501550

1551+
func parseDatabaseDialect(val string) (databasepb.DatabaseDialect, error) {
1552+
val = strings.ToUpper(val)
1553+
if dialect, ok := databasepb.DatabaseDialect_value[val]; ok {
1554+
return databasepb.DatabaseDialect(dialect), nil
1555+
}
1556+
if !strings.HasPrefix(val, "DATABASEDIALECT_") {
1557+
if dialect, ok := databasepb.DatabaseDialect_value["DATABASEDIALECT_"+val]; ok {
1558+
return databasepb.DatabaseDialect(dialect), nil
1559+
}
1560+
}
1561+
return databasepb.DatabaseDialect_DATABASE_DIALECT_UNSPECIFIED, status.Errorf(codes.InvalidArgument, "invalid or unsupported dialect: %v", val)
1562+
}
1563+
15511564
func parseIsolationLevel(val string) (sql.IsolationLevel, error) {
15521565
s := strings.ToLower(strings.TrimSpace(val))
15531566
s = strings.ReplaceAll(s, " ", "")

emulator_util.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,20 @@ import (
2020

2121
"cloud.google.com/go/spanner"
2222
database "cloud.google.com/go/spanner/admin/database/apiv1"
23-
databasepb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
23+
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
2424
instance "cloud.google.com/go/spanner/admin/instance/apiv1"
25-
instancepb "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
25+
"cloud.google.com/go/spanner/admin/instance/apiv1/instancepb"
2626
"google.golang.org/api/option"
2727
"google.golang.org/grpc/codes"
2828
)
2929

30-
func autoConfigEmulator(ctx context.Context, host, project, instance, database string, opts []option.ClientOption) error {
30+
func autoConfigEmulator(ctx context.Context, host, project, instance, database string, dialect databasepb.DatabaseDialect, opts []option.ClientOption) error {
3131
if err := createInstance(project, instance, opts); err != nil {
3232
if spanner.ErrCode(err) != codes.AlreadyExists {
3333
return err
3434
}
3535
}
36-
if err := createDatabase(project, instance, database, opts); err != nil {
36+
if err := createDatabase(project, instance, database, dialect, opts); err != nil {
3737
if spanner.ErrCode(err) != codes.AlreadyExists {
3838
return err
3939
}
@@ -47,7 +47,7 @@ func createInstance(projectId, instanceId string, opts []option.ClientOption) er
4747
if err != nil {
4848
return err
4949
}
50-
defer instanceAdmin.Close()
50+
defer func() { _ = instanceAdmin.Close() }()
5151
op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{
5252
Parent: fmt.Sprintf("projects/%s", projectId),
5353
InstanceId: instanceId,
@@ -67,16 +67,21 @@ func createInstance(projectId, instanceId string, opts []option.ClientOption) er
6767
return nil
6868
}
6969

70-
func createDatabase(projectId, instanceId, databaseId string, opts []option.ClientOption) error {
70+
func createDatabase(projectId, instanceId, databaseId string, dialect databasepb.DatabaseDialect, opts []option.ClientOption) error {
7171
ctx := context.Background()
7272
databaseAdminClient, err := database.NewDatabaseAdminClient(ctx, opts...)
7373
if err != nil {
7474
return err
7575
}
76-
defer databaseAdminClient.Close()
76+
defer func() { _ = databaseAdminClient.Close() }()
77+
createStatement := fmt.Sprintf("CREATE DATABASE `%s`", databaseId)
78+
if dialect == databasepb.DatabaseDialect_POSTGRESQL {
79+
createStatement = fmt.Sprintf(`CREATE DATABASE "%s"`, databaseId)
80+
}
7781
opDB, err := databaseAdminClient.CreateDatabase(ctx, &databasepb.CreateDatabaseRequest{
7882
Parent: fmt.Sprintf("projects/%s/instances/%s", projectId, instanceId),
79-
CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseId),
83+
CreateStatement: createStatement,
84+
DatabaseDialect: dialect,
8085
})
8186
if err != nil {
8287
return err

integration_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,35 @@ func TestAutoConfigEmulator(t *testing.T) {
298298
}
299299
}
300300

301+
func TestAutoConfigEmulatorPostgreSql(t *testing.T) {
302+
skipIfShort(t)
303+
if !runsOnEmulator() {
304+
t.Skip("autoConfigEmulator=true only works when connected to the emulator")
305+
}
306+
t.Parallel()
307+
308+
ctx := context.Background()
309+
for range 2 {
310+
db, err := sql.Open("spanner", "projects/emulator-project/instances/test-instance/databases/test-database;autoConfigEmulator=true;dialect=postgresql")
311+
if err != nil {
312+
t.Fatalf("could not connect to emulator: %v", err)
313+
}
314+
// Execute a query that only works on PostgreSQL.
315+
row := db.QueryRowContext(ctx, "select $1", "Hello World")
316+
if row.Err() != nil {
317+
t.Fatalf("could not execute select: %v", row.Err())
318+
}
319+
var msg string
320+
if err := row.Scan(&msg); err != nil {
321+
t.Fatalf("could not scan value from select: %v", err)
322+
}
323+
if g, w := msg, "Hello World"; g != w {
324+
t.Fatalf("value mismatch:\n Got %v\nWant %v", g, w)
325+
}
326+
_ = db.Close()
327+
}
328+
}
329+
301330
func TestQueryContext(t *testing.T) {
302331
skipIfShort(t)
303332
t.Parallel()

0 commit comments

Comments
 (0)