1// Copyright (C) 2019 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
2//
3// Use of this source code is governed by an MIT-style
4// license that can be found in the LICENSE file.
5
6// +build cgo
7
8package sqlite3
9
10import (
11	"database/sql"
12	"fmt"
13	"os"
14	"testing"
15	"time"
16)
17
18// The number of rows of test data to create in the source database.
19// Can be used to control how many pages are available to be backed up.
20const testRowCount = 100
21
22// The maximum number of seconds after which the page-by-page backup is considered to have taken too long.
23const usePagePerStepsTimeoutSeconds = 30
24
25// Test the backup functionality.
26func testBackup(t *testing.T, testRowCount int, usePerPageSteps bool) {
27	// This function will be called multiple times.
28	// It uses sql.Register(), which requires the name parameter value to be unique.
29	// There does not currently appear to be a way to unregister a registered driver, however.
30	// So generate a database driver name that will likely be unique.
31	var driverName = fmt.Sprintf("sqlite3_testBackup_%v_%v_%v", testRowCount, usePerPageSteps, time.Now().UnixNano())
32
33	// The driver's connection will be needed in order to perform the backup.
34	driverConns := []*SQLiteConn{}
35	sql.Register(driverName, &SQLiteDriver{
36		ConnectHook: func(conn *SQLiteConn) error {
37			driverConns = append(driverConns, conn)
38			return nil
39		},
40	})
41
42	// Connect to the source database.
43	srcTempFilename := TempFilename(t)
44	defer os.Remove(srcTempFilename)
45	srcDb, err := sql.Open(driverName, srcTempFilename)
46	if err != nil {
47		t.Fatal("Failed to open the source database:", err)
48	}
49	defer srcDb.Close()
50	err = srcDb.Ping()
51	if err != nil {
52		t.Fatal("Failed to connect to the source database:", err)
53	}
54
55	// Connect to the destination database.
56	destTempFilename := TempFilename(t)
57	defer os.Remove(destTempFilename)
58	destDb, err := sql.Open(driverName, destTempFilename)
59	if err != nil {
60		t.Fatal("Failed to open the destination database:", err)
61	}
62	defer destDb.Close()
63	err = destDb.Ping()
64	if err != nil {
65		t.Fatal("Failed to connect to the destination database:", err)
66	}
67
68	// Check the driver connections.
69	if len(driverConns) != 2 {
70		t.Fatalf("Expected 2 driver connections, but found %v.", len(driverConns))
71	}
72	srcDbDriverConn := driverConns[0]
73	if srcDbDriverConn == nil {
74		t.Fatal("The source database driver connection is nil.")
75	}
76	destDbDriverConn := driverConns[1]
77	if destDbDriverConn == nil {
78		t.Fatal("The destination database driver connection is nil.")
79	}
80
81	// Generate some test data for the given ID.
82	var generateTestData = func(id int) string {
83		return fmt.Sprintf("test-%v", id)
84	}
85
86	// Populate the source database with a test table containing some test data.
87	tx, err := srcDb.Begin()
88	if err != nil {
89		t.Fatal("Failed to begin a transaction when populating the source database:", err)
90	}
91	_, err = srcDb.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")
92	if err != nil {
93		tx.Rollback()
94		t.Fatal("Failed to create the source database \"test\" table:", err)
95	}
96	for id := 0; id < testRowCount; id++ {
97		_, err = srcDb.Exec("INSERT INTO test (id, value) VALUES (?, ?)", id, generateTestData(id))
98		if err != nil {
99			tx.Rollback()
100			t.Fatal("Failed to insert a row into the source database \"test\" table:", err)
101		}
102	}
103	err = tx.Commit()
104	if err != nil {
105		t.Fatal("Failed to populate the source database:", err)
106	}
107
108	// Confirm that the destination database is initially empty.
109	var destTableCount int
110	err = destDb.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'").Scan(&destTableCount)
111	if err != nil {
112		t.Fatal("Failed to check the destination table count:", err)
113	}
114	if destTableCount != 0 {
115		t.Fatalf("The destination database is not empty; %v table(s) found.", destTableCount)
116	}
117
118	// Prepare to perform the backup.
119	backup, err := destDbDriverConn.Backup("main", srcDbDriverConn, "main")
120	if err != nil {
121		t.Fatal("Failed to initialize the backup:", err)
122	}
123
124	// Allow the initial page count and remaining values to be retrieved.
125	// According to <https://www.sqlite.org/c3ref/backup_finish.html>, the page count and remaining values are "... only updated by sqlite3_backup_step()."
126	isDone, err := backup.Step(0)
127	if err != nil {
128		t.Fatal("Unable to perform an initial 0-page backup step:", err)
129	}
130	if isDone {
131		t.Fatal("Backup is unexpectedly done.")
132	}
133
134	// Check that the page count and remaining values are reasonable.
135	initialPageCount := backup.PageCount()
136	if initialPageCount <= 0 {
137		t.Fatalf("Unexpected initial page count value: %v", initialPageCount)
138	}
139	initialRemaining := backup.Remaining()
140	if initialRemaining <= 0 {
141		t.Fatalf("Unexpected initial remaining value: %v", initialRemaining)
142	}
143	if initialRemaining != initialPageCount {
144		t.Fatalf("Initial remaining value differs from the initial page count value; remaining: %v; page count: %v", initialRemaining, initialPageCount)
145	}
146
147	// Perform the backup.
148	if usePerPageSteps {
149		var startTime = time.Now().Unix()
150
151		// Test backing-up using a page-by-page approach.
152		var latestRemaining = initialRemaining
153		for {
154			// Perform the backup step.
155			isDone, err = backup.Step(1)
156			if err != nil {
157				t.Fatal("Failed to perform a backup step:", err)
158			}
159
160			// The page count should remain unchanged from its initial value.
161			currentPageCount := backup.PageCount()
162			if currentPageCount != initialPageCount {
163				t.Fatalf("Current page count differs from the initial page count; initial page count: %v; current page count: %v", initialPageCount, currentPageCount)
164			}
165
166			// There should now be one less page remaining.
167			currentRemaining := backup.Remaining()
168			expectedRemaining := latestRemaining - 1
169			if currentRemaining != expectedRemaining {
170				t.Fatalf("Unexpected remaining value; expected remaining value: %v; actual remaining value: %v", expectedRemaining, currentRemaining)
171			}
172			latestRemaining = currentRemaining
173
174			if isDone {
175				break
176			}
177
178			// Limit the runtime of the backup attempt.
179			if (time.Now().Unix() - startTime) > usePagePerStepsTimeoutSeconds {
180				t.Fatal("Backup is taking longer than expected.")
181			}
182		}
183	} else {
184		// Test the copying of all remaining pages.
185		isDone, err = backup.Step(-1)
186		if err != nil {
187			t.Fatal("Failed to perform a backup step:", err)
188		}
189		if !isDone {
190			t.Fatal("Backup is unexpectedly not done.")
191		}
192	}
193
194	// Check that the page count and remaining values are reasonable.
195	finalPageCount := backup.PageCount()
196	if finalPageCount != initialPageCount {
197		t.Fatalf("Final page count differs from the initial page count; initial page count: %v; final page count: %v", initialPageCount, finalPageCount)
198	}
199	finalRemaining := backup.Remaining()
200	if finalRemaining != 0 {
201		t.Fatalf("Unexpected remaining value: %v", finalRemaining)
202	}
203
204	// Finish the backup.
205	err = backup.Finish()
206	if err != nil {
207		t.Fatal("Failed to finish backup:", err)
208	}
209
210	// Confirm that the "test" table now exists in the destination database.
211	var doesTestTableExist bool
212	err = destDb.QueryRow("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'test' LIMIT 1) AS test_table_exists").Scan(&doesTestTableExist)
213	if err != nil {
214		t.Fatal("Failed to check if the \"test\" table exists in the destination database:", err)
215	}
216	if !doesTestTableExist {
217		t.Fatal("The \"test\" table could not be found in the destination database.")
218	}
219
220	// Confirm that the number of rows in the destination database's "test" table matches that of the source table.
221	var actualTestTableRowCount int
222	err = destDb.QueryRow("SELECT COUNT(*) FROM test").Scan(&actualTestTableRowCount)
223	if err != nil {
224		t.Fatal("Failed to determine the rowcount of the \"test\" table in the destination database:", err)
225	}
226	if testRowCount != actualTestTableRowCount {
227		t.Fatalf("Unexpected destination \"test\" table row count; expected: %v; found: %v", testRowCount, actualTestTableRowCount)
228	}
229
230	// Check each of the rows in the destination database.
231	for id := 0; id < testRowCount; id++ {
232		var checkedValue string
233		err = destDb.QueryRow("SELECT value FROM test WHERE id = ?", id).Scan(&checkedValue)
234		if err != nil {
235			t.Fatal("Failed to query the \"test\" table in the destination database:", err)
236		}
237
238		var expectedValue = generateTestData(id)
239		if checkedValue != expectedValue {
240			t.Fatalf("Unexpected value in the \"test\" table in the destination database; expected value: %v; actual value: %v", expectedValue, checkedValue)
241		}
242	}
243}
244
245func TestBackupStepByStep(t *testing.T) {
246	testBackup(t, testRowCount, true)
247}
248
249func TestBackupAllRemainingPages(t *testing.T) {
250	testBackup(t, testRowCount, false)
251}
252
253// Test the error reporting when preparing to perform a backup.
254func TestBackupError(t *testing.T) {
255	const driverName = "sqlite3_TestBackupError"
256
257	// The driver's connection will be needed in order to perform the backup.
258	var dbDriverConn *SQLiteConn
259	sql.Register(driverName, &SQLiteDriver{
260		ConnectHook: func(conn *SQLiteConn) error {
261			dbDriverConn = conn
262			return nil
263		},
264	})
265
266	// Connect to the database.
267	dbTempFilename := TempFilename(t)
268	defer os.Remove(dbTempFilename)
269	db, err := sql.Open(driverName, dbTempFilename)
270	if err != nil {
271		t.Fatal("Failed to open the database:", err)
272	}
273	defer db.Close()
274	db.Ping()
275
276	// Need the driver connection in order to perform the backup.
277	if dbDriverConn == nil {
278		t.Fatal("Failed to get the driver connection.")
279	}
280
281	// Prepare to perform the backup.
282	// Intentionally using the same connection for both the source and destination databases, to trigger an error result.
283	backup, err := dbDriverConn.Backup("main", dbDriverConn, "main")
284	if err == nil {
285		t.Fatal("Failed to get the expected error result.")
286	}
287	const expectedError = "source and destination must be distinct"
288	if err.Error() != expectedError {
289		t.Fatalf("Unexpected error message; expected value: \"%v\"; actual value: \"%v\"", expectedError, err.Error())
290	}
291	if backup != nil {
292		t.Fatal("Failed to get the expected nil backup result.")
293	}
294}
295