Skip to content

Commit 757187c

Browse files
authored
feat: Managed databases with any accessible server (#3421)
Managed databases only work using database servers hosted by sqlc Cloud. I'm going to add support for using managed databases with any database server you can access. Database servers will be configured in a separate servers collection in the configuration file. Each entry will have a mandatory uri field and an optional name. I may add an optional engine field if the URI scheme isn't enough to infer the engine type. When using a database server not hosted by sqlc Cloud, sqlc will automatically create databases as needed based on the schema for the associated query set. All operations against these databases will be read-only, so they can be created once and re-used.
1 parent f498122 commit 757187c

File tree

12 files changed

+226
-37
lines changed

12 files changed

+226
-37
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ install:
99
test:
1010
go test ./...
1111

12+
test-managed:
13+
MYSQL_SERVER_URI="invalid" POSTGRESQL_SERVER_URI="postgres://postgres:mysecretpassword@localhost:5432/postgres" go test -v ./...
14+
1215
vet:
1316
go vet ./...
1417

internal/cmd/options.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
)
88

99
type Options struct {
10-
Env Env
11-
Stderr io.Writer
12-
MutateConfig func(*config.Config)
10+
Env Env
11+
Stderr io.Writer
1312
// TODO: Move these to a command-specific struct
1413
Tags []string
1514
Against string
15+
16+
// Testing only
17+
MutateConfig func(*config.Config)
1618
}
1719

1820
func (o *Options) ReadConfig(dir, filename string) (string, *config.Config, error) {

internal/compiler/engine.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import (
66

77
"github.com/sqlc-dev/sqlc/internal/analyzer"
88
"github.com/sqlc-dev/sqlc/internal/config"
9+
"github.com/sqlc-dev/sqlc/internal/dbmanager"
910
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
1011
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
1112
pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer"
1213
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
1314
"github.com/sqlc-dev/sqlc/internal/opts"
14-
"github.com/sqlc-dev/sqlc/internal/quickdb"
15-
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
1615
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
1716
)
1817

@@ -23,7 +22,7 @@ type Compiler struct {
2322
parser Parser
2423
result *Result
2524
analyzer analyzer.Analyzer
26-
client pb.QuickClient
25+
client dbmanager.Client
2726

2827
schema []string
2928
}
@@ -32,10 +31,7 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
3231
c := &Compiler{conf: conf, combo: combo}
3332

3433
if conf.Database != nil && conf.Database.Managed {
35-
client, err := quickdb.NewClientFromConfig(combo.Global.Cloud)
36-
if err != nil {
37-
return nil, fmt.Errorf("client error: %w", err)
38-
}
34+
client := dbmanager.NewClient(combo.Global.Servers)
3935
c.client = client
4036
}
4137

@@ -89,4 +85,7 @@ func (c *Compiler) Close(ctx context.Context) {
8985
if c.analyzer != nil {
9086
c.analyzer.Close(ctx)
9187
}
88+
if c.client != nil {
89+
c.client.Close(ctx)
90+
}
9291
}

internal/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,20 @@ const (
5959
type Config struct {
6060
Version string `json:"version" yaml:"version"`
6161
Cloud Cloud `json:"cloud" yaml:"cloud"`
62+
Servers []Server `json:"servers" yaml:"servers"`
6263
SQL []SQL `json:"sql" yaml:"sql"`
6364
Overrides Overrides `json:"overrides,omitempty" yaml:"overrides"`
6465
Plugins []Plugin `json:"plugins" yaml:"plugins"`
6566
Rules []Rule `json:"rules" yaml:"rules"`
6667
Options map[string]yaml.Node `json:"options" yaml:"options"`
6768
}
6869

70+
type Server struct {
71+
Name string `json:"name,omitempty" yaml:"name"`
72+
Engine Engine `json:"engine,omitempty" yaml:"engine"`
73+
URI string `json:"uri" yaml:"uri"`
74+
}
75+
6976
type Database struct {
7077
URI string `json:"uri" yaml:"uri"`
7178
Managed bool `json:"managed" yaml:"managed"`

internal/dbmanager/client.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package dbmanager
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"hash/fnv"
7+
"io"
8+
"net/url"
9+
"strings"
10+
11+
"github.com/jackc/pgx/v5"
12+
"golang.org/x/sync/singleflight"
13+
14+
"github.com/sqlc-dev/sqlc/internal/config"
15+
"github.com/sqlc-dev/sqlc/internal/pgx/poolcache"
16+
"github.com/sqlc-dev/sqlc/internal/shfmt"
17+
)
18+
19+
type CreateDatabaseRequest struct {
20+
Engine string
21+
Migrations []string
22+
}
23+
24+
type CreateDatabaseResponse struct {
25+
Uri string
26+
}
27+
28+
type Client interface {
29+
CreateDatabase(context.Context, *CreateDatabaseRequest) (*CreateDatabaseResponse, error)
30+
Close(context.Context)
31+
}
32+
33+
var flight singleflight.Group
34+
35+
type ManagedClient struct {
36+
cache *poolcache.Cache
37+
replacer *shfmt.Replacer
38+
servers []config.Server
39+
}
40+
41+
func dbid(migrations []string) string {
42+
h := fnv.New64()
43+
for _, query := range migrations {
44+
io.WriteString(h, query)
45+
}
46+
return fmt.Sprintf("%x", h.Sum(nil))
47+
}
48+
49+
func (m *ManagedClient) CreateDatabase(ctx context.Context, req *CreateDatabaseRequest) (*CreateDatabaseResponse, error) {
50+
hash := dbid(req.Migrations)
51+
name := fmt.Sprintf("sqlc_managed_%s", hash)
52+
53+
var base string
54+
for _, server := range m.servers {
55+
if server.Engine == config.EnginePostgreSQL {
56+
base = server.URI
57+
break
58+
}
59+
}
60+
61+
if strings.TrimSpace(base) == "" {
62+
return nil, fmt.Errorf("no PostgreSQL database server found")
63+
}
64+
65+
serverUri := m.replacer.Replace(base)
66+
pool, err := m.cache.Open(ctx, serverUri)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
uri, err := url.Parse(serverUri)
72+
if err != nil {
73+
return nil, err
74+
}
75+
uri.Path = name
76+
77+
key := uri.String()
78+
_, err, _ = flight.Do(key, func() (interface{}, error) {
79+
// TODO: Use a parameterized query
80+
row := pool.QueryRow(ctx,
81+
fmt.Sprintf(`SELECT datname FROM pg_database WHERE datname = '%s'`, name))
82+
83+
var datname string
84+
if err := row.Scan(&datname); err == nil {
85+
return nil, nil
86+
}
87+
88+
if _, err := pool.Exec(ctx, fmt.Sprintf(`CREATE DATABASE "%s"`, name)); err != nil {
89+
return nil, err
90+
}
91+
92+
conn, err := pgx.Connect(ctx, uri.String())
93+
if err != nil {
94+
return nil, fmt.Errorf("connect %s: %s", name, err)
95+
}
96+
defer conn.Close(ctx)
97+
98+
var migrationErr error
99+
for _, q := range req.Migrations {
100+
if len(strings.TrimSpace(q)) == 0 {
101+
continue
102+
}
103+
if _, err := conn.Exec(ctx, q); err != nil {
104+
migrationErr = fmt.Errorf("%s: %s", q, err)
105+
break
106+
}
107+
}
108+
109+
if migrationErr != nil {
110+
pool.Exec(ctx, fmt.Sprintf(`DROP DATABASE "%s" IF EXISTS WITH (FORCE)`, name))
111+
return nil, migrationErr
112+
}
113+
114+
return nil, nil
115+
})
116+
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
return &CreateDatabaseResponse{Uri: key}, err
122+
}
123+
124+
func (m *ManagedClient) Close(ctx context.Context) {
125+
m.cache.Close()
126+
}
127+
128+
func NewClient(servers []config.Server) *ManagedClient {
129+
return &ManagedClient{
130+
cache: poolcache.New(),
131+
servers: servers,
132+
replacer: shfmt.NewReplacer(nil),
133+
}
134+
}

internal/endtoend/endtoend_test.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,28 @@ func TestReplay(t *testing.T) {
120120
"managed-db": {
121121
Mutate: func(t *testing.T, path string) func(*config.Config) {
122122
return func(c *config.Config) {
123+
c.Servers = []config.Server{
124+
{
125+
Name: "postgres",
126+
Engine: config.EnginePostgreSQL,
127+
URI: local.PostgreSQLServer(),
128+
},
129+
130+
{
131+
Name: "mysql",
132+
Engine: config.EngineMySQL,
133+
URI: local.MySQLServer(),
134+
},
135+
}
123136
for i := range c.SQL {
124-
files := []string{}
125-
for _, s := range c.SQL[i].Schema {
126-
files = append(files, filepath.Join(path, s))
127-
}
128137
switch c.SQL[i].Engine {
129138
case config.EnginePostgreSQL:
130-
uri := local.ReadOnlyPostgreSQL(t, files)
131139
c.SQL[i].Database = &config.Database{
132-
URI: uri,
140+
Managed: true,
133141
}
134142
case config.EngineMySQL:
135-
uri := local.MySQL(t, files)
136143
c.SQL[i].Database = &config.Database{
137-
URI: uri,
144+
Managed: true,
138145
}
139146
default:
140147
// pass
@@ -165,8 +172,6 @@ func TestReplay(t *testing.T) {
165172
for _, replay := range FindTests(t, "testdata", name) {
166173
tc := replay
167174
t.Run(filepath.Join(name, tc.Name), func(t *testing.T) {
168-
t.Parallel()
169-
170175
var stderr bytes.Buffer
171176
var output map[string]string
172177
var err error
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT 1;

internal/endtoend/testdata/pg_timezone_names/sqlc.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"sql": [
44
{
55
"engine": "postgresql",
6-
"schema": "query.sql",
6+
"schema": "schema.sql",
77
"queries": "query.sql",
88
"gen": {
99
"go": {
@@ -15,7 +15,7 @@
1515
},
1616
{
1717
"engine": "postgresql",
18-
"schema": "query.sql",
18+
"schema": "schema.sql",
1919
"queries": "query.sql",
2020
"gen": {
2121
"go": {
@@ -27,7 +27,7 @@
2727
},
2828
{
2929
"engine": "postgresql",
30-
"schema": "query.sql",
30+
"schema": "schema.sql",
3131
"queries": "query.sql",
3232
"gen": {
3333
"go": {

internal/engine/postgresql/analyzer/analyze.go

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

1414
core "github.com/sqlc-dev/sqlc/internal/analysis"
1515
"github.com/sqlc-dev/sqlc/internal/config"
16+
"github.com/sqlc-dev/sqlc/internal/dbmanager"
1617
"github.com/sqlc-dev/sqlc/internal/opts"
17-
pb "github.com/sqlc-dev/sqlc/internal/quickdb/v1"
1818
"github.com/sqlc-dev/sqlc/internal/shfmt"
1919
"github.com/sqlc-dev/sqlc/internal/sql/ast"
2020
"github.com/sqlc-dev/sqlc/internal/sql/named"
@@ -23,7 +23,7 @@ import (
2323

2424
type Analyzer struct {
2525
db config.Database
26-
client pb.QuickClient
26+
client dbmanager.Client
2727
pool *pgxpool.Pool
2828
dbg opts.Debug
2929
replacer *shfmt.Replacer
@@ -32,7 +32,7 @@ type Analyzer struct {
3232
tables sync.Map
3333
}
3434

35-
func New(client pb.QuickClient, db config.Database) *Analyzer {
35+
func New(client dbmanager.Client, db config.Database) *Analyzer {
3636
return &Analyzer{
3737
db: db,
3838
dbg: opts.DebugFromEnv(),
@@ -201,7 +201,7 @@ func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrat
201201
if a.client == nil {
202202
return nil, fmt.Errorf("client is nil")
203203
}
204-
edb, err := a.client.CreateEphemeralDatabase(ctx, &pb.CreateEphemeralDatabaseRequest{
204+
edb, err := a.client.CreateDatabase(ctx, &dbmanager.CreateDatabaseRequest{
205205
Engine: "postgresql",
206206
Migrations: migrations,
207207
})

0 commit comments

Comments
 (0)