チーム開発参加の記録【2023-10～2024-03】(2) Go言語からTursoを使ってみた

# 本シリーズのリンク

- [チーム開発参加の記録【2023-10～2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み（SQLite使用）](https://qiita.com/kanedaq/items/97ca682602b626d5d717)
- チーム開発参加の記録【2023-10～2024-03】(2) Go言語からTursoを使ってみた
- [チーム開発参加の記録【2023-10～2024-03】(3) slogを使ってリクエスト・レスポンス情報をログ出力してみた](https://qiita.com/kanedaq/items/3914de42cb111c8817bc)
- [チーム開発参加の記録【2023-10～2024-03】(4) Go言語用ORM「Bun」をDBファーストで使う試み（PostgreSQL使用）](https://qiita.com/kanedaq/items/ec9bd42fefc755559df6)
- [チーム開発参加の記録【2023-10～2024-03】(5) Go言語用ORM「Bun」でトランザクション、UPSERT、JOINを使ってみた](https://qiita.com/kanedaq/items/0d3f5f44202548839d78)
- [チーム開発参加の記録【2023-10～2024-03】(6) Go言語用ORM「Bun」で複数のクエリーをまとめてDBサーバーで実行](https://qiita.com/kanedaq/items/c446f937cee101e24c3e)
- [チーム開発参加の記録【2023-10～2024-03】(7) slogでErrorレベルのログをSlackに飛ばしてみた](https://qiita.com/kanedaq/items/90584280dbc248e21ff2)

# 本記事で行うこと

[前回の記事]((https://qiita.com/kanedaq/items/97ca682602b626d5d717))ではSQLiteでORM「Bun」を使ってみました。
本記事では、前回作ったGoプログラムを[Turso](https://turso.tech/)で動かしてみます。

# Tursoチュートリアルに沿ってDBを用意する

[チュートリアル](https://docs.turso.tech/tutorials/)を見ながら、CLIをインストールするところから始めて、テーブル作成＆テストデータINSERTまで行います。

## [CLIインストール](https://docs.turso.tech/tutorials/get-started-turso-cli/step-01-installation)

私はLinux環境ですので、以下のコマンドを実行しました。

```sh
curl -sSfL https://get.tur.so/install.sh | bash
```

バージョン確認

```
$ turso --version
turso version v0.87.6
```

## [サインアップ](https://docs.turso.tech/tutorials/get-started-turso-cli/step-02-sign-up)

以下を実行後、ブラウザでサインアップします。

```sh
turso auth signup
```

## [論理データベース作成](https://docs.turso.tech/tutorials/get-started-turso-cli/step-03-create-database)

以下を実行してDBを作成しました。
DB名は何でも良いですが、本記事ではチュートリアルのまま「my-db」にしました。

```sh
turso db create my-db
```

DB作成後、以下を実行してDBの情報を取得します。

```sh
turso db show my-db
```

表示された情報：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/11f3e4aa-59dc-cb6c-ce0a-29581fd57aad.png)

この情報のうち、URLはGo言語のプログラムに埋め込んで使います。

## [DBのコマンドラインツールでテーブルを作成する](https://docs.turso.tech/tutorials/get-started-turso-cli/step-04-make-queries-with-shell)

以下を実行してDBのコマンドラインツールを立ち上げます。

```sh
turso db shell my-db
```

前回の記事と同じテーブルを作成します。

```sql
CREATE TABLE foo (
    col_text           TEXT    NOT NULL,
    col_text_null      TEXT,
    col_int            INTEGER NOT NULL,
    col_int_null       INTEGER,
    col_real           REAL    NOT NULL,
    col_real_null      REAL,
    col_timestamp      REAL    NOT NULL,
    col_timestamp_null REAL,
    col_json           TEXT    NOT NULL,
    col_json_null      TEXT,
    col_array          TEXT    NOT NULL,
    col_array_null     TEXT
) STRICT;
```

スクリーンショット：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/22c659a2-679c-2253-ab9a-1a75aa2ede86.png)

引き続き、前回の記事と同じデータをINSERTします。

```sql
INSERT INTO foo
(
    col_text,
    col_text_null,
    col_int,
    col_int_null,
    col_real,
    col_real_null,
    col_timestamp,
    col_timestamp_null,
    col_json,
    col_json_null,
    col_array,
    col_array_null
)
VALUES
(
    'text_data',
    NULL,
    1234567890123456789,
    NULL,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    NULL,
    JULIANDAY('now'),
    NULL,
    '{"a":100,"b":"bbb"}',
    NULL,
    '[10,null]',
    NULL
),
(
    'text_data',
    'text_data',
    1234567890123456789,
    1234567890123456789,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117,
    JULIANDAY('now'),
    JULIANDAY('now'),
    '{"a":200,"b":null}',
    '{"a":300,"z":"zzz"}',
    '[30,null,50]',
    '[]'
);
```

INSERTしたら、fooテーブルにSELECT文を発行してみます。

```sql
SELECT
    col_text,
    col_text_null,
    col_int,
    col_int_null,
    col_real,
    col_real_null,
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp,
    STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null,
    col_json,
    col_json_null,
    col_array,
    col_array_null
FROM
    foo;
```

スクリーンショット：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/e7487028-05b4-9391-f4ee-01e9c862de40.png)

結果が見づらいですが、データがINSERTされたことが確認できました。

DBのコマンドラインツールから抜けるには、Ctrl+dを押すか、以下のようにコマンドを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/ef9697d6-51c9-8645-3eae-6e3f4ae5d874.png)

# Go言語からTursoのDBを操作する

それではGo言語に行きます。
前回の記事とほぼ同じプログラムですが、Turso向けに変更を加えてあります。

## app.go

```app.go
package main

import (
	"context"
	"database/sql"
	"net/http"
	"os"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	_ "github.com/tursodatabase/libsql-client-go/libsql"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/sqlitedialect"
	"github.com/uptrace/bun/extra/bundebug"
)

type (
	httpError struct {
		Error string `json:"error"`
	}

	httpMessage struct {
		Message string `json:"message"`
	}
)

var (
	db *bun.DB
)

func selectFoo(c echo.Context) error {
	type (
		jsonType struct {
			A *int    `json:"a"`
			B *string `json:"b"`
		}

		resultSetType struct {
			ColText          string       `json:"colText"`
			ColTextNull      *string      `json:"colTextNull"`
			ColInt           int64        `json:"colInt"`
			ColIntNull       *int64       `json:"colIntNull"`
			ColReal          float64      `json:"colReal"`
			ColRealNull      *float64     `json:"colRealNull"`
			ColTimestamp     time.Time    `json:"colTimestamp"`
			ColTimestampNull bun.NullTime `json:"colTimestampNull"`
			ColJson          jsonType     `json:"colJson"`
			ColJsonNull      *jsonType    `json:"colJsonNull"`
			ColArray         []*int64     `json:"colArray"`
			ColArrayNull     *[]*int64    `json:"colArrayNull"`
		}
	)

	ctx := context.TODO()

	resultSetFoo := make([]resultSetType, 0)
	if err := db.NewSelect().
		Column("col_text").
		Column("col_text_null").
		Column("col_int").
		Column("col_int_null").
		Column("col_real").
		Column("col_real_null").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null").
		Column("col_json").
		Column("col_json_null").
		Column("col_array").
		Column("col_array_null").
		Table("foo").
		Scan(ctx, &resultSetFoo); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, resultSetFoo)
}

func updateFoo(c echo.Context) error {
	ctx := context.TODO()

	if _, err := db.NewUpdate().
		Table("foo").
		SetColumn("col_text_null", "?", "ABC").
		Where("col_text_null IS NULL").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func insertFoo(c echo.Context) error {
	type dummyModel struct{}

	ctx := context.TODO()

	if _, err := db.NewInsert().
		Model((*dummyModel)(nil)).
		ModelTableExpr("foo").
		Value("col_text", "?", "XYZ").
		Value("col_text_null", "?", nil).
		Value("col_int", "?", 123).
		Value("col_int_null", "?", nil).
		Value("col_real", "?", 3.14).
		Value("col_real_null", "?", nil).
		Value("col_timestamp", "JULIANDAY('now')").
		Value("col_timestamp_null", "?", nil).
		Value("col_json", "?", "{}").
		Value("col_json_null", "?", nil).
		Value("col_array", "?", "[]").
		Value("col_array_null", "?", nil).
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func deleteFoo(c echo.Context) error {
	ctx := context.TODO()

	if _, err := db.NewDelete().
		Table("foo").
		Where("col_text = ?", "XYZ").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func main() {
	tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN")
	dbUrl := "libsql://my-db-kanedaq.turso.io?authToken=" + tursoAuthToken
	sqlite, err := sql.Open("libsql", dbUrl)
	if err != nil {
		panic(err)
	}
	sqlite.SetMaxOpenConns(1)

	db = bun.NewDB(sqlite, sqlitedialect.New())
	db.AddQueryHook(bundebug.NewQueryHook(
		bundebug.WithVerbose(true),
		bundebug.FromEnv("BUNDEBUG"),
	))

	// EchoでAPIサーバー
	e := echo.New()
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routing
	api := e.Group("/api")
	api.GET("/selectFoo", selectFoo)
	api.PUT("/updateFoo", updateFoo)
	api.POST("/insertFoo", insertFoo)
	api.DELETE("/deleteFoo", deleteFoo)

	e.Logger.Fatal(e.Start(":1323"))
}
```

前回の記事のapp.goとの差分は以下の通りです。
Tursoは、SQLiteをフォークした[libSQL](https://docs.turso.tech/libsql/)を使っていますので、その[Go SDK](https://docs.turso.tech/libsql/client-access/go-sdk)を使うように変更しました。


```diff
@@ -4,13 +4,14 @@
        "context"
        "database/sql"
        "net/http"
+       "os"
        "time"

        "github.com/labstack/echo/v4"
        "github.com/labstack/echo/v4/middleware"
+       _ "github.com/tursodatabase/libsql-client-go/libsql"
        "github.com/uptrace/bun"
        "github.com/uptrace/bun/dialect/sqlitedialect"
-       "github.com/uptrace/bun/driver/sqliteshim"
        "github.com/uptrace/bun/extra/bundebug"
 )

@@ -134,7 +135,9 @@
 }

 func main() {
-       sqlite, err := sql.Open(sqliteshim.ShimName, "/home/user/DataGripProjects/sqlite/exercise.sqlite3")
+       tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN")
+       dbUrl := "libsql://my-db-kanedaq.turso.io?authToken=" + tursoAuthToken
+       sqlite, err := sql.Open("libsql", dbUrl)
        if err != nil {
                panic(err)
        }
```

## go.mod

```go.mod
module exercise_turso

go 1.21

require (
	github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230512164433-5d1fd1a340c9 // indirect
	github.com/fatih/color v1.15.0 // indirect
	github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/klauspost/compress v1.15.15 // indirect
	github.com/labstack/echo/v4 v4.11.3 // indirect
	github.com/labstack/gommon v0.4.0 // indirect
	github.com/tursodatabase/libsql-client-go v0.0.0-20231216154754-8383a53d618f // indirect
	github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.19 // indirect
	github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
	github.com/uptrace/bun v1.1.16 // indirect
	github.com/uptrace/bun/dialect/sqlitedialect v1.1.16 // indirect
	github.com/uptrace/bun/extra/bundebug v1.1.16 // indirect
	github.com/valyala/bytebufferpool v1.0.0 // indirect
	github.com/valyala/fasttemplate v1.2.2 // indirect
	github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
	golang.org/x/crypto v0.14.0 // indirect
	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
	golang.org/x/net v0.17.0 // indirect
	golang.org/x/sys v0.13.0 // indirect
	golang.org/x/text v0.13.0 // indirect
	golang.org/x/time v0.3.0 // indirect
	nhooyr.io/websocket v1.8.7 // indirect
)
```

## 実行準備

上記プログラムからTursoのDBに接続するために、[認証トークンが必要](https://docs.turso.tech/reference/turso-cli#database-client-authentication-tokens)です。
以下を実行して認証トークンを作成します。

```sh
turso db tokens create my-db
```

app.go用に、このトークンを環境変数TURSO_AUTH_TOKENに設定すれば、実行準備完了です。

## 実行

4つのAPIを動かしたところ、前回の記事と同じ結果になりました。
以下、selectFoo APIの結果のみ載せておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/8189bf8e-263f-06b3-9512-0f6339edd092.png)

以上、Tursoで前回の記事と同じ処理ができることが確認できました。

# [libSQL server](https://docs.turso.tech/libsql/server/setup)をローカルで動かす

Tursoに接続しなくても、[libSQL server](https://github.com/tursodatabase/libsql/tree/main/libsql-server)をローカルで動かすこともできます。
[開発中](https://docs.turso.tech/reference/local-development)はこちらを使うと良いでしょう。

## libSQL serverのインストールと起動

[このページ](https://github.com/libsql/sqld/releases)から「sqld-installer.sh」をダウンロードして起動し、指示に従ってバイナリをインストール後、sqldコマンドでlibSQL serverを起動しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/26c405b6-182c-e5d5-7d38-551cf38dfff5.png)

ホームディレクトリでsqldを起動したら、~/data.sqld/dbs/default というディレクトリが作成され、そこに以下のファイルが作られていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/db8c3af8-edce-fbfe-b9b6-9cc6f4b453eb.png)

このうち、dataというファイルがデータベースファイルでした。
以下のように、sqlite3コマンドでこのdataファイルに接続して、DBを操作できました。

```sh
sqlite3 ~/data.sqld/dbs/default/data
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/3e2e21a3-a2fc-9c07-636b-623ac6e3b74b.png)

## Go言語からローカルのlibSQL DBを操作する

それではGo言語に行きます。
Turso向けとの違いは、

- URLがローカルホスト
- 認証トークンが不要

```app.go
package main

import (
	"context"
	"database/sql"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	_ "github.com/tursodatabase/libsql-client-go/libsql"
	"github.com/uptrace/bun"
	"github.com/uptrace/bun/dialect/sqlitedialect"
	"github.com/uptrace/bun/extra/bundebug"
)

type (
	httpError struct {
		Error string `json:"error"`
	}

	httpMessage struct {
		Message string `json:"message"`
	}
)

var (
	db *bun.DB
)

func selectFoo(c echo.Context) error {
	type (
		jsonType struct {
			A *int    `json:"a"`
			B *string `json:"b"`
		}

		resultSetType struct {
			ColText          string       `json:"colText"`
			ColTextNull      *string      `json:"colTextNull"`
			ColInt           int64        `json:"colInt"`
			ColIntNull       *int64       `json:"colIntNull"`
			ColReal          float64      `json:"colReal"`
			ColRealNull      *float64     `json:"colRealNull"`
			ColTimestamp     time.Time    `json:"colTimestamp"`
			ColTimestampNull bun.NullTime `json:"colTimestampNull"`
			ColJson          jsonType     `json:"colJson"`
			ColJsonNull      *jsonType    `json:"colJsonNull"`
			ColArray         []*int64     `json:"colArray"`
			ColArrayNull     *[]*int64    `json:"colArrayNull"`
		}
	)

	ctx := context.TODO()

	resultSetFoo := make([]resultSetType, 0)
	if err := db.NewSelect().
		Column("col_text").
		Column("col_text_null").
		Column("col_int").
		Column("col_int_null").
		Column("col_real").
		Column("col_real_null").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp) AS col_timestamp").
		ColumnExpr("STRFTIME('%Y-%m-%dT%H:%M:%fZ', col_timestamp_null) AS col_timestamp_null").
		Column("col_json").
		Column("col_json_null").
		Column("col_array").
		Column("col_array_null").
		Table("foo").
		Scan(ctx, &resultSetFoo); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, resultSetFoo)
}

func updateFoo(c echo.Context) error {
	ctx := context.TODO()

	if _, err := db.NewUpdate().
		Table("foo").
		SetColumn("col_text_null", "?", "ABC").
		Where("col_text_null IS NULL").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func insertFoo(c echo.Context) error {
	type dummyModel struct{}

	ctx := context.TODO()

	if _, err := db.NewInsert().
		Model((*dummyModel)(nil)).
		ModelTableExpr("foo").
		Value("col_text", "?", "XYZ").
		Value("col_text_null", "?", nil).
		Value("col_int", "?", 123).
		Value("col_int_null", "?", nil).
		Value("col_real", "?", 3.14).
		Value("col_real_null", "?", nil).
		Value("col_timestamp", "JULIANDAY('now')").
		Value("col_timestamp_null", "?", nil).
		Value("col_json", "?", "{}").
		Value("col_json_null", "?", nil).
		Value("col_array", "?", "[]").
		Value("col_array_null", "?", nil).
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func deleteFoo(c echo.Context) error {
	ctx := context.TODO()

	if _, err := db.NewDelete().
		Table("foo").
		Where("col_text = ?", "XYZ").
		Exec(ctx); err != nil {

		return c.JSON(http.StatusInternalServerError, httpError{Error: err.Error()})
	}

	return c.JSON(http.StatusOK, httpMessage{Message: "ok"})
}

func main() {
	dbUrl := "http://127.0.0.1:8080"
	sqlite, err := sql.Open("libsql", dbUrl)
	if err != nil {
		panic(err)
	}
	sqlite.SetMaxOpenConns(1)

	db = bun.NewDB(sqlite, sqlitedialect.New())
	db.AddQueryHook(bundebug.NewQueryHook(
		bundebug.WithVerbose(true),
		bundebug.FromEnv("BUNDEBUG"),
	))

	// EchoでAPIサーバー
	e := echo.New()
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routing
	api := e.Group("/api")
	api.GET("/selectFoo", selectFoo)
	api.PUT("/updateFoo", updateFoo)
	api.POST("/insertFoo", insertFoo)
	api.DELETE("/deleteFoo", deleteFoo)

	e.Logger.Fatal(e.Start(":1323"))
}
```

Turso版app.goとの差分は以下の通りです。

```diff
@@ -4,7 +4,6 @@
        "context"
        "database/sql"
        "net/http"
-       "os"
        "time"

        "github.com/labstack/echo/v4"
@@ -135,8 +134,7 @@
 }

 func main() {
-       tursoAuthToken := os.Getenv("TURSO_AUTH_TOKEN")
-       dbUrl := "libsql://my-db-kanedaq.turso.io?authToken=" + tursoAuthToken
+       dbUrl := "http://127.0.0.1:8080"
        sqlite, err := sql.Open("libsql", dbUrl)
        if err != nil {
                panic(err)
```

動作確認したところ、Turso版と同じ結果が得られました。

# まとめ

初めてTursoを使いましたが、SQLite向けのプログラムを少し変更するだけで動いてくれました。