mirror of https://github.com/matrix-org/go-neb.git
Kegan Dougal
8 years ago
14 changed files with 14675 additions and 3579 deletions
-
2vendor/manifest
-
264vendor/src/github.com/mattn/go-sqlite3/_example/trace/main.go
-
290vendor/src/github.com/mattn/go-sqlite3/backup_test.go
-
4vendor/src/github.com/mattn/go-sqlite3/callback.go
-
2vendor/src/github.com/mattn/go-sqlite3/doc.go
-
15973vendor/src/github.com/mattn/go-sqlite3/sqlite3-binding.c
-
1693vendor/src/github.com/mattn/go-sqlite3/sqlite3-binding.h
-
129vendor/src/github.com/mattn/go-sqlite3/sqlite3.go
-
4vendor/src/github.com/mattn/go-sqlite3/sqlite3_load_extension.go
-
3vendor/src/github.com/mattn/go-sqlite3/sqlite3_test.go
-
26vendor/src/github.com/mattn/go-sqlite3/sqlite3ext.h
-
78vendor/src/github.com/mattn/go-sqlite3/tool/upgrade.go
-
415vendor/src/github.com/mattn/go-sqlite3/tracecallback.go
-
9vendor/src/github.com/mattn/go-sqlite3/tracecallback_noimpl.go
@ -0,0 +1,264 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"database/sql" |
||||
|
"fmt" |
||||
|
"log" |
||||
|
"os" |
||||
|
|
||||
|
sqlite3 "github.com/gimpldo/go-sqlite3" |
||||
|
) |
||||
|
|
||||
|
func traceCallback(info sqlite3.TraceInfo) int { |
||||
|
// Not very readable but may be useful; uncomment next line in case of doubt:
|
||||
|
//fmt.Printf("Trace: %#v\n", info)
|
||||
|
|
||||
|
var dbErrText string |
||||
|
if info.DBError.Code != 0 || info.DBError.ExtendedCode != 0 { |
||||
|
dbErrText = fmt.Sprintf("; DB error: %#v", info.DBError) |
||||
|
} else { |
||||
|
dbErrText = "." |
||||
|
} |
||||
|
|
||||
|
// Show the Statement-or-Trigger text in curly braces ('{', '}')
|
||||
|
// since from the *paired* ASCII characters they are
|
||||
|
// the least used in SQL syntax, therefore better visual delimiters.
|
||||
|
// Maybe show 'ExpandedSQL' the same way as 'StmtOrTrigger'.
|
||||
|
//
|
||||
|
// A known use of curly braces (outside strings) is
|
||||
|
// for ODBC escape sequences. Not likely to appear here.
|
||||
|
//
|
||||
|
// Template languages, etc. don't matter, we should see their *result*
|
||||
|
// at *this* level.
|
||||
|
// Strange curly braces in SQL code that reached the database driver
|
||||
|
// suggest that there is a bug in the application.
|
||||
|
// The braces are likely to be either template syntax or
|
||||
|
// a programming language's string interpolation syntax.
|
||||
|
|
||||
|
var expandedText string |
||||
|
if info.ExpandedSQL != "" { |
||||
|
if info.ExpandedSQL == info.StmtOrTrigger { |
||||
|
expandedText = " = exp" |
||||
|
} else { |
||||
|
expandedText = fmt.Sprintf(" expanded {%q}", info.ExpandedSQL) |
||||
|
} |
||||
|
} else { |
||||
|
expandedText = "" |
||||
|
} |
||||
|
|
||||
|
// SQLite docs as of September 6, 2016: Tracing and Profiling Functions
|
||||
|
// https://www.sqlite.org/c3ref/profile.html
|
||||
|
//
|
||||
|
// The profile callback time is in units of nanoseconds, however
|
||||
|
// the current implementation is only capable of millisecond resolution
|
||||
|
// so the six least significant digits in the time are meaningless.
|
||||
|
// Future versions of SQLite might provide greater resolution on the profiler callback.
|
||||
|
|
||||
|
var runTimeText string |
||||
|
if info.RunTimeNanosec == 0 { |
||||
|
if info.EventCode == sqlite3.TraceProfile { |
||||
|
//runTimeText = "; no time" // seems confusing
|
||||
|
runTimeText = "; time 0" // no measurement unit
|
||||
|
} else { |
||||
|
//runTimeText = "; no time" // seems useless and confusing
|
||||
|
} |
||||
|
} else { |
||||
|
const nanosPerMillisec = 1000000 |
||||
|
if info.RunTimeNanosec%nanosPerMillisec == 0 { |
||||
|
runTimeText = fmt.Sprintf("; time %d ms", info.RunTimeNanosec/nanosPerMillisec) |
||||
|
} else { |
||||
|
// unexpected: better than millisecond resolution
|
||||
|
runTimeText = fmt.Sprintf("; time %d ns!!!", info.RunTimeNanosec) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var modeText string |
||||
|
if info.AutoCommit { |
||||
|
modeText = "-AC-" |
||||
|
} else { |
||||
|
modeText = "+Tx+" |
||||
|
} |
||||
|
|
||||
|
fmt.Printf("Trace: ev %d %s conn 0x%x, stmt 0x%x {%q}%s%s%s\n", |
||||
|
info.EventCode, modeText, info.ConnHandle, info.StmtHandle, |
||||
|
info.StmtOrTrigger, expandedText, |
||||
|
runTimeText, |
||||
|
dbErrText) |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func main() { |
||||
|
eventMask := sqlite3.TraceStmt | sqlite3.TraceProfile | sqlite3.TraceRow | sqlite3.TraceClose |
||||
|
|
||||
|
sql.Register("sqlite3_tracing", |
||||
|
&sqlite3.SQLiteDriver{ |
||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error { |
||||
|
err := conn.SetTrace(&sqlite3.TraceConfig{ |
||||
|
Callback: traceCallback, |
||||
|
EventMask: uint(eventMask), |
||||
|
WantExpandedSQL: true, |
||||
|
}) |
||||
|
return err |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
os.Exit(dbMain()) |
||||
|
} |
||||
|
|
||||
|
// Harder to do DB work in main().
|
||||
|
// It's better with a separate function because
|
||||
|
// 'defer' and 'os.Exit' don't go well together.
|
||||
|
//
|
||||
|
// DO NOT use 'log.Fatal...' below: remember that it's equivalent to
|
||||
|
// Print() followed by a call to os.Exit(1) --- and
|
||||
|
// we want to avoid Exit() so 'defer' can do cleanup.
|
||||
|
// Use 'log.Panic...' instead.
|
||||
|
|
||||
|
func dbMain() int { |
||||
|
db, err := sql.Open("sqlite3_tracing", ":memory:") |
||||
|
if err != nil { |
||||
|
fmt.Printf("Failed to open database: %#+v\n", err) |
||||
|
return 1 |
||||
|
} |
||||
|
defer db.Close() |
||||
|
|
||||
|
err = db.Ping() |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
|
||||
|
dbSetup(db) |
||||
|
|
||||
|
dbDoInsert(db) |
||||
|
dbDoInsertPrepared(db) |
||||
|
dbDoSelect(db) |
||||
|
dbDoSelectPrepared(db) |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
// 'DDL' stands for "Data Definition Language":
|
||||
|
|
||||
|
// Note: "INTEGER PRIMARY KEY NOT NULL AUTOINCREMENT" causes the error
|
||||
|
// 'near "AUTOINCREMENT": syntax error'; without "NOT NULL" it works.
|
||||
|
const tableDDL = `CREATE TABLE t1 ( |
||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
note VARCHAR NOT NULL |
||||
|
)` |
||||
|
|
||||
|
// 'DML' stands for "Data Manipulation Language":
|
||||
|
|
||||
|
const insertDML = "INSERT INTO t1 (note) VALUES (?)" |
||||
|
const selectDML = "SELECT id, note FROM t1 WHERE note LIKE ?" |
||||
|
|
||||
|
const textPrefix = "bla-1234567890-" |
||||
|
const noteTextPattern = "%Prep%" |
||||
|
|
||||
|
const nGenRows = 4 // Number of Rows to Generate (for *each* approach tested)
|
||||
|
|
||||
|
func dbSetup(db *sql.DB) { |
||||
|
var err error |
||||
|
|
||||
|
_, err = db.Exec("DROP TABLE IF EXISTS t1") |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
_, err = db.Exec(tableDDL) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func dbDoInsert(db *sql.DB) { |
||||
|
const Descr = "DB-Exec" |
||||
|
for i := 0; i < nGenRows; i++ { |
||||
|
result, err := db.Exec(insertDML, textPrefix+Descr) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
|
||||
|
resultDoCheck(result, Descr, i) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func dbDoInsertPrepared(db *sql.DB) { |
||||
|
const Descr = "DB-Prepare" |
||||
|
|
||||
|
stmt, err := db.Prepare(insertDML) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
defer stmt.Close() |
||||
|
|
||||
|
for i := 0; i < nGenRows; i++ { |
||||
|
result, err := stmt.Exec(textPrefix + Descr) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
|
||||
|
resultDoCheck(result, Descr, i) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func resultDoCheck(result sql.Result, callerDescr string, callIndex int) { |
||||
|
lastID, err := result.LastInsertId() |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
nAffected, err := result.RowsAffected() |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
|
||||
|
log.Printf("Exec result for %s (%d): ID = %d, affected = %d\n", callerDescr, callIndex, lastID, nAffected) |
||||
|
} |
||||
|
|
||||
|
func dbDoSelect(db *sql.DB) { |
||||
|
const Descr = "DB-Query" |
||||
|
|
||||
|
rows, err := db.Query(selectDML, noteTextPattern) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
rowsDoFetch(rows, Descr) |
||||
|
} |
||||
|
|
||||
|
func dbDoSelectPrepared(db *sql.DB) { |
||||
|
const Descr = "DB-Prepare" |
||||
|
|
||||
|
stmt, err := db.Prepare(selectDML) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
defer stmt.Close() |
||||
|
|
||||
|
rows, err := stmt.Query(noteTextPattern) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
rowsDoFetch(rows, Descr) |
||||
|
} |
||||
|
|
||||
|
func rowsDoFetch(rows *sql.Rows, callerDescr string) { |
||||
|
var nRows int |
||||
|
var id int64 |
||||
|
var note string |
||||
|
|
||||
|
for rows.Next() { |
||||
|
err := rows.Scan(&id, ¬e) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
log.Printf("Row for %s (%d): id=%d, note=%q\n", |
||||
|
callerDescr, nRows, id, note) |
||||
|
nRows++ |
||||
|
} |
||||
|
if err := rows.Err(); err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
log.Printf("Total %d rows for %s.\n", nRows, callerDescr) |
||||
|
} |
@ -0,0 +1,290 @@ |
|||||
|
// 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.") |
||||
|
} |
||||
|
} |
15973
vendor/src/github.com/mattn/go-sqlite3/sqlite3-binding.c
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1693
vendor/src/github.com/mattn/go-sqlite3/sqlite3-binding.h
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,78 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"archive/zip" |
||||
|
"bytes" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"io/ioutil" |
||||
|
"log" |
||||
|
"net/http" |
||||
|
"os" |
||||
|
"path" |
||||
|
"path/filepath" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/PuerkitoBio/goquery" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
site := "https://www.sqlite.org/download.html" |
||||
|
fmt.Printf("scraping %v\n", site) |
||||
|
doc, err := goquery.NewDocument(site) |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
var url string |
||||
|
doc.Find("a").Each(func(_ int, s *goquery.Selection) { |
||||
|
if url == "" && strings.HasPrefix(s.Text(), "sqlite-amalgamation-") { |
||||
|
url = "https://www.sqlite.org/2016/" + s.Text() |
||||
|
} |
||||
|
}) |
||||
|
if url == "" { |
||||
|
return |
||||
|
} |
||||
|
fmt.Printf("downloading %v\n", url) |
||||
|
resp, err := http.Get(url) |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
defer resp.Body.Close() |
||||
|
|
||||
|
b, err := ioutil.ReadAll(resp.Body) |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
fmt.Printf("extracting %v\n", path.Base(url)) |
||||
|
r, err := zip.NewReader(bytes.NewReader(b), resp.ContentLength) |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
for _, zf := range r.File { |
||||
|
var f *os.File |
||||
|
switch path.Base(zf.Name) { |
||||
|
case "sqlite3.c": |
||||
|
f, err = os.Create("sqlite3-binding.c") |
||||
|
case "sqlite3.h": |
||||
|
f, err = os.Create("sqlite3-binding.h") |
||||
|
case "sqlite3ext.h": |
||||
|
f, err = os.Create("sqlite3ext.h") |
||||
|
default: |
||||
|
continue |
||||
|
} |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
zr, err := zf.Open() |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
_, err = io.Copy(f, zr) |
||||
|
f.Close() |
||||
|
if err != nil { |
||||
|
log.Fatal(err) |
||||
|
} |
||||
|
fmt.Printf("extracted %v\n", filepath.Base(f.Name())) |
||||
|
} |
||||
|
} |
@ -0,0 +1,415 @@ |
|||||
|
// Copyright (C) 2016 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||
|
// TODO: add "Gimpl do foo" team?
|
||||
|
//
|
||||
|
// Use of this source code is governed by an MIT-style
|
||||
|
// license that can be found in the LICENSE file.
|
||||
|
// +build trace
|
||||
|
|
||||
|
package sqlite3 |
||||
|
|
||||
|
/* |
||||
|
#ifndef USE_LIBSQLITE3 |
||||
|
#include <sqlite3-binding.h> |
||||
|
#else |
||||
|
#include <sqlite3.h> |
||||
|
#endif |
||||
|
#include <stdlib.h> |
||||
|
|
||||
|
void stepTrampoline(sqlite3_context*, int, sqlite3_value**); |
||||
|
void doneTrampoline(sqlite3_context*); |
||||
|
void traceCallbackTrampoline(unsigned traceEventCode, void *ctx, void *p, void *x); |
||||
|
*/ |
||||
|
import "C" |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"reflect" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"unsafe" |
||||
|
) |
||||
|
|
||||
|
// Trace... constants identify the possible events causing callback invocation.
|
||||
|
// Values are same as the corresponding SQLite Trace Event Codes.
|
||||
|
const ( |
||||
|
TraceStmt = C.SQLITE_TRACE_STMT |
||||
|
TraceProfile = C.SQLITE_TRACE_PROFILE |
||||
|
TraceRow = C.SQLITE_TRACE_ROW |
||||
|
TraceClose = C.SQLITE_TRACE_CLOSE |
||||
|
) |
||||
|
|
||||
|
type TraceInfo struct { |
||||
|
// Pack together the shorter fields, to keep the struct smaller.
|
||||
|
// On a 64-bit machine there would be padding
|
||||
|
// between EventCode and ConnHandle; having AutoCommit here is "free":
|
||||
|
EventCode uint32 |
||||
|
AutoCommit bool |
||||
|
ConnHandle uintptr |
||||
|
|
||||
|
// Usually filled, unless EventCode = TraceClose = SQLITE_TRACE_CLOSE:
|
||||
|
// identifier for a prepared statement:
|
||||
|
StmtHandle uintptr |
||||
|
|
||||
|
// Two strings filled when EventCode = TraceStmt = SQLITE_TRACE_STMT:
|
||||
|
// (1) either the unexpanded SQL text of the prepared statement, or
|
||||
|
// an SQL comment that indicates the invocation of a trigger;
|
||||
|
// (2) expanded SQL, if requested and if (1) is not an SQL comment.
|
||||
|
StmtOrTrigger string |
||||
|
ExpandedSQL string // only if requested (TraceConfig.WantExpandedSQL = true)
|
||||
|
|
||||
|
// filled when EventCode = TraceProfile = SQLITE_TRACE_PROFILE:
|
||||
|
// estimated number of nanoseconds that the prepared statement took to run:
|
||||
|
RunTimeNanosec int64 |
||||
|
|
||||
|
DBError Error |
||||
|
} |
||||
|
|
||||
|
// TraceUserCallback gives the signature for a trace function
|
||||
|
// provided by the user (Go application programmer).
|
||||
|
// SQLite 3.14 documentation (as of September 2, 2016)
|
||||
|
// for SQL Trace Hook = sqlite3_trace_v2():
|
||||
|
// The integer return value from the callback is currently ignored,
|
||||
|
// though this may change in future releases. Callback implementations
|
||||
|
// should return zero to ensure future compatibility.
|
||||
|
type TraceUserCallback func(TraceInfo) int |
||||
|
|
||||
|
type TraceConfig struct { |
||||
|
Callback TraceUserCallback |
||||
|
EventMask uint |
||||
|
WantExpandedSQL bool |
||||
|
} |
||||
|
|
||||
|
func fillDBError(dbErr *Error, db *C.sqlite3) { |
||||
|
// See SQLiteConn.lastError(), in file 'sqlite3.go' at the time of writing (Sept 5, 2016)
|
||||
|
dbErr.Code = ErrNo(C.sqlite3_errcode(db)) |
||||
|
dbErr.ExtendedCode = ErrNoExtended(C.sqlite3_extended_errcode(db)) |
||||
|
dbErr.err = C.GoString(C.sqlite3_errmsg(db)) |
||||
|
} |
||||
|
|
||||
|
func fillExpandedSQL(info *TraceInfo, db *C.sqlite3, pStmt unsafe.Pointer) { |
||||
|
if pStmt == nil { |
||||
|
panic("No SQLite statement pointer in P arg of trace_v2 callback") |
||||
|
} |
||||
|
|
||||
|
expSQLiteCStr := C.sqlite3_expanded_sql((*C.sqlite3_stmt)(pStmt)) |
||||
|
if expSQLiteCStr == nil { |
||||
|
fillDBError(&info.DBError, db) |
||||
|
return |
||||
|
} |
||||
|
info.ExpandedSQL = C.GoString(expSQLiteCStr) |
||||
|
} |
||||
|
|
||||
|
//export traceCallbackTrampoline
|
||||
|
func traceCallbackTrampoline( |
||||
|
traceEventCode uint, |
||||
|
// Parameter named 'C' in SQLite docs = Context given at registration:
|
||||
|
ctx unsafe.Pointer, |
||||
|
// Parameter named 'P' in SQLite docs (Primary event data?):
|
||||
|
p unsafe.Pointer, |
||||
|
// Parameter named 'X' in SQLite docs (eXtra event data?):
|
||||
|
xValue unsafe.Pointer) int { |
||||
|
|
||||
|
if ctx == nil { |
||||
|
panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode)) |
||||
|
} |
||||
|
|
||||
|
contextDB := (*C.sqlite3)(ctx) |
||||
|
connHandle := uintptr(ctx) |
||||
|
|
||||
|
var traceConf TraceConfig |
||||
|
var found bool |
||||
|
if traceEventCode == TraceClose { |
||||
|
// clean up traceMap: 'pop' means get and delete
|
||||
|
traceConf, found = popTraceMapping(connHandle) |
||||
|
} else { |
||||
|
traceConf, found = lookupTraceMapping(connHandle) |
||||
|
} |
||||
|
|
||||
|
if !found { |
||||
|
panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)", |
||||
|
connHandle, traceEventCode)) |
||||
|
} |
||||
|
|
||||
|
var info TraceInfo |
||||
|
|
||||
|
info.EventCode = uint32(traceEventCode) |
||||
|
info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0) |
||||
|
info.ConnHandle = connHandle |
||||
|
|
||||
|
switch traceEventCode { |
||||
|
case TraceStmt: |
||||
|
info.StmtHandle = uintptr(p) |
||||
|
|
||||
|
var xStr string |
||||
|
if xValue != nil { |
||||
|
xStr = C.GoString((*C.char)(xValue)) |
||||
|
} |
||||
|
info.StmtOrTrigger = xStr |
||||
|
if !strings.HasPrefix(xStr, "--") { |
||||
|
// Not SQL comment, therefore the current event
|
||||
|
// is not related to a trigger.
|
||||
|
// The user might want to receive the expanded SQL;
|
||||
|
// let's check:
|
||||
|
if traceConf.WantExpandedSQL { |
||||
|
fillExpandedSQL(&info, contextDB, p) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case TraceProfile: |
||||
|
info.StmtHandle = uintptr(p) |
||||
|
|
||||
|
if xValue == nil { |
||||
|
panic("NULL pointer in X arg of trace_v2 callback for SQLITE_TRACE_PROFILE event") |
||||
|
} |
||||
|
|
||||
|
info.RunTimeNanosec = *(*int64)(xValue) |
||||
|
|
||||
|
// sample the error //TODO: is it safe? is it useful?
|
||||
|
fillDBError(&info.DBError, contextDB) |
||||
|
|
||||
|
case TraceRow: |
||||
|
info.StmtHandle = uintptr(p) |
||||
|
|
||||
|
case TraceClose: |
||||
|
handle := uintptr(p) |
||||
|
if handle != info.ConnHandle { |
||||
|
panic(fmt.Sprintf("Different conn handle 0x%x (expected 0x%x) in SQLITE_TRACE_CLOSE event.", |
||||
|
handle, info.ConnHandle)) |
||||
|
} |
||||
|
|
||||
|
default: |
||||
|
// Pass unsupported events to the user callback (if configured);
|
||||
|
// let the user callback decide whether to panic or ignore them.
|
||||
|
} |
||||
|
|
||||
|
// Do not execute user callback when the event was not requested by user!
|
||||
|
// Remember that the Close event is always selected when
|
||||
|
// registering this callback trampoline with SQLite --- for cleanup.
|
||||
|
// In the future there may be more events forced to "selected" in SQLite
|
||||
|
// for the driver's needs.
|
||||
|
if traceConf.EventMask&traceEventCode == 0 { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
r := 0 |
||||
|
if traceConf.Callback != nil { |
||||
|
r = traceConf.Callback(info) |
||||
|
} |
||||
|
return r |
||||
|
} |
||||
|
|
||||
|
type traceMapEntry struct { |
||||
|
config TraceConfig |
||||
|
} |
||||
|
|
||||
|
var traceMapLock sync.Mutex |
||||
|
var traceMap = make(map[uintptr]traceMapEntry) |
||||
|
|
||||
|
func addTraceMapping(connHandle uintptr, traceConf TraceConfig) { |
||||
|
traceMapLock.Lock() |
||||
|
defer traceMapLock.Unlock() |
||||
|
|
||||
|
oldEntryCopy, found := traceMap[connHandle] |
||||
|
if found { |
||||
|
panic(fmt.Sprintf("Adding trace config %v: handle 0x%x already registered (%v).", |
||||
|
traceConf, connHandle, oldEntryCopy.config)) |
||||
|
} |
||||
|
traceMap[connHandle] = traceMapEntry{config: traceConf} |
||||
|
fmt.Printf("Added trace config %v: handle 0x%x.\n", traceConf, connHandle) |
||||
|
} |
||||
|
|
||||
|
func lookupTraceMapping(connHandle uintptr) (TraceConfig, bool) { |
||||
|
traceMapLock.Lock() |
||||
|
defer traceMapLock.Unlock() |
||||
|
|
||||
|
entryCopy, found := traceMap[connHandle] |
||||
|
return entryCopy.config, found |
||||
|
} |
||||
|
|
||||
|
// 'pop' = get and delete from map before returning the value to the caller
|
||||
|
func popTraceMapping(connHandle uintptr) (TraceConfig, bool) { |
||||
|
traceMapLock.Lock() |
||||
|
defer traceMapLock.Unlock() |
||||
|
|
||||
|
entryCopy, found := traceMap[connHandle] |
||||
|
if found { |
||||
|
delete(traceMap, connHandle) |
||||
|
fmt.Printf("Pop handle 0x%x: deleted trace config %v.\n", connHandle, entryCopy.config) |
||||
|
} |
||||
|
return entryCopy.config, found |
||||
|
} |
||||
|
|
||||
|
// RegisterAggregator makes a Go type available as a SQLite aggregation function.
|
||||
|
//
|
||||
|
// Because aggregation is incremental, it's implemented in Go with a
|
||||
|
// type that has 2 methods: func Step(values) accumulates one row of
|
||||
|
// data into the accumulator, and func Done() ret finalizes and
|
||||
|
// returns the aggregate value. "values" and "ret" may be any type
|
||||
|
// supported by RegisterFunc.
|
||||
|
//
|
||||
|
// RegisterAggregator takes as implementation a constructor function
|
||||
|
// that constructs an instance of the aggregator type each time an
|
||||
|
// aggregation begins. The constructor must return a pointer to a
|
||||
|
// type, or an interface that implements Step() and Done().
|
||||
|
//
|
||||
|
// The constructor function and the Step/Done methods may optionally
|
||||
|
// return an error in addition to their other return values.
|
||||
|
//
|
||||
|
// See _example/go_custom_funcs for a detailed example.
|
||||
|
func (c *SQLiteConn) RegisterAggregator(name string, impl interface{}, pure bool) error { |
||||
|
var ai aggInfo |
||||
|
ai.constructor = reflect.ValueOf(impl) |
||||
|
t := ai.constructor.Type() |
||||
|
if t.Kind() != reflect.Func { |
||||
|
return errors.New("non-function passed to RegisterAggregator") |
||||
|
} |
||||
|
if t.NumOut() != 1 && t.NumOut() != 2 { |
||||
|
return errors.New("SQLite aggregator constructors must return 1 or 2 values") |
||||
|
} |
||||
|
if t.NumOut() == 2 && !t.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { |
||||
|
return errors.New("Second return value of SQLite function must be error") |
||||
|
} |
||||
|
if t.NumIn() != 0 { |
||||
|
return errors.New("SQLite aggregator constructors must not have arguments") |
||||
|
} |
||||
|
|
||||
|
agg := t.Out(0) |
||||
|
switch agg.Kind() { |
||||
|
case reflect.Ptr, reflect.Interface: |
||||
|
default: |
||||
|
return errors.New("SQlite aggregator constructor must return a pointer object") |
||||
|
} |
||||
|
stepFn, found := agg.MethodByName("Step") |
||||
|
if !found { |
||||
|
return errors.New("SQlite aggregator doesn't have a Step() function") |
||||
|
} |
||||
|
step := stepFn.Type |
||||
|
if step.NumOut() != 0 && step.NumOut() != 1 { |
||||
|
return errors.New("SQlite aggregator Step() function must return 0 or 1 values") |
||||
|
} |
||||
|
if step.NumOut() == 1 && !step.Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) { |
||||
|
return errors.New("type of SQlite aggregator Step() return value must be error") |
||||
|
} |
||||
|
|
||||
|
stepNArgs := step.NumIn() |
||||
|
start := 0 |
||||
|
if agg.Kind() == reflect.Ptr { |
||||
|
// Skip over the method receiver
|
||||
|
stepNArgs-- |
||||
|
start++ |
||||
|
} |
||||
|
if step.IsVariadic() { |
||||
|
stepNArgs-- |
||||
|
} |
||||
|
for i := start; i < start+stepNArgs; i++ { |
||||
|
conv, err := callbackArg(step.In(i)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
ai.stepArgConverters = append(ai.stepArgConverters, conv) |
||||
|
} |
||||
|
if step.IsVariadic() { |
||||
|
conv, err := callbackArg(t.In(start + stepNArgs).Elem()) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
ai.stepVariadicConverter = conv |
||||
|
// Pass -1 to sqlite so that it allows any number of
|
||||
|
// arguments. The call helper verifies that the minimum number
|
||||
|
// of arguments is present for variadic functions.
|
||||
|
stepNArgs = -1 |
||||
|
} |
||||
|
|
||||
|
doneFn, found := agg.MethodByName("Done") |
||||
|
if !found { |
||||
|
return errors.New("SQlite aggregator doesn't have a Done() function") |
||||
|
} |
||||
|
done := doneFn.Type |
||||
|
doneNArgs := done.NumIn() |
||||
|
if agg.Kind() == reflect.Ptr { |
||||
|
// Skip over the method receiver
|
||||
|
doneNArgs-- |
||||
|
} |
||||
|
if doneNArgs != 0 { |
||||
|
return errors.New("SQlite aggregator Done() function must have no arguments") |
||||
|
} |
||||
|
if done.NumOut() != 1 && done.NumOut() != 2 { |
||||
|
return errors.New("SQLite aggregator Done() function must return 1 or 2 values") |
||||
|
} |
||||
|
if done.NumOut() == 2 && !done.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { |
||||
|
return errors.New("second return value of SQLite aggregator Done() function must be error") |
||||
|
} |
||||
|
|
||||
|
conv, err := callbackRet(done.Out(0)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
ai.doneRetConverter = conv |
||||
|
ai.active = make(map[int64]reflect.Value) |
||||
|
ai.next = 1 |
||||
|
|
||||
|
// ai must outlast the database connection, or we'll have dangling pointers.
|
||||
|
c.aggregators = append(c.aggregators, &ai) |
||||
|
|
||||
|
cname := C.CString(name) |
||||
|
defer C.free(unsafe.Pointer(cname)) |
||||
|
opts := C.SQLITE_UTF8 |
||||
|
if pure { |
||||
|
opts |= C.SQLITE_DETERMINISTIC |
||||
|
} |
||||
|
rv := C._sqlite3_create_function(c.db, cname, C.int(stepNArgs), C.int(opts), C.uintptr_t(newHandle(c, &ai)), nil, (*[0]byte)(unsafe.Pointer(C.stepTrampoline)), (*[0]byte)(unsafe.Pointer(C.doneTrampoline))) |
||||
|
if rv != C.SQLITE_OK { |
||||
|
return c.lastError() |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// SetTrace installs or removes the trace callback for the given database connection.
|
||||
|
// It's not named 'RegisterTrace' because only one callback can be kept and called.
|
||||
|
// Calling SetTrace a second time on same database connection
|
||||
|
// overrides (cancels) any prior callback and all its settings:
|
||||
|
// event mask, etc.
|
||||
|
func (c *SQLiteConn) SetTrace(requested *TraceConfig) error { |
||||
|
connHandle := uintptr(unsafe.Pointer(c.db)) |
||||
|
|
||||
|
_, _ = popTraceMapping(connHandle) |
||||
|
|
||||
|
if requested == nil { |
||||
|
// The traceMap entry was deleted already by popTraceMapping():
|
||||
|
// can disable all events now, no need to watch for TraceClose.
|
||||
|
err := c.setSQLiteTrace(0) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
reqCopy := *requested |
||||
|
|
||||
|
// Disable potentially expensive operations
|
||||
|
// if their result will not be used. We are doing this
|
||||
|
// just in case the caller provided nonsensical input.
|
||||
|
if reqCopy.EventMask&TraceStmt == 0 { |
||||
|
reqCopy.WantExpandedSQL = false |
||||
|
} |
||||
|
|
||||
|
addTraceMapping(connHandle, reqCopy) |
||||
|
|
||||
|
// The callback trampoline function does cleanup on Close event,
|
||||
|
// regardless of the presence or absence of the user callback.
|
||||
|
// Therefore it needs the Close event to be selected:
|
||||
|
actualEventMask := reqCopy.EventMask | TraceClose |
||||
|
err := c.setSQLiteTrace(actualEventMask) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
func (c *SQLiteConn) setSQLiteTrace(sqliteEventMask uint) error { |
||||
|
rv := C.sqlite3_trace_v2(c.db, |
||||
|
C.uint(sqliteEventMask), |
||||
|
(*[0]byte)(unsafe.Pointer(C.traceCallbackTrampoline)), |
||||
|
unsafe.Pointer(c.db)) // Fourth arg is same as first: we are
|
||||
|
// passing the database connection handle as callback context.
|
||||
|
|
||||
|
if rv != C.SQLITE_OK { |
||||
|
return c.lastError() |
||||
|
} |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
// +build !trace
|
||||
|
|
||||
|
package sqlite3 |
||||
|
|
||||
|
import "errors" |
||||
|
|
||||
|
func (c *SQLiteConn) RegisterAggregator(name string, impl interface{}, pure bool) error { |
||||
|
return errors.New("This feature is not implemented") |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue