diff --git a/internal/sql/sqlpath/read.go b/internal/sql/sqlpath/read.go index 24072001c9..02c8b2855c 100644 --- a/internal/sql/sqlpath/read.go +++ b/internal/sql/sqlpath/read.go @@ -9,14 +9,33 @@ import ( "github.com/sqlc-dev/sqlc/internal/migrations" ) -// Return a list of SQL files in the listed paths. Only includes files ending -// in .sql. Omits hidden files, directories, and migrations. -func Glob(paths []string) ([]string, error) { - var files []string +// Return a list of SQL files in the listed paths. +// +// Only includes files ending in .sql. Omits hidden files, directories, and +// down migrations. + +// If a path contains *, ?, [, or ], treat the path as a pattern and expand it +// filepath.Glob. +func Glob(patterns []string) ([]string, error) { + var files, paths []string + for _, pattern := range patterns { + if strings.ContainsAny(pattern, "*?[]") { + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + // if len(matches) == 0 { + // slog.Warn("zero files matched", "pattern", pattern) + // } + paths = append(paths, matches...) + } else { + paths = append(paths, pattern) + } + } for _, path := range paths { f, err := os.Stat(path) if err != nil { - return nil, fmt.Errorf("path %s does not exist", path) + return nil, fmt.Errorf("path error: %w", err) } if f.IsDir() { listing, err := os.ReadDir(path) diff --git a/internal/sql/sqlpath/read_test.go b/internal/sql/sqlpath/read_test.go new file mode 100644 index 0000000000..774561057e --- /dev/null +++ b/internal/sql/sqlpath/read_test.go @@ -0,0 +1,208 @@ +package sqlpath + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +// Returns a list of SQL files from given paths. +func TestReturnsListOfSQLFiles(t *testing.T) { + // Arrange + paths := []string{"testdata/file1.sql", "testdata/file2.sql"} + + // Act + result, err := Glob(paths) + + // Assert + expected := []string{"testdata/file1.sql", "testdata/file2.sql"} + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v, %v", expected, result, cmp.Diff(expected, result)) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestReturnsNilListWhenNoSQLFilesFound(t *testing.T) { + // Arrange + paths := []string{"testdata/extra.txt"} + + // Act + result, err := Glob(paths) + // Assert + var expected []string + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v, %v", expected, result, cmp.Diff(expected, result)) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestIgnoresHiddenFilesWhenSearchingForSQLFiles(t *testing.T) { + // Arrange + paths := []string{"testdata/.hidden.sql"} + + // Act + result, err := Glob(paths) + + // Assert + var expected []string + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestIgnoresNonSQLFilesWhenSearchingForSQLFiles(t *testing.T) { + // Arrange + paths := []string{"testdata/extra.txt"} + + // Act + result, err := Glob(paths) + + // Assert + var expected []string + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestExcludesSQLFilesEndingWithDownSQLWhenSearchingForSQLFiles(t *testing.T) { + // Arrange + paths := []string{"testdata/file1.sql", "testdata/file3.down.sql"} + + // Act + result, err := Glob(paths) + + // Assert + expected := []string{"testdata/file1.sql"} + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestReturnsErrorWhenPathDoesNotExist(t *testing.T) { + // Arrange + paths := []string{"non_existent_path"} + + // Act + result, err := Glob(paths) + + // Assert + var expected []string + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err == nil { + t.Errorf("Expected an error, but got nil") + } else { + expectedError := fmt.Errorf("path error: stat non_existent_path: no such file or directory") + if !cmp.Equal(err.Error(), expectedError.Error()) { + t.Errorf("Expected error %v, but got %v", expectedError, err) + } + } +} + +func TestReturnsErrorWhenDirectoryCannotBeRead(t *testing.T) { + // Arrange + paths := []string{"testdata/unreadable"} + + // Act + result, err := Glob(paths) + + // Assert + var expected []string + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err == nil { + t.Errorf("Expected an error, but got nil") + } else { + expectedError := fmt.Errorf("path error: stat testdata/unreadable: no such file or directory") + if !cmp.Equal(err.Error(), expectedError.Error()) { + t.Errorf("Expected error %v, but got %v", expectedError, err) + } + } +} + +func TestDoesNotIncludesSQLFilesWithUppercaseExtension(t *testing.T) { + // Arrange + paths := []string{"testdata/file4.SQL"} + + // Act + result, err := Glob(paths) + + // Assert + var expected []string + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestNotIncludesHiddenFilesAnyPath(t *testing.T) { + // Arrange + paths := []string{ + "./testdata/.hiddendir/file1.sql", // pass + "./testdata/.hidden.sql", // skip + } + + // Act + result, err := Glob(paths) + + // Assert + expectedAny := [][]string{ + {"./testdata/.hiddendir/file1.sql"}, + {"testdata/.hiddendir/file1.sql"}, + } + + match := false + for _, expected := range expectedAny { + if cmp.Equal(result, expected) { + match = true + break + } + } + if !match { + t.Errorf("Expected any of %v, but got %v", expectedAny, result) + } + + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} + +func TestFollowSymlinks(t *testing.T) { + // Arrange + paths := []string{"testdata/symlink", "testdata/file1.symlink.sql"} + + // Act + result, err := Glob(paths) + + // Assert + expected := []string{ + "testdata/symlink/file1.sql", + "testdata/symlink/file1.symlink.sql", + "testdata/symlink/file2.sql", + "testdata/file1.symlink.sql", + } + if !cmp.Equal(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + if err != nil { + t.Errorf("Expected no error, but got %v", err) + } +} diff --git a/internal/sql/sqlpath/testdata/.hidden.sql b/internal/sql/sqlpath/testdata/.hidden.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/.hiddendir/file1.sql b/internal/sql/sqlpath/testdata/.hiddendir/file1.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/extra.txt b/internal/sql/sqlpath/testdata/extra.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/file1.sql b/internal/sql/sqlpath/testdata/file1.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/file1.symlink.sql b/internal/sql/sqlpath/testdata/file1.symlink.sql new file mode 120000 index 0000000000..7c823b3226 --- /dev/null +++ b/internal/sql/sqlpath/testdata/file1.symlink.sql @@ -0,0 +1 @@ +./file1.sql \ No newline at end of file diff --git a/internal/sql/sqlpath/testdata/file2.sql b/internal/sql/sqlpath/testdata/file2.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/file3.down.sql b/internal/sql/sqlpath/testdata/file3.down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/file4.SQL b/internal/sql/sqlpath/testdata/file4.SQL new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/subdir/file2.sql b/internal/sql/sqlpath/testdata/subdir/file2.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/sql/sqlpath/testdata/symlink b/internal/sql/sqlpath/testdata/symlink new file mode 120000 index 0000000000..945c9b46d6 --- /dev/null +++ b/internal/sql/sqlpath/testdata/symlink @@ -0,0 +1 @@ +. \ No newline at end of file