///|
/// Internal IR Definition — P0-004.
/// Semantic boundary between protobuf adapter (P0-003) and MoonBit AST (P0-005).
/// All types are independent of both protobuf schema and MoonBit syntax.
//
// 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.
//
///|
/// Query cardinality — determines generated function signature shape.
pub enum QueryCardinality {
OneRow
ManyRows
ExecResult
ExecCount
}
///|
impl Show for QueryCardinality with fn to_string(self) -> String {
match self {
OneRow => "OneRow"
ManyRows => "ManyRows"
ExecResult => "ExecResult"
ExecCount => "ExecCount"
}
}
///|
/// IR type — resolved or unresolved type reference.
pub enum InternalType {
/// Named type (e.g. "int4", "text", "bool") — resolved by Type Mapping Layer
Named(String)
/// Array of element type
Arr(InternalType)
}
///|
impl Show for InternalType with fn to_string(self) -> String {
match self {
Named(s) => "Named(\"" + s + "\")"
Arr(elem) => "Arr(" + elem.to_string() + ")"
}
}
///|
/// IR field — a named field in a query result or table struct.
pub struct InternalField {
name : String
ty : InternalType
not_null : Bool
comment : String
rename_to : String?
}
///|
/// IR parameter — a query parameter with position and type.
pub struct InternalParameter {
number : Int
name : String
ty : InternalType
not_null : Bool
}
///|
/// IR result shape — describes the structure of query results.
pub enum InternalResultShape {
/// Named columns (for :one/:many)
Rows(Array[InternalField])
/// No result rows (for :exec/:execrows)
None
}
///|
/// IR query — complete codegen-ready query representation.
pub struct InternalQuery {
name : String
text : String
cmd : QueryCardinality
raw_cmd : QueryCmd
params : Array[InternalParameter]
result_shape : InternalResultShape
comments : Array[String]
filename : String
}
// === Converters: adapter types → IR ===
///|
fn cmd_to_cardinality(cmd : QueryCmd) -> QueryCardinality {
match cmd {
One => OneRow
Many => ManyRows
Exec => ExecResult
ExecRows => ExecCount
CopyFrom => ExecResult
Batch => ExecResult
ExecLastId => ExecResult
}
}
///|
/// Look up a rename mapping by original name.
/// Returns Some(new_name) if a mapping exists, None otherwise.
fn lookup_rename(name : String, renames : Array[(String, String)]) -> String? {
for r in renames {
if r.0 == name {
return Some(r.1)
}
}
None
}
///|
fn adapt_column_to_field(
col : AdaptColumn,
renames : Array[(String, String)],
) -> InternalField {
let rename_to = lookup_rename(col.name, renames)
{
name: col.name,
ty: if col.is_array {
Arr(Named(col.ty.name))
} else {
Named(col.ty.name)
},
not_null: col.not_null,
comment: col.comment,
rename_to,
}
}
///|
fn adapt_parameter_to_ir(p : AdaptParameter) -> InternalParameter {
{
number: p.number,
name: p.column.name,
ty: if p.column.is_array {
Arr(Named(p.column.ty.name))
} else {
Named(p.column.ty.name)
},
not_null: p.column.not_null,
}
}
///|
fn build_result_shape(
cmd : QueryCardinality,
fields : Array[InternalField],
) -> InternalResultShape {
match cmd {
// PostgreSQL RETURNING *: exec-like commands may have result columns
ExecResult | ExecCount =>
if fields.length() > 0 {
Rows(fields)
} else {
None
}
OneRow | ManyRows => Rows(fields)
}
}
///|
fn build_query(
q : AdaptQuery,
renames : Array[(String, String)],
) -> InternalQuery {
let card = cmd_to_cardinality(q.cmd)
let fields = q.columns.map(fn(col) { adapt_column_to_field(col, renames) })
{
name: q.name,
text: q.text,
cmd: card,
raw_cmd: q.cmd,
params: q.params.map(adapt_parameter_to_ir),
result_shape: build_result_shape(card, fields),
comments: q.comments,
filename: q.filename,
}
}
///|
/// Convert an adapted request into IR queries.
pub fn build_ir(
req : AdaptRequest,
renames : Array[(String, String)],
) -> Array[InternalQuery] {
req.queries.map(fn(q) { build_query(q, renames) })
}
// === Tests ===
///|
test "cmd_to_cardinality: One → OneRow" {
let c = cmd_to_cardinality(One)
inspect(c, content="OneRow")
}
///|
test "cmd_to_cardinality: Many → ManyRows" {
let c = cmd_to_cardinality(Many)
inspect(c, content="ManyRows")
}
///|
test "cmd_to_cardinality: Exec → ExecResult" {
let c = cmd_to_cardinality(Exec)
inspect(c, content="ExecResult")
}
///|
test "cmd_to_cardinality: ExecRows → ExecCount" {
let c = cmd_to_cardinality(ExecRows)
inspect(c, content="ExecCount")
}
///|
test "cmd_to_cardinality: CopyFrom → ExecResult" {
let c = cmd_to_cardinality(CopyFrom)
inspect(c, content="ExecResult")
}
///|
test "cmd_to_cardinality: Batch → ExecResult" {
let c = cmd_to_cardinality(Batch)
inspect(c, content="ExecResult")
}
///|
test "cmd_to_cardinality: ExecLastId → ExecResult" {
let c = cmd_to_cardinality(ExecLastId)
inspect(c, content="ExecResult")
}
///|
test "build_ir: empty queries" {
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir.length(), content="0")
}
///|
test "build_ir: :one query produces Rows result shape" {
let col : AdaptColumn = {
name: "id",
not_null: true,
is_array: false,
ty: { catalog: "", schema: "", name: "int4" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "users" },
}
let q : AdaptQuery = {
text: "SELECT id FROM users WHERE id = $1",
name: "get_user",
cmd: One,
columns: [col],
params: [],
comments: [],
filename: "query.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir.length(), content="1")
inspect(ir[0].name, content="get_user")
inspect(ir[0].cmd, content="OneRow")
inspect(ir[0].params.length(), content="0")
// result_shape should be Rows with 1 field
let has_rows = match ir[0].result_shape {
Rows(_) => true
None => false
}
inspect(has_rows, content="true")
let fields = match ir[0].result_shape {
Rows(f) => f
None => []
}
inspect(fields.length(), content="1")
inspect(fields[0].name, content="id")
inspect(fields[0].ty, content="Named(\"int4\")")
inspect(fields[0].not_null, content="true")
}
///|
test "build_ir: :many query with multiple columns" {
let col1 : AdaptColumn = {
name: "id",
not_null: true,
is_array: false,
ty: { catalog: "", schema: "", name: "int4" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "users" },
}
let col2 : AdaptColumn = {
name: "email",
not_null: false,
is_array: false,
ty: { catalog: "", schema: "", name: "text" },
length: 255,
comment: "user email",
table: { catalog: "", schema: "public", name: "users" },
}
let q : AdaptQuery = {
text: "SELECT id, email FROM users",
name: "list_users",
cmd: Many,
columns: [col1, col2],
params: [],
comments: ["list all users"],
filename: "query.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ManyRows")
inspect(ir[0].comments.length(), content="1")
inspect(ir[0].comments[0], content="list all users")
let fields = match ir[0].result_shape {
Rows(f) => f
None => []
}
inspect(fields.length(), content="2")
inspect(fields[0].name, content="id")
inspect(fields[1].name, content="email")
inspect(fields[1].not_null, content="false")
inspect(fields[1].ty, content="Named(\"text\")")
inspect(fields[1].comment, content="user email")
}
///|
test "build_ir: :exec produces None result shape" {
let q : AdaptQuery = {
text: "DELETE FROM users WHERE id = $1",
name: "delete_user",
cmd: Exec,
columns: [],
params: [],
comments: [],
filename: "query.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecResult")
let is_none = match ir[0].result_shape {
None => true
Rows(_) => false
}
inspect(is_none, content="true")
}
///|
test "build_ir: :execrows produces None result shape" {
let q : AdaptQuery = {
text: "UPDATE users SET name = $1 WHERE id = $2",
name: "update_user",
cmd: ExecRows,
columns: [],
params: [],
comments: [],
filename: "query.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecCount")
let is_none = match ir[0].result_shape {
None => true
Rows(_) => false
}
inspect(is_none, content="true")
}
///|
test "build_ir: query with parameters" {
let param_col : AdaptColumn = {
name: "min_age",
not_null: true,
is_array: false,
ty: { catalog: "", schema: "", name: "int4" },
length: 0,
comment: "",
table: { catalog: "", schema: "", name: "" },
}
let param : AdaptParameter = { number: 1, column: param_col }
let q : AdaptQuery = {
text: "SELECT * FROM users WHERE age > $1",
name: "older_than",
cmd: Many,
columns: [],
params: [param],
comments: [],
filename: "query.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].params.length(), content="1")
inspect(ir[0].params[0].number, content="1")
inspect(ir[0].params[0].name, content="min_age")
inspect(ir[0].params[0].ty, content="Named(\"int4\")")
inspect(ir[0].params[0].not_null, content="true")
}
///|
test "InternalType::Array construction" {
let elem : InternalType = Named("int4")
let arr : InternalType = Arr(elem)
inspect(arr, content="Arr(Named(\"int4\"))")
}
///|
test "build_ir: multiple queries" {
let q1 : AdaptQuery = {
text: "SELECT * FROM users",
name: "q1",
cmd: Many,
columns: [],
params: [],
comments: [],
filename: "a.sql",
}
let q2 : AdaptQuery = {
text: "SELECT * FROM posts",
name: "q2",
cmd: One,
columns: [],
params: [],
comments: [],
filename: "b.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q1, q2],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir.length(), content="2")
inspect(ir[0].name, content="q1")
inspect(ir[1].name, content="q2")
}
///|
test "adapt_column_to_field: is_array wraps type in Arr" {
let col : AdaptColumn = {
name: "tags",
not_null: true,
is_array: true,
ty: { catalog: "", schema: "", name: "text" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "posts" },
}
let f = adapt_column_to_field(col, [])
inspect(f.name, content="tags")
inspect(f.ty, content="Arr(Named(\"text\"))")
inspect(f.not_null, content="true")
}
///|
test "adapt_column_to_field: is_array false does not wrap" {
let col : AdaptColumn = {
name: "id",
not_null: true,
is_array: false,
ty: { catalog: "", schema: "", name: "int4" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "users" },
}
let f = adapt_column_to_field(col, [])
inspect(f.ty, content="Named(\"int4\")")
}
///|
test "build_ir: :many with array column preserves Arr type" {
let col : AdaptColumn = {
name: "tags",
not_null: true,
is_array: true,
ty: { catalog: "", schema: "", name: "text" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "posts" },
}
let q : AdaptQuery = {
text: "SELECT tags FROM posts",
name: "list_tags",
cmd: Many,
columns: [col],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
let fields = match ir[0].result_shape {
Rows(f) => f
None => []
}
inspect(fields.length(), content="1")
inspect(fields[0].ty, content="Arr(Named(\"text\"))")
}
///|
test "build_ir: :exec with RETURNING * produces Rows when columns non-empty" {
let col : AdaptColumn = {
name: "id",
not_null: true,
is_array: false,
ty: { catalog: "", schema: "", name: "int4" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "users" },
}
let q : AdaptQuery = {
text: "INSERT INTO users DEFAULT VALUES RETURNING id",
name: "create_user",
cmd: Exec,
columns: [col],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecResult")
let has_rows = match ir[0].result_shape {
Rows(_) => true
None => false
}
inspect(has_rows, content="true")
let fields = match ir[0].result_shape {
Rows(f) => f
None => []
}
inspect(fields.length(), content="1")
inspect(fields[0].name, content="id")
}
///|
test "build_ir: :execrows with RETURNING * produces Rows when columns non-empty" {
let col : AdaptColumn = {
name: "new_name",
not_null: false,
is_array: false,
ty: { catalog: "", schema: "", name: "text" },
length: 0,
comment: "",
table: { catalog: "", schema: "public", name: "users" },
}
let q : AdaptQuery = {
text: "UPDATE users SET name = $1 WHERE id = $2 RETURNING name",
name: "update_user",
cmd: ExecRows,
columns: [col],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecCount")
let has_rows = match ir[0].result_shape {
Rows(_) => true
None => false
}
inspect(has_rows, content="true")
}
///|
test "build_ir: :copyfrom produces ExecResult with None result shape" {
let q : AdaptQuery = {
text: "COPY users FROM stdin",
name: "load_users",
cmd: CopyFrom,
columns: [],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecResult")
let is_none = match ir[0].result_shape {
None => true
Rows(_) => false
}
inspect(is_none, content="true")
}
///|
test "build_ir: :batch produces ExecResult with None result shape" {
let q : AdaptQuery = {
text: "INSERT INTO t VALUES (1), (2)",
name: "batch_insert",
cmd: Batch,
columns: [],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecResult")
let is_none = match ir[0].result_shape {
None => true
Rows(_) => false
}
inspect(is_none, content="true")
}
///|
test "build_ir: :execlastid produces ExecResult with None result shape" {
let q : AdaptQuery = {
text: "INSERT INTO users (name) VALUES ($1)",
name: "create_user",
cmd: ExecLastId,
columns: [],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
inspect(ir[0].cmd, content="ExecResult")
let is_none = match ir[0].result_shape {
None => true
Rows(_) => false
}
inspect(is_none, content="true")
}
///|
test "build_ir: :exec with empty columns still produces None" {
let q : AdaptQuery = {
text: "DELETE FROM users WHERE id = $1",
name: "del",
cmd: Exec,
columns: [],
params: [],
comments: [],
filename: "q.sql",
}
let req : AdaptRequest = {
settings: { version: "", engine: "" },
catalog: { name: "", default_schema: "", schemas: [] },
queries: [q],
sqlc_version: "",
}
let ir = build_ir(req, [])
let is_none = match ir[0].result_shape {
None => true
Rows(_) => false
}
inspect(is_none, content="true")
}
///|
test "adapt_parameter_to_ir: is_array wraps type in Arr" {
let col : AdaptColumn = {
name: "ids",
not_null: true,
is_array: true,
ty: { catalog: "", schema: "", name: "int4" },
length: 0,
comment: "",
table: { catalog: "", schema: "", name: "" },
}
let p : AdaptParameter = { number: 1, column: col }
let param = adapt_parameter_to_ir(p)
inspect(param.ty, content="Arr(Named(\"int4\"))")
}