Skip to content

Commit dd893d9

Browse files
Ajit Pratap Singhclaude
authored andcommitted
fix: use *bool pointers for config boolean fields (closes #134)
This fixes the boolean merging logic issue where boolean fields could not be explicitly set to false via environment variables to override config file values. Changes: - Change all boolean config fields from `bool` to `*bool` pointers: - Format.UppercaseKeywords, Format.Compact - Validation.StrictMode, Validation.Recursive - Output.Verbose - Analyze.Security, Analyze.Performance, Analyze.Complexity, Analyze.All - Server.MetricsEnabled - Add helper functions in config.go: - Bool(v bool) *bool - creates a pointer to a bool - BoolValue(p *bool) bool - returns false if nil - BoolValueOr(p *bool, defaultVal bool) bool - returns default if nil - Update mergeInto() to use != nil checks instead of truthy checks, allowing explicit false values to override defaults - Update LoadFromEnvironment() to use Bool() helper - Update ToLSPSettings/FromLSPSettings to use BoolValue()/Bool() - Update all tests to use BoolValue() for assertions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2420f05 commit dd893d9

6 files changed

Lines changed: 98 additions & 80 deletions

File tree

pkg/config/config.go

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ import (
55
"time"
66
)
77

8+
// Bool returns a pointer to the given bool value.
9+
// Use this helper when setting boolean config values.
10+
func Bool(v bool) *bool {
11+
return &v
12+
}
13+
14+
// BoolValue returns the value of a bool pointer, or false if nil.
15+
func BoolValue(p *bool) bool {
16+
if p == nil {
17+
return false
18+
}
19+
return *p
20+
}
21+
22+
// BoolValueOr returns the value of a bool pointer, or the default if nil.
23+
func BoolValueOr(p *bool, defaultVal bool) bool {
24+
if p == nil {
25+
return defaultVal
26+
}
27+
return *p
28+
}
29+
830
// Config represents unified GoSQLX configuration that can be shared across
931
// CLI, LSP server, and VSCode extension. It supports loading from files,
1032
// environment variables, and LSP initialization options.
@@ -20,17 +42,17 @@ type Config struct {
2042

2143
// FormatConfig holds SQL formatting options
2244
type FormatConfig struct {
23-
Indent int `yaml:"indent" json:"indent"` // Number of spaces for indentation (default: 2)
24-
UppercaseKeywords bool `yaml:"uppercase_keywords" json:"uppercaseKeywords"` // Convert SQL keywords to uppercase (default: true)
25-
MaxLineLength int `yaml:"max_line_length" json:"maxLineLength"` // Maximum line length before wrapping (default: 120)
26-
Compact bool `yaml:"compact" json:"compact"` // Use compact formatting (default: false)
45+
Indent int `yaml:"indent" json:"indent"` // Number of spaces for indentation (default: 2)
46+
UppercaseKeywords *bool `yaml:"uppercase_keywords" json:"uppercaseKeywords"` // Convert SQL keywords to uppercase (default: true)
47+
MaxLineLength int `yaml:"max_line_length" json:"maxLineLength"` // Maximum line length before wrapping (default: 120)
48+
Compact *bool `yaml:"compact" json:"compact"` // Use compact formatting (default: false)
2749
}
2850

2951
// ValidationConfig holds SQL validation options
3052
type ValidationConfig struct {
3153
Dialect string `yaml:"dialect" json:"dialect"` // SQL dialect: postgresql, mysql, sqlserver, oracle, sqlite (default: "postgresql")
32-
StrictMode bool `yaml:"strict_mode" json:"strictMode"` // Enable strict validation mode (default: false)
33-
Recursive bool `yaml:"recursive" json:"recursive"` // Recursively validate files in directories (default: false)
54+
StrictMode *bool `yaml:"strict_mode" json:"strictMode"` // Enable strict validation mode (default: false)
55+
Recursive *bool `yaml:"recursive" json:"recursive"` // Recursively validate files in directories (default: false)
3456
Pattern string `yaml:"pattern" json:"pattern"` // File pattern for recursive validation (default: "*.sql")
3557
Security SecurityConfig `yaml:"security" json:"security"` // Security validation settings
3658
}
@@ -43,15 +65,15 @@ type SecurityConfig struct {
4365
// OutputConfig holds output formatting options
4466
type OutputConfig struct {
4567
Format string `yaml:"format" json:"format"` // Output format: text, json, yaml (default: "text")
46-
Verbose bool `yaml:"verbose" json:"verbose"` // Enable verbose output (default: false)
68+
Verbose *bool `yaml:"verbose" json:"verbose"` // Enable verbose output (default: false)
4769
}
4870

4971
// AnalyzeConfig holds analysis options
5072
type AnalyzeConfig struct {
51-
Security bool `yaml:"security" json:"security"` // Enable security analysis (default: false)
52-
Performance bool `yaml:"performance" json:"performance"` // Enable performance analysis (default: false)
53-
Complexity bool `yaml:"complexity" json:"complexity"` // Enable complexity analysis (default: false)
54-
All bool `yaml:"all" json:"all"` // Enable all analysis types (default: false)
73+
Security *bool `yaml:"security" json:"security"` // Enable security analysis (default: false)
74+
Performance *bool `yaml:"performance" json:"performance"` // Enable performance analysis (default: false)
75+
Complexity *bool `yaml:"complexity" json:"complexity"` // Enable complexity analysis (default: false)
76+
All *bool `yaml:"all" json:"all"` // Enable all analysis types (default: false)
5577
}
5678

5779
// LSPConfig holds LSP server-specific settings
@@ -68,7 +90,7 @@ type LSPConfig struct {
6890
type ServerConfig struct {
6991
LogLevel string `yaml:"log_level" json:"logLevel"` // Log level: debug, info, warn, error (default: "info")
7092
LogFile string `yaml:"log_file" json:"logFile"` // Log file path (default: "" for stderr)
71-
MetricsEnabled bool `yaml:"metrics_enabled" json:"metricsEnabled"` // Enable metrics collection (default: true)
93+
MetricsEnabled *bool `yaml:"metrics_enabled" json:"metricsEnabled"` // Enable metrics collection (default: true)
7294
ShutdownTimeout time.Duration `yaml:"shutdown_timeout" json:"shutdownTimeout"` // Graceful shutdown timeout (default: 5s)
7395
}
7496

@@ -77,28 +99,28 @@ func DefaultConfig() *Config {
7799
return &Config{
78100
Format: FormatConfig{
79101
Indent: 2,
80-
UppercaseKeywords: true,
102+
UppercaseKeywords: Bool(true),
81103
MaxLineLength: 120,
82-
Compact: false,
104+
Compact: Bool(false),
83105
},
84106
Validation: ValidationConfig{
85107
Dialect: "postgresql",
86-
StrictMode: false,
87-
Recursive: false,
108+
StrictMode: Bool(false),
109+
Recursive: Bool(false),
88110
Pattern: "*.sql",
89111
Security: SecurityConfig{
90112
MaxFileSize: 10 * 1024 * 1024, // 10MB
91113
},
92114
},
93115
Output: OutputConfig{
94116
Format: "text",
95-
Verbose: false,
117+
Verbose: Bool(false),
96118
},
97119
Analyze: AnalyzeConfig{
98-
Security: false,
99-
Performance: false,
100-
Complexity: false,
101-
All: false,
120+
Security: Bool(false),
121+
Performance: Bool(false),
122+
Complexity: Bool(false),
123+
All: Bool(false),
102124
},
103125
LSP: LSPConfig{
104126
RateLimitRequests: 100,
@@ -111,7 +133,7 @@ func DefaultConfig() *Config {
111133
Server: ServerConfig{
112134
LogLevel: "info",
113135
LogFile: "",
114-
MetricsEnabled: true,
136+
MetricsEnabled: Bool(true),
115137
ShutdownTimeout: 5 * time.Second,
116138
},
117139
Source: "default",

pkg/config/config_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func TestDefaultConfig(t *testing.T) {
1212
if cfg.Format.Indent != 2 {
1313
t.Errorf("expected indent=2, got %d", cfg.Format.Indent)
1414
}
15-
if !cfg.Format.UppercaseKeywords {
15+
if !BoolValue(cfg.Format.UppercaseKeywords) {
1616
t.Error("expected uppercase_keywords=true")
1717
}
1818
if cfg.Format.MaxLineLength != 120 {
@@ -50,7 +50,7 @@ func TestDefaultConfig(t *testing.T) {
5050
if cfg.Server.LogLevel != "info" {
5151
t.Errorf("expected log_level=info, got %s", cfg.Server.LogLevel)
5252
}
53-
if !cfg.Server.MetricsEnabled {
53+
if !BoolValue(cfg.Server.MetricsEnabled) {
5454
t.Error("expected metrics_enabled=true")
5555
}
5656

pkg/config/loader.go

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func LoadFromEnvironment(prefix string) (*Config, error) {
109109
}
110110
if v := os.Getenv(prefix + "FORMAT_UPPERCASE_KEYWORDS"); v != "" {
111111
if b, err := strconv.ParseBool(v); err == nil {
112-
config.Format.UppercaseKeywords = b
112+
config.Format.UppercaseKeywords = Bool(b)
113113
}
114114
}
115115
if v := os.Getenv(prefix + "FORMAT_MAX_LINE_LENGTH"); v != "" {
@@ -119,7 +119,7 @@ func LoadFromEnvironment(prefix string) (*Config, error) {
119119
}
120120
if v := os.Getenv(prefix + "FORMAT_COMPACT"); v != "" {
121121
if b, err := strconv.ParseBool(v); err == nil {
122-
config.Format.Compact = b
122+
config.Format.Compact = Bool(b)
123123
}
124124
}
125125

@@ -129,12 +129,12 @@ func LoadFromEnvironment(prefix string) (*Config, error) {
129129
}
130130
if v := os.Getenv(prefix + "VALIDATION_STRICT_MODE"); v != "" {
131131
if b, err := strconv.ParseBool(v); err == nil {
132-
config.Validation.StrictMode = b
132+
config.Validation.StrictMode = Bool(b)
133133
}
134134
}
135135
if v := os.Getenv(prefix + "VALIDATION_RECURSIVE"); v != "" {
136136
if b, err := strconv.ParseBool(v); err == nil {
137-
config.Validation.Recursive = b
137+
config.Validation.Recursive = Bool(b)
138138
}
139139
}
140140
if v := os.Getenv(prefix + "VALIDATION_PATTERN"); v != "" {
@@ -152,29 +152,29 @@ func LoadFromEnvironment(prefix string) (*Config, error) {
152152
}
153153
if v := os.Getenv(prefix + "OUTPUT_VERBOSE"); v != "" {
154154
if b, err := strconv.ParseBool(v); err == nil {
155-
config.Output.Verbose = b
155+
config.Output.Verbose = Bool(b)
156156
}
157157
}
158158

159159
// Analyze settings
160160
if v := os.Getenv(prefix + "ANALYZE_SECURITY"); v != "" {
161161
if b, err := strconv.ParseBool(v); err == nil {
162-
config.Analyze.Security = b
162+
config.Analyze.Security = Bool(b)
163163
}
164164
}
165165
if v := os.Getenv(prefix + "ANALYZE_PERFORMANCE"); v != "" {
166166
if b, err := strconv.ParseBool(v); err == nil {
167-
config.Analyze.Performance = b
167+
config.Analyze.Performance = Bool(b)
168168
}
169169
}
170170
if v := os.Getenv(prefix + "ANALYZE_COMPLEXITY"); v != "" {
171171
if b, err := strconv.ParseBool(v); err == nil {
172-
config.Analyze.Complexity = b
172+
config.Analyze.Complexity = Bool(b)
173173
}
174174
}
175175
if v := os.Getenv(prefix + "ANALYZE_ALL"); v != "" {
176176
if b, err := strconv.ParseBool(v); err == nil {
177-
config.Analyze.All = b
177+
config.Analyze.All = Bool(b)
178178
}
179179
}
180180

@@ -217,7 +217,7 @@ func LoadFromEnvironment(prefix string) (*Config, error) {
217217
}
218218
if v := os.Getenv(prefix + "SERVER_METRICS_ENABLED"); v != "" {
219219
if b, err := strconv.ParseBool(v); err == nil {
220-
config.Server.MetricsEnabled = b
220+
config.Server.MetricsEnabled = Bool(b)
221221
}
222222
}
223223
if v := os.Getenv(prefix + "SERVER_SHUTDOWN_TIMEOUT"); v != "" {
@@ -257,36 +257,32 @@ func Merge(configs ...*Config) *Config {
257257
return result
258258
}
259259

260-
// mergeInto merges src into dst, with non-zero values from src taking precedence.
261-
//
262-
// NOTE: Boolean fields are only merged when true. This is a known limitation that
263-
// prevents setting booleans to false via environment variables to override a config
264-
// file that has them set to true. A proper fix would require using *bool pointers
265-
// for all boolean fields to distinguish between "not set" and "explicitly set to false".
266-
// See: https://github.com/ajitpratap0/GoSQLX/issues/134 for tracking issue.
260+
// mergeInto merges src into dst, with non-nil/non-zero values from src taking precedence.
261+
// Boolean fields use *bool pointers to distinguish between "not set" (nil) and
262+
// "explicitly set to false" (*false), allowing proper override behavior.
267263
func mergeInto(dst, src *Config) {
268264
// Merge Format
269265
if src.Format.Indent != 0 {
270266
dst.Format.Indent = src.Format.Indent
271267
}
272-
if src.Format.UppercaseKeywords {
268+
if src.Format.UppercaseKeywords != nil {
273269
dst.Format.UppercaseKeywords = src.Format.UppercaseKeywords
274270
}
275271
if src.Format.MaxLineLength != 0 {
276272
dst.Format.MaxLineLength = src.Format.MaxLineLength
277273
}
278-
if src.Format.Compact {
274+
if src.Format.Compact != nil {
279275
dst.Format.Compact = src.Format.Compact
280276
}
281277

282278
// Merge Validation
283279
if src.Validation.Dialect != "" {
284280
dst.Validation.Dialect = src.Validation.Dialect
285281
}
286-
if src.Validation.StrictMode {
282+
if src.Validation.StrictMode != nil {
287283
dst.Validation.StrictMode = src.Validation.StrictMode
288284
}
289-
if src.Validation.Recursive {
285+
if src.Validation.Recursive != nil {
290286
dst.Validation.Recursive = src.Validation.Recursive
291287
}
292288
if src.Validation.Pattern != "" {
@@ -300,21 +296,21 @@ func mergeInto(dst, src *Config) {
300296
if src.Output.Format != "" {
301297
dst.Output.Format = src.Output.Format
302298
}
303-
if src.Output.Verbose {
299+
if src.Output.Verbose != nil {
304300
dst.Output.Verbose = src.Output.Verbose
305301
}
306302

307303
// Merge Analyze
308-
if src.Analyze.Security {
304+
if src.Analyze.Security != nil {
309305
dst.Analyze.Security = src.Analyze.Security
310306
}
311-
if src.Analyze.Performance {
307+
if src.Analyze.Performance != nil {
312308
dst.Analyze.Performance = src.Analyze.Performance
313309
}
314-
if src.Analyze.Complexity {
310+
if src.Analyze.Complexity != nil {
315311
dst.Analyze.Complexity = src.Analyze.Complexity
316312
}
317-
if src.Analyze.All {
313+
if src.Analyze.All != nil {
318314
dst.Analyze.All = src.Analyze.All
319315
}
320316

@@ -345,7 +341,7 @@ func mergeInto(dst, src *Config) {
345341
if src.Server.LogFile != "" {
346342
dst.Server.LogFile = src.Server.LogFile
347343
}
348-
if src.Server.MetricsEnabled {
344+
if src.Server.MetricsEnabled != nil {
349345
dst.Server.MetricsEnabled = src.Server.MetricsEnabled
350346
}
351347
if src.Server.ShutdownTimeout != 0 {

pkg/config/loader_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func TestLoadFromEnvironment(t *testing.T) {
210210
if cfg.Validation.Dialect != "mysql" {
211211
t.Errorf("expected dialect=mysql, got %s", cfg.Validation.Dialect)
212212
}
213-
if !cfg.Validation.StrictMode {
213+
if !BoolValue(cfg.Validation.StrictMode) {
214214
t.Error("expected strict_mode=true")
215215
}
216216
if cfg.LSP.RateLimitRequests != 200 {

0 commit comments

Comments
 (0)