/ sqldb / sqlite.go
sqlite.go
  1  //go:build !js && !(windows && (arm || 386)) && !(linux && (ppc64 || mips || mipsle || mips64))
  2  
  3  package sqldb
  4  
  5  import (
  6  	"context"
  7  	"database/sql"
  8  	"fmt"
  9  	"net/url"
 10  	"path/filepath"
 11  	"testing"
 12  
 13  	sqlite_migrate "github.com/golang-migrate/migrate/v4/database/sqlite"
 14  	"github.com/lightningnetwork/lnd/sqldb/sqlc"
 15  	"github.com/stretchr/testify/require"
 16  	_ "modernc.org/sqlite" // Register relevant drivers.
 17  )
 18  
 19  const (
 20  	// sqliteOptionPrefix is the string prefix sqlite uses to set various
 21  	// options. This is used in the following format:
 22  	//   * sqliteOptionPrefix || option_name = option_value.
 23  	sqliteOptionPrefix = "_pragma"
 24  
 25  	// sqliteTxLockImmediate is a dsn option used to ensure that write
 26  	// transactions are started immediately.
 27  	sqliteTxLockImmediate = "_txlock=immediate"
 28  )
 29  
 30  var (
 31  	// sqliteSchemaReplacements maps schema strings to their SQLite
 32  	// compatible replacements. Currently, no replacements are needed as our
 33  	// SQL schema definition files are designed for SQLite compatibility.
 34  	sqliteSchemaReplacements = map[string]string{}
 35  
 36  	// Make sure SqliteStore implements the MigrationExecutor interface.
 37  	_ MigrationExecutor = (*SqliteStore)(nil)
 38  
 39  	// Make sure SqliteStore implements the DB interface.
 40  	_ DB = (*SqliteStore)(nil)
 41  )
 42  
 43  // pragmaOption holds a key-value pair for a SQLite pragma setting.
 44  type pragmaOption struct {
 45  	name  string
 46  	value string
 47  }
 48  
 49  // SqliteStore is a database store implementation that uses a sqlite backend.
 50  type SqliteStore struct {
 51  	cfg *SqliteConfig
 52  
 53  	*BaseDB
 54  }
 55  
 56  // NewSqliteStore attempts to open a new sqlite database based on the passed
 57  // config.
 58  func NewSqliteStore(cfg *SqliteConfig, dbPath string) (*SqliteStore, error) {
 59  	// The set of pragma options are accepted using query options. For now
 60  	// we only want to ensure that foreign key constraints are properly
 61  	// enforced.
 62  	pragmaOptions := []pragmaOption{
 63  		{
 64  			name:  "foreign_keys",
 65  			value: "on",
 66  		},
 67  		{
 68  			name:  "journal_mode",
 69  			value: "WAL",
 70  		},
 71  		{
 72  			name:  "busy_timeout",
 73  			value: fmt.Sprintf("%d", cfg.busyTimeoutMs()),
 74  		},
 75  		{
 76  			// With the WAL mode, this ensures that we also do an
 77  			// extra WAL sync after each transaction. The normal
 78  			// sync mode skips this and gives better performance,
 79  			// but risks durability.
 80  			name:  "synchronous",
 81  			value: "full",
 82  		},
 83  		{
 84  			// This is used to ensure proper durability for users
 85  			// running on Mac OS. It uses the correct fsync system
 86  			// call to ensure items are fully flushed to disk.
 87  			name:  "fullfsync",
 88  			value: "true",
 89  		},
 90  		{
 91  			name:  "auto_vacuum",
 92  			value: "incremental",
 93  		},
 94  	}
 95  	sqliteOptions := make(url.Values)
 96  	for _, option := range pragmaOptions {
 97  		sqliteOptions.Add(
 98  			sqliteOptionPrefix,
 99  			fmt.Sprintf("%v=%v", option.name, option.value),
100  		)
101  	}
102  
103  	// Then we add any user specified pragma options. Note that these can
104  	// be of the form: "key=value", "key(N)" or "key".
105  	for _, option := range cfg.PragmaOptions {
106  		sqliteOptions.Add(sqliteOptionPrefix, option)
107  	}
108  
109  	// Construct the DSN which is just the database file name, appended
110  	// with the series of pragma options as a query URL string. For more
111  	// details on the formatting here, see the modernc.org/sqlite docs:
112  	// https://pkg.go.dev/modernc.org/sqlite#Driver.Open.
113  	dsn := fmt.Sprintf(
114  		"%v?%v&%v", dbPath, sqliteOptions.Encode(),
115  		sqliteTxLockImmediate,
116  	)
117  	db, err := sql.Open("sqlite", dsn)
118  	if err != nil {
119  		return nil, err
120  	}
121  
122  	// Create the migration tracker table before starting migrations to
123  	// ensure it can be used to track migration progress. Note that a
124  	// corresponding SQLC migration also creates this table, making this
125  	// operation a no-op in that context. Its purpose is to ensure
126  	// compatibility with SQLC query generation.
127  	migrationTrackerSQL := `
128  	CREATE TABLE IF NOT EXISTS migration_tracker (
129  		version INTEGER UNIQUE NOT NULL,
130  		migration_time TIMESTAMP NOT NULL
131  	);`
132  
133  	_, err = db.Exec(migrationTrackerSQL)
134  	if err != nil {
135  		return nil, fmt.Errorf("error creating migration tracker: %w",
136  			err)
137  	}
138  
139  	db.SetMaxOpenConns(cfg.MaxConns())
140  	db.SetMaxIdleConns(cfg.MaxConns())
141  	db.SetConnMaxLifetime(connIdleLifetime)
142  	queries := sqlc.New(db)
143  
144  	s := &SqliteStore{
145  		cfg: cfg,
146  		BaseDB: &BaseDB{
147  			DB:      db,
148  			Queries: queries,
149  		},
150  	}
151  
152  	return s, nil
153  }
154  
155  // GetBaseDB returns the underlying BaseDB instance for the SQLite store.
156  // It is a trivial helper method to comply with the sqldb.DB interface.
157  func (s *SqliteStore) GetBaseDB() *BaseDB {
158  	return s.BaseDB
159  }
160  
161  // ApplyAllMigrations applies both the SQLC and custom in-code migrations to the
162  // SQLite database.
163  func (s *SqliteStore) ApplyAllMigrations(ctx context.Context,
164  	migrations []MigrationConfig) error {
165  
166  	// Execute migrations unless configured to skip them.
167  	if s.cfg.SkipMigrations {
168  		return nil
169  	}
170  
171  	return ApplyMigrations(ctx, s.BaseDB, s, migrations)
172  }
173  
174  func errSqliteMigration(err error) error {
175  	return fmt.Errorf("error creating sqlite migration: %w", err)
176  }
177  
178  // ExecuteMigrations runs migrations for the sqlite database, depending on the
179  // target given, either all migrations or up to a given version.
180  func (s *SqliteStore) ExecuteMigrations(target MigrationTarget) error {
181  	driver, err := sqlite_migrate.WithInstance(
182  		s.DB, &sqlite_migrate.Config{},
183  	)
184  	if err != nil {
185  		return errSqliteMigration(err)
186  	}
187  
188  	// Populate the database with our set of schemas based on our embedded
189  	// in-memory file system.
190  	sqliteFS := newReplacerFS(sqlSchemas, sqliteSchemaReplacements)
191  	return applyMigrations(
192  		sqliteFS, driver, "sqlc/migrations", "sqlite", target,
193  	)
194  }
195  
196  // GetSchemaVersion returns the current schema version of the SQLite database.
197  func (s *SqliteStore) GetSchemaVersion() (int, bool, error) {
198  	driver, err := sqlite_migrate.WithInstance(
199  		s.DB, &sqlite_migrate.Config{},
200  	)
201  	if err != nil {
202  		return 0, false, errSqliteMigration(err)
203  	}
204  
205  	version, dirty, err := driver.Version()
206  	if err != nil {
207  		return 0, dirty, err
208  	}
209  
210  	return version, dirty, nil
211  }
212  
213  // SetSchemaVersion sets the schema version of the SQLite database.
214  //
215  // NOTE: This alters the internal database schema tracker. USE WITH CAUTION!!!
216  func (s *SqliteStore) SetSchemaVersion(version int, dirty bool) error {
217  	driver, err := sqlite_migrate.WithInstance(
218  		s.DB, &sqlite_migrate.Config{},
219  	)
220  	if err != nil {
221  		return errSqliteMigration(err)
222  	}
223  
224  	return driver.SetVersion(version, dirty)
225  }
226  
227  // NewTestSqliteDB is a helper function that creates an SQLite database for
228  // testing.
229  func NewTestSqliteDB(t testing.TB) *SqliteStore {
230  	t.Helper()
231  
232  	t.Logf("Creating new SQLite DB for testing")
233  
234  	// TODO(roasbeef): if we pass :memory: for the file name, then we get
235  	// an in mem version to speed up tests
236  	dbFileName := filepath.Join(t.TempDir(), "tmp.db")
237  	sqlDB, err := NewSqliteStore(&SqliteConfig{
238  		SkipMigrations: false,
239  	}, dbFileName)
240  	require.NoError(t, err)
241  
242  	require.NoError(t, sqlDB.ApplyAllMigrations(
243  		context.Background(), GetMigrations()),
244  	)
245  
246  	t.Cleanup(func() {
247  		require.NoError(t, sqlDB.DB.Close())
248  	})
249  
250  	return sqlDB
251  }
252  
253  // NewTestSqliteDBWithVersion is a helper function that creates an SQLite
254  // database for testing and migrates it to the given version.
255  func NewTestSqliteDBWithVersion(t *testing.T, version uint) *SqliteStore {
256  	t.Helper()
257  
258  	t.Logf("Creating new SQLite DB for testing, migrating to version %d",
259  		version)
260  
261  	// TODO(roasbeef): if we pass :memory: for the file name, then we get
262  	// an in mem version to speed up tests
263  	dbFileName := filepath.Join(t.TempDir(), "tmp.db")
264  	sqlDB, err := NewSqliteStore(&SqliteConfig{
265  		SkipMigrations: true,
266  	}, dbFileName)
267  	require.NoError(t, err)
268  
269  	err = sqlDB.ExecuteMigrations(TargetVersion(version))
270  	require.NoError(t, err)
271  
272  	t.Cleanup(func() {
273  		require.NoError(t, sqlDB.DB.Close())
274  	})
275  
276  	return sqlDB
277  }