(chars: &[char], pos: &mut usize)
| 235 | } |
| 236 | |
| 237 | fn parse_object(chars: &[char], pos: &mut usize) -> Result<HashMap<String, Value>, SqlError> { |
| 238 | // Expect '{' |
| 239 | if *pos >= chars.len() || chars[*pos] != '{' { |
| 240 | return Err(SqlError::Parse { |
| 241 | detail: format!( |
| 242 | "expected '{{' at position {pos}, found {:?}", |
| 243 | chars.get(*pos) |
| 244 | ), |
| 245 | }); |
| 246 | } |
| 247 | *pos += 1; // consume '{' |
| 248 | let mut map = HashMap::new(); |
| 249 | loop { |
| 250 | skip_ws(chars, pos); |
| 251 | if *pos >= chars.len() { |
| 252 | return Err(SqlError::Parse { |
| 253 | detail: "unterminated object literal".to_string(), |
| 254 | }); |
| 255 | } |
| 256 | if chars[*pos] == '}' { |
| 257 | *pos += 1; // consume '}' |
| 258 | break; |
| 259 | } |
| 260 | // Trailing comma: skip and re-check for '}' |
| 261 | if chars[*pos] == ',' { |
| 262 | *pos += 1; |
| 263 | continue; |
| 264 | } |
| 265 | |
| 266 | // Parse key (identifier or JSON-style quoted key). |
| 267 | skip_ws(chars, pos); |
| 268 | if *pos >= chars.len() { |
| 269 | return Err(SqlError::Parse { |
| 270 | detail: "expected key, reached end of input".to_string(), |
| 271 | }); |
| 272 | } |
| 273 | let key = if chars[*pos] == '"' { |
| 274 | parse_double_quoted_string(chars, pos)? |
| 275 | } else { |
| 276 | let first = chars[*pos]; |
| 277 | if !(first.is_ascii_alphabetic() || first == '_') { |
| 278 | return Err(SqlError::Parse { |
| 279 | detail: format!("expected identifier key at position {pos}, found '{first}'"), |
| 280 | }); |
| 281 | } |
| 282 | parse_ident(chars, pos) |
| 283 | }; |
| 284 | if key.is_empty() { |
| 285 | return Err(SqlError::Parse { |
| 286 | detail: format!("expected non-empty key at position {pos}"), |
| 287 | }); |
| 288 | } |
| 289 | |
| 290 | // Expect ':' |
| 291 | skip_ws(chars, pos); |
| 292 | if *pos >= chars.len() || chars[*pos] != ':' { |
| 293 | return Err(SqlError::Parse { |
| 294 | detail: format!( |
no test coverage detected