Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Parrot now generates a `sqlc.json` instead of a yaml configuration. (https://github.com/daniellionel01/parrot/pull/71)<br />
Thank you szelbi! (https://github.com/sz3lbi)

## [2.1.1] - 2025-09-30

- Bug Fix: quotes are escaped properly in generated gleam code. (https://github.com/daniellionel01/parrot/pull/65)
Expand Down
1 change: 1 addition & 0 deletions integration/mysql/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ run:

test:
#!/bin/bash
rm -rf build/.parrot
rm -f src/app/sql.gleam
../../bin/mysql.sh
./priv/reset.sh
Expand Down
4 changes: 2 additions & 2 deletions integration/mysql/priv/schema.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
drop table if exists users;
drop table if exists posts;
drop table if exists users;

create table users (
id int auto_increment primary key,
Expand All @@ -14,7 +14,7 @@ create table posts (
created_at timestamp default current_timestamp,
title varchar(255) not null unique,

user_id integer not null,
user_id integer,

foreign key (user_id) references users(id) on delete cascade
);
8 changes: 4 additions & 4 deletions integration/mysql/src/app/sql.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ pub fn get_user_by_username_decoder() -> decode.Decoder(GetUserByUsername) {
}

pub type PostsByUsername {
PostsByUsername(id: Int, title: String, user_id: Int)
PostsByUsername(id: Int, title: String, user_id: Option(Int))
}

pub fn posts_by_username(username username: String) {
Expand All @@ -184,12 +184,12 @@ where user_id = (
pub fn posts_by_username_decoder() -> decode.Decoder(PostsByUsername) {
use id <- decode.field(0, decode.int)
use title <- decode.field(1, decode.string)
use user_id <- decode.field(2, decode.int)
use user_id <- decode.field(2, decode.optional(decode.int))
decode.success(PostsByUsername(id:, title:, user_id:))
}

pub type PostsByAdmins {
PostsByAdmins(id: Int, title: String, user_id: Int)
PostsByAdmins(id: Int, title: String, user_id: Option(Int))
}

pub fn posts_by_admins() {
Expand All @@ -210,6 +210,6 @@ where user_id in (
pub fn posts_by_admins_decoder() -> decode.Decoder(PostsByAdmins) {
use id <- decode.field(0, decode.int)
use title <- decode.field(1, decode.string)
use user_id <- decode.field(2, decode.int)
use user_id <- decode.field(2, decode.optional(decode.int))
decode.success(PostsByAdmins(id:, title:, user_id:))
}
1 change: 1 addition & 0 deletions integration/psql/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ run:

test:
#!/bin/bash
rm -rf build/.parrot
rm -f src/app/sql.gleam
../../bin/psql.sh
./priv/reset.sh
Expand Down
1 change: 1 addition & 0 deletions integration/sqlite/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ parrot:

test:
#!/bin/bash
rm -rf build/.parrot
rm -f src/app/sql.gleam
rm -f {{SQLITE_FILE}}
sqlite3 {{SQLITE_FILE}} < ./priv/schema.sql
Expand Down
23 changes: 14 additions & 9 deletions src/parrot.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub fn main() {
|> result.map(fn(a) { cli.Generate(a.0, a.1) })
}
["--sqlite", file_path] -> {
Ok(cli.Generate(cli.SQlite, file_path))
Ok(cli.Generate(sqlc.SQLite, file_path))
}
["help"] -> Ok(cli.Usage)
_ -> Ok(cli.Usage)
Expand All @@ -57,7 +57,7 @@ pub fn main() {
}
}

fn cmd_gen(engine: cli.Engine, db: String) -> Result(Nil, errors.ParrotError) {
fn cmd_gen(engine: sqlc.Engine, db: String) -> Result(Nil, errors.ParrotError) {
let db = case db {
"sqlite://" <> db -> db
"sqlite:" <> db -> db
Expand All @@ -83,7 +83,7 @@ fn cmd_gen(engine: cli.Engine, db: String) -> Result(Nil, errors.ParrotError) {
let sqlc_binary = sqlc.sqlc_binary_path()
let sqlc_dir = filepath.directory_name(sqlc_binary)
let schema_file = filepath.join(sqlc_dir, "schema.sql")
let sqlc_file = filepath.join(sqlc_dir, "sqlc.yaml")
let sqlc_file = filepath.join(sqlc_dir, "sqlc.json")
let queries_file = filepath.join(sqlc_dir, "queries.json")
let _ = simplifile.create_directory_all(sqlc_dir)

Expand All @@ -105,26 +105,26 @@ fn cmd_gen(engine: cli.Engine, db: String) -> Result(Nil, errors.ParrotError) {
Ok(_) -> spinner.complete_current(spinner, spinner.green_checkmark())
}

let sqlc_yaml = sqlc.gen_sqlc_yaml(engine, queries)
let _ = simplifile.write(sqlc_file, sqlc_yaml)
let sqlc_json = sqlc.gen_sqlc_json(engine, queries)
let _ = simplifile.write(sqlc_file, sqlc_json)

let spinner =
spinner.new("fetching schema")
|> spinner.start()

use schema_sql <- result.try(case engine {
cli.MySQL -> {
sqlc.MySQL -> {
use schema <- result.try(db.fetch_schema_mysql(db))
Ok(schema)
}
cli.PostgreSQL -> {
sqlc.PostgreSQL -> {
use schema <- result.try(db.fetch_schema_postgresql(db))
let assert Ok(re) =
regexp.from_string("(?m)^\\\\restrict.*\n|^\\\\unrestrict.*\n")
let schema = regexp.replace(re, schema, "")
Ok(schema)
}
cli.SQlite -> {
sqlc.SQLite -> {
use schema <- result.try(db.fetch_schema_sqlite(db))
let sql = string.trim(schema)
Ok(sql)
Expand All @@ -139,7 +139,12 @@ fn cmd_gen(engine: cli.Engine, db: String) -> Result(Nil, errors.ParrotError) {
|> spinner.start()

let gen_result =
shellout.command(run: "./sqlc", with: ["generate"], in: sqlc_dir, opt: [])
shellout.command(
run: "./sqlc",
with: ["generate", "--file", "sqlc.json"],
in: sqlc_dir,
opt: [],
)

use _ <- given.error(gen_result, return: fn(err) {
let #(_, err) = err
Expand Down
21 changes: 7 additions & 14 deletions src/parrot/internal/cli.gleam
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import envoy
import given
import parrot/internal/errors
import parrot/internal/sqlc

pub const usage = "
🦜 Parrot - type-safe SQL in gleam for sqlite, postgresql & mysql
Expand Down Expand Up @@ -54,27 +55,19 @@ pub const usage = "

pub type Command {
Usage
Generate(engine: Engine, db: String)
}

pub type Engine {
SQlite
MySQL
PostgreSQL
Generate(engine: sqlc.Engine, db: String)
}

pub fn engine_from_env(str: String) {
case str {
"postgres" <> _ -> Ok(PostgreSQL)
"mysql" <> _ -> Ok(MySQL)
"file" | "sqlite" <> _ -> Ok(SQlite)
_ -> {
Error(errors.UnknownEngine(str))
}
"postgres" <> _ -> Ok(sqlc.PostgreSQL)
"mysql" <> _ -> Ok(sqlc.MySQL)
"file" | "sqlite" <> _ -> Ok(sqlc.SQLite)
_ -> Error(errors.UnknownEngine(str))
}
}

pub fn parse_env(env: String) -> Result(#(Engine, String), String) {
pub fn parse_env(env: String) -> Result(#(sqlc.Engine, String), String) {
let env_result = envoy.get(env)
use env_var <- given.error(in: env_result, return: fn(_) {
Error("Environment Variable \"DATABASE_URL\" is empty!")
Expand Down
170 changes: 143 additions & 27 deletions src/parrot/internal/sqlc.gleam
Original file line number Diff line number Diff line change
@@ -1,22 +1,142 @@
//// This module decodes the JSON generated by sqlc
////
//// This module generates the JSON config, which is used when running sqlc, and
//// decodes the JSON that sqlc generates.

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/json
import gleam/option.{type Option, Some}
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 Engine {
SQLite
MySQL
PostgreSQL
}

type Queries {
QueriesSingle(String)
QueriesMultiple(List(String))
}

type GenJson {
GenJson(out: Option(String), indent: Option(String), filename: Option(String))
}

type Gen {
Gen(json: Option(GenJson))
}

type Sql {
Sql(
schema: Option(String),
queries: Option(Queries),
engine: Engine,
gen: Option(Gen),
)
}

type Version2 {
Version2
}

type Config {
Config(version: Version2, sql: List(Sql))
}

fn queries_to_json(queries: Queries) -> json.Json {
case queries {
QueriesSingle(query) -> json.string(query)
QueriesMultiple(queries) -> json.array(queries, json.string)
}
}

fn engine_to_json(engine: Engine) -> json.Json {
let engine = case engine {
SQLite -> "sqlite"
MySQL -> "mysql"
PostgreSQL -> "postgresql"
}
json.string(engine)
}

fn gen_json_to_json(gen_json: GenJson) -> json.Json {
let GenJson(out:, indent:, filename:) = gen_json
let json_object = case out {
option.None -> []
option.Some(out) -> [#("out", json.string(out))]
}
let json_object = case indent {
option.None -> json_object
option.Some(indent) -> [#("indent", json.string(indent)), ..json_object]
}
let json_object = case filename {
option.None -> json_object
option.Some(filename) -> [
#("filename", json.string(filename)),
..json_object
]
}
json.object(json_object)
}

fn sql_to_json(sql: Sql) -> json.Json {
let Sql(schema:, queries:, engine:, gen:) = sql
let json_object = [#("engine", engine_to_json(engine))]
let json_object = case schema {
option.None -> json_object
option.Some(schema) -> [#("schema", json.string(schema)), ..json_object]
}
let json_object = case queries {
option.None -> json_object
option.Some(queries) -> [
#("queries", queries_to_json(queries)),
..json_object
]
}
let json_object = case gen {
option.None -> json_object
option.Some(gen) -> [#("gen", gen_to_json(gen)), ..json_object]
}
json.object(json_object)
}

fn gen_to_json(gen: Gen) -> json.Json {
let Gen(json:) = gen
let json_object = case json {
option.None -> []
option.Some(json) -> [#("json", gen_json_to_json(json))]
}
json.object(json_object)
}

fn version2_to_json(_: Version2) -> json.Json {
json.string("2")
}

fn config_to_json(config: Config) -> json.Json {
case config {
Config(version:, sql:) ->
json.object([
#("version", version2_to_json(version)),
#("sql", json.array(sql, sql_to_json)),
])
}
}

fn config_to_json_string(config: Config) -> String {
config_to_json(config) |> json.to_string
}

pub type TypeRef {
TypeRef(catalog: String, schema: String, name: String)
}
Expand Down Expand Up @@ -266,29 +386,25 @@ pub fn decode_sqlc(data: dynamic.Dynamic) {
decode.run(data, decoder)
}

pub fn gen_sqlc_yaml(engine: cli.Engine, queries: List(String)) {
let result = "
version: \"2\"
sql:
- schema: schema.sql
queries: [" <> string.join(queries, ", ") <> "]
engine: " <> engine_to_sqlc_string(engine) <> "
gen:
json:
out: .
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 gen_sqlc_json(engine: Engine, queries: List(String)) -> String {
let config =
Config(version: Version2, sql: [
Sql(
schema: Some("schema.sql"),
queries: Some(QueriesMultiple(queries)),
engine:,
gen: Some(
Gen(
json: Some(GenJson(
out: Some("."),
indent: Some(" "),
filename: Some("queries.json"),
)),
),
),
),
])
config_to_json_string(config)
}

pub fn sqlc_binary_path() {
Expand Down