Current section
Files
Jump to
Current section
Files
src/parrot/internal/sqlc.gleam
//// This module decodes the JSON generated by sqlc
////
import filepath
import given
import gleam/bit_array
import gleam/crypto
import gleam/dynamic
import gleam/dynamic/decode
import gleam/option.{type Option}
import gleam/result
import gleam/set
import gleam/string
import parrot/internal/cli
import parrot/internal/errors
import parrot/internal/project
import parrot/internal/shellout
import simplifile.{Execute, FilePermissions, Read, Write}
pub type TypeRef {
TypeRef(catalog: String, schema: String, name: String)
}
pub type TableColumn {
TableColumn(
name: String,
not_null: Bool,
is_array: Bool,
comment: String,
length: Int,
is_named_param: Bool,
is_func_call: Bool,
scope: String,
table_alias: String,
is_sqlc_slice: Bool,
original_name: String,
unsigned: Bool,
array_dims: Int,
table: Option(TableRef),
type_ref: TypeRef,
)
}
pub type TableRef {
TableRef(catalog: String, schema: String, name: String)
}
pub type Table {
Table(rel: TableRef, comment: String, columns: List(TableColumn))
}
pub type Schema {
Schema(comment: String, name: String, tables: List(Table), enums: List(Enum))
}
pub type Enum {
Enum(name: String, vals: List(String), comment: String)
}
pub type Catalog {
Catalog(
comment: String,
default_schema: String,
name: String,
schemas: List(Schema),
)
}
pub type QueryCmd {
One
Many
Exec
ExecResult
}
pub type QueryParam {
QueryParam(number: Int, column: TableColumn)
}
pub type Query {
Query(
text: String,
name: String,
cmd: QueryCmd,
filename: String,
columns: List(TableColumn),
insert_into_table: Option(TableRef),
comments: List(String),
params: List(QueryParam),
)
}
pub type SQLC {
SQLC(
sqlc_version: String,
plugin_options: String,
global_options: String,
catalog: Catalog,
queries: List(Query),
)
}
pub fn decode_sqlc(data: dynamic.Dynamic) {
let table_ref_decoder = {
use name <- decode.field("name", decode.string)
use schema <- decode.field("schema", decode.string)
use catalog <- decode.field("catalog", decode.string)
decode.success(TableRef(catalog, schema, name))
}
let type_ref_decoder = {
use schema <- decode.field("schema", decode.string)
use catalog <- decode.field("catalog", decode.string)
use name <- decode.field("name", decode.string)
decode.success(TypeRef(catalog, schema, name))
}
let table_col_decoder = {
use name <- decode.field("name", decode.string)
use not_null <- decode.field("not_null", decode.bool)
use is_array <- decode.field("is_array", decode.bool)
use comment <- decode.field("comment", decode.string)
use length <- decode.field("length", decode.int)
use is_named_param <- decode.field("is_named_param", decode.bool)
use is_func_call <- decode.field("is_func_call", decode.bool)
use scope <- decode.field("scope", decode.string)
use table_alias <- decode.field("table_alias", decode.string)
use is_sqlc_slice <- decode.field("is_sqlc_slice", decode.bool)
use original_name <- decode.field("original_name", decode.string)
use unsigned <- decode.field("unsigned", decode.bool)
use array_dims <- decode.field("array_dims", decode.int)
use table <- decode.field("table", decode.optional(table_ref_decoder))
use type_ref <- decode.field("type", type_ref_decoder)
TableColumn(
name,
not_null,
is_array,
comment,
length,
is_named_param,
is_func_call,
scope,
table_alias,
is_sqlc_slice,
original_name,
unsigned,
array_dims,
table,
type_ref,
)
|> decode.success()
}
let table_decoder = {
use rel <- decode.field("rel", table_ref_decoder)
use comment <- decode.field("comment", decode.string)
use columns <- decode.field("columns", decode.list(table_col_decoder))
decode.success(Table(rel, comment, columns))
}
let enum_decoder = {
use name <- decode.field("name", decode.string)
use vals <- decode.field("vals", decode.list(decode.string))
use comment <- decode.field("comment", decode.string)
decode.success(Enum(name, vals, comment))
}
let schema_decoder = {
use comment <- decode.field("comment", decode.string)
use name <- decode.field("name", decode.string)
use tables <- decode.field("tables", decode.list(table_decoder))
use enums <- decode.field("enums", decode.list(enum_decoder))
decode.success(Schema(comment, name, tables, enums))
}
let catalog_decoder = {
use comment <- decode.field("comment", decode.string)
use default_schema <- decode.field("default_schema", decode.string)
use name <- decode.field("name", decode.string)
use schemas <- decode.field("schemas", decode.list(schema_decoder))
decode.success(Catalog(comment, default_schema, name, schemas))
}
let params_decoder = {
use number <- decode.field("number", decode.int)
use column <- decode.field("column", table_col_decoder)
decode.success(QueryParam(number, column))
}
let cmd_decoder = {
use cmd <- decode.then(decode.string)
case cmd {
":one" -> decode.success(One)
":many" -> decode.success(Many)
":exec" -> decode.success(ExecResult)
":execresult" -> decode.success(ExecResult)
_ -> decode.failure(One, "QueryCmd")
}
}
let query_decoder = {
use text <- decode.field("text", decode.string)
use name <- decode.field("name", decode.string)
use cmd <- decode.field("cmd", cmd_decoder)
use filename <- decode.field("filename", decode.string)
use columns <- decode.field("columns", decode.list(table_col_decoder))
use insert_into_table <- decode.field(
"insert_into_table",
decode.optional(table_ref_decoder),
)
use comments <- decode.field("comments", decode.list(decode.string))
use params <- decode.field("params", decode.list(params_decoder))
Query(
text,
name,
cmd,
filename,
columns,
insert_into_table,
comments,
params,
)
|> decode.success()
}
let decoder = {
use sqlc_version <- decode.field("sqlc_version", decode.string)
use plugin_options <- decode.field("plugin_options", decode.string)
use global_options <- decode.field("global_options", decode.string)
use catalog <- decode.field("catalog", catalog_decoder)
use queries <- decode.field("queries", decode.list(query_decoder))
SQLC(sqlc_version, plugin_options, global_options, catalog, queries)
|> decode.success()
}
decode.run(data, decoder)
}
pub fn gen_sqlc_yaml(engine: cli.Engine, queries: List(String)) {
let result = "
version: \"2\"
plugins:
- name: jsonb
wasm:
url: https://github.com/daniellionel01/sqlc-gen-json/releases/download/v1.0.0/sqlc-gen-json.wasm
sha256: ffbd8cfaecc971d8cdf145591eac28731ffb50b7348131868ce66cc0e3192b7e
sql:
- schema: schema.sql
queries: [" <> string.join(queries, ", ") <> "]
engine: " <> engine_to_sqlc_string(engine) <> "
codegen:
- out: .
plugin: jsonb
options:
indent: \" \"
filename: queries.json
"
string.trim(result)
}
fn engine_to_sqlc_string(engine: cli.Engine) {
case engine {
cli.MySQL -> "mysql"
cli.PostgreSQL -> "postgresql"
cli.SQlite -> "sqlite"
}
}
pub fn sqlc_binary_path() {
filepath.join(project.root(), "build/.parrot/sqlc")
}
fn binary_exists(path) {
case simplifile.is_file(path) {
Ok(True) -> True
Ok(False) | Error(_) -> False
}
}
const sqlc_version = "1.29.0"
fn get_download_path_and_hash() -> Result(#(String, String), errors.ParrotError) {
let base = "https://downloads.sqlc.dev/sqlc_" <> sqlc_version
let os = get_os()
let cpu = get_cpu()
let platform = case os, cpu {
"darwin", "arm64" | "darwin", "aarch64" ->
Ok(#(
"_darwin_arm64.tar.gz",
"A63ED937BA43530B739BE40EF52F81C146A251460F0E4170474426F0A7248424",
))
"darwin", "amd64" | "darwin", "x86_64" | "darwin", "x64" ->
Ok(#(
"_darwin_amd64.tar.gz",
"B6F6FDBB390A3E40CB080CFB54F3C1308BAD56C32C3033DB7CF4CCD3A02CFDDB",
))
"linux", "arm64" | "linux", "aarch64" ->
Ok(#(
"_linux_arm64.tar.gz",
"F386B669B63BF8FA6227AB61CDA000C8D25A2F5A4C997ECF7D7B89EA5385FE45",
))
"linux", "amd64" | "linux", "x86_64" | "linux", "x64" ->
Ok(#(
"_linux_amd64.tar.gz",
"005C9409272F98DF050C3A0687E8A80001E59E1F7ABF638E3BC32E880774F8AE",
))
"win32", "amd64" | "win32", "x86_64" | "win32", "x64" ->
Ok(#(
"_windows_amd64.tar.gz",
"0B203324BA2995E60187943B344D2383DDA7C0A010C0CBD3C7C3DFC47B71A2D4",
))
_, _ -> Error(Nil)
}
use #(platform, hash) <- given.ok(platform, else_return: fn(_) {
Error(errors.SqlcDownloadError(
"unsupported platform: " <> os <> ", " <> cpu,
))
})
Ok(#(base <> platform, hash))
}
fn check_sqlc_integrity(bin: BitArray, expected_hash: String) {
let hash = crypto.hash(crypto.Sha256, bin)
let hash_string = bit_array.base16_encode(hash)
case hash_string == expected_hash {
True -> Nil
False -> panic as "sqlc binary hash did not match expected hash!"
}
}
pub fn verify_binary() -> Result(Nil, errors.ParrotError) {
use #(download, _) <- result.try(get_download_path_and_hash())
let path = sqlc_binary_path()
let dir = filepath.directory_name(path)
let gen_res =
shellout.command(run: "./sqlc", with: ["version"], in: dir, opt: [])
case gen_res {
Error(_) -> {
let information =
[
"download path: " <> download,
"os: " <> get_os(),
"cpu: " <> get_cpu(),
"sqlc binary path: " <> path,
]
|> string.join("\n")
Error(errors.SqlcDownloadError(
"could not verify sqlc binary. information:\n" <> information,
))
}
Ok(v) -> {
let sqlc_version = "v" <> sqlc_version
let v = string.trim(v)
case v == sqlc_version {
True -> Ok(Nil)
False ->
Error(errors.SqlcDownloadError(
"Could not match sqlc version. Wanted "
<> sqlc_version
<> ". Received "
<> v,
))
}
}
}
}
pub fn download_binary() -> Result(Nil, errors.ParrotError) {
let path = sqlc_binary_path()
let dir = filepath.directory_name(path)
let assert Ok(_) = simplifile.create_directory_all(dir)
use #(download, hash) <- result.try(get_download_path_and_hash())
let binary_exists = binary_exists(path)
use <- given.that(binary_exists, return: fn() {
use bin <- result.try(
simplifile.read_bits(path)
|> result.map_error(fn(_) { errors.SqlcDownloadError("could not verify") }),
)
check_sqlc_integrity(bin, hash)
Ok(Nil)
})
use tarball <- result.try(
download_zip(download)
|> result.map_error(fn(_) {
errors.SqlcDownloadError("could not curl the sqlc binary")
}),
)
use bin <- result.try(
extract_sqlc_binary(tarball)
|> result.map_error(fn(_) {
errors.SqlcDownloadError("could not unzip the sqlc binary")
}),
)
check_sqlc_integrity(bin, hash)
let assert Ok(_) = simplifile.write_bits(path, bin)
let permissions =
FilePermissions(
user: set.from_list([Read, Write, Execute]),
group: set.from_list([Read, Execute]),
other: set.from_list([Read, Execute]),
)
let _ = simplifile.set_permissions(path, permissions)
Ok(Nil)
}
@external(erlang, "parrot_ffi", "get_os")
pub fn get_os() -> String
@external(erlang, "parrot_ffi", "get_cpu")
pub fn get_cpu() -> String
@external(erlang, "parrot_ffi", "download_zip")
pub fn download_zip(url: String) -> Result(BitArray, dynamic.Dynamic)
@external(erlang, "parrot_ffi", "extract_sqlc_binary")
pub fn extract_sqlc_binary(
tarball: BitArray,
) -> Result(BitArray, dynamic.Dynamic)