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
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,27 @@ gleam add pog # for PostgreSQL with native runtime
gleam add sqlight # for SQLite with native runtime
```

### Self-contained generation (`vendor_runtime`)

Setting `gen.gleam.vendor_runtime: true` asks sqlode to copy the
`sqlode/runtime` module into the output directory as `runtime.gleam`
and rewrite the generated imports to point at the local copy. The
generated package no longer needs sqlode as a runtime dependency,
only as a dev dependency (the tool you invoke with `sqlode generate`).
Native adapters still need their driver package (`pog` / `sqlight`).

```yaml
gen:
gleam:
out: "src/db"
runtime: "raw"
vendor_runtime: true
```

Trade-offs: shared-runtime code is smaller and auto-updates with
`gleam update sqlode`; vendored code is self-contained at the cost of
re-running `sqlode generate` to pick up runtime changes.

## Adapter generation

When `runtime` is set to `native`, sqlode generates adapter modules that wrap [pog](https://hexdocs.pm/pog/) (PostgreSQL) or [sqlight](https://hexdocs.pm/sqlight/) (SQLite).
Expand Down Expand Up @@ -588,7 +609,7 @@ sqlode follows sqlc conventions, so most SQL files work without changes. Key dif
| Init | `sqlc init` | `sqlode init` |
| Vet/Verify | `sqlc vet`, `sqlc verify` | Not supported |
| Target language | Go, Python, Kotlin, etc. | Gleam |
| Runtime | Generated code is self-contained | Generated code imports `sqlode/runtime` |
| Runtime | Generated code is self-contained | Generated code imports `sqlode/runtime` by default; set `vendor_runtime: true` to vendor a copy and drop the runtime dependency (see [Self-contained generation](#self-contained-generation-vendor_runtime)) |

### Migration steps

Expand Down
54 changes: 54 additions & 0 deletions integration_test/cases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,59 @@ case_compile_all_types() {
expected_files="params.gleam queries.gleam models.gleam"
}

# Verify the self-contained generation mode produces a project that
# builds with no sqlode dependency. The generated gleam.toml created
# below deliberately does NOT depend on sqlode (unlike every other
# case), and the vendored runtime.gleam in src/db/ must provide
# everything the other generated modules need.
case_compile_vendor_runtime() {
_icv_label="raw mode with vendor_runtime"
_icv_dir="$INTEGRATION_TMP_BASE/integration_compile_vendor_runtime"

echo ""
echo "--- $_icv_label ---"

integration_clean "$_icv_dir"
mkdir -p "$_icv_dir/src/db"

cat > "$_icv_dir/gleam.toml" <<TOML
name = "integration_compile_vendor_runtime"
version = "0.1.0"
target = "erlang"

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
TOML

cat > "$_icv_dir/sqlode.yaml" <<YAML
version: "2"
sql:
- schema: "$PROJECT_ROOT/test/fixtures/schema.sql"
queries: "$PROJECT_ROOT/test/fixtures/query.sql"
engine: "postgresql"
gen:
gleam:
out: "$_icv_dir/src/db"
runtime: "raw"
vendor_runtime: true
YAML

echo "Generating code..."
integration_generate "$_icv_dir"

for f in params.gleam queries.gleam models.gleam runtime.gleam; do
if [ ! -f "$_icv_dir/src/db/$f" ]; then
echo "FAIL: expected file $f not generated" >&2
return 1
fi
done

echo "Building generated project (no sqlode dependency)..."
integration_build "$_icv_dir"

echo "PASS: $_icv_label"
}

case_sqlite_basic() {
run_integration_case \
label="SQLite real database" \
Expand Down Expand Up @@ -135,6 +188,7 @@ ALL_INTEGRATION_CASES="
case_compile_raw
case_compile_complex
case_compile_all_types
case_compile_vendor_runtime
case_sqlite_basic
case_sqlite_extended
case_sqlite_advanced
Expand Down
2 changes: 1 addition & 1 deletion src/sqlode/codegen/adapter.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ fn render_adapter(
// Every generated adapter now calls `runtime.expand_slice_placeholders`
// to substitute the placeholder markers emitted by the query parser,
// so the import is always required (not only when slices are used).
["import sqlode/runtime"],
["import " <> common.runtime_import_path(gleam)],
case has_results || has_enums {
True -> ["import " <> module_path <> "/models"]
False -> []
Expand Down
12 changes: 12 additions & 0 deletions src/sqlode/codegen/common.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ pub fn out_to_module_path(out: String) -> String {
}
}

/// Import path used by generated modules for the sqlode runtime. When
/// `gleam.vendor_runtime` is enabled, the runtime source is written
/// into `<out>/runtime.gleam` and the generated code points at that
/// local copy (e.g. `db/runtime`). Otherwise the shared
/// `sqlode/runtime` dependency is used.
pub fn runtime_import_path(gleam: model.GleamOutput) -> String {
case gleam.vendor_runtime {
True -> out_to_module_path(gleam.out) <> "/runtime"
False -> "sqlode/runtime"
}
}

/// Render a single-constructor Gleam type declaration.
///
/// gleam_type("UserId", "Int") →
Expand Down
10 changes: 5 additions & 5 deletions src/sqlode/codegen/params.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub fn render(
queries: List(model.AnalyzedQuery),
type_mapping: model.TypeMapping,
module_path: String,
runtime_import: String,
) -> String {
let has_slices = common.queries_have_slices(queries)

Expand All @@ -28,17 +29,16 @@ pub fn render(
list.any(q.params, fn(p) { type_mapping.is_rich_type(p.scalar_type) })
})

let runtime_import_line = "import " <> runtime_import <> ".{type Value}"

let imports = case needs_option_import(queries) {
True ->
list.flatten([
case has_slices || has_arrays {
True -> ["import gleam/list"]
False -> []
},
[
"import gleam/option.{type Option, None, Some}",
"import sqlode/runtime.{type Value}",
],
["import gleam/option.{type Option, None, Some}", runtime_import_line],
case has_enums || needs_models_for_strong {
True -> ["import " <> module_path <> "/models"]
False -> []
Expand All @@ -51,7 +51,7 @@ pub fn render(
True -> ["import gleam/list"]
False -> []
},
["import sqlode/runtime.{type Value}"],
[runtime_import_line],
case has_enums || needs_models_for_strong {
True -> ["import " <> module_path <> "/models"]
False -> []
Expand Down
2 changes: 1 addition & 1 deletion src/sqlode/codegen/queries.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn render(
True -> ["import gleam/list"]
False -> []
},
["import sqlode/runtime"],
["import " <> common.runtime_import_path(gleam)],
case has_any_params {
True -> ["import " <> module_path <> "/params"]
False -> []
Expand Down
6 changes: 5 additions & 1 deletion src/sqlode/config.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ fn parse_sql_block(node: yay.Node) -> Result(model.SqlBlock, ConfigError) {
gleam_node,
[
"out", "runtime", "type_mapping", "emit_sql_as_comment",
"emit_exact_table_names", "omit_unused_models",
"emit_exact_table_names", "omit_unused_models", "vendor_runtime",
],
"sql.gen.gleam.",
))
Expand Down Expand Up @@ -163,6 +163,9 @@ fn parse_sql_block(node: yay.Node) -> Result(model.SqlBlock, ConfigError) {
let omit_unused_models =
optional_bool(gleam_node, "omit_unused_models")
|> option.unwrap(False)
let vendor_runtime =
optional_bool(gleam_node, "vendor_runtime")
|> option.unwrap(False)

use overrides <- result.try(parse_overrides(node))

Expand All @@ -178,6 +181,7 @@ fn parse_sql_block(node: yay.Node) -> Result(model.SqlBlock, ConfigError) {
emit_sql_as_comment:,
emit_exact_table_names:,
omit_unused_models:,
vendor_runtime:,
),
overrides:,
))
Expand Down
40 changes: 39 additions & 1 deletion src/sqlode/generate.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ fn base_output_files(
analyzed,
gleam.type_mapping,
common.out_to_module_path(out),
common.runtime_import_path(gleam),
),
),
writer.GeneratedFile(
Expand All @@ -222,7 +223,7 @@ fn base_output_files(
),
]

case has_models {
let files_with_models = case has_models {
True -> {
let effective_catalog = case gleam.omit_unused_models {
True -> prune_catalog_to_used(catalog, analyzed)
Expand All @@ -245,6 +246,43 @@ fn base_output_files(
}
False -> files
}

case gleam.vendor_runtime {
True ->
case read_runtime_source() {
Ok(source) ->
list.append(files_with_models, [
writer.GeneratedFile(
directory: out,
path: "runtime.gleam",
content: source,
),
])
Error(_) -> files_with_models
}
False -> files_with_models
}
}

/// Return the `sqlode/runtime` module source. Tries known paths in
/// order: a development checkout (when sqlode itself runs the
/// generator), then the extracted hex package layout a user project
/// would have after `gleam add sqlode`. When none are found, emit
/// nothing rather than a broken file — the flag still defaults off,
/// so a user who opted in will at least notice that the file they
/// asked for is missing.
fn read_runtime_source() -> Result(String, Nil) {
let candidates = [
"src/sqlode/runtime.gleam",
"build/packages/sqlode/src/sqlode/runtime.gleam",
"build/dev/erlang/sqlode/src/sqlode/runtime.gleam",
]
list.find_map(candidates, fn(path) {
case simplifile.read(path) {
Ok(content) -> Ok(content)
Error(_) -> Error(Nil)
}
})
}

/// Drop tables and enums from the catalog that no generated query
Expand Down
6 changes: 6 additions & 0 deletions src/sqlode/model.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ pub type GleamOutput {
/// tables / query columns / query params). Defaults to False to
/// preserve the existing full-catalog output.
omit_unused_models: Bool,
/// When True, emit a copy of `sqlode/runtime` into the output
/// directory as `runtime.gleam` and rewrite the generated module
/// imports to point at the local copy. The target project no
/// longer needs `sqlode` as a runtime dependency. Defaults to
/// False so the shared-runtime path stays the default.
vendor_runtime: Bool,
)
}

Expand Down
Loading