go.temporal.io/server@v1.23.0/common/persistence/sql/sqlplugin/sqlite/plugin.go (about) 1 // The MIT License 2 // 3 // Copyright (c) 2021 Datadog, Inc. 4 // 5 // Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. 6 // 7 // Copyright (c) 2020 Uber Technologies, Inc. 8 // 9 // Permission is hereby granted, free of charge, to any person obtaining a copy 10 // of this software and associated documentation files (the "Software"), to deal 11 // in the Software without restriction, including without limitation the rights 12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 // copies of the Software, and to permit persons to whom the Software is 14 // furnished to do so, subject to the following conditions: 15 // 16 // The above copyright notice and this permission notice shall be included in 17 // all copies or substantial portions of the Software. 18 // 19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 // THE SOFTWARE. 26 27 package sqlite 28 29 import ( 30 "fmt" 31 "net/url" 32 "strings" 33 34 "github.com/iancoleman/strcase" 35 "github.com/jmoiron/sqlx" 36 37 "go.temporal.io/server/common/config" 38 "go.temporal.io/server/common/persistence/sql" 39 "go.temporal.io/server/common/persistence/sql/sqlplugin" 40 "go.temporal.io/server/common/resolver" 41 sqliteschema "go.temporal.io/server/schema/sqlite" 42 ) 43 44 const ( 45 // PluginName is the name of the plugin 46 PluginName = "sqlite" 47 ) 48 49 // List of non-pragma parameters 50 // Taken from https://www.sqlite.org/uri.html 51 var queryParameters = map[string]struct{}{ 52 "cache": {}, 53 "immutable": {}, 54 "mode": {}, 55 "modeof": {}, 56 "nolock": {}, 57 "psow": {}, 58 "setup": {}, 59 "vfs": {}, 60 } 61 62 type plugin struct { 63 connPool *connPool 64 } 65 66 var sqlitePlugin = &plugin{} 67 68 func init() { 69 sqlitePlugin.connPool = newConnPool() 70 sql.RegisterPlugin(PluginName, sqlitePlugin) 71 } 72 73 // CreateDB initialize the db object 74 func (p *plugin) CreateDB( 75 dbKind sqlplugin.DbKind, 76 cfg *config.SQL, 77 r resolver.ServiceResolver, 78 ) (sqlplugin.DB, error) { 79 conn, err := p.connPool.Allocate(cfg, r, p.createDBConnection) 80 if err != nil { 81 return nil, err 82 } 83 84 db := newDB(dbKind, cfg.DatabaseName, conn, nil) 85 db.OnClose(func() { p.connPool.Close(cfg) }) // remove reference 86 87 return db, nil 88 } 89 90 // CreateAdminDB initialize the db object 91 func (p *plugin) CreateAdminDB( 92 dbKind sqlplugin.DbKind, 93 cfg *config.SQL, 94 r resolver.ServiceResolver, 95 ) (sqlplugin.AdminDB, error) { 96 conn, err := p.connPool.Allocate(cfg, r, p.createDBConnection) 97 if err != nil { 98 return nil, err 99 } 100 101 db := newDB(dbKind, cfg.DatabaseName, conn, nil) 102 db.OnClose(func() { p.connPool.Close(cfg) }) // remove reference 103 104 return db, nil 105 } 106 107 // createDBConnection creates a returns a reference to a logical connection to the 108 // underlying SQL database. The returned object is tied to a single 109 // SQL database and the object can be used to perform CRUD operations on 110 // the tables in the database. 111 func (p *plugin) createDBConnection( 112 cfg *config.SQL, 113 _ resolver.ServiceResolver, 114 ) (*sqlx.DB, error) { 115 dsn, err := buildDSN(cfg) 116 if err != nil { 117 return nil, fmt.Errorf("error building DSN: %w", err) 118 } 119 120 db, err := sqlx.Connect(goSqlDriverName, dsn) 121 if err != nil { 122 return nil, err 123 } 124 125 // The following options are set based on advice from https://github.com/mattn/go-sqlite3#faq 126 // 127 // Dealing with the error `database is locked` 128 // > ... set the database connections of the SQL package to 1. 129 db.SetMaxOpenConns(1) 130 // Settings for in-memory database (should be fine for file mode as well) 131 // > Note that if the last database connection in the pool closes, the in-memory database is deleted. 132 // > Make sure the max idle connection limit is > 0, and the connection lifetime is infinite. 133 db.SetMaxIdleConns(1) 134 db.SetConnMaxIdleTime(0) 135 136 // Maps struct names in CamelCase to snake without need for db struct tags. 137 db.MapperFunc(strcase.ToSnake) 138 139 switch { 140 case cfg.ConnectAttributes["mode"] == "memory": 141 // creates temporary DB overlay in order to configure database and schemas 142 if err := p.setupSQLiteDatabase(cfg, db); err != nil { 143 _ = db.Close() 144 return nil, err 145 } 146 case cfg.ConnectAttributes["setup"] == "true": // file mode, optional setting to setup the schema 147 if err := p.setupSQLiteDatabase(cfg, db); err != nil && !isTableExistsError(err) { // benign error indicating tables already exist 148 _ = db.Close() 149 return nil, err 150 } 151 152 } 153 154 return db, nil 155 } 156 157 func (p *plugin) setupSQLiteDatabase(cfg *config.SQL, conn *sqlx.DB) error { 158 db := newDB(sqlplugin.DbKindUnknown, cfg.DatabaseName, conn, nil) 159 defer func() { _ = db.Close() }() 160 161 err := db.CreateDatabase(cfg.DatabaseName) 162 if err != nil { 163 return err 164 } 165 166 // init tables 167 return sqliteschema.SetupSchemaOnDB(db) 168 } 169 170 func buildDSN(cfg *config.SQL) (string, error) { 171 if cfg.ConnectAttributes == nil { 172 cfg.ConnectAttributes = make(map[string]string) 173 } 174 vals, err := buildDSNAttr(cfg) 175 if err != nil { 176 return "", err 177 } 178 dsn := fmt.Sprintf( 179 "file:%s?%v", 180 cfg.DatabaseName, 181 vals.Encode(), 182 ) 183 return dsn, nil 184 } 185 186 func buildDSNAttr(cfg *config.SQL) (url.Values, error) { 187 parameters := url.Values{} 188 for k, v := range cfg.ConnectAttributes { 189 key := strings.TrimSpace(k) 190 value := strings.TrimSpace(v) 191 if parameters.Get(key) != "" { 192 return nil, fmt.Errorf("duplicate connection attr: %v:%v, %v:%v", 193 key, 194 parameters.Get(key), 195 key, value, 196 ) 197 } 198 199 if _, isValidQueryParameter := queryParameters[key]; isValidQueryParameter { 200 parameters.Set(key, value) 201 continue 202 } 203 204 // assume pragma 205 parameters.Add("_pragma", fmt.Sprintf("%s=%s", key, value)) 206 } 207 // set time format 208 parameters.Add("_time_format", "sqlite") 209 return parameters, nil 210 }