github.com/duskeagle/pop@v4.10.1-0.20190417200916-92f2b794aab5+incompatible/dialect_sqlite.go (about) 1 // +build sqlite 2 3 package pop 4 5 import ( 6 "context" 7 "fmt" 8 "io" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/gobuffalo/fizz" 17 "github.com/gobuffalo/fizz/translators" 18 "github.com/gobuffalo/pop/columns" 19 "github.com/gobuffalo/pop/logging" 20 "github.com/markbates/going/defaults" 21 _ "github.com/mattn/go-sqlite3" // Load SQLite3 CGo driver 22 "github.com/pkg/errors" 23 ) 24 25 const nameSQLite3 = "sqlite3" 26 27 func init() { 28 AvailableDialects = append(AvailableDialects, nameSQLite3) 29 dialectSynonyms["sqlite"] = nameSQLite3 30 urlParser[nameSQLite3] = urlParserSQLite3 31 newConnection[nameSQLite3] = newSQLite 32 } 33 34 var _ dialect = &sqlite{} 35 36 type sqlite struct { 37 commonDialect 38 gil *sync.Mutex 39 smGil *sync.Mutex 40 } 41 42 func (m *sqlite) Name() string { 43 return nameSQLite3 44 } 45 46 func (m *sqlite) Details() *ConnectionDetails { 47 return m.ConnectionDetails 48 } 49 50 func (m *sqlite) URL() string { 51 return m.ConnectionDetails.Database + "?_busy_timeout=5000" 52 } 53 54 func (m *sqlite) MigrationURL() string { 55 return m.ConnectionDetails.URL 56 } 57 58 func (m *sqlite) Create(s store, model *Model, cols columns.Columns) error { 59 return m.locker(m.smGil, func() error { 60 keyType := model.PrimaryKeyType() 61 switch keyType { 62 case "int", "int64": 63 var id int64 64 w := cols.Writeable() 65 var query string 66 if len(w.Cols) > 0 { 67 query = fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", model.TableName(), w.String(), w.SymbolizedString()) 68 } else { 69 query = fmt.Sprintf("INSERT INTO %s DEFAULT VALUES", model.TableName()) 70 } 71 log(logging.SQL, query) 72 res, err := s.NamedExec(query, model.Value) 73 if err != nil { 74 return errors.WithStack(err) 75 } 76 id, err = res.LastInsertId() 77 if err == nil { 78 model.setID(id) 79 } 80 if err != nil { 81 return errors.WithStack(err) 82 } 83 return nil 84 } 85 return errors.Wrap(genericCreate(s, model, cols), "sqlite create") 86 }) 87 } 88 89 func (m *sqlite) Update(s store, model *Model, cols columns.Columns) error { 90 return m.locker(m.smGil, func() error { 91 return errors.Wrap(genericUpdate(s, model, cols), "sqlite update") 92 }) 93 } 94 95 func (m *sqlite) Destroy(s store, model *Model) error { 96 return m.locker(m.smGil, func() error { 97 return errors.Wrap(genericDestroy(s, model), "sqlite destroy") 98 }) 99 } 100 101 func (m *sqlite) SelectOne(s store, model *Model, query Query) error { 102 return m.locker(m.smGil, func() error { 103 return errors.Wrap(genericSelectOne(s, model, query), "sqlite select one") 104 }) 105 } 106 107 func (m *sqlite) SelectMany(s store, models *Model, query Query) error { 108 return m.locker(m.smGil, func() error { 109 return errors.Wrap(genericSelectMany(s, models, query), "sqlite select many") 110 }) 111 } 112 113 func (m *sqlite) Lock(fn func() error) error { 114 return m.locker(m.gil, fn) 115 } 116 117 func (m *sqlite) locker(l *sync.Mutex, fn func() error) error { 118 if defaults.String(m.Details().Options["lock"], "true") == "true" { 119 defer l.Unlock() 120 l.Lock() 121 } 122 err := fn() 123 attempts := 0 124 for err != nil && err.Error() == "database is locked" && attempts <= m.Details().RetryLimit() { 125 time.Sleep(m.Details().RetrySleep()) 126 err = fn() 127 attempts++ 128 } 129 return err 130 } 131 132 func (m *sqlite) CreateDB() error { 133 d := filepath.Dir(m.ConnectionDetails.Database) 134 err := os.MkdirAll(d, 0766) 135 if err != nil { 136 return errors.Wrapf(err, "could not create SQLite database %s", m.ConnectionDetails.Database) 137 } 138 _, err = os.Create(m.ConnectionDetails.Database) 139 if err != nil { 140 return errors.Wrapf(err, "could not create SQLite database %s", m.ConnectionDetails.Database) 141 } 142 143 log(logging.Info, "created database %s", m.ConnectionDetails.Database) 144 return nil 145 } 146 147 func (m *sqlite) DropDB() error { 148 err := os.Remove(m.ConnectionDetails.Database) 149 if err != nil { 150 return errors.Wrapf(err, "could not drop SQLite database %s", m.ConnectionDetails.Database) 151 } 152 log(logging.Info, "dropped database %s", m.ConnectionDetails.Database) 153 return nil 154 } 155 156 func (m *sqlite) TranslateSQL(sql string) string { 157 return sql 158 } 159 160 func (m *sqlite) FizzTranslator() fizz.Translator { 161 return translators.NewSQLite(m.Details().Database) 162 } 163 164 func (m *sqlite) DumpSchema(w io.Writer) error { 165 cmd := exec.Command("sqlite3", m.Details().Database, ".schema") 166 return genericDumpSchema(m.Details(), cmd, w) 167 } 168 169 func (m *sqlite) LoadSchema(r io.Reader) error { 170 cmd := exec.Command("sqlite3", m.ConnectionDetails.Database) 171 in, err := cmd.StdinPipe() 172 if err != nil { 173 return err 174 } 175 go func() { 176 defer in.Close() 177 io.Copy(in, r) 178 }() 179 log(logging.SQL, strings.Join(cmd.Args, " ")) 180 err = cmd.Start() 181 if err != nil { 182 return err 183 } 184 185 err = cmd.Wait() 186 if err != nil { 187 return err 188 } 189 190 log(logging.Info, "loaded schema for %s", m.Details().Database) 191 return nil 192 } 193 194 func (m *sqlite) TruncateAll(tx *Connection) error { 195 const tableNames = `SELECT name FROM sqlite_master WHERE type = "table"` 196 names := []struct { 197 Name string `db:"name"` 198 }{} 199 200 err := tx.RawQuery(tableNames).All(context.TODO(), &names) 201 if err != nil { 202 return err 203 } 204 if len(names) == 0 { 205 return nil 206 } 207 stmts := []string{} 208 for _, n := range names { 209 stmts = append(stmts, fmt.Sprintf("DELETE FROM %s", n.Name)) 210 } 211 return tx.RawQuery(strings.Join(stmts, "; ")).Exec() 212 } 213 214 func newSQLite(deets *ConnectionDetails) (dialect, error) { 215 deets.URL = fmt.Sprintf("sqlite3://%s", deets.Database) 216 cd := &sqlite{ 217 gil: &sync.Mutex{}, 218 smGil: &sync.Mutex{}, 219 commonDialect: commonDialect{ConnectionDetails: deets}, 220 } 221 222 return cd, nil 223 } 224 225 func urlParserSQLite3(cd *ConnectionDetails) error { 226 db := strings.TrimPrefix(cd.URL, "sqlite://") 227 cd.Database = strings.TrimPrefix(db, "sqlite3://") 228 return nil 229 }