Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cmd/gosqlx/cmd/cmd_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cmd

import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// Valid output format constants
const (
OutputFormatText = "text"
OutputFormatJSON = "json"
OutputFormatSARIF = "sarif"
)

// ValidOutputFormats lists all supported output formats for validation
var ValidOutputFormats = []string{OutputFormatText, OutputFormatJSON, OutputFormatSARIF}

// trackChangedFlags returns a map of flag names that were explicitly set on the command line.
// This includes both local flags and parent persistent flags.
func trackChangedFlags(cmd *cobra.Command) map[string]bool {
flagsChanged := make(map[string]bool)
cmd.Flags().Visit(func(f *pflag.Flag) {
flagsChanged[f.Name] = true
})
if cmd.Parent() != nil && cmd.Parent().PersistentFlags() != nil {
cmd.Parent().PersistentFlags().Visit(func(f *pflag.Flag) {
flagsChanged[f.Name] = true
})
}
return flagsChanged
}
70 changes: 50 additions & 20 deletions cmd/gosqlx/cmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cmd
import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/config"
)
Expand Down Expand Up @@ -58,6 +58,13 @@ func formatRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("no input provided: specify file paths or pipe SQL via stdin")
}

// If single argument that looks like inline SQL (not a file), format it directly
if len(args) == 1 {
if _, err := os.Stat(args[0]); err != nil && looksLikeSQL(args[0]) {
return formatInlineSQL(cmd, args[0])
}
}

// Load configuration with CLI flag overrides
cfg, err := config.LoadDefault()
if err != nil {
Expand All @@ -66,15 +73,7 @@ func formatRun(cmd *cobra.Command, args []string) error {
}

// Track which flags were explicitly set
flagsChanged := make(map[string]bool)
cmd.Flags().Visit(func(f *pflag.Flag) {
flagsChanged[f.Name] = true
})
if cmd.Parent() != nil && cmd.Parent().PersistentFlags() != nil {
cmd.Parent().PersistentFlags().Visit(func(f *pflag.Flag) {
flagsChanged[f.Name] = true
})
}
flagsChanged := trackChangedFlags(cmd)

// Create formatter options from config and flags
opts := FormatterOptionsFromConfig(cfg, flagsChanged, FormatterFlags{
Expand Down Expand Up @@ -135,15 +134,7 @@ func formatFromStdin(cmd *cobra.Command) error {
}

// Track which flags were explicitly set
flagsChanged := make(map[string]bool)
cmd.Flags().Visit(func(f *pflag.Flag) {
flagsChanged[f.Name] = true
})
if cmd.Parent() != nil && cmd.Parent().PersistentFlags() != nil {
cmd.Parent().PersistentFlags().Visit(func(f *pflag.Flag) {
flagsChanged[f.Name] = true
})
}
flagsChanged := trackChangedFlags(cmd)

// Create formatter options
opts := FormatterOptionsFromConfig(cfg, flagsChanged, FormatterFlags{
Expand Down Expand Up @@ -179,14 +170,53 @@ func formatFromStdin(cmd *cobra.Command) error {
return nil
}

// Write formatted output
// Write formatted output with trailing newline
if !strings.HasSuffix(formattedSQL, "\n") {
formattedSQL += "\n"
}
if err := WriteOutput([]byte(formattedSQL), outputFile, cmd.OutOrStdout()); err != nil {
return err
}

return nil
}

// formatInlineSQL formats inline SQL passed as a command argument
func formatInlineSQL(cmd *cobra.Command, sql string) error {
// Load configuration
cfg, err := config.LoadDefault()
if err != nil {
cfg = config.DefaultConfig()
}

// Track which flags were explicitly set
flagsChanged := trackChangedFlags(cmd)

opts := FormatterOptionsFromConfig(cfg, flagsChanged, FormatterFlags{
InPlace: false,
IndentSize: formatIndentSize,
Uppercase: formatUppercase,
Compact: formatCompact,
Check: formatCheck,
MaxLine: formatMaxLine,
Verbose: verbose,
Output: outputFile,
})

formatter := NewFormatter(cmd.OutOrStdout(), cmd.ErrOrStderr(), opts)
formattedSQL, err := formatter.formatSQL(sql)
if err != nil {
return fmt.Errorf("formatting failed: %w", err)
}

// Ensure trailing newline
if !strings.HasSuffix(formattedSQL, "\n") {
formattedSQL += "\n"
}

return WriteOutput([]byte(formattedSQL), outputFile, cmd.OutOrStdout())
}

func init() {
rootCmd.AddCommand(formatCmd)

Expand Down
6 changes: 5 additions & 1 deletion cmd/gosqlx/cmd/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,11 @@ func (f *Formatter) Format(args []string) (*FormatterResult, error) {
}
result.FormattedFiles++
} else {
fmt.Fprint(f.Out, fileResult.Formatted)
output := fileResult.Formatted
if !strings.HasSuffix(output, "\n") {
output += "\n"
}
fmt.Fprint(f.Out, output)
result.FormattedFiles++
}
}
Expand Down
18 changes: 6 additions & 12 deletions cmd/gosqlx/cmd/input_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,23 +163,17 @@ func isValidSQLFileExtension(ext string) bool {

// looksLikeSQL performs basic heuristic checks to see if input looks like SQL
func looksLikeSQL(input string) bool {
// Convert to uppercase for case-insensitive checking
upperInput := strings.ToUpper(strings.TrimSpace(input))

// Check for common SQL keywords at the beginning
sqlKeywords := []string{
upper := strings.ToUpper(strings.TrimSpace(input))
keywords := []string{
"SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER",
"WITH", "EXPLAIN", "ANALYZE", "SHOW", "DESCRIBE", "DESC", "MERGE",
"TRUNCATE", "WITH", "MERGE", "EXPLAIN", "ANALYZE", "SHOW", "DESCRIBE", "DESC",
}

for _, keyword := range sqlKeywords {
if strings.HasPrefix(upperInput, keyword) {
for _, kw := range keywords {
if strings.HasPrefix(upper, kw+" ") || strings.HasPrefix(upper, kw+"\n") || strings.HasPrefix(upper, kw+"\t") || upper == kw {
return true
}
}

// If no keywords found but contains semicolon, might still be SQL
return strings.Contains(input, ";")
return false
}

// ExpandFileArgs expands file arguments, handling directories and wildcards
Expand Down
2 changes: 1 addition & 1 deletion cmd/gosqlx/cmd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func TestLooksLikeSQL(t *testing.T) {
{"CreateStatement", "CREATE TABLE users (id INT)", true},
{"WithCTE", "WITH cte AS (SELECT 1) SELECT * FROM cte", true},
{"LowercaseSelect", "select id from users", true},
{"SQLWithSemicolon", "some random text; but has semicolon", true},
{"SQLWithSemicolon", "some random text; but has semicolon", false},
{"PlainText", "this is just plain text", false},
{"Filename", "myfile.sql", false},
{"Path", "/path/to/file", false},
Expand Down
62 changes: 58 additions & 4 deletions cmd/gosqlx/cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ func lintRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("no input provided: specify file paths or pipe SQL via stdin")
}

// If single argument that looks like inline SQL (not a file), lint it directly
if len(args) == 1 {
if _, statErr := os.Stat(args[0]); statErr != nil && looksLikeSQL(args[0]) {
return lintInlineSQL(cmd, args[0])
}
}

// Create linter with default rules
l := createLinter()

Expand Down Expand Up @@ -167,7 +174,7 @@ func lintRun(cmd *cobra.Command, args []string) error {
}
}

// Exit with error code if there were violations or file errors
// Return error if there were violations or file errors
errorCount := 0
warningCount := 0
fileErrorCount := 0
Expand All @@ -185,12 +192,11 @@ func lintRun(cmd *cobra.Command, args []string) error {
}
}

// Exit with error if there are file errors, lint errors, or warnings with fail-on-warn flag
if fileErrorCount > 0 {
return fmt.Errorf("%d file(s) had errors", fileErrorCount)
}
if errorCount > 0 || (lintFailOnWarn && warningCount > 0) {
os.Exit(1)
return fmt.Errorf("%d error(s) and %d warning(s) found", errorCount, warningCount)
}

return nil
Expand Down Expand Up @@ -274,7 +280,55 @@ func lintFromStdin(cmd *cobra.Command) error {
}

if errorCount > 0 || (lintFailOnWarn && warningCount > 0) {
os.Exit(1)
return fmt.Errorf("%d error(s) and %d warning(s) found", errorCount, warningCount)
}

return nil
}

// lintInlineSQL lints inline SQL passed as a command argument
func lintInlineSQL(cmd *cobra.Command, sql string) error {
l := createLinter()
result := l.LintString(sql, "inline")

var outputBuf bytes.Buffer
outWriter := io.Writer(cmd.OutOrStdout())
if outputFile != "" {
outWriter = &outputBuf
}

if len(result.Violations) == 0 {
fmt.Fprintln(outWriter, "No violations found.")
if outputFile != "" {
return WriteOutput(outputBuf.Bytes(), outputFile, cmd.OutOrStdout())
}
return nil
}

fmt.Fprintf(outWriter, "Found %d violation(s):\n\n", len(result.Violations))
for i, violation := range result.Violations {
fmt.Fprintf(outWriter, "%d. %s\n", i+1, linter.FormatViolation(violation))
}

if outputFile != "" {
if err := WriteOutput(outputBuf.Bytes(), outputFile, cmd.OutOrStdout()); err != nil {
return err
}
}

errorCount := 0
warningCount := 0
for _, violation := range result.Violations {
switch violation.Severity {
case linter.SeverityError:
errorCount++
case linter.SeverityWarning:
warningCount++
}
}

if errorCount > 0 || (lintFailOnWarn && warningCount > 0) {
return fmt.Errorf("%d error(s) and %d warning(s) found", errorCount, warningCount)
}

return nil
Expand Down
9 changes: 5 additions & 4 deletions cmd/gosqlx/cmd/stdin_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,13 @@ func ReadInputWithFallback(args []string) (*InputResult, error) {
return GetInputSource(inputArg)
}

// ShouldReadFromStdin determines if we should read from stdin based on args
// This is a simple helper for commands that need to check stdin state
// ShouldReadFromStdin determines if we should read from stdin based on args.
// Returns true only when stdin actually has piped data available.
// When stdin is a TTY (interactive terminal), returns false to avoid blocking.
func ShouldReadFromStdin(args []string) bool {
// Explicit stdin marker
// Explicit stdin marker — only honor if stdin is actually piped
if len(args) > 0 && args[0] == "-" {
return true
return IsStdinPipe()
}

// No args and stdin is piped
Expand Down
Loading
Loading