// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Rewrite moondb's dialect-neutral `?` positional placeholders into
/// PostgreSQL's numbered `$1`, `$2`, … form. moondb fixes the *calling
/// convention* (an ordered params array) but leaves the placeholder spelling to
/// the driver; PostgreSQL's extended-query protocol requires `$n`, so a query
/// layer that emits `?` for portability is translated here before Parse.
///
/// A `?` is only a placeholder in SQL text — never inside a single-quoted string
/// literal, a dollar-quoted string, a `"`-quoted identifier, or a `--` / `/* */`
/// comment. Those spans are scanned through verbatim so a literal `?` in data or
/// a comment is left untouched and does not shift the parameter numbering. A
/// literal `?` an application genuinely needs in output can be written `??`,
/// which collapses to a single `?` (mirroring JDBC-style escaping).
pub fn translate_placeholders(sql : String) -> String {
let sb = StringBuilder::new()
let n = sql.length()
let mut i = 0
let mut param = 0
while i < n {
let c = sql[i]
if c == '\'' {
// single-quoted string literal: '' is an escaped quote
sb.write_char('\'')
i += 1
while i < n {
let d = sql[i]
sb.write_char(d.unsafe_to_char())
if d == '\'' {
if i + 1 < n && sql[i + 1] == '\'' {
sb.write_char('\'')
i += 2
continue
}
i += 1
break
}
i += 1
}
} else if c == '"' {
// double-quoted identifier: "" is an escaped quote
sb.write_char('"')
i += 1
while i < n {
let d = sql[i]
sb.write_char(d.unsafe_to_char())
if d == '"' {
if i + 1 < n && sql[i + 1] == '"' {
sb.write_char('"')
i += 2
continue
}
i += 1
break
}
i += 1
}
} else if c == '$' && i + 1 < n && is_dollar_tag_char(sql[i + 1], true) {
// dollar-quoted string: $tag$ ... $tag$
let tag_end = dollar_tag_end(sql, i)
if tag_end < 0 {
sb.write_char('$')
i += 1
} else {
let tag = sql[i:tag_end + 1].to_owned()
push_str(sb, tag)
i = tag_end + 1
// scan body until the matching closing tag
let close = find_substring(sql, tag, i)
if close < 0 {
while i < n {
sb.write_char(sql[i].unsafe_to_char())
i += 1
}
} else {
let body = sql[i:close + tag.length()].to_owned()
push_str(sb, body)
i = close + tag.length()
}
}
} else if c == '-' && i + 1 < n && sql[i + 1] == '-' {
// line comment to end of line
while i < n && sql[i] != '\n' {
sb.write_char(sql[i].unsafe_to_char())
i += 1
}
} else if c == '/' && i + 1 < n && sql[i + 1] == '*' {
// block comment (PostgreSQL nests them)
sb.write_char('/')
sb.write_char('*')
i += 2
let mut depth = 1
while i < n && depth > 0 {
if i + 1 < n && sql[i] == '/' && sql[i + 1] == '*' {
depth += 1
sb.write_char('/')
sb.write_char('*')
i += 2
} else if i + 1 < n && sql[i] == '*' && sql[i + 1] == '/' {
depth -= 1
sb.write_char('*')
sb.write_char('/')
i += 2
} else {
sb.write_char(sql[i].unsafe_to_char())
i += 1
}
}
} else if c == '?' {
if i + 1 < n && sql[i + 1] == '?' {
sb.write_char('?')
i += 2
} else {
param += 1
sb.write_char('$')
push_str(sb, param.to_string())
i += 1
}
} else {
sb.write_char(c.unsafe_to_char())
i += 1
}
}
sb.to_string()
}
///|
/// The number of `?` placeholders [`translate_placeholders`] would consume — the
/// count of parameters a statement expects. Shares the same scanner discipline
/// so `??` escapes and quoted/comment spans do not count.
pub fn count_placeholders(sql : String) -> Int {
let translated = translate_placeholders(sql)
// The translation is 1:1 on placeholder count; recount `$n` we emitted by
// re-running the scan is wasteful, so instead count during a light re-scan.
// Simpler and correct: highest `$n` emitted equals the parameter count.
let n = translated.length()
let mut i = 0
let mut max = 0
while i < n {
let c = translated[i]
if c == '$' &&
i + 1 < n &&
translated[i + 1] >= '0' &&
translated[i + 1] <= '9' {
i += 1
let mut v = 0
while i < n && translated[i] >= '0' && translated[i] <= '9' {
v = v * 10 + (translated[i].to_int() - 48)
i += 1
}
if v > max {
max = v
}
} else {
i += 1
}
}
max
}
///|
fn push_str(sb : StringBuilder, s : String) -> Unit {
for i in 0.. Bool {
let ci = c.to_int()
if ci == 36 {
return true // '$' closes the tag
}
let is_alpha = (ci >= 65 && ci <= 90) || (ci >= 97 && ci <= 122) || ci == 95
if first {
is_alpha
} else {
is_alpha || (ci >= 48 && ci <= 57)
}
}
///|
/// Index of the `$` that closes a dollar-quote tag beginning at `start`, or `-1`
/// if the run from `start` is not a valid opening tag.
fn dollar_tag_end(sql : String, start : Int) -> Int {
let n = sql.length()
let mut i = start + 1
while i < n {
let c = sql[i]
if c == '$' {
return i
}
if !is_dollar_tag_char(c, false) {
return -1
}
i += 1
}
-1
}
///|
/// First index ≥ `from` at which `needle` occurs in `hay`, or `-1`.
fn find_substring(hay : String, needle : String, from : Int) -> Int {
let hn = hay.length()
let nn = needle.length()
if nn == 0 {
return from
}
let mut i = from
while i + nn <= hn {
let mut j = 0
while j < nn && hay[i + j] == needle[j] {
j += 1
}
if j == nn {
return i
}
i += 1
}
-1
}