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  }