Pular para o conteúdo

Chisel

Chisel e uma camada de acesso a dados tipada para PetraDB escrita em Scala 3. Ela mapeia Values para tipos Scala, constroi SQL seguro contra injecao com um interpolador de strings e gera CRUD para uma tabela — sem o peso de um ORM completo (sem identity map, sem unit of work, sem proxies lazy). Funciona em todos os lugares onde PetraDB funciona: JVM, Node.js (Scala.js) e Native, usando derivacao em tempo de compilacao em vez de reflexao em tempo de execucao.

Chisel conversa diretamente com a Session do engine via parametros vinculados, entao nao precisa de JDBC e nao renderiza nenhum valor no texto SQL.

libraryDependencies += "io.github.edadma" %%% "petradb-chisel" % "1.5.1"

Chisel depende do engine para uma sessao:

libraryDependencies += "io.github.edadma" %%% "petradb-engine" % "1.5.5"

Tudo roda sobre uma Session e precisa de um ExecutionContext. As operacoes do Chisel retornam Futures.

import io.github.edadma.petradb.Session
import io.github.edadma.petradb.engine.MemoryDB
import io.github.edadma.petradb.chisel.*
import scala.concurrent.ExecutionContext.Implicits.global
given Session = new MemoryDB().connect()

A sessao e passada implicitamente para cada operacao do Chisel, entao um given Session em escopo e tudo o que e necessario.

Chisel mapeia entre valores Scala e Values do PetraDB atraves de quatro type classes:

Type classDirecaoPapel
Get[A]Value => Adecodifica uma coluna
Put[A]A => Valuecodifica um valor (vincula parametros)
Read[A]Row => Adecodifica uma linha inteira
Write[A]A => Seq[(String, Value)]codifica uma linha inteira

Instancias de Get e Put sao fornecidas para os tipos escalares comuns:

Tipo ScalaTipo PetraDB
Int, Long, Short, Bytefamilia integer
Double, Floatponto flutuante
BigDecimalNUMERIC
StringTEXT
BooleanBOOLEAN
java.time.LocalDateDATE
java.time.LocalTimeTIME
java.time.LocalDateTimeTIMESTAMP
java.time.OffsetDateTimeTIMESTAMPTZ
java.time.OffsetTimeTIMETZ
java.time.DurationINTERVAL
Array[Byte]BYTEA
java.util.UUIDUUID
Option[A]A nulavel (None ⇄ SQL NULL)

Uma coluna cujo tipo nao corresponde levanta DecodeException.

Derive Read e Write para uma case class para mapear linhas inteiras. A derivacao e por nome de campo, entao ela e robusta a reordenacao de colunas e funciona diretamente com SELECT *:

case class User(id: Long, name: String, age: Int) derives Read, Write

Campos Option mapeiam para colunas nulaveis:

case class Account(id: Long, nickname: Option[String]) derives Read, Write

Qualquer product deriva, incluindo uma named tuple — util para projecoes ad-hoc sem declarar uma classe:

val r = Read.derived[(id: Long, name: String)]

Get/Read tem .map e Put/Write tem .contramap para adaptar a tipos que o Chisel nao conhece:

enum Color:
case Red, Green, Blue
given Get[Color] = Get[String].map(Color.valueOf)
given Put[Color] = Put[String].contramap[Color](_.toString)

sql"…" constroi um Fragment — texto SQL mais parametros vinculados. Cada argumento interpolado e codificado atraves de seu Put e vinculado como um parametro, entao os valores nunca se tornam texto SQL e a injecao e impossivel:

val minAge = 18
val frag = sql"select id, name, age from users where age >= $minAge"

Um Fragment renderiza para a forma de placeholder $1, $2, … do engine:

frag.sql // "select id, name, age from users where age >= $1"
frag.params // Seq(NumberValue(18))

Option e Values pre-construidos tambem podem ser interpolados — None vincula SQL NULL.

Fragments sao armazenados sem renderizar, entao eles compoem com ++ e renumeram placeholders automaticamente. Fragment.const contribui com texto bruto (para identificadores confiaveis como nomes de tabela), Fragment.param um unico valor vinculado:

val table = "users"
val q = sql"select * from " ++ Fragment.const(table) ++ sql" where age >= $minAge"

Anexe um decodificador de linha com .query[A], depois escolha como coletar o resultado:

val users: Future[List[User]] =
sql"select * from users order by id".query[User].toList
val one: Future[Option[User]] =
sql"select * from users where id = ${1L}".query[User].option
val exactlyOne: Future[User] =
sql"select * from users where id = ${1L}".query[User].unique
MetodoResultadoNotas
.toListFuture[List[A]]todas as linhas
.toVectorFuture[Vector[A]]todas as linhas
.optionFuture[Option[A]]primeira linha ou None
.uniqueFuture[A]exatamente uma linha; falha caso contrario

INSERT … RETURNING tambem decodifica atraves de .query:

val inserted: Future[User] =
sql"insert into users (name, age) values (${"alice"}, ${30}) returning *"
.query[User].unique

Leia a primeira coluna da primeira linha diretamente com queryValue — para count(*), exists, max e similares:

val total: Future[Long] = sql"select count(*) from users".queryValue[Long]
val maybeMax: Future[Option[Int]] = sql"select max(age) from users".queryValueOption[Int]

Para SQL que nao e de consulta, .update retorna a contagem de linhas afetadas e .run retorna os resultados brutos do engine:

val changed: Future[Int] =
sql"update users set age = ${31} where id = ${1L}".update
val raw: Future[Seq[Result]] =
sql"create table users (id serial primary key, name text, age integer)".run

Repo[A, Id] gera CRUD para uma unica tabela a partir das instancias Read/Write da entidade. Use sql"…" para qualquer coisa que ele nao cubra.

case class User(id: Long, name: String, age: Int) derives Read, Write
val users = Repo[User, Long]("users") // idColumn = "id", generatedId = true

Por padrao, Repo assume uma chave gerada pelo banco de dados (SERIAL), entao insert/insertReturning omitem a coluna id e o banco de dados a atribui. Passe um id placeholder ao construir a entidade — ele e ignorado no insert:

val saved: Future[User] = users.insertReturning(User(0, "alice", 30))
// saved.id e a chave gerada
users.findById(1L) // Future[Option[User]]
users.findAll // Future[List[User]]
users.count // Future[Long]
users.existsById(1L) // Future[Boolean]
users.insert(User(0, "bob", 25)) // Future[Int] — linhas inseridas
users.insertReturning(User(0, "eve", 22)) // Future[User] — recupera o id gerado
users.update(saved.copy(age = 31)) // Future[Int] — define todas as colunas nao-id onde id corresponde
users.deleteById(1L) // Future[Int]
users.deleteAll // Future[Int]

Para tabelas cuja chave primaria e fornecida pela aplicacao (nao gerada), defina generatedId = false e nomeie a coluna id se ela nao for "id":

case class Widget(sku: String, label: String) derives Read, Write
val widgets = Repo[Widget, String]("widgets", idColumn = "sku", generatedId = false)
widgets.insert(Widget("w-1", "Sprocket"))
widgets.findById("w-1")

Chisel e publicado para JVM, Scala.js e Scala Native a partir de uma unica fonte. A decodificacao de linha usa derivacao inline de Mirror do Scala 3, entao nao ha reflexao em tempo de execucao em nenhuma plataforma.

summon[Session].close()