123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- // Use of this source code is governed by an MIT-style
- // license that can be found in the LICENSE file.
- package sqlite3
- import (
- "database/sql"
- "fmt"
- "os"
- "testing"
- "time"
- )
- // The number of rows of test data to create in the source database.
- // Can be used to control how many pages are available to be backed up.
- const testRowCount = 100
- // The maximum number of seconds after which the page-by-page backup is considered to have taken too long.
- const usePagePerStepsTimeoutSeconds = 30
- // Test the backup functionality.
- func testBackup(t *testing.T, testRowCount int, usePerPageSteps bool) {
- // This function will be called multiple times.
- // It uses sql.Register(), which requires the name parameter value to be unique.
- // There does not currently appear to be a way to unregister a registered driver, however.
- // So generate a database driver name that will likely be unique.
- var driverName = fmt.Sprintf("sqlite3_testBackup_%v_%v_%v", testRowCount, usePerPageSteps, time.Now().UnixNano())
- // The driver's connection will be needed in order to perform the backup.
- driverConns := []*SQLiteConn{}
- sql.Register(driverName, &SQLiteDriver{
- ConnectHook: func(conn *SQLiteConn) error {
- driverConns = append(driverConns, conn)
- return nil
- },
- })
- // Connect to the source database.
- srcTempFilename := TempFilename(t)
- defer os.Remove(srcTempFilename)
- srcDb, err := sql.Open(driverName, srcTempFilename)
- if err != nil {
- t.Fatal("Failed to open the source database:", err)
- }
- defer srcDb.Close()
- err = srcDb.Ping()
- if err != nil {
- t.Fatal("Failed to connect to the source database:", err)
- }
- // Connect to the destination database.
- destTempFilename := TempFilename(t)
- defer os.Remove(destTempFilename)
- destDb, err := sql.Open(driverName, destTempFilename)
- if err != nil {
- t.Fatal("Failed to open the destination database:", err)
- }
- defer destDb.Close()
- err = destDb.Ping()
- if err != nil {
- t.Fatal("Failed to connect to the destination database:", err)
- }
- // Check the driver connections.
- if len(driverConns) != 2 {
- t.Fatalf("Expected 2 driver connections, but found %v.", len(driverConns))
- }
- srcDbDriverConn := driverConns[0]
- if srcDbDriverConn == nil {
- t.Fatal("The source database driver connection is nil.")
- }
- destDbDriverConn := driverConns[1]
- if destDbDriverConn == nil {
- t.Fatal("The destination database driver connection is nil.")
- }
- // Generate some test data for the given ID.
- var generateTestData = func(id int) string {
- return fmt.Sprintf("test-%v", id)
- }
- // Populate the source database with a test table containing some test data.
- tx, err := srcDb.Begin()
- if err != nil {
- t.Fatal("Failed to begin a transaction when populating the source database:", err)
- }
- _, err = srcDb.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")
- if err != nil {
- tx.Rollback()
- t.Fatal("Failed to create the source database \"test\" table:", err)
- }
- for id := 0; id < testRowCount; id++ {
- _, err = srcDb.Exec("INSERT INTO test (id, value) VALUES (?, ?)", id, generateTestData(id))
- if err != nil {
- tx.Rollback()
- t.Fatal("Failed to insert a row into the source database \"test\" table:", err)
- }
- }
- err = tx.Commit()
- if err != nil {
- t.Fatal("Failed to populate the source database:", err)
- }
- // Confirm that the destination database is initially empty.
- var destTableCount int
- err = destDb.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'").Scan(&destTableCount)
- if err != nil {
- t.Fatal("Failed to check the destination table count:", err)
- }
- if destTableCount != 0 {
- t.Fatalf("The destination database is not empty; %v table(s) found.", destTableCount)
- }
- // Prepare to perform the backup.
- backup, err := destDbDriverConn.Backup("main", srcDbDriverConn, "main")
- if err != nil {
- t.Fatal("Failed to initialize the backup:", err)
- }
- // Allow the initial page count and remaining values to be retrieved.
- // According to <https://www.sqlite.org/c3ref/backup_finish.html>, the page count and remaining values are "... only updated by sqlite3_backup_step()."
- isDone, err := backup.Step(0)
- if err != nil {
- t.Fatal("Unable to perform an initial 0-page backup step:", err)
- }
- if isDone {
- t.Fatal("Backup is unexpectedly done.")
- }
- // Check that the page count and remaining values are reasonable.
- initialPageCount := backup.PageCount()
- if initialPageCount <= 0 {
- t.Fatalf("Unexpected initial page count value: %v", initialPageCount)
- }
- initialRemaining := backup.Remaining()
- if initialRemaining <= 0 {
- t.Fatalf("Unexpected initial remaining value: %v", initialRemaining)
- }
- if initialRemaining != initialPageCount {
- t.Fatalf("Initial remaining value differs from the initial page count value; remaining: %v; page count: %v", initialRemaining, initialPageCount)
- }
- // Perform the backup.
- if usePerPageSteps {
- var startTime = time.Now().Unix()
- // Test backing-up using a page-by-page approach.
- var latestRemaining = initialRemaining
- for {
- // Perform the backup step.
- isDone, err = backup.Step(1)
- if err != nil {
- t.Fatal("Failed to perform a backup step:", err)
- }
- // The page count should remain unchanged from its initial value.
- currentPageCount := backup.PageCount()
- if currentPageCount != initialPageCount {
- t.Fatalf("Current page count differs from the initial page count; initial page count: %v; current page count: %v", initialPageCount, currentPageCount)
- }
- // There should now be one less page remaining.
- currentRemaining := backup.Remaining()
- expectedRemaining := latestRemaining - 1
- if currentRemaining != expectedRemaining {
- t.Fatalf("Unexpected remaining value; expected remaining value: %v; actual remaining value: %v", expectedRemaining, currentRemaining)
- }
- latestRemaining = currentRemaining
- if isDone {
- break
- }
- // Limit the runtime of the backup attempt.
- if (time.Now().Unix() - startTime) > usePagePerStepsTimeoutSeconds {
- t.Fatal("Backup is taking longer than expected.")
- }
- }
- } else {
- // Test the copying of all remaining pages.
- isDone, err = backup.Step(-1)
- if err != nil {
- t.Fatal("Failed to perform a backup step:", err)
- }
- if !isDone {
- t.Fatal("Backup is unexpectedly not done.")
- }
- }
- // Check that the page count and remaining values are reasonable.
- finalPageCount := backup.PageCount()
- if finalPageCount != initialPageCount {
- t.Fatalf("Final page count differs from the initial page count; initial page count: %v; final page count: %v", initialPageCount, finalPageCount)
- }
- finalRemaining := backup.Remaining()
- if finalRemaining != 0 {
- t.Fatalf("Unexpected remaining value: %v", finalRemaining)
- }
- // Finish the backup.
- err = backup.Finish()
- if err != nil {
- t.Fatal("Failed to finish backup:", err)
- }
- // Confirm that the "test" table now exists in the destination database.
- var doesTestTableExist bool
- err = destDb.QueryRow("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'test' LIMIT 1) AS test_table_exists").Scan(&doesTestTableExist)
- if err != nil {
- t.Fatal("Failed to check if the \"test\" table exists in the destination database:", err)
- }
- if !doesTestTableExist {
- t.Fatal("The \"test\" table could not be found in the destination database.")
- }
- // Confirm that the number of rows in the destination database's "test" table matches that of the source table.
- var actualTestTableRowCount int
- err = destDb.QueryRow("SELECT COUNT(*) FROM test").Scan(&actualTestTableRowCount)
- if err != nil {
- t.Fatal("Failed to determine the rowcount of the \"test\" table in the destination database:", err)
- }
- if testRowCount != actualTestTableRowCount {
- t.Fatalf("Unexpected destination \"test\" table row count; expected: %v; found: %v", testRowCount, actualTestTableRowCount)
- }
- // Check each of the rows in the destination database.
- for id := 0; id < testRowCount; id++ {
- var checkedValue string
- err = destDb.QueryRow("SELECT value FROM test WHERE id = ?", id).Scan(&checkedValue)
- if err != nil {
- t.Fatal("Failed to query the \"test\" table in the destination database:", err)
- }
- var expectedValue = generateTestData(id)
- if checkedValue != expectedValue {
- t.Fatalf("Unexpected value in the \"test\" table in the destination database; expected value: %v; actual value: %v", expectedValue, checkedValue)
- }
- }
- }
- func TestBackupStepByStep(t *testing.T) {
- testBackup(t, testRowCount, true)
- }
- func TestBackupAllRemainingPages(t *testing.T) {
- testBackup(t, testRowCount, false)
- }
- // Test the error reporting when preparing to perform a backup.
- func TestBackupError(t *testing.T) {
- const driverName = "sqlite3_TestBackupError"
- // The driver's connection will be needed in order to perform the backup.
- var dbDriverConn *SQLiteConn
- sql.Register(driverName, &SQLiteDriver{
- ConnectHook: func(conn *SQLiteConn) error {
- dbDriverConn = conn
- return nil
- },
- })
- // Connect to the database.
- dbTempFilename := TempFilename(t)
- defer os.Remove(dbTempFilename)
- db, err := sql.Open(driverName, dbTempFilename)
- if err != nil {
- t.Fatal("Failed to open the database:", err)
- }
- defer db.Close()
- db.Ping()
- // Need the driver connection in order to perform the backup.
- if dbDriverConn == nil {
- t.Fatal("Failed to get the driver connection.")
- }
- // Prepare to perform the backup.
- // Intentionally using the same connection for both the source and destination databases, to trigger an error result.
- backup, err := dbDriverConn.Backup("main", dbDriverConn, "main")
- if err == nil {
- t.Fatal("Failed to get the expected error result.")
- }
- const expectedError = "source and destination must be distinct"
- if err.Error() != expectedError {
- t.Fatalf("Unexpected error message; expected value: \"%v\"; actual value: \"%v\"", expectedError, err.Error())
- }
- if backup != nil {
- t.Fatal("Failed to get the expected nil backup result.")
- }
- }
|