1use super::render::{truncate_vector_literals, value_to_literal};
4pub use super::scan::{
5 ident_at, is_ident_continue, is_ident_start, is_placeholder_start, skip_protected,
6};
7
8use crate::ast::Value;
9use crate::error::{QqlError, Span};
10use alloc::format;
11use alloc::string::String;
12
13pub fn bind_named<F>(source: &str, lookup: F) -> Result<String, QqlError>
17where
18 F: Fn(&str) -> Option<Value>,
19{
20 let bytes = source.as_bytes();
21 let len = bytes.len();
22 let mut out = String::with_capacity(source.len());
23 let mut i = 0;
24 let mut run = 0;
25
26 while i < len {
27 if let Some(next) = skip_protected(bytes, i) {
28 i = next;
29 continue;
30 }
31
32 let ch = bytes[i];
33 if ch == b'?' {
34 return Err(QqlError::validation(
35 "QQL-BIND-MIXED-STYLE",
36 "query contains positional placeholder '?' — use bind_positional / execute_with_positional_params instead of named parameter binding",
37 Some(Span {
38 start: i,
39 end: i + 1,
40 }),
41 ));
42 }
43
44 if ch == b':' && i + 1 < len && is_placeholder_start(bytes, i) {
45 let next = bytes[i + 1];
46 if next != b':' && is_ident_start(next) {
47 out.push_str(&source[run..i]);
48 let colon_pos = i;
49 i += 1;
50 let name_start = i;
51 while i < len {
52 if is_ident_continue(bytes[i]) {
53 i += 1;
54 } else if bytes[i] == b'.' && i + 1 < len && is_ident_start(bytes[i + 1]) {
55 i += 2;
56 } else {
57 break;
58 }
59 }
60 let name = ident_at(source, name_start, i);
61 let span = Some(Span {
62 start: colon_pos,
63 end: i,
64 });
65 if let Some(val) = lookup(name) {
66 if matches!(val, Value::Null) {
67 return Err(QqlError::validation(
68 "QQL-BIND-NULL-PARAM",
69 format!(
70 "parameter ':{name}' is null; QQL cannot bind null — pass a concrete value or remove the placeholder"
71 ),
72 span,
73 ));
74 }
75 out.push_str(&value_to_literal(&val)?);
76 } else {
77 return Err(QqlError::validation(
78 "QQL-BIND-MISSING-PARAM",
79 format!("missing value for named parameter ':{}'", name),
80 span,
81 ));
82 }
83 run = i;
84 continue;
85 }
86 }
87
88 i += 1;
89 }
90
91 out.push_str(&source[run..len]);
92 Ok(out)
93}
94
95pub fn bind_positional(source: &str, params: &[Value]) -> Result<String, QqlError> {
97 let bytes = source.as_bytes();
98 let len = bytes.len();
99 let mut out = String::with_capacity(source.len());
100 let mut i = 0;
101 let mut run = 0;
102 let mut param_index = 0;
103
104 while i < len {
105 if let Some(next) = skip_protected(bytes, i) {
106 i = next;
107 continue;
108 }
109
110 let ch = bytes[i];
111 if ch == b':' && i + 1 < len && is_placeholder_start(bytes, i) {
112 let next = bytes[i + 1];
113 if next != b':' && is_ident_start(next) {
114 return Err(QqlError::validation(
115 "QQL-BIND-MIXED-STYLE",
116 "query contains named placeholder ':name' — use bind_named / execute_with_params instead of positional parameter binding",
117 Some(Span {
118 start: i,
119 end: i + 2,
120 }),
121 ));
122 }
123 }
124
125 if ch == b'?' {
126 let q_span = Some(Span {
127 start: i,
128 end: i + 1,
129 });
130 if param_index < params.len() {
131 if matches!(params[param_index], Value::Null) {
132 return Err(QqlError::validation(
133 "QQL-BIND-NULL-PARAM",
134 format!(
135 "positional parameter ?{} is null; QQL cannot bind null — pass a concrete value or remove the placeholder",
136 param_index + 1
137 ),
138 q_span,
139 ));
140 }
141 out.push_str(&source[run..i]);
142 out.push_str(&value_to_literal(¶ms[param_index])?);
143 param_index += 1;
144 i += 1;
145 run = i;
146 continue;
147 }
148 return Err(QqlError::validation(
149 "QQL-BIND-MISSING-PARAM",
150 format!(
151 "missing value for positional parameter ?{} (only {} parameter(s) provided)",
152 param_index + 1,
153 params.len()
154 ),
155 q_span,
156 ));
157 }
158
159 i += 1;
160 }
161
162 out.push_str(&source[run..len]);
163
164 if param_index < params.len() {
165 let msg = if param_index == 0 {
166 format!(
167 "no '?' placeholders found in query, but {} positional parameters were supplied",
168 params.len()
169 )
170 } else {
171 format!(
172 "too many positional parameters provided: bound {}, but {} were supplied",
173 param_index,
174 params.len()
175 )
176 };
177 return Err(QqlError::validation("QQL-BIND-UNUSED-PARAMS", msg, None));
178 }
179
180 Ok(out)
181}
182
183pub fn bind_named_readable<F>(source: &str, lookup: F, max_dims: usize) -> Result<String, QqlError>
185where
186 F: Fn(&str) -> Option<Value>,
187{
188 let bound = bind_named(source, lookup)?;
189 Ok(truncate_vector_literals(&bound, max_dims))
190}
191
192pub fn bind_positional_readable(
194 source: &str,
195 params: &[Value],
196 max_dims: usize,
197) -> Result<String, QqlError> {
198 let bound = bind_positional(source, params)?;
199 Ok(truncate_vector_literals(&bound, max_dims))
200}