diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index f8327cf..2cdec15 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -1,6 +1,6 @@ FROM gitpod/workspace-base:latest -ENV GO_VERSION=1.21.6 +ENV GO_VERSION=1.22.0 # For ref, see: https://github.com/gitpod-io/workspace-images/blob/61df77aad71689504112e1087bb7e26d45a43d10/chunks/lang-go/Dockerfile#L10 ENV GOPATH=$HOME/go-packages diff --git a/01.Open/Taskfile.yml b/01.Open/Taskfile.yml index a29c8f5..4c9f3f9 100644 --- a/01.Open/Taskfile.yml +++ b/01.Open/Taskfile.yml @@ -2,7 +2,11 @@ version: '3' +vars: + DBFILE: chinook.db + tasks: default: cmds: + - cp -f ../{{.DBFILE}} . - go run main.go diff --git a/01.Open/main.go b/01.Open/main.go index e17f1bf..fb994ac 100644 --- a/01.Open/main.go +++ b/01.Open/main.go @@ -83,7 +83,7 @@ func main() { const ( driver = "sqlite3" - datasource = "../chinook.db" + datasource = "./chinook.db" ) var ( diff --git a/02.Query/Taskfile.yml b/02.Query/Taskfile.yml index a29c8f5..4c9f3f9 100644 --- a/02.Query/Taskfile.yml +++ b/02.Query/Taskfile.yml @@ -2,7 +2,11 @@ version: '3' +vars: + DBFILE: chinook.db + tasks: default: cmds: + - cp -f ../{{.DBFILE}} . - go run main.go diff --git a/02.Query/main.go b/02.Query/main.go index f5705eb..87e47ad 100644 --- a/02.Query/main.go +++ b/02.Query/main.go @@ -31,7 +31,7 @@ func main() { const ( driver = "sqlite3" - datasource = "../chinook.db" + datasource = "./chinook.db" ) type ( diff --git a/03.QueryRow/Taskfile.yml b/03.QueryRow/Taskfile.yml index a29c8f5..4c9f3f9 100644 --- a/03.QueryRow/Taskfile.yml +++ b/03.QueryRow/Taskfile.yml @@ -2,7 +2,11 @@ version: '3' +vars: + DBFILE: chinook.db + tasks: default: cmds: + - cp -f ../{{.DBFILE}} . - go run main.go diff --git a/03.QueryRow/main.go b/03.QueryRow/main.go index 941cd0a..d5074db 100644 --- a/03.QueryRow/main.go +++ b/03.QueryRow/main.go @@ -27,7 +27,7 @@ func main() { const ( driver = "sqlite3" - datasource = "../chinook.db" + datasource = "./chinook.db" ) type ( diff --git a/04.Exec/Taskfile.yml b/04.Exec/Taskfile.yml new file mode 100644 index 0000000..2127d23 --- /dev/null +++ b/04.Exec/Taskfile.yml @@ -0,0 +1,13 @@ +# https://taskfile.dev + +version: '3' + +vars: + DBFILE: chinook.db + +tasks: + default: + cmds: + - cp -f ../{{.DBFILE}} . + - go run main.go + - echo "SELECT * FROM artists WHERE ArtistId=999" | sqlite3 -header -table ./{{.DBFILE}} diff --git a/04.Exec/main.go b/04.Exec/main.go new file mode 100644 index 0000000..accf564 --- /dev/null +++ b/04.Exec/main.go @@ -0,0 +1,125 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + log.SetFlags(0) +} +func main() { + if err := run(); err != nil { + log.Panic(err) + } + + /* + $ task -d 04.Exec/ + task: [default] go run main.go + LastInsertId: 999 RowsAffected: 1 + task: [default] echo "SELECT * FROM artists WHERE ArtistId=999" | sqlite3 -header -table ../chinook.db + +----------+------+ + | ArtistId | Name | + +----------+------+ + | 999 | test | + +----------+------+ + task: [default] echo "DELETE FROM artists WHERE ArtistId=999" | sqlite3 -header -table ../chinook.db + */ +} + +const ( + driver = "sqlite3" + datasource = "./chinook.db" +) + +type ( + Artist struct { + Id int + Name string + } +) + +var ( + db *sql.DB +) + +// 04.Exec +// +// データベースに対してINSERT,UPDATE,DELETEを発行するには *DB.Exec() を利用する。 +// +// - https://pkg.go.dev/database/sql@go1.21.6#DB.Exec +// +// *DB.Exec() は、行データを返さず、代わりに sql.Result を返す。 +// +// - https://pkg.go.dev/database/sql@go1.21.6#Result +// +// sql.Result からは、最後に追加されたID値と影響を受けた行数が取得できる。 +// sql.Result.LastInsertId()は、Auto Incrementな列の場合に利用できる。 +// +// 今回は artists テーブルに新たなレコードをINSERTする。 +// artistsテーブルのレイアウトは以下。 +// +// $ sqlite3 chinook.db +// SQLite version 3.37.2 2022-01-06 13:25:41 +// Enter ".help" for usage hints. +// sqlite> .headers on +// sqlite> .mode table +// sqlite> pragma table_info(artists); +// +-----+----------+---------------+---------+------------+----+ +// | cid | name | type | notnull | dflt_value | pk | +// +-----+----------+---------------+---------+------------+----+ +// | 0 | ArtistId | INTEGER | 1 | | 1 | +// | 1 | Name | NVARCHAR(120) | 0 | | 0 | +// +-----+----------+---------------+---------+------------+----+ +// sqlite> SELECT MAX(ArtistId) FROM artists; +// +---------------+ +// | MAX(ArtistId) | +// +---------------+ +// | 275 | +// +---------------+ +// +// # REFERENCES +// - https://go.dev/doc/tutorial/database-access#add_data +func run() error { + var ( + err error + ) + + db, err = sql.Open(driver, datasource) + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + + err = db.Ping() + if err != nil { + return fmt.Errorf("db.Ping: %w", err) + } + + var ( + result sql.Result + lastId int64 + affected int64 + ) + + result, err = db.Exec("INSERT INTO artists (ArtistId, Name) VALUES (?, ?)", 999, "test") + if err != nil { + return fmt.Errorf("db.Exec: %w", err) + } + + lastId, err = result.LastInsertId() + if err != nil { + return fmt.Errorf("Result.LastInsertId: %w", err) + } + + affected, err = result.RowsAffected() + if err != nil { + return fmt.Errorf("Result.RowsAffected: %w", err) + } + + log.Printf("LastInsertId: %v\tRowsAffected: %v", lastId, affected) + + return nil +} diff --git a/05.Transaction/Taskfile.yml b/05.Transaction/Taskfile.yml new file mode 100644 index 0000000..bc4539d --- /dev/null +++ b/05.Transaction/Taskfile.yml @@ -0,0 +1,13 @@ +# https://taskfile.dev + +version: '3' + +vars: + DBFILE: chinook.db + +tasks: + default: + cmds: + - cp -f ../{{.DBFILE}} . + - go run main.go + - echo "SELECT * FROM artists ORDER BY ArtistId DESC LIMIT 10" | sqlite3 -header -table ./{{.DBFILE}} diff --git a/05.Transaction/main.go b/05.Transaction/main.go new file mode 100644 index 0000000..eb1057f --- /dev/null +++ b/05.Transaction/main.go @@ -0,0 +1,118 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + log.SetFlags(0) +} + +// 05.Transaction +// +// トランザクションを開始する場合、 *sql.DB.Begin() を利用する。 +// トランザクションは *sql.Tx で表される。 +// +// 基本的な使い方は、他の言語と同様で +// +// - *sql.Tx.Query() +// - *sql.Tx.QueryRow() +// - *sql.Tx.Exec() +// - *sql.Tx.Rollback() +// - *sql.Tx.Commit() +// +// を用いてトランザクションを操作する。 +// +// 定型文として、トランザクションを開始したら +// +// defer tx.Rollback() +// +// を呼び出しておく。これにより、エラー発生などに +// ロールバックが行われる。(コミットした後のロールバックは何も影響しない) +// +// https://go.dev/doc/database/execute-transactions に `Best Practice` として以下が記載されている。 +// +// > Use the APIs described in this section to manage transactions. +// Do not use transaction-related SQL statements such as BEGIN and COMMIT directly—doing so can leave your database in an unpredictable state, +// especially in concurrent programs. +// +// > トランザクションを管理するには、このセクションで説明するAPIを使用してください。 +// BEGINやCOMMITのようなトランザクション関連のSQL文を直接使用しないでください。 +// +// > When using a transaction, take care not to call the non-transaction sql.DB methods directly, too, as those will execute outside the transaction, +// giving your code an inconsistent view of the state of the database or even causing deadlocks. +// +// > トランザクションを使用する場合、トランザクション以外のsql.DBメソッドも直接呼び出さないように注意してください。 +// これらのメソッドはトランザクションの外で実行されるため、 +// コードの中でデータベースの状態に一貫性がなくなったり、デッドロックの原因になったりします。 +// +// https://pkg.go.dev/database/sql@go1.21.6#Tx には、以下の記載がある。 +// +// > A transaction must end with a call to Commit or Rollback. +// +// > トランザクションは必ず Commit もしくは Rollback で完了する必要があります。 +// +// > After a call to Commit or Rollback, all operations on the transaction fail with ErrTxDone. +// +// > コミットまたはロールバックを呼び出した後、トランザクションに対するすべての操作は ErrTxDone で失敗する。 +// +// > The statements prepared for a transaction by calling the transaction's Prepare or Stmt methods are closed by the call to Commit or Rollback. +// +// > トランザクションのPrepareメソッドまたはStmtメソッドを呼び出してトランザクションに準備されたステートメントは、CommitまたはRollbackの呼び出しによって閉じられます。 +// +// # REFERENCES +// - https://go.dev/doc/database/execute-transactions +// - https://pkg.go.dev/database/sql@go1.21.6#DB.Begin +// - https://pkg.go.dev/database/sql@go1.21.6#Tx +// - https://stackoverflow.com/a/25327191 +func main() { + if err := run(); err != nil { + log.Panic(err) + } +} + +func run() error { + var ( + db *sql.DB + err error + ) + + db, err = sql.Open("sqlite3", "./chinook.db") + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + defer db.Close() + + err = db.Ping() + if err != nil { + return fmt.Errorf("db.Ping: %w", err) + } + + var ( + tx *sql.Tx + ) + + tx, err = db.Begin() + if err != nil { + return fmt.Errorf("db.Begin: %w", err) + } + defer tx.Rollback() + + for i := 990; i < 1000; i++ { + _, err = tx.Exec("INSERT INTO artists (ArtistId, Name) VALUES (?, ?)", i, fmt.Sprintf("test%d", i)) + if err != nil { + return fmt.Errorf("tx.Exec: %w (%d)", err, i) + } + } + + err = tx.Commit() + if err != nil { + return fmt.Errorf("tx.Commit: %w", err) + } + + return nil +} diff --git a/06.PreparedQuery/Taskfile.yml b/06.PreparedQuery/Taskfile.yml new file mode 100644 index 0000000..4c9f3f9 --- /dev/null +++ b/06.PreparedQuery/Taskfile.yml @@ -0,0 +1,12 @@ +# https://taskfile.dev + +version: '3' + +vars: + DBFILE: chinook.db + +tasks: + default: + cmds: + - cp -f ../{{.DBFILE}} . + - go run main.go diff --git a/06.PreparedQuery/main.go b/06.PreparedQuery/main.go new file mode 100644 index 0000000..07ed0b4 --- /dev/null +++ b/06.PreparedQuery/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "log" + "sync" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + log.SetFlags(0) +} + +// 06.PreparedQuery +// +// Prepared Queryを利用する場合は、*sql.DB.Prepare() を使う。 +// *sql.DB.Prepare() は、*sql.Stmt を返し、これにパラメータを指定することにより実行できる。 +// *sql.Stmt は、利用が終わったら Close() を呼び出す必要がある。 +// +// *sql.Stmt は、複数のgoroutineにて同時に利用することが出来る。 +// +// > Prepare creates a prepared statement for later queries or executions. +// Multiple queries or executions may be run concurrently from the returned statement. +// The caller must call the statement's Close method when the statement is no longer needed. +// +// > Prepareは、後のクエリーや実行のために準備されたステートメントを作成します。 +// 返されたステートメントから複数のクエリや実行を同時に実行することができます。 +// ステートメントが不要になったら、呼び出し元はステートメントの Close メソッドを呼び出さなければなりません。 +// +// # REFERENCES +// - https://go.dev/doc/database/prepared-statements +// - https://pkg.go.dev/database/sql@go1.21.6#DB.Prepare +// - https://stackoverflow.com/a/25327191 +func main() { + if err := run(); err != nil { + log.Panic(err) + } + + /* + $ task -d 06.PreparedQuery/ + task: [default] cp -f ../chinook.db . + task: [default] go run main.go + id=10 name=Billy Cobham + id=8 name=Audioslave + id=9 name=BackBeat + id=6 name=Antônio Carlos Jobim + id=1 name=AC/DC + id=3 name=Aerosmith + id=7 name=Apocalyptica + id=4 name=Alanis Morissette + id=2 name=Accept + id=5 name=Alice In Chains + */ +} + +func run() error { + var ( + db *sql.DB + err error + ) + + db, err = sql.Open("sqlite3", "./chinook.db") + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + defer db.Close() + + err = db.Ping() + if err != nil { + return fmt.Errorf("db.Ping: %w", err) + } + + var ( + stmt *sql.Stmt + ) + + stmt, err = db.Prepare("SELECT * FROM artists WHERE ArtistId = ?") + if err != nil { + return fmt.Errorf("db.Prepare: %w", err) + } + defer stmt.Close() + + const LOOP_COUNT = 10 + var ( + wg sync.WaitGroup + errCh = make(chan error, LOOP_COUNT) + ) + + wg.Add(LOOP_COUNT) + + for i := 0; i < LOOP_COUNT; i++ { + go func(i int) { + defer wg.Done() + + var ( + row *sql.Row + id int + name string + ) + + row = stmt.QueryRow(i) + err = row.Scan(&id, &name) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + errCh <- fmt.Errorf("ErrNoRows: %d", i) + return + } + + errCh <- fmt.Errorf("sql.Row.Scan: %w", err) + return + } + + log.Printf("id=%v\tname=%v", id, name) + }(i + 1) + } + + wg.Wait() + close(errCh) + + for e := range errCh { + return e + } + + return nil +} diff --git a/07.PreparedQueryInTx/Taskfile.yml b/07.PreparedQueryInTx/Taskfile.yml new file mode 100644 index 0000000..bc4539d --- /dev/null +++ b/07.PreparedQueryInTx/Taskfile.yml @@ -0,0 +1,13 @@ +# https://taskfile.dev + +version: '3' + +vars: + DBFILE: chinook.db + +tasks: + default: + cmds: + - cp -f ../{{.DBFILE}} . + - go run main.go + - echo "SELECT * FROM artists ORDER BY ArtistId DESC LIMIT 10" | sqlite3 -header -table ./{{.DBFILE}} diff --git a/07.PreparedQueryInTx/main.go b/07.PreparedQueryInTx/main.go new file mode 100644 index 0000000..42e14c7 --- /dev/null +++ b/07.PreparedQueryInTx/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + log.SetFlags(0) +} + +// 07.PreparedQueryInTx +// +// トランザクション (*sql.Tx) からも、Prepared Query を作成することが出来る。 +// この場合、その Prepared Query は、当該トランザクションに紐づいた状態となり +// トランザクションの完了(Commit or Rollback)で、自動的にクローズされる。 +// +// # REFERENCES +// - https://go.dev/doc/database/execute-transactions +// - https://go.dev/doc/database/prepared-statements +// - https://pkg.go.dev/database/sql@go1.21.6#Tx +// - https://pkg.go.dev/database/sql@go1.21.6#Tx.Prepare +// - https://stackoverflow.com/a/25327191 +func main() { + if err := run(); err != nil { + log.Panic(err) + } + + /* + $ task -d 07.PreparedQueryInTx/ + task: [default] cp -f ../chinook.db . + task: [default] go run main.go + id=990 affected=1 + id=991 affected=1 + id=992 affected=1 + id=993 affected=1 + id=994 affected=1 + id=995 affected=1 + id=996 affected=1 + id=997 affected=1 + id=998 affected=1 + id=999 affected=1 + task: [default] echo "SELECT * FROM artists ORDER BY ArtistId DESC LIMIT 10" | sqlite3 -header -table ./chinook.db + +----------+---------+ + | ArtistId | Name | + +----------+---------+ + | 999 | test999 | + | 998 | test998 | + | 997 | test997 | + | 996 | test996 | + | 995 | test995 | + | 994 | test994 | + | 993 | test993 | + | 992 | test992 | + | 991 | test991 | + | 990 | test990 | + +----------+---------+ + */ +} + +func run() error { + var ( + db *sql.DB + err error + ) + + db, err = sql.Open("sqlite3", "chinook.db") + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + defer db.Close() + + var ( + tx *sql.Tx + ) + + tx, err = db.Begin() + if err != nil { + return fmt.Errorf("db.Begin: %w", err) + } + defer tx.Rollback() + + var ( + stmt *sql.Stmt + ) + + stmt, err = tx.Prepare("INSERT INTO artists (ArtistId, Name) VALUES (?, ?)") + if err != nil { + return fmt.Errorf("db.Prepare: %w", err) + } + defer stmt.Close() // tx経由で *sql.Stmt を作成した場合、トランザクションと共にクローズされるので無くても良い + + var ( + dropErr = func(v any, _ error) any { return v } + ) + + for i := 990; i < 1000; i++ { + var ( + rslt sql.Result + ) + + rslt, err = stmt.Exec(i, fmt.Sprintf("test%d", i)) + if err != nil { + return fmt.Errorf("*sql.Stmt.Exec (in tx): %w", err) + } + + log.Printf("id=%v\taffected=%v", dropErr(rslt.LastInsertId()), dropErr(rslt.RowsAffected())) + } + + err = tx.Commit() + if err != nil { + return fmt.Errorf("tx.Commit: %w", err) + } + + return nil +} diff --git a/08.Conn/Taskfile.yml b/08.Conn/Taskfile.yml new file mode 100644 index 0000000..4c9f3f9 --- /dev/null +++ b/08.Conn/Taskfile.yml @@ -0,0 +1,12 @@ +# https://taskfile.dev + +version: '3' + +vars: + DBFILE: chinook.db + +tasks: + default: + cmds: + - cp -f ../{{.DBFILE}} . + - go run main.go diff --git a/08.Conn/main.go b/08.Conn/main.go new file mode 100644 index 0000000..4331fae --- /dev/null +++ b/08.Conn/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + log.SetFlags(0) +} + +// 08.Conn +// +// db.Query()などの関数を利用すると、内部のコネクションプールから +// 任意のコネクションが利用される。単一の接続を取得して処理を行いたい場合は +// db.Conn()を利用して、コネクションを取得する。 +// +// >Conn returns a single connection by either opening a new connection or returning an existing connection from the connection pool. +// Conn will block until either a connection is returned or ctx is canceled. +// Queries run on the same Conn will be run in the same database session. +// +// >Connは、新しい接続をオープンするか、接続プールから既存の接続を返すことによって、単一の接続を返します。 +// Connは、接続が返されるかctxがキャンセルされるまでブロックします。 +// 同じConnで実行されるクエリは、同じデータベース・セッションで実行されます。 +// +// 取得したコネクションは Close メソッドを呼び出してリソースを解放する必要がある。 +// +// クエリの発行方法などは、*sql.DB と同じである。 +func main() { + if err := run(); err != nil { + log.Panic(err) + } + + /* + $ task -d 08.Conn/ + task: [default] cp -f ../chinook.db . + task: [default] go run main.go + AC/DC + */ +} + +func run() error { + var ( + db *sql.DB + err error + ) + + db, err = sql.Open("sqlite3", "chinook.db") + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + defer db.Close() + + err = db.Ping() + if err != nil { + return fmt.Errorf("db.Ping: %w", err) + } + + var ( + ctx = context.Background() + conn *sql.Conn + ) + + conn, err = db.Conn(ctx) + if err != nil { + return fmt.Errorf("db.Conn: %w", err) + } + defer conn.Close() + + err = conn.PingContext(ctx) + if err != nil { + return fmt.Errorf("conn.PingContext: %w", err) + } + + var ( + row *sql.Row + name string + ) + + row = conn.QueryRowContext(ctx, "SELECT Name from artists") + err = row.Scan(&name) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("conn.QueryContext: %w", err) + } + + log.Println(name) + + return nil +} diff --git a/09.Columns/Taskfile.yml b/09.Columns/Taskfile.yml new file mode 100644 index 0000000..234d788 --- /dev/null +++ b/09.Columns/Taskfile.yml @@ -0,0 +1,13 @@ +# https://taskfile.dev + +version: "3" + +vars: + DBFILE: chinook.db + +tasks: + default: + cmds: + - cp -f ../{{.DBFILE}} . + - echo "PRAGMA table_info(tracks)" | sqlite3 -header -table ./{{.DBFILE}} + - go run main.go diff --git a/09.Columns/main.go b/09.Columns/main.go new file mode 100644 index 0000000..1b61925 --- /dev/null +++ b/09.Columns/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "database/sql" + "log" + + _ "github.com/mattn/go-sqlite3" +) + +func init() { + log.SetFlags(0) +} + +// 09.Columns +// +// *sql.DB.Query() などで *sql.Rows を取得した際に +// カラム名を知りたい場合は *sql.Rows.Columns() を利用する。 +// +// https://pkg.go.dev/database/sql@go1.22.0#Rows.Columns +// +// 結果は []string で返ってくる。 +// SELECTで指定した並びで設定されている。 +// +// 上記ドキュメントには以下の記載がある。 +// +// >Columns returns the column names. Columns returns an error if the rows are closed. +// +// >Columnsはカラム名を返す。Columnsは、行が閉じられている場合はエラーを返します。 +func main() { + if err := run(); err != nil { + log.Panic(err) + } +} + +func run() error { + var ( + db *sql.DB + err error + ) + + db, err = sql.Open("sqlite3", "./chinook.db") + if err != nil { + return err + } + defer db.Close() + + var ( + rows *sql.Rows + ) + + rows, err = db.Query("SELECT * FROM tracks LIMIT 1") + if err != nil { + return err + } + defer rows.Close() + + var ( + columns []string + ) + + columns, err = rows.Columns() + if err != nil { + return err + } + + log.Println(columns) + + return nil +} diff --git a/go.mod b/go.mod index ed4dde4..18d07b6 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/devlights/try-golang-database -go 1.21.6 +go 1.22 -require github.com/mattn/go-sqlite3 v1.14.19 +require github.com/mattn/go-sqlite3 v1.14.22 diff --git a/go.sum b/go.sum index 3042612..e8d092a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=