// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// The placeholder the `n`th bound value takes, from 1.
fn placeholder(dialect : String, n : Int32) -> String {
if dialect == "postgres" {
"$\{n}"
} else {
"?"
}
}
///|
fn is_name(text : String) -> Bool {
if text.is_empty() {
return false
}
for i, c in text.to_array() {
let letter = c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
if !letter && !(i > 0 && c >= '0' && c <= '9') {
return false
}
}
true
}
///|
/// A statement with every `#{path}` turned into a bound value: the SQL for
/// `dialect`, and the paths in the order they bind. A quoted string is left as it
/// is, so `'#{x}'` is text. There is no `${x}`: nothing is ever spliced into the
/// statement, and a placeholder written by hand would not line up with the values.
fn bind_values(
text : String,
dialect : String,
names : Array[String],
where_ : String,
) -> (String, Array[String]) raise {
let chars = text.to_array()
let out = StringBuilder()
let paths = []
let mut quote : Char? = None
let mut i = 0
while i < chars.length() {
let c = chars[i]
if quote is Some(q) {
out.write_char(c)
if c == q {
quote = None
}
i = i + 1
continue
}
let next = if i + 1 < chars.length() { Some(chars[i + 1]) } else { None }
match (c, next) {
('\'' | '"' | '`', _) => {
quote = Some(c)
out.write_char(c)
i = i + 1
}
('#', Some('{')) => {
let mut end = i + 2
while end < chars.length() && chars[end] != '}' {
end = end + 1
}
if end == chars.length() {
fail("\{where_}: `#{` is never closed")
}
let path = String::from_array(chars[i + 2:end].to_owned()).trim(
chars=" ",
)
let parts = path.split(".").map(s => s.to_owned()).collect()
if !parts.iter().all(is_name) {
fail("\{where_}: `#{\{path}}` is not a parameter")
}
if !names.contains(parts[0]) {
fail("\{where_}: `#{\{path}}` names no parameter of this function")
}
paths.push(path.to_owned())
out.write_string(placeholder(dialect, paths.length()))
i = end + 1
}
('$', Some('{')) =>
fail("\{where_}: there is no `${...}`: a value is bound with `#{...}`")
('?', _) | ('$', Some('0'..<='9')) =>
fail(
"\{where_}: write a bound value as `#{name}`, not as a placeholder",
)
_ => {
out.write_char(c)
i = i + 1
}
}
}
(out.to_string(), paths)
}
///|
/// The names a `SELECT` gives its columns, `None` when they cannot be told from
/// the text: `*`, or no `FROM` at the top level.
fn selected(text : String) -> Array[String]? {
let lower = text.to_lower()
guard lower.trim(chars=" \n").has_prefix("select") else { return None }
let chars = lower.to_array()
let original = text.to_array()
let start = lower.find("select").unwrap() + "select".length()
let items : Array[String] = []
let mut depth = 0
let mut from = start
let mut i = start
let word_at = (at : Int32) => {
at + 6 <= chars.length() &&
chars[at:at + 6].iter().collect() == [' ', 'f', 'r', 'o', 'm', ' ']
}
let take = (a : Int32, b : Int32) => {
items.push(String::from_array(original[a:b].to_owned()))
}
while i < chars.length() {
match chars[i] {
'(' => depth = depth + 1
')' => depth = depth - 1
',' if depth == 0 => {
take(from, i)
from = i + 1
}
_ =>
if depth == 0 && word_at(i) {
take(from, i)
return items.map(column_name) |> all_named
}
}
i = i + 1
}
None
}
///|
fn all_named(names : Array[String?]) -> Array[String]? {
let out = []
for name in names {
guard name is Some(n) else { return None }
out.push(n)
}
Some(out)
}
///|
/// What one item of a select list is called: its alias, or its last word.
fn column_name(item : String) -> String? {
let words = item.trim(chars=" \n").split(" ").map(s => s.to_owned()).collect()
let last = words.filter(w => !w.is_empty()).last().unwrap_or("")
let name = last
.rev_find(".")
.map(at => last[at + 1:].to_owned())
.unwrap_or(last)
if is_name(name) {
Some(name)
} else {
None
}
}
///|
/// The type inside `head[...]`.
fn inside(ty : String, head : String) -> String? {
if ty.has_prefix(head) && ty.has_suffix("]") {
Some(
ty
.exact_view(start=head.length(), end=ty.length() - 1)
.trim(chars=" ")
.to_owned(),
)
} else {
None
}
}
///|
/// The type inside `T?`.
fn optional_of(ty : String) -> String? {
if ty.has_suffix("?") {
Some(ty.exact_view(end=ty.length() - 1).trim(chars=" ").to_owned())
} else {
inside(ty, "Option[")
}
}
///|
/// Whether a value of `ty` reads straight from one column.
fn is_scalar(ty : String) -> Bool {
ty
is ("Bool"
| "Int"
| "Int32"
| "Int64"
| "UInt"
| "UInt64"
| "Double"
| "Float"
| "String"
| "Bytes")
}