Skip to content

Commit e9fbc2e

Browse files
committed
feat(mssql): gracefully skip MSSQL when database unavailable
- Add ErrDatabaseUnavailable error type to detect when a database engine requires a connection but none is available - Modify parse() to return a parseResult enum (parseSuccess, parseFailed, parseSkipped) instead of a boolean to distinguish between failures and skipped configurations - When MSSQL database URI is not set, skip the configuration with a warning instead of failing the entire generate/vet command - Add "requires" field to exec.json for tests to specify required databases (postgres, mysql, mssql) - Skip tests that require databases that are not available - Fix MSSQL parser to track statement boundaries for proper AST positions - Add support for SchemaObjectFunctionTableReference in INSERT statements - Initialize ReturningList on INSERT/UPDATE/DELETE statements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 2c2463b commit e9fbc2e

File tree

9 files changed

+266
-40
lines changed

9 files changed

+266
-40
lines changed

internal/cmd/generate.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,16 @@ func remoteGenerate(ctx context.Context, configPath string, conf *config.Config,
293293
return output, nil
294294
}
295295

296-
func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, stderr io.Writer) (*compiler.Result, bool) {
296+
// parseResult indicates the outcome of parsing a SQL configuration
297+
type parseResult int
298+
299+
const (
300+
parseSuccess parseResult = iota
301+
parseFailed
302+
parseSkipped // Configuration was skipped (e.g., database unavailable)
303+
)
304+
305+
func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, stderr io.Writer) (*compiler.Result, parseResult) {
297306
defer trace.StartRegion(ctx, "parse").End()
298307
c, err := compiler.NewCompiler(sql, combo, parserOpts)
299308
defer func() {
@@ -302,8 +311,15 @@ func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.C
302311
}
303312
}()
304313
if err != nil {
314+
// Check if this is a database unavailability error (e.g., missing env var)
315+
// In this case, skip the configuration gracefully rather than failing
316+
if compiler.IsDatabaseUnavailable(err) {
317+
fmt.Fprintf(stderr, "# package %s\n", name)
318+
fmt.Fprintf(stderr, "skipping: %s\n", err)
319+
return nil, parseSkipped
320+
}
305321
fmt.Fprintf(stderr, "error creating compiler: %s\n", err)
306-
return nil, true
322+
return nil, parseFailed
307323
}
308324
if err := c.ParseCatalog(sql.Schema); err != nil {
309325
fmt.Fprintf(stderr, "# package %s\n", name)
@@ -314,7 +330,7 @@ func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.C
314330
} else {
315331
fmt.Fprintf(stderr, "error parsing schema: %s\n", err)
316332
}
317-
return nil, true
333+
return nil, parseFailed
318334
}
319335
if parserOpts.Debug.DumpCatalog {
320336
debug.Dump(c.Catalog())
@@ -328,9 +344,9 @@ func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.C
328344
} else {
329345
fmt.Fprintf(stderr, "error parsing queries: %s\n", err)
330346
}
331-
return nil, true
347+
return nil, parseFailed
332348
}
333-
return c.Result(), false
349+
return c.Result(), parseSuccess
334350
}
335351

336352
func codegen(ctx context.Context, combo config.CombinedSettings, sql OutputPair, result *compiler.Result) (string, *plugin.GenerateResponse, error) {

internal/cmd/process.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,17 @@ func processQuerySets(ctx context.Context, rp ResultProcessor, conf *config.Conf
104104
packageRegion := trace.StartRegion(gctx, "package")
105105
trace.Logf(gctx, "", "name=%s dir=%s plugin=%s", name, dir, lang)
106106

107-
result, failed := parse(gctx, name, dir, sql.SQL, combo, parseOpts, errout)
108-
if failed {
107+
result, status := parse(gctx, name, dir, sql.SQL, combo, parseOpts, errout)
108+
switch status {
109+
case parseFailed:
109110
packageRegion.End()
110111
errored = true
111112
return nil
113+
case parseSkipped:
114+
// Configuration was skipped (e.g., database unavailable)
115+
// This is not an error - just skip this configuration
116+
packageRegion.End()
117+
return nil
112118
}
113119
if err := rp.ProcessResult(gctx, combo, sql, result); err != nil {
114120
fmt.Fprintf(errout, "# package %s\n", name)
@@ -122,12 +128,15 @@ func processQuerySets(ctx context.Context, rp ResultProcessor, conf *config.Conf
122128
if err := grp.Wait(); err != nil {
123129
return err
124130
}
125-
if errored {
126-
for i, _ := range stderrs {
131+
// Always print stderr buffers (includes skip messages and errors)
132+
for i := range stderrs {
133+
if stderrs[i].Len() > 0 {
127134
if _, err := io.Copy(stderr, &stderrs[i]); err != nil {
128135
return err
129136
}
130137
}
138+
}
139+
if errored {
131140
return fmt.Errorf("errored")
132141
}
133142
return nil

internal/cmd/vet.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,14 @@ func (c *checker) checkSQL(ctx context.Context, s config.SQL) error {
484484
Debug: debug.Debug,
485485
}
486486

487-
result, failed := parse(ctx, name, c.Dir, s, combo, parseOpts, c.Stderr)
488-
if failed {
487+
result, status := parse(ctx, name, c.Dir, s, combo, parseOpts, c.Stderr)
488+
switch status {
489+
case parseFailed:
489490
return ErrFailedChecks
491+
case parseSkipped:
492+
// Configuration was skipped (e.g., database unavailable)
493+
// This is not an error for vet - just skip this configuration
494+
return nil
490495
}
491496

492497
var prep preparer

internal/compiler/engine.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
1616
sqliteanalyze "github.com/sqlc-dev/sqlc/internal/engine/sqlite/analyzer"
1717
"github.com/sqlc-dev/sqlc/internal/opts"
18+
"github.com/sqlc-dev/sqlc/internal/shfmt"
1819
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
1920
"github.com/sqlc-dev/sqlc/internal/x/expander"
2021
)
@@ -119,12 +120,20 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
119120
c.catalog = mssql.NewCatalog()
120121
c.selector = newDefaultSelector()
121122

122-
// MSSQL only supports database-only mode
123+
// MSSQL requires database-only mode because the teesql parser
124+
// doesn't provide AST node positions needed for query analysis
123125
if conf.Database == nil {
124-
return nil, fmt.Errorf("mssql engine requires database configuration")
126+
return nil, fmt.Errorf("%w: mssql engine requires database configuration", ErrDatabaseUnavailable)
125127
}
126-
if conf.Database.URI == "" && !conf.Database.Managed {
127-
return nil, fmt.Errorf("mssql engine requires database.uri or database.managed")
128+
hasURI := conf.Database.URI != ""
129+
if hasURI && !conf.Database.Managed {
130+
// Check if the URI is empty after environment variable substitution
131+
replacer := shfmt.NewReplacer(nil)
132+
expandedURI := replacer.Replace(conf.Database.URI)
133+
hasURI = expandedURI != ""
134+
}
135+
if !hasURI && !conf.Database.Managed {
136+
return nil, fmt.Errorf("%w: mssql engine requires database.uri to be set (ensure the environment variable is defined)", ErrDatabaseUnavailable)
128137
}
129138
c.databaseOnlyMode = true
130139
// Create the MSSQL analyzer (implements Analyzer interface)

internal/compiler/errors.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package compiler
2+
3+
import "errors"
4+
5+
// ErrDatabaseUnavailable is returned when a database engine requires a database
6+
// connection but the database is not available (e.g., missing environment variable
7+
// or no database URI configured). This error can be handled gracefully by skipping
8+
// the affected configuration.
9+
var ErrDatabaseUnavailable = errors.New("database unavailable")
10+
11+
// IsDatabaseUnavailable checks if an error indicates that a database is unavailable.
12+
// This can be used to decide whether to skip a configuration rather than fail.
13+
func IsDatabaseUnavailable(err error) bool {
14+
return errors.Is(err, ErrDatabaseUnavailable)
15+
}

internal/endtoend/case_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Exec struct {
2929
OS []string `json:"os"`
3030
Env map[string]string `json:"env"`
3131
Meta ExecMeta `json:"meta"`
32+
Requires []string `json:"requires"` // Required databases: "postgres", "mysql", "mssql"
3233
}
3334

3435
func parseStderr(t *testing.T, dir, testctx string) []byte {

internal/endtoend/endtoend_test.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ func BenchmarkExamples(b *testing.B) {
101101
}
102102

103103
type textContext struct {
104-
Mutate func(*testing.T, string) func(*config.Config)
105-
Enabled func() bool
104+
Mutate func(*testing.T, string) func(*config.Config)
105+
Enabled func() bool
106+
AvailableDatabases map[string]bool // "postgres", "mysql", "mssql"
106107
}
107108

108109
func TestReplay(t *testing.T) {
@@ -191,10 +192,17 @@ func TestReplay(t *testing.T) {
191192
t.Logf("MySQL available: %v (URI: %s)", mysqlURI != "", mysqlURI)
192193
t.Logf("MSSQL available: %v (URI: %s)", mssqlURI != "", mssqlURI)
193194

195+
availableDatabases := map[string]bool{
196+
"postgres": postgresURI != "",
197+
"mysql": mysqlURI != "",
198+
"mssql": mssqlURI != "",
199+
}
200+
194201
contexts := map[string]textContext{
195202
"base": {
196-
Mutate: func(t *testing.T, path string) func(*config.Config) { return func(c *config.Config) {} },
197-
Enabled: func() bool { return true },
203+
Mutate: func(t *testing.T, path string) func(*config.Config) { return func(c *config.Config) {} },
204+
Enabled: func() bool { return true },
205+
AvailableDatabases: availableDatabases,
198206
},
199207
"managed-db": {
200208
Mutate: func(t *testing.T, path string) func(*config.Config) {
@@ -246,6 +254,7 @@ func TestReplay(t *testing.T) {
246254
// Enabled if at least one database URI is available
247255
return postgresURI != "" || mysqlURI != "" || mssqlURI != ""
248256
},
257+
AvailableDatabases: availableDatabases,
249258
},
250259
}
251260

@@ -290,6 +299,13 @@ func TestReplay(t *testing.T) {
290299
}
291300
}
292301

302+
// Skip tests that require databases that are not available
303+
for _, required := range args.Requires {
304+
if !testctx.AvailableDatabases[required] {
305+
t.Skipf("required database not available: %s", required)
306+
}
307+
}
308+
293309
opts := cmd.Options{
294310
Env: cmd.Env{
295311
Debug: opts.DebugFromString(args.Env["SQLCDEBUG"]),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"requires": ["mssql"]
3+
}

0 commit comments

Comments
 (0)