Repository: ariga/atlas
Pull request #372 (2022-01-05 13:49:03)

cmd/action: sqlite make sure file exist


Patch:
diff --git a/cmd/action/apply.go b/cmd/action/apply.go
index ed891d66..33e5e9cb 100644
--- a/cmd/action/apply.go
+++ b/cmd/action/apply.go
@@ -54,7 +54,7 @@ func CmdApplyRun(cmd *cobra.Command, args []string) {
 
 func applyRun(d *Driver, dsn string, file string) {
 	ctx := context.Background()
-	name, err := schemaNameFromDSN(dsn)
+	name, err := SchemaNameFromDSN(dsn)
 	cobra.CheckErr(err)
 	s, err := d.InspectSchema(ctx, name, nil)
 	cobra.CheckErr(err)
diff --git a/cmd/action/inspect.go b/cmd/action/inspect.go
index f3ba602a..908a24b8 100644
--- a/cmd/action/inspect.go
+++ b/cmd/action/inspect.go
@@ -47,7 +47,7 @@ func CmdInspectRun(cmd *cobra.Command, args []string) {
 
 func inspectRun(d *Driver, dsn string) {
 	ctx := context.Background()
-	name, err := schemaNameFromDSN(dsn)
+	name, err := SchemaNameFromDSN(dsn)
 	cobra.CheckErr(err)
 	s, err := d.InspectSchema(ctx, name, nil)
 	cobra.CheckErr(err)
diff --git a/cmd/action/mux.go b/cmd/action/mux.go
index e4f91944..03eeef6a 100644
--- a/cmd/action/mux.go
+++ b/cmd/action/mux.go
@@ -4,11 +4,15 @@ import (
 	"context"
 	"database/sql"
 	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
 	"strings"
 
 	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/sqlite"
+
 	"github.com/go-sql-driver/mysql"
 )
 
@@ -33,7 +37,10 @@ func NewMux() *Mux {
 	}
 }
 
-var defaultMux = NewMux()
+var (
+	defaultMux = NewMux()
+	inMemory   = regexp.MustCompile("^file:.*:memory:$|:memory:|^file:.*mode=memory.*")
+)
 
 // RegisterProvider is used to register a Driver provider by key.
 func (u *Mux) RegisterProvider(key string, p func(string) (*Driver, error)) {
@@ -57,14 +64,15 @@ func (u *Mux) OpenAtlas(dsn string) (*Driver, error) {
 }
 
 func parseDSN(url string) (string, string, error) {
-	a := strings.Split(url, "://")
+	a := strings.SplitN(url, "://", 2)
 	if len(a) != 2 {
 		return "", "", fmt.Errorf("failed to parse dsn")
 	}
 	return a[0], a[1], nil
 }
 
-func schemaNameFromDSN(url string) (string, error) {
+// SchemaNameFromDSN parses the dsn the returns schema name
+func SchemaNameFromDSN(url string) (string, error) {
 	key, dsn, err := parseDSN(url)
 	if err != nil {
 		return "", err
@@ -86,6 +94,10 @@ func schemaNameFromDSN(url string) (string, error) {
 }
 
 func schemaName(dsn string) (string, error) {
+	err := sqliteFileExists(dsn)
+	if err != nil {
+		return "", err
+	}
 	db, err := sql.Open("sqlite3", dsn)
 	if err != nil {
 		return "", err
@@ -98,8 +110,31 @@ func schemaName(dsn string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	if len(r.Schemas) > 1 {
-		return "", fmt.Errorf("number of schemas > 1, n=%d", len(r.Schemas))
+	if len(r.Schemas) != 1 {
+		return "", fmt.Errorf("must have exactly 1 schema, got: %d", len(r.Schemas))
 	}
 	return r.Schemas[0].Name, nil
 }
+
+func sqliteFileExists(dsn string) error {
+	if !inMemory.MatchString(dsn) {
+		return fileExists(dsn)
+	}
+	return nil
+}
+
+func fileExists(dsn string) error {
+	s := strings.Split(dsn, "?")
+	f := dsn
+	if len(s) == 2 {
+		f = s[0]
+	}
+	if strings.Contains(f, "file:") {
+		f = strings.SplitAfter(f, "file:")[1]
+	}
+	f = filepath.Clean(f)
+	if _, err := os.Stat(f); err != nil {
+		return fmt.Errorf("failed opening %q: %w", f, err)
+	}
+	return nil
+}
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index 5b188cc7..d856e3a0 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -4,6 +4,7 @@ import (
 	"os"
 
 	"ariga.io/atlas/cmd/action"
+
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
 	_ "github.com/mattn/go-sqlite3"


Test patch:
diff --git a/cmd/action/mux_test.go b/cmd/action/mux_test.go
index 14d9c58f..676557ff 100644
--- a/cmd/action/mux_test.go
+++ b/cmd/action/mux_test.go
@@ -1,9 +1,13 @@
 package action_test
 
 import (
+	"io/ioutil"
+	"os"
 	"testing"
 
 	"ariga.io/atlas/cmd/action"
+
+	_ "github.com/mattn/go-sqlite3"
 	"github.com/stretchr/testify/require"
 )
 
@@ -39,3 +43,76 @@ func Test_GetDriverSuccess(t *testing.T) {
 	_, err := u.OpenAtlas("key://open")
 	require.NoError(t, err)
 }
+
+func Test_SQLiteFileDoestNotExist(t *testing.T) {
+	var tests = []struct {
+		dsn      string
+		expected string
+	}{
+		{
+			dsn:      "sqlite://test.db",
+			expected: `failed opening "test.db": stat test.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://some_random_string_like_this",
+			expected: `failed opening "some_random_string_like_this": stat some_random_string_like_this: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file:/home/fred/data.db",
+			expected: `failed opening "/home/fred/data.db": stat /home/fred/data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file:///home/fred/data.db",
+			expected: `failed opening "/home/fred/data.db": stat /home/fred/data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file://localhost/home/fred/data.db",
+			expected: `failed opening "/localhost/home/fred/data.db": stat /localhost/home/fred/data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file://darkstar/home/fred/data.db",
+			expected: `failed opening "/darkstar/home/fred/data.db": stat /darkstar/home/fred/data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file:data.db?mode=ro&cache=private",
+			expected: `failed opening "data.db": stat data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file:/home/fred/data.db?vfs=unix-dotfile",
+			expected: `failed opening "/home/fred/data.db": stat /home/fred/data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://file:data.db?mode=readonly",
+			expected: `failed opening "data.db": stat data.db: no such file or directory`,
+		},
+		{
+			dsn:      "sqlite://asdad?cache=shared&mode=memory",
+			expected: `failed opening "asdad": stat asdad: no such file or directory`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.dsn, func(t *testing.T) {
+			_, err := action.SchemaNameFromDSN(tt.dsn)
+			require.EqualError(t, err, tt.expected)
+		})
+	}
+}
+
+func Test_SQLiteFileExist(t *testing.T) {
+	r := require.New(t)
+	file, err := ioutil.TempFile("", "tmp")
+	r.NoError(err)
+	t.Cleanup(func() {
+		err := os.Remove(file.Name())
+		r.NoError(err)
+	})
+	dsn := "sqlite://file://" + file.Name()
+	_, err = action.SchemaNameFromDSN(dsn)
+	r.NoError(err)
+}
+
+func Test_SQLiteInMemory(t *testing.T) {
+	r := require.New(t)
+	_, err := action.SchemaNameFromDSN("sqlite://file:test.db?cache=shared&mode=memory")
+	r.NoError(err)
+}