diff --git a/CHANGELOG.md b/CHANGELOG.md index c33fd53..65cbc9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)
+ 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) diff --git a/integration/mysql/Justfile b/integration/mysql/Justfile index d9a0251..18b86fe 100644 --- a/integration/mysql/Justfile +++ b/integration/mysql/Justfile @@ -6,6 +6,7 @@ run: test: #!/bin/bash + rm -rf build/.parrot rm -f src/app/sql.gleam ../../bin/mysql.sh ./priv/reset.sh diff --git a/integration/mysql/priv/schema.sql b/integration/mysql/priv/schema.sql index 41265c1..21df024 100644 --- a/integration/mysql/priv/schema.sql +++ b/integration/mysql/priv/schema.sql @@ -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, @@ -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 ); diff --git a/integration/mysql/src/app/sql.gleam b/integration/mysql/src/app/sql.gleam index 638da47..0f7981a 100644 --- a/integration/mysql/src/app/sql.gleam +++ b/integration/mysql/src/app/sql.gleam @@ -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) { @@ -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() { @@ -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:)) } diff --git a/integration/psql/Justfile b/integration/psql/Justfile index 4ce0d34..2031960 100644 --- a/integration/psql/Justfile +++ b/integration/psql/Justfile @@ -6,6 +6,7 @@ run: test: #!/bin/bash + rm -rf build/.parrot rm -f src/app/sql.gleam ../../bin/psql.sh ./priv/reset.sh diff --git a/integration/sqlite/Justfile b/integration/sqlite/Justfile index 598750d..d1afecb 100644 --- a/integration/sqlite/Justfile +++ b/integration/sqlite/Justfile @@ -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 diff --git a/src/parrot.gleam b/src/parrot.gleam index 9e32dc4..7e9d3c1 100644 --- a/src/parrot.gleam +++ b/src/parrot.gleam @@ -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) @@ -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 @@ -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) @@ -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) @@ -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 diff --git a/src/parrot/internal/cli.gleam b/src/parrot/internal/cli.gleam index 142ed62..49ee9d0 100644 --- a/src/parrot/internal/cli.gleam +++ b/src/parrot/internal/cli.gleam @@ -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 @@ -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!") diff --git a/src/parrot/internal/sqlc.gleam b/src/parrot/internal/sqlc.gleam index 5b63d1c..b3687db 100644 --- a/src/parrot/internal/sqlc.gleam +++ b/src/parrot/internal/sqlc.gleam @@ -1,5 +1,5 @@ -//// 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 @@ -7,16 +7,136 @@ 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) } @@ -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() {