///|
/// sqlc WASM plugin — pure codegen pipeline.
/// I/O protocol over WASI fd_read/fd_write via inline WAT FFI (wasi_io.mbt).
/// process_message is the pure computation entry point (protocol.mbt).
//
// Copyright 2026 Mairzzcllo
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///|
/// WASM _start entry point. Delegates to the I/O protocol loop.
fn main {
run_io_loop()
}
///|
/// Process a GenerateRequest and produce a GenerateResponse.
/// Uses codegen.out from sqlc config as the output directory prefix.
/// Produces two output files: types.mbt and queries.mbt.
fn process_request(req : GenerateRequest) -> GenerateResponse raise {
validate_engine(req.settings.engine)
let global_opts = parse_plugin_options(req.global_options)
let plugin_opts = parse_plugin_options(req.plugin_options)
let pkg = if plugin_opts.package_name.length() > 0 {
plugin_opts.package_name
} else {
global_opts.package_name
}
let opts = {
package_name: pkg,
overrides: plugin_opts.overrides,
emit_json_tags: plugin_opts.emit_json_tags,
emit_db_tags: plugin_opts.emit_db_tags,
renames: plugin_opts.renames,
emit_sql_as_comment: plugin_opts.emit_sql_as_comment,
omit_unused_structs: plugin_opts.omit_unused_structs,
emit_empty_slices: plugin_opts.emit_empty_slices,
initialisms: plugin_opts.initialisms,
json_tags_case_style: plugin_opts.json_tags_case_style,
emit_exact_table_names: plugin_opts.emit_exact_table_names,
emit_methods_with_db_argument: plugin_opts.emit_methods_with_db_argument,
column_overrides: plugin_opts.column_overrides,
nullable_overrides: plugin_opts.nullable_overrides,
strict_types: plugin_opts.strict_types,
runtime_alias: plugin_opts.runtime_alias,
}
let adapted = convert_request(req)
validate_strict_types(
adapted.catalog,
adapted.queries,
opts.overrides,
opts.strict_types,
)
let ir = build_ir(adapted, opts.renames)
validate_codegen_type_names(
adapted.catalog,
ir,
adapted.queries,
opts.omit_unused_structs,
opts.emit_exact_table_names,
opts.initialisms,
)
let sources = generate_sources(req, opts)
let out_name = req.settings.codegen.out
// sqlc v2 commonly sends out="." for current directory; treat as empty prefix.
let normalized_out = if out_name == "." { "" } else { out_name }
let dir = if normalized_out.length() > 0 {
validate_output_path(normalized_out)
let dir_str = if normalized_out[normalized_out.length() - 1] == '/' {
normalized_out[0:normalized_out.length() - 1].to_owned()
} else {
normalized_out
}
if dir_str.length() == 0 {
reject_absolute(normalized_out)
}
dir_str
} else {
""
}
let files : Array[File] = []
for s in sources {
let fname = if dir.length() > 0 { dir + "/" + s.0 } else { s.0 }
files.push({ name: fname, contents: @utf8.encode(s.1) })
}
{ files, }
}
///|
/// Reject unsupported SQL engines at codegen entry (GAP-5: PostgreSQL only).
fn validate_engine(engine : String) -> Unit raise {
let normalized = engine.trim().to_lower()
if normalized.length() == 0 || normalized == "postgresql" {
()
} else {
raise Failure::Failure(
"unsupported SQL engine: " + engine + " (only postgresql is supported)",
)
}
}
///|
/// Validate an output directory path for path traversal and absolute path attacks.
/// Returns Unit on success, raises Failure error on invalid paths.
/// Rejects:
/// - Paths containing ".." as a complete path segment (path traversal)
/// - Paths starting with "/" or "\\" (Unix absolute paths)
/// - Windows drive-letter paths (e.g., C:\foo, D:/bar)
/// Note: ".." embedded in a filename (e.g. "foo..bar") is NOT traversal and is allowed.
fn validate_output_path(path : String) -> Unit raise {
if path.length() == 0 {
return ()
}
// Reject Unix absolute paths
if path[0] == '/' {
reject_absolute(path)
}
if path[0] == '\\' {
reject_absolute(path)
}
// Reject Windows drive-letter absolute paths (e.g., C:\foo, D:/bar)
// path[1] == ':' catches X:... patterns. X:..\.. is also dangerous
// (relative on Windows but traverses within the drive).
if path.length() >= 2 && path[1] == ':' {
reject_absolute(path)
}
// Reject path traversal via ".." as a complete path segment.
// A ".." is a traversal segment only when it is a complete directory name
// (bounded by path separators or string boundaries), not when it is
// part of a larger filename like "foo..bar".
if path == ".." {
reject_traversal(path)
}
if path.has_prefix("../") || path.has_prefix("..\\") {
reject_traversal(path)
}
if path.has_suffix("/..") || path.has_suffix("\\..") {
reject_traversal(path)
}
if path.contains("/../") ||
path.contains("\\..\\") ||
path.contains("/..\\") ||
path.contains("\\../") {
reject_traversal(path)
}
()
}
///|
/// Helper: raise absolute path rejection error.
fn reject_absolute(path : String) -> Unit raise {
let msg = "output path must not be absolute: \{path}"
raise Failure::Failure(msg)
}
///|
/// Helper: raise path traversal rejection error.
fn reject_traversal(path : String) -> Unit raise {
let msg = "output path contains traversal segment \"..\": \{path}"
raise Failure::Failure(msg)
}
///|
/// Deduplicate import items in a TopLevel array.
/// Keeps only the first import for each unique path.
/// Non-import items pass through unchanged.
fn dedup_imports(items : Array[TopLevel]) -> Array[TopLevel] {
let seen : Array[String] = []
let result : Array[TopLevel] = []
for item in items {
match item {
Import(def) => {
let mut is_dup = false
for s in seen {
if s == def.path {
is_dup = true
break
}
}
if !is_dup {
seen.push(def.path)
result.push(item)
}
}
_ => result.push(item)
}
}
result
}
///|
/// Run the full codegen pipeline: adapter → IR → type_gen → query_gen → emitter.
/// Returns an array of (filename, source_content) pairs for multi-file output.
/// Types (structs, enums, decode functions) go into "types.mbt".
/// Query functions go into "queries.mbt".
pub fn generate_sources(
req : GenerateRequest,
opts : PluginOptions,
) -> Array[(String, String)] raise {
set_runtime_alias(opts.runtime_alias)
let adapted = convert_request(req)
let ir = build_ir(adapted, opts.renames)
let types = generate_types(
adapted.catalog,
ir,
adapted.queries,
opts.emit_json_tags,
opts.emit_db_tags,
opts.overrides,
opts.column_overrides,
opts.nullable_overrides,
opts.renames,
opts.omit_unused_structs,
opts.emit_exact_table_names,
opts.json_tags_case_style,
opts.initialisms,
)
let parsed_inits = parse_initialisms(opts.initialisms)
let query_fns = generate_query_fns(
ir,
opts.overrides,
opts.nullable_overrides,
opts.emit_sql_as_comment,
opts.emit_methods_with_db_argument,
opts.emit_empty_slices,
parsed_inits,
)
// types.mbt: structs, enums, decode functions (package/import via consumer moon.mod + moon.pkg)
let types_items : Array[TopLevel] = []
for t in types {
types_items.push(t)
}
let types_ast : SourceFile = { items: types_items }
let types_source = emit_source_file(types_ast)
// queries.mbt: query functions only
let queries_items : Array[TopLevel] = []
for f in query_fns {
queries_items.push(f)
}
let queries_ast : SourceFile = { items: queries_items }
let queries_source = emit_source_file(queries_ast)
[("types.mbt", types_source), ("queries.mbt", queries_source)]
}
///|
/// Run the full codegen pipeline: adapter �?IR �?type_gen �?query_gen �?emitter.
/// Accepts parsed PluginOptions for codegen configuration (package_name, etc.).
pub fn generate_source(
req : GenerateRequest,
opts : PluginOptions,
) -> String raise {
set_runtime_alias(opts.runtime_alias)
let adapted = convert_request(req)
let ir = build_ir(adapted, opts.renames)
let types = generate_types(
adapted.catalog,
ir,
adapted.queries,
opts.emit_json_tags,
opts.emit_db_tags,
opts.overrides,
opts.column_overrides,
opts.nullable_overrides,
opts.renames,
opts.omit_unused_structs,
opts.emit_exact_table_names,
opts.json_tags_case_style,
opts.initialisms,
)
let parsed_inits = parse_initialisms(opts.initialisms)
let query_fns = generate_query_fns(
ir,
opts.overrides,
opts.nullable_overrides,
opts.emit_sql_as_comment,
opts.emit_methods_with_db_argument,
opts.emit_empty_slices,
parsed_inits,
)
let ast_items : Array[TopLevel] = []
for t in types {
ast_items.push(t)
}
for f in query_fns {
ast_items.push(f)
}
let ast : SourceFile = { items: ast_items }
emit_source_file(ast)
}
///|
test "dedup_imports: no duplicates passes through" {
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(
Import({ path: "moonbitlang/core/array", import_alias: Some("array") }),
)
let result = dedup_imports(items)
inspect(result.length(), content="2")
}
///|
test "dedup_imports: duplicate import removed" {
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
let result = dedup_imports(items)
inspect(result.length(), content="1")
}
///|
test "dedup_imports: duplicate with different alias kept as one" {
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(
Import({ path: "moonbitlang/core/builtin", import_alias: Some("builtin") }),
)
let result = dedup_imports(items)
inspect(result.length(), content="1")
}
///|
test "dedup_imports: non-import items preserved" {
let s : StructDef = { name: "Foo", fields: [], doc: None, is_pub: true }
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(Struct(s))
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
let result = dedup_imports(items)
inspect(result.length(), content="2")
}
///|
test "dedup_imports: empty array returns empty" {
let items : Array[TopLevel] = []
let result = dedup_imports(items)
inspect(result.length(), content="0")
}
///|
test "dedup_imports: all identical imports returns one" {
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
let result = dedup_imports(items)
inspect(result.length(), content="1")
}
///|
test "dedup_imports: three duplicates with mixed non-import items preserved" {
let s : StructDef = { name: "Foo", fields: [], doc: None, is_pub: true }
let f : FnDef = {
name: "bar",
params: [],
return_ty: Named("Unit"),
body: [Unit],
doc: None,
is_pub: false,
}
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(Struct(s))
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
items.push(Fn(f))
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
let result = dedup_imports(items)
// Expected: 1 import + 1 struct + 1 fn = 3
inspect(result.length(), content="3")
}
///|
test "dedup_imports: non-consecutive duplicates removed" {
let items : Array[TopLevel] = []
items.push(Import({ path: "a", import_alias: None }))
items.push(Import({ path: "b", import_alias: None }))
items.push(Import({ path: "a", import_alias: None }))
items.push(Import({ path: "b", import_alias: None }))
let result = dedup_imports(items)
inspect(result.length(), content="2")
}
///|
test "dedup_imports: single import without duplicates" {
let items : Array[TopLevel] = []
items.push(Import({ path: "moonbitlang/core/builtin", import_alias: None }))
let result = dedup_imports(items)
inspect(result.length(), content="1")
}
///|
test "dedup_imports: only non-import items pass through unchanged" {
let s1 : StructDef = { name: "A", fields: [], doc: None, is_pub: true }
let s2 : StructDef = { name: "B", fields: [], doc: None, is_pub: true }
let items : Array[TopLevel] = [Struct(s1), Struct(s2)]
let result = dedup_imports(items)
inspect(result.length(), content="2")
}
///|
test "dedup_imports: five imports with only two unique paths" {
let items : Array[TopLevel] = []
items.push(Import({ path: "x", import_alias: None }))
items.push(Import({ path: "y", import_alias: None }))
items.push(Import({ path: "x", import_alias: None }))
items.push(Import({ path: "y", import_alias: None }))
items.push(Import({ path: "x", import_alias: None }))
let result = dedup_imports(items)
inspect(result.length(), content="2")
}
///|
test "process_request: global_options fallback when plugin_options empty" {
let base = make_users_request()
let req : GenerateRequest = {
settings: base.settings,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"",
global_options: b"package_name=fallback_test",
}
let resp = process_request(req)
inspect(resp.files.length(), content="2")
// First file is types.mbt �?check package declaration
let source = @utf8.decode(resp.files[0].contents)
inspect(source.contains("package fallback_test"), content="false")
inspect(source.contains("@runtime.Row"), content="true")
}
///|
test "process_request: plugin_options takes priority over global_options" {
let base = make_users_request()
let req : GenerateRequest = {
settings: base.settings,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"package_name=plugin_opt",
global_options: b"package_name=global_opt",
}
let resp = process_request(req)
inspect(resp.files.length(), content="2")
// Both files have the same package declaration; check types.mbt
let source = @utf8.decode(resp.files[0].contents)
inspect(source.contains("package plugin_opt"), content="false")
inspect(source.contains("package global_opt"), content="false")
inspect(source.contains("pub struct User"), content="true")
}
///|
test "process_request: json plugin_options package_name (sqlc v1.31)" {
let base = make_users_request()
let req : GenerateRequest = {
settings: base.settings,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"{\"package_name\":\"users\"}",
global_options: b"",
}
let resp = process_request(req)
inspect(resp.files.length(), content="2")
let types_source = @utf8.decode(resp.files[0].contents)
let queries_source = @utf8.decode(resp.files[1].contents)
inspect(types_source.contains("package users"), content="false")
inspect(types_source.contains("package main"), content="false")
inspect(queries_source.contains("package users"), content="false")
}
///|
test "process_request: both empty defaults to main package" {
let base = make_users_request()
let req : GenerateRequest = {
settings: base.settings,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"",
global_options: b"",
}
let resp = process_request(req)
inspect(resp.files.length(), content="2")
// Both files default to package main when options are empty
let source = @utf8.decode(resp.files[0].contents)
inspect(source.contains("package main"), content="false")
inspect(source.contains("pub struct User"), content="true")
}
///|
test "validate_output_path: empty path returns ok" {
// Empty path is valid (no output directory specified)
validate_output_path("")
inspect("ok", content="ok")
}
///|
test "validate_output_path: simple relative path returns ok" {
validate_output_path("output")
inspect("ok", content="ok")
}
///|
test "validate_output_path: nested relative path returns ok" {
validate_output_path("gen/moonbit")
inspect("ok", content="ok")
}
///|
test "validate_output_path: dotted directory name returns ok" {
// Single dot in directory name (e.g. "foo.bar") is fine - not path traversal
validate_output_path("foo.bar")
inspect("ok", content="ok")
}
///|
test "validate_output_path: absolute path starting with / is rejected" {
let result = try {
validate_output_path("/tmp/foo")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: absolute path starting with \\ is rejected" {
let result = try {
validate_output_path("\\Windows\\Temp")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: path traversal with .. is rejected" {
let result = try {
validate_output_path("../../etc")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: path traversal with .. in middle is rejected" {
let result = try {
validate_output_path("foo/../bar")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: path traversal with .. at start is rejected" {
let result = try {
validate_output_path("../out")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: path traversal with backslash .. is rejected" {
let result = try {
validate_output_path("..\\out")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: dots embedded in directory name passes" {
// ".." as part of a filename (e.g. "foo..bar") is NOT path traversal
validate_output_path("foo..bar")
inspect("ok", content="ok")
}
///|
test "validate_output_path: single dot segment passes" {
// "." is the current directory, not traversal
validate_output_path("foo/./bar")
inspect("ok", content="ok")
}
///|
test "validate_output_path: deeply nested relative path passes" {
validate_output_path("a/b/c/d/e")
inspect("ok", content="ok")
}
///|
test "validate_output_path: normal forwardslash path passes" {
validate_output_path("foo/bar/baz")
inspect("ok", content="ok")
}
///|
test "validate_output_path: path with only .. is rejected" {
let result = try {
validate_output_path("..")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: very long path passes" {
// Build a long but valid relative path
let path = "a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/1/2/3/4/5/6/7/8/9/0"
validate_output_path(path)
inspect("ok", content="ok")
}
///|
test "validate_output_path: unicode characters in path passes" {
validate_output_path("目录/文件/subdir")
inspect("ok", content="ok")
}
///|
test "process_request: path traversal in out name raises error" {
let base = make_users_request()
let settings_with_traversal = {
version: base.settings.version,
engine: base.settings.engine,
schema: base.settings.schema,
queries: base.settings.queries,
codegen: {
out: "../../etc",
plugin: base.settings.codegen.plugin,
options: base.settings.codegen.options,
env: base.settings.codegen.env,
process: base.settings.codegen.process,
wasm: base.settings.codegen.wasm,
},
}
let req : GenerateRequest = {
settings: settings_with_traversal,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"package_name=test",
global_options: b"",
}
let result = try {
let _ = process_request(req)
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "process_request: absolute path in out name raises error" {
let base = make_users_request()
let settings_with_abs = {
version: base.settings.version,
engine: base.settings.engine,
schema: base.settings.schema,
queries: base.settings.queries,
codegen: {
out: "/tmp/foo",
plugin: base.settings.codegen.plugin,
options: base.settings.codegen.options,
env: base.settings.codegen.env,
process: base.settings.codegen.process,
wasm: base.settings.codegen.wasm,
},
}
let req : GenerateRequest = {
settings: settings_with_abs,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"package_name=test",
global_options: b"",
}
let result = try {
let _ = process_request(req)
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: Windows drive-letter C:\\ is rejected" {
let result = try {
validate_output_path("C:\\Windows\\Temp")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: Windows drive-letter D:/ is rejected" {
let result = try {
validate_output_path("D:/home/user")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: lowercase drive-letter c:\\ is rejected" {
let result = try {
validate_output_path("c:\\users\\admin")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "validate_output_path: drive-letter with traversal is rejected" {
let result = try {
validate_output_path("C:..\\..\\etc")
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "process_request: root path / as out_name raises error" {
let base = make_users_request()
let settings_with_root = {
version: base.settings.version,
engine: base.settings.engine,
schema: base.settings.schema,
queries: base.settings.queries,
codegen: {
out: "/",
plugin: base.settings.codegen.plugin,
options: base.settings.codegen.options,
env: base.settings.codegen.env,
process: base.settings.codegen.process,
wasm: base.settings.codegen.wasm,
},
}
let req : GenerateRequest = {
settings: settings_with_root,
catalog: base.catalog,
queries: base.queries,
sqlc_version: base.sqlc_version,
plugin_options: b"package_name=test",
global_options: b"",
}
let result = try {
let _ = process_request(req)
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}