///|
/// Protobuf Adapter Layer — P0-003.
/// Converts raw protobuf types (from types.mbt) into the plugin's internal model.
/// This is the semantic boundary between protobuf schema and downstream IR/codegen.
//
// 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.
//
///|
/// Parsed query command kind from sqlc's `:one`, `:many`, `:exec`, `:execrows`, `:copyfrom`, `:batch`, `:execlastid`.
pub enum QueryCmd {
One
Many
Exec
ExecRows
CopyFrom
Batch
ExecLastId
}
///|
impl Show for QueryCmd with fn to_string(self) -> String {
match self {
One => "One"
Many => "Many"
Exec => "Exec"
ExecRows => "ExecRows"
CopyFrom => "CopyFrom"
Batch => "Batch"
ExecLastId => "ExecLastId"
}
}
///|
/// Parse a sqlc cmd string into a QueryCmd.
/// Unknown or empty cmd strings fail codegen (no silent :exec fallback).
pub fn parse_query_cmd(cmd : String) -> QueryCmd raise {
match cmd {
":one" => One
":many" => Many
":exec" => Exec
":execrows" => ExecRows
":copyfrom" => CopyFrom
":batch" => Batch
":execlastid" => ExecLastId
"" =>
raise Failure::Failure(
"empty query cmd (expected :one, :many, :exec, :execrows, :copyfrom, :batch, or :execlastid)",
)
_ =>
raise Failure::Failure(
"unknown query cmd: '" +
cmd +
"' (expected :one, :many, :exec, :execrows, :copyfrom, :batch, or :execlastid)",
)
}
}
///|
/// Internal model — cleaned-up identifier.
pub struct AdaptIdentifier {
catalog : String
schema : String
name : String
}
///|
/// Internal model — cleaned-up column.
pub struct AdaptColumn {
name : String
not_null : Bool
is_array : Bool
ty : AdaptIdentifier
length : Int
comment : String
table : AdaptIdentifier
}
///|
/// Internal model — cleaned-up enum.
pub struct AdaptEnum {
name : String
vals : Array[String]
comment : String
}
///|
/// Internal model — cleaned-up table.
pub struct AdaptTable {
name : String
schema : String
columns : Array[AdaptColumn]
comment : String
}
///|
/// Internal model — cleaned-up parameter.
pub struct AdaptParameter {
number : Int
column : AdaptColumn
}
///|
/// Internal model — cleaned-up query with parsed cmd.
pub struct AdaptQuery {
text : String
name : String
cmd : QueryCmd
columns : Array[AdaptColumn]
params : Array[AdaptParameter]
comments : Array[String]
filename : String
}
///|
/// Internal model — cleaned-up schema.
pub struct AdaptSchema {
name : String
comment : String
tables : Array[AdaptTable]
enums : Array[AdaptEnum]
}
///|
/// Internal model — cleaned-up catalog.
pub struct AdaptCatalog {
name : String
default_schema : String
schemas : Array[AdaptSchema]
}
///|
/// Internal model — cleaned-up settings.
pub struct AdaptSettings {
version : String
engine : String
}
///|
/// Top-level internal model for a code generation request.
pub struct AdaptRequest {
settings : AdaptSettings
catalog : AdaptCatalog
queries : Array[AdaptQuery]
sqlc_version : String
}
///|
/// A column-level type override: maps a specific table.column to a MoonBit type.
pub struct ColumnOverride {
table : String
column : String
moonbit_type : String
}
///|
/// Parsed plugin/global options from codegen configuration.
pub struct PluginOptions {
/// Package name for generated code (e.g. "myapp" from codegen.options.package_name).
package_name : String
/// Type overrides: Array of (pg_type, moonbit_type) pairs.
/// Parsed from lines like `override_int4=String` in plugin_options bytes.
overrides : Array[(String, String)]
/// Emit @json.tag("column_name") annotations on struct fields.
emit_json_tags : Bool
/// Emit @db.tag("column_name") annotations on struct fields.
emit_db_tags : Bool
/// Rename mappings: Array of (original_name, new_name) pairs.
/// Parsed from lines like `rename_created_at=CreatedAt` in plugin_options bytes.
renames : Array[(String, String)]
/// Emit SQL as doc comment above generated functions (default: true).
emit_sql_as_comment : Bool
/// Omit structs not referenced by any query (default: false).
omit_unused_structs : Bool
/// When true, return empty array instead of error for :many with no rows (default: false).
emit_empty_slices : Bool
/// Comma-separated list of initialisms (e.g. "ID,URL,HTTP") for naming conversion (default: "").
initialisms : String
/// Case style for JSON tags: "snake", "camel", "pascal" (default: "snake").
json_tags_case_style : String
/// Use exact table names instead of singularized form for struct names (default: false).
emit_exact_table_names : Bool
/// Generate standalone functions accepting DB as argument instead of method receivers (default: false).
emit_methods_with_db_argument : Bool
/// Column-level overrides: Array of ColumnOverride, parsed from `override_column_.=`.
column_overrides : Array[ColumnOverride]
/// PG type names to always treat as nullable, parsed from `override_nullable_=true`.
nullable_overrides : Array[String]
/// When true, unknown PG types fail codegen instead of falling back to String.
strict_types : Bool
/// Import alias for runtime types in generated code (default: "@runtime").
/// Must match consumer `moon.pkg` alias, e.g. `import { ".../runtime" @db, }`.
runtime_alias : String
}
///|
/// Find first occurrence of a character in a string.
fn find_char(s : String, c : Char) -> Int? {
let n = s.length()
let target = c.to_int()
for i = 0; i < n; i = i + 1 {
if s[i].to_int() == target {
return Some(i)
}
}
None
}
///|
/// Fail codegen on invalid plugin option syntax or unknown keys.
fn fail_plugin_option(msg : String) -> Unit raise {
raise Failure::Failure("plugin option error: " + msg)
}
///|
/// Note non-fatal plugin option behavior (duplicate keys keep last value).
fn note_plugin_option(msg : String) -> Unit {
debug_inspect(msg) catch {
_ => ()
}
}
///|
fn default_plugin_options() -> PluginOptions {
{
package_name: "",
overrides: [],
emit_json_tags: false,
emit_db_tags: false,
renames: [],
emit_sql_as_comment: true,
omit_unused_structs: false,
emit_empty_slices: false,
initialisms: "",
json_tags_case_style: "snake",
emit_exact_table_names: false,
emit_methods_with_db_argument: false,
column_overrides: [],
nullable_overrides: [],
strict_types: false,
runtime_alias: default_runtime_alias(),
}
}
///|
/// Validate and normalize a runtime_alias plugin option.
fn validate_runtime_alias(raw : String) -> String raise {
let trimmed = raw.trim().to_owned()
if trimmed.length() == 0 {
fail_plugin_option("runtime_alias cannot be empty")
}
if trimmed[0] != '@' {
fail_plugin_option("runtime_alias must start with @")
}
let name = trimmed[1:]
if name.length() == 0 {
fail_plugin_option("runtime_alias must have a name after @")
}
let first = name[0]
if !((first >= 'a' && first <= 'z') ||
(first >= 'A' && first <= 'Z') ||
first == '_') {
fail_plugin_option(
"runtime_alias name must start with letter or underscore",
)
}
for i = 1; i < name.length(); i = i + 1 {
let c = name[i]
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_') {
fail_plugin_option("runtime_alias name contains invalid character")
}
}
trimmed
}
///|
fn json_as_string(j : Json) -> String? {
match j {
String(s) => Some(s)
_ => None
}
}
///|
fn json_as_bool(j : Json) -> Bool? {
match j {
True => Some(true)
False => Some(false)
String(s) =>
if s == "true" {
Some(true)
} else if s == "false" {
Some(false)
} else {
None
}
_ => None
}
}
///|
/// sqlc v1.31+ sends plugin_options as JSON bytes, e.g. `{"package_name":"users"}`.
fn parse_plugin_options_json(obj : Map[String, Json]) -> PluginOptions raise {
let mut package_name = ""
let mut has_package_name = false
let overrides : Array[(String, String)] = []
let renames : Array[(String, String)] = []
let mut emit_json_tags = false
let mut emit_db_tags = false
let mut emit_sql_as_comment = true
let mut omit_unused_structs = false
let mut emit_empty_slices = false
let mut initialisms = ""
let mut json_tags_case_style = "snake"
let mut emit_exact_table_names = false
let mut emit_methods_with_db_argument = false
let mut strict_types = false
let mut runtime_alias = default_runtime_alias()
let column_overrides : Array[ColumnOverride] = []
let nullable_overrides : Array[String] = []
obj.each(fn(key, value) raise {
if key == "package_name" || key == "package" {
match json_as_string(value) {
Some(name) => {
if has_package_name {
note_plugin_option(
"duplicate package_name option: keeping last value",
)
}
package_name = name.trim().to_owned()
has_package_name = true
}
None =>
fail_plugin_option("plugin_options JSON: " + key + " must be string")
}
} else if key.has_prefix("override_column_") {
match json_as_string(value) {
Some(moon_type) => {
let col_key = key[16:].trim().to_owned()
let dot_pos = col_key.rev_find(".")
match dot_pos {
Some(d) => {
let table = col_key[:d].to_owned()
let column = col_key[d + 1:].to_owned()
column_overrides.push({
table,
column,
moonbit_type: moon_type.trim().to_owned(),
})
}
None =>
fail_plugin_option(
"override_column_ missing dot separator: " + key,
)
}
}
None =>
fail_plugin_option("plugin_options JSON: " + key + " must be string")
}
} else if key.has_prefix("override_nullable_") {
match json_as_bool(value) {
Some(true) =>
nullable_overrides.push(key[18:].trim().to_owned().to_lower())
Some(false) => ()
None =>
fail_plugin_option("plugin_options JSON: " + key + " must be boolean")
}
} else if key.has_prefix("override_") {
match json_as_string(value) {
Some(moon_type) => {
let pg_type = key[9:].trim().to_owned()
overrides.push((pg_type.to_lower(), moon_type.trim().to_owned()))
}
None =>
fail_plugin_option("plugin_options JSON: " + key + " must be string")
}
} else if key.has_prefix("rename_") {
match json_as_string(value) {
Some(val) =>
renames.push((key[7:].trim().to_owned(), val.trim().to_owned()))
None =>
fail_plugin_option("plugin_options JSON: " + key + " must be string")
}
} else if key == "emit_json_tags" || key == "emit-json-tags" {
match json_as_bool(value) {
Some(v) => emit_json_tags = v
None =>
fail_plugin_option(
"plugin_options JSON: emit_json_tags must be boolean",
)
}
} else if key == "emit_db_tags" || key == "emit-db-tags" {
match json_as_bool(value) {
Some(v) => emit_db_tags = v
None =>
fail_plugin_option(
"plugin_options JSON: emit_db_tags must be boolean",
)
}
} else if key == "emit_sql_as_comment" || key == "emit-sql-as-comment" {
match json_as_bool(value) {
Some(v) => emit_sql_as_comment = v
None =>
fail_plugin_option(
"plugin_options JSON: emit_sql_as_comment must be boolean",
)
}
} else if key == "omit_unused_structs" || key == "omit-unused-structs" {
match json_as_bool(value) {
Some(v) => omit_unused_structs = v
None =>
fail_plugin_option(
"plugin_options JSON: omit_unused_structs must be boolean",
)
}
} else if key == "emit_empty_slices" || key == "emit-empty-slices" {
match json_as_bool(value) {
Some(v) => emit_empty_slices = v
None =>
fail_plugin_option(
"plugin_options JSON: emit_empty_slices must be boolean",
)
}
} else if key == "emit_exact_table_names" || key == "emit-exact-table-names" {
match json_as_bool(value) {
Some(v) => emit_exact_table_names = v
None =>
fail_plugin_option(
"plugin_options JSON: emit_exact_table_names must be boolean",
)
}
} else if key == "emit_methods_with_db_argument" ||
key == "emit-methods-with-db-argument" {
match json_as_bool(value) {
Some(v) => emit_methods_with_db_argument = v
None =>
fail_plugin_option(
"plugin_options JSON: emit_methods_with_db_argument must be boolean",
)
}
} else if key == "initialisms" {
match json_as_string(value) {
Some(v) => initialisms = v.trim().to_owned()
None =>
fail_plugin_option("plugin_options JSON: initialisms must be string")
}
} else if key == "json_tags_case_style" || key == "json-tags-case-style" {
match json_as_string(value) {
Some(v) => json_tags_case_style = v.trim().to_owned()
None =>
fail_plugin_option(
"plugin_options JSON: json_tags_case_style must be string",
)
}
} else if key == "strict_types" || key == "strict-types" {
match json_as_bool(value) {
Some(v) => strict_types = v
None =>
fail_plugin_option(
"plugin_options JSON: strict_types must be boolean",
)
}
} else if key == "runtime_alias" || key == "runtime-alias" {
match json_as_string(value) {
Some(v) => runtime_alias = validate_runtime_alias(v)
None =>
fail_plugin_option(
"plugin_options JSON: runtime_alias must be string",
)
}
} else {
fail_plugin_option("unknown option: " + key)
}
})
{
package_name,
overrides,
emit_json_tags,
emit_db_tags,
renames,
emit_sql_as_comment,
omit_unused_structs,
emit_empty_slices,
initialisms,
json_tags_case_style,
emit_exact_table_names,
emit_methods_with_db_argument,
column_overrides,
nullable_overrides,
strict_types,
runtime_alias,
}
}
///|
/// Parse plugin_options bytes into a PluginOptions struct.
/// Returns defaults on empty data; warns on malformed or unknown options.
/// Accepts sqlc JSON (`{"package_name":"users"}`) and legacy key=value lines.
pub fn parse_plugin_options(data : Bytes) -> PluginOptions raise {
if data.length() == 0 {
return default_plugin_options()
}
let text = @utf8.decode_lossy(data[:])
let trimmed = text.trim().to_owned()
if trimmed.has_prefix("{") {
try {
match @json.parse(trimmed[:]) {
Object(obj) => return parse_plugin_options_json(obj)
_ => fail_plugin_option("plugin_options JSON root must be object")
}
} catch {
_ => fail_plugin_option("plugin_options JSON parse failed")
}
}
let mut package_name = ""
let mut has_package_name = false
let overrides : Array[(String, String)] = []
let renames : Array[(String, String)] = []
let mut emit_json_tags = false
let mut emit_db_tags = false
let mut emit_sql_as_comment = true
let mut omit_unused_structs = false
let mut emit_empty_slices = false
let mut initialisms = ""
let mut json_tags_case_style = "snake"
let mut emit_exact_table_names = false
let mut emit_methods_with_db_argument = false
let mut strict_types = false
let mut runtime_alias = default_runtime_alias()
let column_overrides : Array[ColumnOverride] = []
let nullable_overrides : Array[String] = []
for line in text.split("\n") {
let s = line.trim().to_owned()
if s.has_prefix("package_name=") {
if has_package_name {
note_plugin_option("duplicate package_name option: keeping last value")
}
package_name = s[13:].trim().to_owned()
has_package_name = true
} else if s.has_prefix("package_name =") {
if has_package_name {
note_plugin_option("duplicate package_name option: keeping last value")
}
package_name = s[14:].trim().to_owned()
has_package_name = true
} else if s.has_prefix("package=") {
if has_package_name {
note_plugin_option("duplicate package_name option: keeping last value")
}
package_name = s[8:].trim().to_owned()
has_package_name = true
} else if s.has_prefix("package =") {
if has_package_name {
note_plugin_option("duplicate package_name option: keeping last value")
}
package_name = s[9:].trim().to_owned()
has_package_name = true
} else if s.has_prefix("override_column_") {
let eq_pos = find_char(s, '=')
match eq_pos {
Some(pos) => {
let key = s[16:pos].trim().to_owned()
let moon_type = s[pos + 1:].trim().to_owned()
let dot_pos = key.rev_find(".")
match dot_pos {
Some(d) => {
let table = key[:d].to_owned()
let column = key[d + 1:].to_owned()
column_overrides.push({ table, column, moonbit_type: moon_type })
}
None =>
fail_plugin_option("override_column_ missing dot separator: " + s)
}
}
None => fail_plugin_option("override_column_ missing '=': " + s)
}
} else if s.has_prefix("override_nullable_") {
let eq_pos = find_char(s, '=')
match eq_pos {
Some(pos) => {
let pg_type = s[18:pos].trim().to_owned()
let val = s[pos + 1:].trim().to_owned()
if val == "true" {
nullable_overrides.push(pg_type.to_lower())
}
}
None => fail_plugin_option("override_nullable_ missing '=': " + s)
}
} else if s.has_prefix("override_") {
let eq_pos = find_char(s, '=')
match eq_pos {
Some(pos) => {
let pg_type = s[9:pos].trim().to_owned()
let moon_type = s[pos + 1:].trim().to_owned()
overrides.push((pg_type.to_lower(), moon_type))
}
None => fail_plugin_option("override_ missing '=': " + s)
}
} else if s.has_prefix("rename_") {
let eq_pos = find_char(s, '=')
match eq_pos {
Some(pos) => {
let key = s[7:pos].trim().to_owned() // after "rename_"
let val = s[pos + 1:].trim().to_owned()
renames.push((key, val))
}
None => fail_plugin_option("rename_ missing '=': " + s)
}
} else if s == "emit_json_tags=true" || s == "emit_json_tags = true" {
emit_json_tags = true
} else if s == "emit_json_tags=false" || s == "emit_json_tags = false" {
emit_json_tags = false
} else if s == "emit_db_tags=true" || s == "emit_db_tags = true" {
emit_db_tags = true
} else if s == "emit_db_tags=false" || s == "emit_db_tags = false" {
emit_db_tags = false
} else if s == "emit_sql_as_comment=true" ||
s == "emit_sql_as_comment = true" {
emit_sql_as_comment = true
} else if s == "emit_sql_as_comment=false" ||
s == "emit_sql_as_comment = false" {
emit_sql_as_comment = false
} else if s == "omit_unused_structs=true" ||
s == "omit_unused_structs = true" {
omit_unused_structs = true
} else if s == "omit_unused_structs=false" ||
s == "omit_unused_structs = false" {
omit_unused_structs = false
} else if s == "emit_empty_slices=true" || s == "emit_empty_slices = true" {
emit_empty_slices = true
} else if s == "emit_empty_slices=false" || s == "emit_empty_slices = false" {
emit_empty_slices = false
} else if s.has_prefix("initialisms=") {
initialisms = s[12:].trim().to_owned()
} else if s.has_prefix("initialisms =") {
initialisms = s[13:].trim().to_owned()
} else if s.has_prefix("json_tags_case_style=") {
json_tags_case_style = s[21:].trim().to_owned()
} else if s.has_prefix("json_tags_case_style =") {
json_tags_case_style = s[22:].trim().to_owned()
} else if s == "emit_exact_table_names=true" ||
s == "emit_exact_table_names = true" {
emit_exact_table_names = true
} else if s == "emit_exact_table_names=false" ||
s == "emit_exact_table_names = false" {
emit_exact_table_names = false
} else if s == "emit_methods_with_db_argument=true" ||
s == "emit_methods_with_db_argument = true" {
emit_methods_with_db_argument = true
} else if s == "emit_methods_with_db_argument=false" ||
s == "emit_methods_with_db_argument = false" {
emit_methods_with_db_argument = false
// Support kebab-case variants for all new options
} else if s == "emit-json-tags=true" || s == "emit-json-tags = true" {
emit_json_tags = true
} else if s == "emit-json-tags=false" || s == "emit-json-tags = false" {
emit_json_tags = false
} else if s == "emit-db-tags=true" || s == "emit-db-tags = true" {
emit_db_tags = true
} else if s == "emit-db-tags=false" || s == "emit-db-tags = false" {
emit_db_tags = false
} else if s == "emit-sql-as-comment=true" ||
s == "emit-sql-as-comment = true" {
emit_sql_as_comment = true
} else if s == "emit-sql-as-comment=false" ||
s == "emit-sql-as-comment = false" {
emit_sql_as_comment = false
} else if s == "omit-unused-structs=true" ||
s == "omit-unused-structs = true" {
omit_unused_structs = true
} else if s == "omit-unused-structs=false" ||
s == "omit-unused-structs = false" {
omit_unused_structs = false
} else if s == "emit-empty-slices=true" || s == "emit-empty-slices = true" {
emit_empty_slices = true
} else if s == "emit-empty-slices=false" || s == "emit-empty-slices = false" {
emit_empty_slices = false
} else if s.has_prefix("json-tags-case-style=") {
json_tags_case_style = s[21:].trim().to_owned()
} else if s.has_prefix("json-tags-case-style =") {
json_tags_case_style = s[22:].trim().to_owned()
} else if s == "emit-exact-table-names=true" ||
s == "emit-exact-table-names = true" {
emit_exact_table_names = true
} else if s == "emit-exact-table-names=false" ||
s == "emit-exact-table-names = false" {
emit_exact_table_names = false
} else if s == "emit-methods-with-db-argument=true" ||
s == "emit-methods-with-db-argument = true" {
emit_methods_with_db_argument = true
} else if s == "emit-methods-with-db-argument=false" ||
s == "emit-methods-with-db-argument = false" {
emit_methods_with_db_argument = false
} else if s == "strict_types=true" ||
s == "strict_types = true" ||
s == "strict-types=true" ||
s == "strict-types = true" {
strict_types = true
} else if s == "strict_types=false" ||
s == "strict_types = false" ||
s == "strict-types=false" ||
s == "strict-types = false" {
strict_types = false
} else if s.has_prefix("runtime_alias=") {
runtime_alias = validate_runtime_alias(s[14:].trim().to_owned())
} else if s.has_prefix("runtime_alias =") {
runtime_alias = validate_runtime_alias(s[15:].trim().to_owned())
} else if s.has_prefix("runtime-alias=") {
runtime_alias = validate_runtime_alias(s[14:].trim().to_owned())
} else if s.has_prefix("runtime-alias =") {
runtime_alias = validate_runtime_alias(s[15:].trim().to_owned())
} else if s.length() > 0 {
fail_plugin_option("unknown option: " + s)
}
}
{
package_name,
overrides,
emit_json_tags,
emit_db_tags,
renames,
emit_sql_as_comment,
omit_unused_structs,
emit_empty_slices,
initialisms,
json_tags_case_style,
emit_exact_table_names,
emit_methods_with_db_argument,
column_overrides,
nullable_overrides,
strict_types,
runtime_alias,
}
}
// === Converters: raw protobuf types → internal model ===
///|
fn convert_identifier(id : Identifier) -> AdaptIdentifier {
{ catalog: id.catalog, schema: id.schema, name: id.name }
}
///|
/// Trim leading and trailing whitespace from a string.
/// MoonBit String currently lacks a @builtin trim, so we implement it manually.
fn trim_str(s : String) -> String {
let mut start = 0
let n = s.length()
while start < n &&
(
s[start] == ' ' ||
s[start] == '\t' ||
s[start] == '\n' ||
s[start] == '\r'
) {
start = start + 1
}
let mut end = n
while end > start &&
(
s[end - 1] == ' ' ||
s[end - 1] == '\t' ||
s[end - 1] == '\n' ||
s[end - 1] == '\r'
) {
end = end - 1
}
s[start:end].to_owned()
}
///|
fn convert_column(col : Column) -> AdaptColumn {
let col_name = if trim_str(col.name).length() == 0 {
"column_unnamed"
} else {
col.name
}
{
name: col_name,
not_null: col.not_null,
is_array: col.is_array,
ty: convert_identifier(col.ty),
length: col.length,
comment: col.comment,
table: convert_identifier(col.table),
}
}
///|
fn convert_parameter(p : Parameter) -> AdaptParameter {
{ number: p.number, column: convert_column(p.column) }
}
///|
fn convert_enum(e : Enum) -> AdaptEnum {
{ name: e.name, vals: e.vals, comment: e.comment }
}
///|
fn convert_table(t : Table) -> AdaptTable {
{
name: t.rel.name,
schema: t.rel.schema,
columns: t.columns.map(convert_column),
comment: t.comment,
}
}
///|
fn convert_schema(s : Schema) -> AdaptSchema {
{
name: s.name,
comment: s.comment,
tables: s.tables.map(convert_table),
enums: s.enums.map(convert_enum),
}
}
///|
fn convert_catalog(c : Catalog) -> AdaptCatalog {
{
name: c.name,
default_schema: c.default_schema,
schemas: c.schemas.map(convert_schema),
}
}
///|
fn convert_settings(s : Settings) -> AdaptSettings {
{ version: s.version, engine: s.engine }
}
///|
fn convert_query(q : Query) -> AdaptQuery raise {
let q_name = if trim_str(q.name).length() == 0 {
"unnamed_query"
} else {
q.name
}
{
text: q.text,
name: q_name,
cmd: parse_query_cmd(q.cmd),
columns: q.columns.map(convert_column),
params: q.params.map(convert_parameter),
comments: q.comments,
filename: q.filename,
}
}
///|
/// Convert a raw GenerateRequest into the internal adapter model.
pub fn convert_request(req : GenerateRequest) -> AdaptRequest raise {
{
settings: convert_settings(req.settings),
catalog: convert_catalog(req.catalog),
queries: req.queries.map(convert_query),
sqlc_version: req.sqlc_version,
}
}
// === Tests ===
///|
test "parse_query_cmd: :one" {
inspect(parse_query_cmd(":one"), content="One")
}
///|
test "parse_query_cmd: :many" {
inspect(parse_query_cmd(":many"), content="Many")
}
///|
test "parse_query_cmd: :exec" {
inspect(parse_query_cmd(":exec"), content="Exec")
}
///|
test "parse_query_cmd: :execrows" {
inspect(parse_query_cmd(":execrows"), content="ExecRows")
}
///|
test "parse_query_cmd: empty string fails" {
let result = try {
ignore(parse_query_cmd(""))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_query_cmd: unknown cmd fails" {
let result = try {
ignore(parse_query_cmd(":unknown"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_query_cmd: :copyfrom" {
inspect(parse_query_cmd(":copyfrom"), content="CopyFrom")
}
///|
test "parse_query_cmd: :batch" {
inspect(parse_query_cmd(":batch"), content="Batch")
}
///|
test "parse_query_cmd: :execlastid" {
inspect(parse_query_cmd(":execlastid"), content="ExecLastId")
}
///|
test "convert_request: minimal empty request" {
let empty_id : Identifier = { catalog: "", schema: "", name: "" }
let cg : Codegen = {
out: "",
plugin: "",
options: b"",
env: Array::make(0, ""),
process: None,
wasm: None,
}
let settings : Settings = {
version: "v1",
engine: "postgresql",
schema: Array::make(0, ""),
queries: Array::make(0, ""),
codegen: cg,
}
let schema : Schema = {
comment: "",
name: "public",
tables: Array::make(0, {
rel: empty_id,
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
comment: "",
}),
enums: Array::make(0, { name: "", vals: Array::make(0, ""), comment: "" }),
composite_types: Array::make(0, { name: "", comment: "" }),
}
let catalog : Catalog = {
comment: "",
default_schema: "public",
name: "testdb",
schemas: [schema],
}
let req : GenerateRequest = {
settings,
catalog,
queries: Array::make(0, {
text: "",
name: "",
cmd: "",
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
params: Array::make(0, {
number: 0,
column: {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
},
}),
comments: Array::make(0, ""),
filename: "",
insert_into_table: empty_id,
}),
sqlc_version: "1.0.0",
plugin_options: b"",
global_options: b"",
}
let adapted = convert_request(req)
inspect(adapted.settings.version, content="v1")
inspect(adapted.settings.engine, content="postgresql")
inspect(adapted.catalog.name, content="testdb")
inspect(adapted.catalog.default_schema, content="public")
inspect(adapted.catalog.schemas.length(), content="1")
inspect(adapted.catalog.schemas[0].name, content="public")
inspect(adapted.sqlc_version, content="1.0.0")
}
///|
test "convert_request: query with parsed cmd" {
let empty_id : Identifier = { catalog: "", schema: "", name: "" }
let cg : Codegen = {
out: "",
plugin: "",
options: b"",
env: Array::make(0, ""),
process: None,
wasm: None,
}
let settings : Settings = {
version: "",
engine: "",
schema: Array::make(0, ""),
queries: Array::make(0, ""),
codegen: cg,
}
let schema : Schema = {
comment: "",
name: "",
tables: Array::make(0, {
rel: empty_id,
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
comment: "",
}),
enums: Array::make(0, { name: "", vals: Array::make(0, ""), comment: "" }),
composite_types: Array::make(0, { name: "", comment: "" }),
}
let catalog : Catalog = {
comment: "",
default_schema: "",
name: "",
schemas: [schema],
}
// Build a query with :many cmd
let col : Column = {
name: "id",
not_null: true,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: { catalog: "", schema: "", name: "int4" },
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "id",
unsigned: false,
array_dims: 0,
}
let query : Query = {
text: "SELECT * FROM users",
name: "list_users",
cmd: ":many",
columns: [col],
params: Array::make(0, {
number: 0,
column: {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
},
}),
comments: Array::make(0, ""),
filename: "queries.sql",
insert_into_table: empty_id,
}
let req : GenerateRequest = {
settings,
catalog,
queries: [query],
sqlc_version: "",
plugin_options: b"",
global_options: b"",
}
let adapted = convert_request(req)
inspect(adapted.queries.length(), content="1")
inspect(adapted.queries[0].name, content="list_users")
inspect(adapted.queries[0].text, content="SELECT * FROM users")
inspect(adapted.queries[0].cmd, content="Many")
inspect(adapted.queries[0].filename, content="queries.sql")
inspect(adapted.queries[0].columns.length(), content="1")
inspect(adapted.queries[0].columns[0].name, content="id")
inspect(adapted.queries[0].columns[0].not_null, content="true")
inspect(adapted.queries[0].columns[0].ty.name, content="int4")
}
///|
test "convert_request: table with identifier resolved to name/schema" {
let empty_id : Identifier = { catalog: "", schema: "", name: "" }
let cg : Codegen = {
out: "",
plugin: "",
options: b"",
env: Array::make(0, ""),
process: None,
wasm: None,
}
let settings : Settings = {
version: "",
engine: "",
schema: Array::make(0, ""),
queries: Array::make(0, ""),
codegen: cg,
}
let tbl_rel : Identifier = { catalog: "", schema: "public", name: "users" }
let tbl : Table = {
rel: tbl_rel,
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
comment: "users table",
}
let schema : Schema = {
comment: "",
name: "public",
tables: [tbl],
enums: Array::make(0, { name: "", vals: Array::make(0, ""), comment: "" }),
composite_types: Array::make(0, { name: "", comment: "" }),
}
let catalog : Catalog = {
comment: "",
default_schema: "public",
name: "test",
schemas: [schema],
}
let req : GenerateRequest = {
settings,
catalog,
queries: Array::make(0, {
text: "",
name: "",
cmd: "",
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
params: Array::make(0, {
number: 0,
column: {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
},
}),
comments: Array::make(0, ""),
filename: "",
insert_into_table: empty_id,
}),
sqlc_version: "",
plugin_options: b"",
global_options: b"",
}
let adapted = convert_request(req)
inspect(adapted.catalog.schemas[0].tables.length(), content="1")
inspect(adapted.catalog.schemas[0].tables[0].name, content="users")
inspect(adapted.catalog.schemas[0].tables[0].schema, content="public")
inspect(adapted.catalog.schemas[0].tables[0].comment, content="users table")
}
///|
test "convert_request: enums in schema" {
let empty_id : Identifier = { catalog: "", schema: "", name: "" }
let cg : Codegen = {
out: "",
plugin: "",
options: b"",
env: Array::make(0, ""),
process: None,
wasm: None,
}
let settings : Settings = {
version: "",
engine: "",
schema: Array::make(0, ""),
queries: Array::make(0, ""),
codegen: cg,
}
let e : Enum = {
name: "mood",
vals: ["happy", "sad", "neutral"],
comment: "",
}
let schema : Schema = {
comment: "",
name: "public",
tables: Array::make(0, {
rel: empty_id,
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
comment: "",
}),
enums: [e],
composite_types: Array::make(0, { name: "", comment: "" }),
}
let catalog : Catalog = {
comment: "",
default_schema: "public",
name: "test",
schemas: [schema],
}
let req : GenerateRequest = {
settings,
catalog,
queries: Array::make(0, {
text: "",
name: "",
cmd: "",
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
params: Array::make(0, {
number: 0,
column: {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
},
}),
comments: Array::make(0, ""),
filename: "",
insert_into_table: empty_id,
}),
sqlc_version: "",
plugin_options: b"",
global_options: b"",
}
let adapted = convert_request(req)
inspect(adapted.catalog.schemas[0].enums.length(), content="1")
inspect(adapted.catalog.schemas[0].enums[0].name, content="mood")
inspect(adapted.catalog.schemas[0].enums[0].vals.length(), content="3")
inspect(adapted.catalog.schemas[0].enums[0].vals[0], content="happy")
inspect(adapted.catalog.schemas[0].enums[0].vals[1], content="sad")
inspect(adapted.catalog.schemas[0].enums[0].vals[2], content="neutral")
}
///|
test "convert_request: query with parameter" {
let empty_id : Identifier = { catalog: "", schema: "", name: "" }
let cg : Codegen = {
out: "",
plugin: "",
options: b"",
env: Array::make(0, ""),
process: None,
wasm: None,
}
let settings : Settings = {
version: "",
engine: "",
schema: Array::make(0, ""),
queries: Array::make(0, ""),
codegen: cg,
}
let schema : Schema = {
comment: "",
name: "",
tables: Array::make(0, {
rel: empty_id,
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
comment: "",
}),
enums: Array::make(0, { name: "", vals: Array::make(0, ""), comment: "" }),
composite_types: Array::make(0, { name: "", comment: "" }),
}
let catalog : Catalog = {
comment: "",
default_schema: "",
name: "",
schemas: [schema],
}
let param_col : Column = {
name: "min_age",
not_null: true,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: { catalog: "", schema: "", name: "int4" },
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "min_age",
unsigned: false,
array_dims: 0,
}
let param : Parameter = { number: 1, column: param_col }
let query : Query = {
text: "SELECT * FROM users WHERE age > $1",
name: "older_than",
cmd: ":many",
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
params: [param],
comments: Array::make(0, ""),
filename: "queries.sql",
insert_into_table: empty_id,
}
let req : GenerateRequest = {
settings,
catalog,
queries: [query],
sqlc_version: "",
plugin_options: b"",
global_options: b"",
}
let adapted = convert_request(req)
inspect(adapted.queries[0].params.length(), content="1")
inspect(adapted.queries[0].params[0].number, content="1")
inspect(adapted.queries[0].params[0].column.name, content="min_age")
inspect(adapted.queries[0].params[0].column.ty.name, content="int4")
}
///|
test "parse_plugin_options: empty bytes returns empty package_name" {
let opts = parse_plugin_options(b"")
inspect(opts.package_name, content="")
inspect(opts.emit_json_tags, content="false")
inspect(opts.emit_db_tags, content="false")
inspect(opts.runtime_alias, content="@runtime")
}
///|
test "parse_plugin_options: package_name= prefix parsed" {
let opts = parse_plugin_options(b"package_name=myapp")
inspect(opts.package_name, content="myapp")
}
///|
test "parse_plugin_options: package= prefix (short form) parsed" {
let opts = parse_plugin_options(b"package=myapp")
inspect(opts.package_name, content="myapp")
}
///|
test "parse_plugin_options: json package_name parsed (sqlc v1.31 format)" {
let opts = parse_plugin_options(b"{\"package_name\":\"users\"}")
inspect(opts.package_name, content="users")
}
///|
test "parse_plugin_options: json package alias parsed" {
let opts = parse_plugin_options(b"{\"package\":\"users\"}")
inspect(opts.package_name, content="users")
}
///|
test "parse_plugin_options: json emit_json_tags parsed" {
let opts = parse_plugin_options(
b"{\"package_name\":\"users\",\"emit_json_tags\":true}",
)
inspect(opts.package_name, content="users")
inspect(opts.emit_json_tags, content="true")
}
///|
test "parse_plugin_options: json override parsed" {
let opts = parse_plugin_options(
b"{\"package_name\":\"users\",\"override_int4\":\"String\"}",
)
inspect(opts.overrides.length(), content="1")
inspect(opts.overrides[0].0, content="int4")
inspect(opts.overrides[0].1, content="String")
}
///|
test "parse_plugin_options: json unknown key fails" {
let result = try {
ignore(
parse_plugin_options(
b"{\"package_name\":\"users\",\"unknown_key\":\"x\"}",
),
)
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_plugin_options: package_name = with spaces parsed" {
let opts = parse_plugin_options(b"package_name = spaced_name")
inspect(opts.package_name, content="spaced_name")
}
///|
test "parse_plugin_options: package = with spaces parsed" {
let opts = parse_plugin_options(b"package = spaced_name")
inspect(opts.package_name, content="spaced_name")
}
///|
test "parse_plugin_options: unknown keys fail" {
let result = try {
ignore(parse_plugin_options(b"unknown=val\npackage_name=myapp"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_plugin_options: multiline with trailing newline" {
let opts = parse_plugin_options(b"package_name=myapp\n")
inspect(opts.package_name, content="myapp")
}
///|
test "parse_plugin_options: emit_json_tags=true parsed" {
let opts = parse_plugin_options(b"emit_json_tags=true")
inspect(opts.emit_json_tags, content="true")
inspect(opts.emit_db_tags, content="false")
}
///|
test "parse_plugin_options: emit_db_tags=true parsed" {
let opts = parse_plugin_options(b"emit_db_tags=true")
inspect(opts.emit_db_tags, content="true")
inspect(opts.emit_json_tags, content="false")
}
///|
test "parse_plugin_options: emit_json_tags and emit_db_tags both true" {
let opts = parse_plugin_options(b"emit_json_tags=true\nemit_db_tags=true")
inspect(opts.emit_json_tags, content="true")
inspect(opts.emit_db_tags, content="true")
}
///|
test "parse_plugin_options: emit_json_tags=false by default" {
let opts = parse_plugin_options(b"package_name=test")
inspect(opts.emit_json_tags, content="false")
inspect(opts.emit_db_tags, content="false")
}
///|
test "parse_plugin_options: emit_json_tags = true with spaces parsed" {
let opts = parse_plugin_options(b"emit_json_tags = true")
inspect(opts.emit_json_tags, content="true")
}
///|
test "parse_plugin_options: emit_db_tags = true with spaces parsed" {
let opts = parse_plugin_options(b"emit_db_tags = true")
inspect(opts.emit_db_tags, content="true")
}
///|
test "parse_plugin_options: single override_ line parsed" {
let opts = parse_plugin_options(
b"package_name=testdb\noverride_numeric=Decimal",
)
inspect(opts.package_name, content="testdb")
inspect(opts.overrides.length(), content="1")
inspect(opts.overrides[0].0, content="numeric")
inspect(opts.overrides[0].1, content="Decimal")
}
///|
test "parse_plugin_options: multiple override_ lines parsed" {
let opts = parse_plugin_options(
b"package_name=testdb\noverride_numeric=Decimal\noverride_int4=String\noverride_bool=Int64",
)
inspect(opts.overrides.length(), content="3")
inspect(opts.overrides[0].0, content="numeric")
inspect(opts.overrides[0].1, content="Decimal")
inspect(opts.overrides[1].0, content="int4")
inspect(opts.overrides[1].1, content="String")
inspect(opts.overrides[2].0, content="bool")
inspect(opts.overrides[2].1, content="Int64")
}
///|
test "parse_plugin_options: override with spaces around = works" {
let opts = parse_plugin_options(
b"package_name=testdb\noverride_numeric = Decimal",
)
inspect(opts.overrides.length(), content="1")
inspect(opts.overrides[0].0, content="numeric")
inspect(opts.overrides[0].1, content="Decimal")
}
///|
test "parse_plugin_options: override without = fails" {
let result = try {
ignore(parse_plugin_options(b"override_numeric"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_plugin_options: empty bytes returns empty overrides" {
let opts = parse_plugin_options(b"")
inspect(opts.overrides.length(), content="0")
}
///|
test "parse_plugin_options: only overrides without package_name" {
let opts = parse_plugin_options(b"override_numeric=Decimal")
inspect(opts.package_name, content="")
inspect(opts.overrides.length(), content="1")
}
///|
test "parse_plugin_options: override with spaces in value" {
let opts = parse_plugin_options(b"override_int4 = Option[Int64]")
inspect(opts.overrides.length(), content="1")
inspect(opts.overrides[0].0, content="int4")
inspect(opts.overrides[0].1, content="Option[Int64]")
}
///|
test "parse_plugin_options: rename_ line parsed" {
let opts = parse_plugin_options(
b"package_name=testdb\nrename_created_at=CreatedAt\nrename_updated_at=UpdatedAt",
)
inspect(opts.renames.length(), content="2")
inspect(opts.renames[0].0, content="created_at")
inspect(opts.renames[0].1, content="CreatedAt")
inspect(opts.renames[1].0, content="updated_at")
inspect(opts.renames[1].1, content="UpdatedAt")
}
///|
test "parse_plugin_options: rename_ with spaces around = works" {
let opts = parse_plugin_options(b"rename_created_at = CreatedAt")
inspect(opts.renames.length(), content="1")
inspect(opts.renames[0].0, content="created_at")
inspect(opts.renames[0].1, content="CreatedAt")
}
///|
test "parse_plugin_options: rename_ without = fails" {
let result = try {
ignore(parse_plugin_options(b"rename_created_at"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_plugin_options: empty bytes returns empty renames" {
let opts = parse_plugin_options(b"")
inspect(opts.renames.length(), content="0")
}
///|
test "parse_plugin_options: unknown keys fail even when rename present" {
let result = try {
ignore(parse_plugin_options(b"rename_foo=bar\nunknown=val"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "convert_request: column with type info preserved" {
let empty_id : Identifier = { catalog: "", schema: "", name: "" }
let cg : Codegen = {
out: "",
plugin: "",
options: b"",
env: Array::make(0, ""),
process: None,
wasm: None,
}
let settings : Settings = {
version: "",
engine: "",
schema: Array::make(0, ""),
queries: Array::make(0, ""),
codegen: cg,
}
let ty_id : Identifier = { catalog: "", schema: "", name: "text" }
let tbl_id : Identifier = { catalog: "", schema: "public", name: "users" }
let col : Column = {
name: "email",
not_null: true,
is_array: false,
comment: "user email",
length: 255,
is_named_param: false,
is_func_call: false,
scope: "",
table: tbl_id,
table_alias: "u",
ty: ty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "email_addr",
unsigned: false,
array_dims: 0,
}
let tbl : Table = { rel: tbl_id, columns: [col], comment: "users table" }
let schema : Schema = {
comment: "",
name: "public",
tables: [tbl],
enums: Array::make(0, { name: "", vals: Array::make(0, ""), comment: "" }),
composite_types: Array::make(0, { name: "", comment: "" }),
}
let catalog : Catalog = {
comment: "",
default_schema: "public",
name: "test",
schemas: [schema],
}
let req : GenerateRequest = {
settings,
catalog,
queries: Array::make(0, {
text: "",
name: "",
cmd: "",
columns: Array::make(0, {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
}),
params: Array::make(0, {
number: 0,
column: {
name: "",
not_null: false,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: empty_id,
table_alias: "",
ty: empty_id,
is_sqlc_slice: false,
embed_table: empty_id,
original_name: "",
unsigned: false,
array_dims: 0,
},
}),
comments: Array::make(0, ""),
filename: "",
insert_into_table: empty_id,
}),
sqlc_version: "",
plugin_options: b"",
global_options: b"",
}
let adapted = convert_request(req)
let adapted_col = adapted.catalog.schemas[0].tables[0].columns[0]
inspect(adapted_col.name, content="email")
inspect(adapted_col.not_null, content="true")
inspect(adapted_col.is_array, content="false")
inspect(adapted_col.ty.name, content="text")
inspect(adapted_col.length, content="255")
inspect(adapted_col.comment, content="user email")
inspect(adapted_col.table.schema, content="public")
inspect(adapted_col.table.name, content="users")
}
// === P0-057: Plugin options inline tests ===
///|
test "parse_plugin_options: emit_sql_as_comment=true parsed" {
let opts = parse_plugin_options(b"emit_sql_as_comment=true")
inspect(opts.emit_sql_as_comment, content="true")
}
///|
test "parse_plugin_options: emit_sql_as_comment=false parsed" {
let opts = parse_plugin_options(b"emit_sql_as_comment=false")
inspect(opts.emit_sql_as_comment, content="false")
}
///|
test "parse_plugin_options: omit_unused_structs=true parsed" {
let opts = parse_plugin_options(b"omit_unused_structs=true")
inspect(opts.omit_unused_structs, content="true")
}
///|
test "parse_plugin_options: emit_empty_slices=true parsed" {
let opts = parse_plugin_options(b"emit_empty_slices=true")
inspect(opts.emit_empty_slices, content="true")
}
///|
test "parse_plugin_options: initialisms=ID,URL,HTTP parsed" {
let opts = parse_plugin_options(b"initialisms=ID,URL,HTTP")
inspect(opts.initialisms, content="ID,URL,HTTP")
}
///|
test "parse_plugin_options: initialisms with spaces parsed" {
let opts = parse_plugin_options(b"initialisms = ID,URL,HTTP")
inspect(opts.initialisms, content="ID,URL,HTTP")
}
///|
test "parse_plugin_options: json_tags_case_style=camel parsed" {
let opts = parse_plugin_options(b"json_tags_case_style=camel")
inspect(opts.json_tags_case_style, content="camel")
}
///|
test "parse_plugin_options: json_tags_case_style with spaces parsed" {
let opts = parse_plugin_options(b"json_tags_case_style = pascal")
inspect(opts.json_tags_case_style, content="pascal")
}
///|
test "parse_plugin_options: emit_exact_table_names=true parsed" {
let opts = parse_plugin_options(b"emit_exact_table_names=true")
inspect(opts.emit_exact_table_names, content="true")
}
///|
test "parse_plugin_options: emit_methods_with_db_argument=true parsed" {
let opts = parse_plugin_options(b"emit_methods_with_db_argument=true")
inspect(opts.emit_methods_with_db_argument, content="true")
}
///|
test "parse_plugin_options: kebab-case emit-sql-as-comment=false parsed" {
let opts = parse_plugin_options(b"emit-sql-as-comment=false")
inspect(opts.emit_sql_as_comment, content="false")
}
///|
test "parse_plugin_options: kebab-case omit-unused-structs=true parsed" {
let opts = parse_plugin_options(b"omit-unused-structs=true")
inspect(opts.omit_unused_structs, content="true")
}
///|
test "parse_plugin_options: kebab-case emit-empty-slices=true parsed" {
let opts = parse_plugin_options(b"emit-empty-slices=true")
inspect(opts.emit_empty_slices, content="true")
}
///|
test "parse_plugin_options: kebab-case json-tags-case-style=pascal parsed" {
let opts = parse_plugin_options(b"json-tags-case-style=pascal")
inspect(opts.json_tags_case_style, content="pascal")
}
///|
test "parse_plugin_options: kebab-case emit-exact-table-names=true parsed" {
let opts = parse_plugin_options(b"emit-exact-table-names=true")
inspect(opts.emit_exact_table_names, content="true")
}
///|
test "parse_plugin_options: kebab-case emit-methods-with-db-argument=true parsed" {
let opts = parse_plugin_options(b"emit-methods-with-db-argument=true")
inspect(opts.emit_methods_with_db_argument, content="true")
}
///|
test "parse_plugin_options: kebab-case with spaces emit-sql-as-comment = false parsed" {
let opts = parse_plugin_options(b"emit-sql-as-comment = false")
inspect(opts.emit_sql_as_comment, content="false")
}
///|
test "parse_plugin_options: kebab-case json-tags-case-style = camel parsed" {
let opts = parse_plugin_options(b"json-tags-case-style = camel")
inspect(opts.json_tags_case_style, content="camel")
}
///|
test "parse_plugin_options: mixed combination emit_empty_slices + json_tags_case_style" {
let opts = parse_plugin_options(
b"emit_empty_slices=true\njson_tags_case_style=camel",
)
inspect(opts.emit_empty_slices, content="true")
inspect(opts.json_tags_case_style, content="camel")
}
///|
test "parse_plugin_options: mixed combination kebab + snake" {
let opts = parse_plugin_options(
b"emit-empty-slices=true\njson_tags_case_style=pascal",
)
inspect(opts.emit_empty_slices, content="true")
inspect(opts.json_tags_case_style, content="pascal")
}
///|
test "parse_plugin_options: all new options simultaneously" {
let opts = parse_plugin_options(
b"emit_sql_as_comment=false\nomit_unused_structs=true\nemit_empty_slices=true\ninitialisms=ID,URL\njson_tags_case_style=camel\nemit_exact_table_names=true\nemit_methods_with_db_argument=true",
)
inspect(opts.emit_sql_as_comment, content="false")
inspect(opts.omit_unused_structs, content="true")
inspect(opts.emit_empty_slices, content="true")
inspect(opts.initialisms, content="ID,URL")
inspect(opts.json_tags_case_style, content="camel")
inspect(opts.emit_exact_table_names, content="true")
inspect(opts.emit_methods_with_db_argument, content="true")
}
///|
test "parse_plugin_options: emit_sql_as_comment default true when not set" {
let opts = parse_plugin_options(b"package_name=test")
inspect(opts.emit_sql_as_comment, content="true")
}
///|
test "parse_plugin_options: omit_unused_structs=false parsed (snake_case)" {
let opts = parse_plugin_options(b"omit_unused_structs=false")
inspect(opts.omit_unused_structs, content="false")
}
///|
test "parse_plugin_options: emit_empty_slices=false parsed (snake_case)" {
let opts = parse_plugin_options(b"emit_empty_slices=false")
inspect(opts.emit_empty_slices, content="false")
}
///|
test "parse_plugin_options: emit_exact_table_names=false parsed (snake_case)" {
let opts = parse_plugin_options(b"emit_exact_table_names=false")
inspect(opts.emit_exact_table_names, content="false")
}
///|
test "parse_plugin_options: emit_methods_with_db_argument=false parsed (snake_case)" {
let opts = parse_plugin_options(b"emit_methods_with_db_argument=false")
inspect(opts.emit_methods_with_db_argument, content="false")
}
///|
test "parse_plugin_options: omit-unused-structs=false parsed (kebab-case)" {
let opts = parse_plugin_options(b"omit-unused-structs=false")
inspect(opts.omit_unused_structs, content="false")
}
///|
test "parse_plugin_options: emit-empty-slices=false parsed (kebab-case)" {
let opts = parse_plugin_options(b"emit-empty-slices=false")
inspect(opts.emit_empty_slices, content="false")
}
///|
test "parse_plugin_options: emit-exact-table-names=false parsed (kebab-case)" {
let opts = parse_plugin_options(b"emit-exact-table-names=false")
inspect(opts.emit_exact_table_names, content="false")
}
///|
test "parse_plugin_options: emit-methods-with-db-argument=false parsed (kebab-case)" {
let opts = parse_plugin_options(b"emit-methods-with-db-argument=false")
inspect(opts.emit_methods_with_db_argument, content="false")
}
///|
test "parse_plugin_options: false branches do not override true values later" {
// Setting false then true should result in true (last wins)
let opts = parse_plugin_options(
b"emit_empty_slices=false\nemit_empty_slices=true",
)
inspect(opts.emit_empty_slices, content="true")
}
///|
test "parse_plugin_options: true branches do not override false values later" {
// Setting true then false should result in false (last wins)
let opts = parse_plugin_options(
b"emit_empty_slices=true\nemit_empty_slices=false",
)
inspect(opts.emit_empty_slices, content="false")
}
///|
test "parse_plugin_options: override_column_ single line parsed" {
let opts = parse_plugin_options(b"override_column_users.id=String")
inspect(opts.column_overrides.length(), content="1")
inspect(opts.column_overrides[0].table, content="users")
inspect(opts.column_overrides[0].column, content="id")
inspect(opts.column_overrides[0].moonbit_type, content="String")
}
///|
test "parse_plugin_options: override_column_ multiple lines parsed" {
let opts = parse_plugin_options(
b"override_column_users.id=String\noverride_column_posts.title=Int64",
)
inspect(opts.column_overrides.length(), content="2")
inspect(opts.column_overrides[0].table, content="users")
inspect(opts.column_overrides[0].column, content="id")
inspect(opts.column_overrides[0].moonbit_type, content="String")
inspect(opts.column_overrides[1].table, content="posts")
inspect(opts.column_overrides[1].column, content="title")
inspect(opts.column_overrides[1].moonbit_type, content="Int64")
}
///|
test "parse_plugin_options: override_column_ malformed no dot fails" {
let result = try {
ignore(parse_plugin_options(b"override_column_usersid=String"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_plugin_options: override_column_ with spaces around = works" {
let opts = parse_plugin_options(b"override_column_users.id = String")
inspect(opts.column_overrides.length(), content="1")
inspect(opts.column_overrides[0].table, content="users")
inspect(opts.column_overrides[0].column, content="id")
inspect(opts.column_overrides[0].moonbit_type, content="String")
}
///|
test "parse_plugin_options: override_column_ no = fails" {
let result = try {
ignore(parse_plugin_options(b"override_column_users.id"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}
///|
test "parse_plugin_options: override_nullable_ true parsed" {
let opts = parse_plugin_options(b"override_nullable_int4=true")
inspect(opts.nullable_overrides.length(), content="1")
inspect(opts.nullable_overrides[0], content="int4")
}
///|
test "parse_plugin_options: override_nullable_ false ignored" {
let opts = parse_plugin_options(b"override_nullable_int4=false")
inspect(opts.nullable_overrides.length(), content="0")
}
///|
test "parse_plugin_options: override_nullable_ multiple types parsed" {
let opts = parse_plugin_options(
b"override_nullable_int4=true\noverride_nullable_text=true",
)
inspect(opts.nullable_overrides.length(), content="2")
inspect(opts.nullable_overrides[0], content="int4")
inspect(opts.nullable_overrides[1], content="text")
}
///|
test "parse_plugin_options: override_nullable_ uppercase normalized to lowercase" {
let opts = parse_plugin_options(b"override_nullable_INT4=true")
inspect(opts.nullable_overrides.length(), content="1")
inspect(opts.nullable_overrides[0], content="int4")
}
///|
test "parse_plugin_options: override_column_ combined with other options" {
let opts = parse_plugin_options(
b"package_name=testdb\noverride_column_users.id=String\noverride_nullable_int4=true\noverride_numeric=Decimal",
)
inspect(opts.package_name, content="testdb")
inspect(opts.column_overrides.length(), content="1")
inspect(opts.column_overrides[0].table, content="users")
inspect(opts.column_overrides[0].moonbit_type, content="String")
inspect(opts.nullable_overrides.length(), content="1")
inspect(opts.nullable_overrides[0], content="int4")
inspect(opts.overrides.length(), content="1")
inspect(opts.overrides[0].0, content="numeric")
inspect(opts.overrides[0].1, content="Decimal")
}
// === P0-066: Empty identifier fallback tests ===
///|
test "P0-066: convert_column handles empty name" {
let id : Identifier = { catalog: "", schema: "", name: "int4" }
let tbl : Identifier = { catalog: "", schema: "public", name: "test_tbl" }
let embed_tbl : Identifier = { catalog: "", schema: "", name: "" }
let col = {
name: "",
not_null: true,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: tbl,
table_alias: "",
ty: id,
is_sqlc_slice: false,
embed_table: embed_tbl,
original_name: "",
unsigned: false,
array_dims: 0,
}
let adapted = convert_column(col)
inspect(adapted.name.length() > 0, content="true")
}
///|
test "P0-066: convert_query handles empty name" {
let id : Identifier = { catalog: "", schema: "", name: "int4" }
let tbl : Identifier = { catalog: "", schema: "", name: "test_tbl" }
let embed_tbl : Identifier = { catalog: "", schema: "", name: "" }
let insert_tbl : Identifier = { catalog: "", schema: "", name: "" }
let col = {
name: "id",
not_null: true,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: tbl,
table_alias: "",
ty: id,
is_sqlc_slice: false,
embed_table: embed_tbl,
original_name: "id",
unsigned: false,
array_dims: 0,
}
let q = {
text: "SELECT id FROM t",
name: "",
cmd: ":many",
columns: [col],
params: [],
comments: [],
filename: "test.sql",
insert_into_table: insert_tbl,
}
let adapted = convert_query(q)
inspect(adapted.name.length() > 0, content="true")
}
///|
test "P0-066: convert_column handles whitespace-only name" {
let id : Identifier = { catalog: "", schema: "", name: "int4" }
let tbl : Identifier = { catalog: "", schema: "public", name: "test_tbl" }
let embed_tbl : Identifier = { catalog: "", schema: "", name: "" }
let col = {
name: " ",
not_null: true,
is_array: false,
comment: "",
length: 0,
is_named_param: false,
is_func_call: false,
scope: "",
table: tbl,
table_alias: "",
ty: id,
is_sqlc_slice: false,
embed_table: embed_tbl,
original_name: " ",
unsigned: false,
array_dims: 0,
}
let adapted = convert_column(col)
inspect(adapted.name.length() > 0, content="true")
}
///|
test "parse_plugin_options: runtime_alias=@myrt parsed" {
let opts = parse_plugin_options(b"runtime_alias=@myrt")
inspect(opts.runtime_alias, content="@myrt")
}
///|
test "parse_plugin_options: json runtime_alias parsed" {
let opts = parse_plugin_options(b"{\"runtime_alias\":\"@db\"}")
inspect(opts.runtime_alias, content="@db")
}
///|
test "parse_plugin_options: invalid runtime_alias rejected" {
let result = try {
ignore(parse_plugin_options(b"runtime_alias=runtime"))
"unexpected_ok"
} catch {
_ => "rejected"
}
inspect(result, content="rejected")
}