Primeiras impressões com o banco de dados Turso

O Turso é um daqueles projetos que você olha e pensa “como ninguém havia feito algo assim antes?”. Venho acompanhando o projeto desde seu lançamento mas somente agora consegui dedicar um tempo para fazer alguns testes, que descrevo neste post.

Simplificando bastante, ele adiciona uma camada de distribuição e sincronização de dados usando um dos bancos de dados mais menosprezados do mercado, o SQLite.

Para começar o teste eu fiz uma conta free no site da empresa, que dá direito a uma boa quantidade de recursos para testes e projetos pessoais.

O próximo passo foi instalar o CLI no meu macOS, usando o comando:

brew install tursodatabase/tap/turso

Na documentação é possível ver como fazer a instalação em outros sistemas operacionais.

O próximo passo foi fazer a autenticação:

turso auth signup

E criar o primeiro banco de dados:

turso db create demo-post

Para este exemplo eu criei uma tabela bem simples, usando os comandos:

turso db shell demo-post


CREATE TABLE books (
  ID INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT,
  author TEXT,
  category int
);
.quit

Como vou usar Go para este exemplo eu criei um novo projeto e instalei a dependência necessária:

mkdir post-turso
go mod init github.com/eminetto/post-turso
go get github.com/tursodatabase/go-libsql

O exemplo mais simples para testar é com um código similar a:

package main

import (
	"database/sql"
	"fmt"
	"github.com/tursodatabase/go-libsql"
	"os"
	"path/filepath"
	"time"
)

func main() {
	dir, err := os.MkdirTemp("", "libsql-*")
	if err != nil {
		fmt.Printf("creating temporary directory: %w", err)
		return
	}

	dbPath := filepath.Join(dir, os.Getenv("DBNAME"))
	syncInterval := time.Second * 30

	connector, err := libsql.NewEmbeddedReplicaConnector(dbPath, os.Getenv("TURSO_DATABASE_URL"),
		libsql.WithAuthToken(os.Getenv("TURSO_AUTH_TOKEN")),
		libsql.WithSyncInterval(syncInterval),
	)

	if err != nil {
		fmt.Printf("creating connector: %w", err)
		return
	}
	db := sql.OpenDB(connector)
	defer func() {
		err := os.RemoveAll(dir)
		if err != nil {
			fmt.Printf("removing temporary directory: %w", err)
		}
		err = connector.Close()
		if err != nil {
			fmt.Printf("closing connector: %w", err)
		}
		err = db.Close()
		if err != nil {
			fmt.Printf("closing repository: %w", err)
		}
	}()
	id, err := insert(db)
	if err != nil {
		fmt.Printf("inserting: %w", err)
		return
	}
	fmt.Println("Inserted ", id)
	err = showAll(db)
	if err != nil {
		fmt.Printf("showing all: %w", err)
		return
	}
}

func insert(db *sql.DB) (int64, error) {
	stmt, err := db.Prepare(`
		insert into books (title, author, category) 
		values(?,?,?)`)
	if err != nil {
		return 0, fmt.Errorf("preparing statement: %w", err)
	}
	result, err := stmt.Exec(
		"Foundation",
		"Isaac Asimov",
		1, //read
	)
	if err != nil {
		return 0, fmt.Errorf("executing statement: %w", err)
	}
	err = stmt.Close()
	if err != nil {
		return 0, fmt.Errorf("closing statement: %w", err)
	}
	id, err := result.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("getting last insert ID: %w", err)
	}

	return id, nil
}

func showAll(db *sql.DB) error {
	rows, err := db.Query("SELECT * FROM books")
	if err != nil {
		return fmt.Errorf("selecting books: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id int64
		var title string
		var author string
		var category int64

		if err := rows.Scan(&id, &title, &author, &category); err != nil {
			return fmt.Errorf("scanning book: %w", err)
		}

		fmt.Println(id, title, author, category)
	}

	if err := rows.Err(); err != nil {
		return fmt.Errorf("interacting with books: %w", err)
	}
	return nil
}

Como é possível visualizar no código, precisamos definir algumas variáveis de ambiente, em especial a TURSO_DATABASE_URL e a TURSO_AUTH_TOKEN. Para isso precisamos gerar estes dados usando os comandos:

turso db show --url demo-post
turso db tokens create demo-post

Estes dois comandos vão mostrar no terminal a url e o token, respectivamente. Com isso podemos exportar as variáveis necessárias:

export DBNAME="local.db"
export TURSO_DATABASE_URL="DATABASE_URL"
export TURSO_AUTH_TOKEN="AUTH_TOKEN"
export PORT="8082"

Vou agora destacar alguns pontos importantes do código acima.

dir, err := os.MkdirTemp("", "libsql-*")
if err != nil {
	fmt.Printf("creating temporary directory: %w", err)
	return
}

dbPath := filepath.Join(dir, os.Getenv("DBNAME"))

Neste trecho definimos um banco de dados local, em um diretório temporário. Isso é um dos detalhes interessantes do Turso. Todas as escritas (inserts, deletes, updates) vão ser realizados no banco de dados remoto, identificado pela URL e token gerados no passo anterior. Enquanto isso, as leituras são realizadas neste banco de dados temporário, garantindo uma grande velocidade na leitura.

Mas e como os dados são sincronizados? É o que vamos configurar no próximo trecho:

syncInterval := time.Second * 30

connector, err := libsql.NewEmbeddedReplicaConnector(dbPath, os.Getenv("TURSO_DATABASE_URL"),
	libsql.WithAuthToken(os.Getenv("TURSO_AUTH_TOKEN")),
	libsql.WithSyncInterval(syncInterval),
)

Isso indica à libsql para que ela faça uma sincronização a cada intervalo determinado, neste caso, 30 segundos. Também é possível realizar a sincronização manualmente, conforme a documentação indica.

O restante do código não tem muita novidade, sendo a manipulação de dados em um banco de dados SQLite.

Outra feature interessante é o dashboard com informações importantes e úteis como latência, linhas escritas/lidas, tamanho do banco de dados, etc:

turso_1

turso_2

Primeiras impressões

Como o título promete, quero aqui tecer minhas primeiras impressões quanto ao produto.

Quando vi a primeira notícia sobre a startup e o produto eu achei a ideia bem interessante, pois o SQLite é quase que onipresente entre as linguagens de programação modernas. “Turbinar” ele com features como distribuição e sincronização me parece uma ótima oportunidade de negócio e que abre uma série de possibilidades. No site é possível vermos alguns depoimentos, bem como áreas de negócio que podem fazer uso destas features.

Eu notei uma latência relativamente alta para realizar a escrita dos dados, mas me parece que é devido ao fato de eu estar usando o plano free e que isso muda nos planos pagos. Mas a performance de leitura é realmente bem atrativa. Também acredito que seja possível fazer tunnings em relação a escrita, especialmente em ambientes como a AWS, mas não realizei este tipo de teste para escrever este texto.

Se a sua aplicação faz mais leituras do que escritas (é possível pensar em vários cenários como um catálogo de produtos, uma aplicação que lê as configurações dos usuários e guarda em um banco local, o uso como área de cache, etc) o Turso pode trazer funcionalidades importante aliadas a simplicidade ao seu projeto.

Vou manter o Turso no meu radar e caso eu consiga aplicá-lo em algum projeto “vida real” eu escrevo um segundo post aprimorando minha opinião com mais informações.

Também escrevi um projeto um pouco mais estruturado do que o exemplo apresentado aqui, que pode ser visualizado no meu Github.

O que achou do Turso? Compartilhe suas opiniões nos comentários.