2019-11-18 12:03:31 +03:00
// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
2016-08-21 01:57:12 +03:00
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
2024-01-25 16:36:27 +03:00
//go:build cgo
2019-11-18 12:03:31 +03:00
// +build cgo
2016-08-21 01:57:12 +03:00
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.
2016-09-07 18:15:10 +03:00
const testRowCount = 100
2016-08-21 01:57:12 +03:00
// 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 )
}
2016-09-23 15:41:32 +03:00
// 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." )
}
}