Skip to main content

firebird_wire/
statement.rs

1//! Instruções preparadas (statements): alocar, preparar, executar e buscar linhas.
2//!
3//! Ciclo de vida (espelha `isc_dsql_*` / fbclient):
4//!
5//! 1. [`Connection::prepare`] envia `op_allocate_statement`, lê o handle do
6//!    servidor, depois `op_prepare_statement` e faz o parsing da resposta de
7//!    info de descrição (describe-info) em metadados de parâmetros de entrada e
8//!    colunas de saída.
9//! 2. [`Statement::execute`] envia `op_execute` (com uma mensagem de entrada
10//!    quando a instrução tem parâmetros). Para um `SELECT` isto abre um cursor.
11//! 3. [`Statement::fetch`] puxa uma linha por vez via `op_fetch` /
12//!    `op_fetch_response` até o cursor se esgotar.
13//! 4. [`Statement::close`] / [`Statement::drop_statement`] liberam o estado no
14//!    servidor.
15//!
16//! Assim como [`Transaction`], um `Statement` é
17//! um handle cujos métodos de I/O emprestam a [`Connection`] dona, então apenas
18//! um empréstimo mutável fica ativo por vez.
19
20use crate::blr::{input_blr, message_blr, prepare_info_items};
21use crate::connection::Connection;
22use crate::error::{Error, Result};
23use crate::message::{decode_row, encode_row};
24use crate::transaction::Transaction;
25use crate::value::{ColumnMeta, Value};
26use crate::wire::consts::*;
27use crate::wire::response::{read_op, read_response, read_response_body};
28use crate::wire::stream::{op_name, op_packet};
29use crate::wire::xdr::{read_le_int, read_le_int_signed};
30
31/// Dialeto SQL enviado com `op_prepare_statement`. O dialeto 3 é o padrão
32/// moderno e o alvo deste driver.
33const SQL_DIALECT: i32 = 3;
34
35/// Tamanho do buffer de resposta de info de descrição (describe-info) solicitado
36/// ao servidor. Generoso para que uma lista SELECT ampla nunca seja truncada; o
37/// servidor só retorna o que precisa.
38const INFO_BUFFER_LEN: i32 = 0xfb80;
39
40/// Quantas linhas pedir por `op_fetch`. O servidor transmite até esse número de
41/// pacotes `op_fetch_response` e então um terminador; nós os armazenamos em
42/// buffer e os entregamos um a um.
43const FETCH_BATCH: i32 = 200;
44
45/// Tamanho do buffer de resposta para a requisição `isc_info_sql_records`.
46/// Os quatro contadores cabem com folga.
47const RECORDS_BUFFER_LEN: i32 = 64;
48
49/// Número de linhas que a última execução afetou, separado por tipo de operação.
50/// Retornado por [`Statement::rows_affected`].
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub struct RowsAffected {
53    /// Linhas selecionadas por comandos que reportam contagem de seleção.
54    pub selected: u64,
55    /// Linhas inseridas pela última execução.
56    pub inserted: u64,
57    /// Linhas atualizadas pela última execução.
58    pub updated: u64,
59    /// Linhas excluídas pela última execução.
60    pub deleted: u64,
61}
62
63impl RowsAffected {
64    /// Total de linhas modificadas (inseridas + atualizadas + excluídas) — o
65    /// número que normalmente interessa após um INSERT/UPDATE/DELETE.
66    pub fn total_modified(&self) -> u64 {
67        self.inserted + self.updated + self.deleted
68    }
69}
70
71/// Uma instrução preparada vinculada ao handle de banco de dados de uma [`Connection`].
72#[derive(Debug)]
73pub struct Statement {
74    handle: i32,
75    stmt_type: i32,
76    params: Vec<ColumnMeta>,
77    columns: Vec<ColumnMeta>,
78    /// Verdadeiro após `execute` abrir um cursor SELECT, até ser esgotado/fechado.
79    cursor_open: bool,
80    /// Linhas já recebidas do servidor mas ainda não entregues ao chamador.
81    buffered: std::collections::VecDeque<Vec<Value>>,
82    /// Verdadeiro quando o servidor sinalizou fim de cursor (status 100).
83    exhausted: bool,
84    /// Abrir o cursor como rolável (`op_execute` com `cursor_flags = 1`).
85    /// Definido por [`Statement::set_scrollable`] antes do `execute`.
86    scrollable: bool,
87    /// Quantas linhas pedir por `op_fetch` (prefetch). Ver [`Statement::set_fetch_size`].
88    fetch_size: i32,
89    dropped: bool,
90}
91
92impl Statement {
93    /// O handle da instrução no lado do servidor.
94    pub fn handle(&self) -> i32 {
95        self.handle
96    }
97
98    /// O tipo da instrução (`stmt_type::*`, ex.: `SELECT`, `INSERT`).
99    pub fn stmt_type(&self) -> i32 {
100        self.stmt_type
101    }
102
103    /// Verdadeiro para instruções que produzem um cursor de linhas.
104    pub fn is_select(&self) -> bool {
105        self.stmt_type == stmt_type::SELECT || self.stmt_type == stmt_type::SELECT_FOR_UPD
106    }
107
108    /// Pede que o próximo [`Self::execute`] abra um cursor **rolável**, habilitando
109    /// [`Self::fetch_scroll`] e seus atalhos (`fetch_prior`, `fetch_absolute`, …).
110    /// Deve ser chamado antes do `execute`; o servidor precisa suportar cursores
111    /// roláveis (FB5+ / protocolo ≥ 17 — ver [`Connection::supports_fetch_scroll`]).
112    pub fn set_scrollable(&mut self, yes: bool) {
113        self.scrollable = yes;
114    }
115
116    /// Se este statement foi marcado para abrir um cursor rolável.
117    pub fn is_scrollable(&self) -> bool {
118        self.scrollable
119    }
120
121    /// Define quantas linhas pedir por `op_fetch` (prefetch). Valores maiores
122    /// reduzem o número de idas ao servidor em SELECTs grandes, ao custo de mais
123    /// memória de buffer por lote; valores menores reduzem a latência da primeira
124    /// linha. O padrão é 200. `n` é fixado em pelo menos 1.
125    pub fn set_fetch_size(&mut self, n: i32) {
126        self.fetch_size = n.max(1);
127    }
128
129    /// O tamanho de prefetch atual (ver [`Self::set_fetch_size`]).
130    pub fn fetch_size(&self) -> i32 {
131        self.fetch_size
132    }
133
134    /// Metadados das colunas de saída (vazio para instruções que não são SELECT).
135    pub fn columns(&self) -> &[ColumnMeta] {
136        &self.columns
137    }
138
139    /// Metadados dos parâmetros de entrada.
140    pub fn params(&self) -> &[ColumnMeta] {
141        &self.params
142    }
143
144    /// Executa a instrução. Para um `SELECT` isto abre um cursor; prossiga com
145    /// [`Self::fetch`] / [`Self::fetch_all`]. Para DML o servidor responde com um
146    /// `op_response` simples.
147    pub fn execute(
148        &mut self,
149        conn: &mut Connection,
150        tx: &Transaction,
151        params: &[Value],
152    ) -> Result<()> {
153        let has_params = !self.params.is_empty();
154        let in_blr = if has_params {
155            input_blr(&self.params)
156        } else {
157            Vec::new()
158        };
159        let message = if has_params {
160            encode_row(&self.params, params, conn.charset())?
161        } else {
162            Vec::new()
163        };
164
165        let mut w = op_packet(op::EXECUTE);
166        w.put_i32(self.handle);
167        w.put_i32(tx.handle());
168        w.put_bytes(&in_blr); // in_blr
169        w.put_i32(0); // in_message_number
170        w.put_i32(if has_params { 1 } else { 0 }); // in_message_count
171        // A mensagem de parâmetros de entrada vem AQUI, logo após in_message_count
172        // e antes dos campos de saída (confirmado por captura strace do fbclient:
173        // bitmap de nulos + valores XDR, formato compacto). Sem parâmetros, nada.
174        if has_params {
175            w.put_raw(&message);
176            w.align();
177        }
178        // O op_execute da v19 carrega campos de saída no estilo execute2 mesmo
179        // quando as linhas são buscadas separadamente; nós os enviamos vazios.
180        w.put_bytes(&[]); // out_blr
181        // Logo após out_blr vem `cursor_flags` (confirmado por captura strace:
182        // do fbclient: a única palavra que muda entre um openCursor normal e um
183        // rolável é esta — 0 = normal, 1 = CURSOR_TYPE_SCROLLABLE). O op_execute
184        // (ao contrário do op_execute2) não carrega out_message_number aqui.
185        w.put_i32(if self.scrollable {
186            cursor_type::SCROLLABLE
187        } else {
188            0
189        });
190        // Campo final único na v19/FB5 (confirmado por captura strace: o pacote
191        // sem parâmetros tem 9 palavras): o tamanho máximo de blob a embutir
192        // inline na resposta do fetch. Enviamos 0 para DESATIVAR o inline — assim
193        // colunas BLOB chegam sempre como um id de 8 bytes e são lidas pelo
194        // protocolo clássico (op_open_blob2/op_get_segment). O fbclient envia
195        // 0xffff aqui; nós optamos pela simplicidade.
196        if conn.protocol_version() >= 18 {
197            w.put_i32(0); // inline_blob_size (FB5): 0 = sem inline
198        }
199        conn.io().send(&w)?;
200        read_response(conn.io())?;
201
202        self.cursor_open = self.is_select();
203        self.buffered.clear();
204        self.exhausted = false;
205        Ok(())
206    }
207
208    /// Busca a próxima linha, ou `None` no fim do cursor. Retorna `None` para uma
209    /// instrução que não tem cursor aberto (que não é SELECT, ou já esgotada).
210    pub fn fetch(&mut self, conn: &mut Connection) -> Result<Option<Vec<Value>>> {
211        loop {
212            if let Some(row) = self.buffered.pop_front() {
213                return Ok(Some(row));
214            }
215            if self.exhausted || !self.cursor_open {
216                self.cursor_open = false;
217                return Ok(None);
218            }
219            self.fetch_batch(conn)?;
220        }
221    }
222
223    /// Envia um `op_fetch` e drena todos os `op_fetch_response` resultantes para o
224    /// buffer, até o pacote terminador (count 0). Define `exhausted` quando o
225    /// servidor sinaliza fim de cursor (status 100).
226    fn fetch_batch(&mut self, conn: &mut Connection) -> Result<()> {
227        let out_blr = message_blr(&self.columns);
228        let mut w = op_packet(op::FETCH);
229        w.put_i32(self.handle);
230        w.put_bytes(&out_blr);
231        w.put_i32(0); // message number
232        w.put_i32(self.fetch_size);
233        conn.io().send(&w)?;
234
235        loop {
236            let code = read_op(conn.io())?;
237            if code != op::FETCH_RESPONSE {
238                if code == op::RESPONSE {
239                    // Um erro veio como op_response; decodifica-o para a mensagem.
240                    read_response_body(conn.io())?.into_result()?;
241                }
242                return Err(Error::protocol(format!(
243                    "expected op_fetch_response, got {} ({code})",
244                    op_name(code)
245                )));
246            }
247
248            let status = conn.io().read_i32()?; // 0 = linha, 100 = fim do cursor
249            let count = conn.io().read_i32()?; // 0 = sem mensagem neste pacote
250            if count == 0 {
251                // Terminador do lote: status 100 ⇒ cursor esgotado; status 0 ⇒
252                // limite do lote atingido, mas ainda há linhas (busque mais).
253                self.exhausted = status == 100;
254                return Ok(());
255            }
256
257            let cs = conn.charset();
258            let row = decode_row(conn.io(), &self.columns, cs)?;
259            self.buffered.push_back(row);
260            if status == 100 {
261                self.exhausted = true;
262                return Ok(());
263            }
264        }
265    }
266
267    /// Esvazia o cursor para um vetor de linhas.
268    pub fn fetch_all(&mut self, conn: &mut Connection) -> Result<Vec<Vec<Value>>> {
269        let mut rows = Vec::new();
270        while let Some(row) = self.fetch(conn)? {
271            rows.push(row);
272        }
273        Ok(rows)
274    }
275
276    /// Cria um stream síncrono sobre as linhas do cursor (ver [`RowStream`]),
277    /// empacotando a conexão para não precisar repassá-la a cada linha. As linhas
278    /// chegam sob demanda (em lotes de [`Self::fetch_size`]), sem materializar
279    /// tudo na memória como [`Self::fetch_all`].
280    ///
281    /// ```text
282    /// let mut rows = stmt.rows(&mut conn);
283    /// while let Some(row) = rows.try_next()? {
284    ///     println!("{row:?}");
285    /// }
286    /// ```
287    pub fn rows<'a>(&'a mut self, conn: &'a mut Connection) -> RowStream<'a> {
288        RowStream { stmt: self, conn }
289    }
290
291    /// Reposiciona o cursor (rolável) e retorna a única linha naquela posição, ou
292    /// `None` se ela cai fora do conjunto de resultados. Envia `op_fetch_scroll`
293    /// (FB5); o cursor precisa ter sido aberto com [`Self::set_scrollable`].
294    ///
295    /// `direction` é uma das constantes [`scroll`]; `offset` é a posição absoluta
296    /// (1-based) para [`scroll::ABSOLUTE`], o deslocamento com sinal para
297    /// [`scroll::RELATIVE`], e ignorado (use 0) nas demais direções.
298    pub fn fetch_scroll(
299        &mut self,
300        conn: &mut Connection,
301        direction: i32,
302        offset: i32,
303    ) -> Result<Option<Vec<Value>>> {
304        if !self.cursor_open {
305            return Ok(None);
306        }
307        // Um salto invalida qualquer linha pré-buscada pelo fetch sequencial.
308        self.buffered.clear();
309
310        let out_blr = message_blr(&self.columns);
311        let mut w = op_packet(op::FETCH_SCROLL);
312        w.put_i32(self.handle);
313        w.put_bytes(&out_blr);
314        w.put_i32(0); // message number
315        w.put_i32(1); // fetch count: uma linha por salto (como faz o fbclient)
316        w.put_i32(direction);
317        w.put_i32(offset);
318        conn.io().send(&w)?;
319
320        // Drena os op_fetch_response até o terminador (count 0), guardando a (única)
321        // linha. status 100 ⇒ posição fora do cursor (nenhuma linha naquele ponto).
322        let mut row = None;
323        loop {
324            let code = read_op(conn.io())?;
325            if code != op::FETCH_RESPONSE {
326                if code == op::RESPONSE {
327                    read_response_body(conn.io())?.into_result()?;
328                }
329                return Err(Error::protocol(format!(
330                    "expected op_fetch_response, got {} ({code})",
331                    op_name(code)
332                )));
333            }
334            let status = conn.io().read_i32()?;
335            let count = conn.io().read_i32()?;
336            if count == 0 {
337                break;
338            }
339            let cs = conn.charset();
340            let r = decode_row(conn.io(), &self.columns, cs)?;
341            if row.is_none() {
342                row = Some(r);
343            }
344            if status == 100 {
345                break;
346            }
347        }
348        // O cursor continua aberto e reposicionável após qualquer salto.
349        self.exhausted = false;
350        Ok(row)
351    }
352
353    /// Próxima linha (rolável). Equivale a [`Self::fetch`] num cursor rolável.
354    pub fn fetch_next(&mut self, conn: &mut Connection) -> Result<Option<Vec<Value>>> {
355        self.fetch_scroll(conn, scroll::NEXT, 0)
356    }
357
358    /// Linha anterior.
359    pub fn fetch_prior(&mut self, conn: &mut Connection) -> Result<Option<Vec<Value>>> {
360        self.fetch_scroll(conn, scroll::PRIOR, 0)
361    }
362
363    /// Primeira linha do conjunto de resultados.
364    pub fn fetch_first(&mut self, conn: &mut Connection) -> Result<Option<Vec<Value>>> {
365        self.fetch_scroll(conn, scroll::FIRST, 0)
366    }
367
368    /// Última linha do conjunto de resultados.
369    pub fn fetch_last(&mut self, conn: &mut Connection) -> Result<Option<Vec<Value>>> {
370        self.fetch_scroll(conn, scroll::LAST, 0)
371    }
372
373    /// Linha na posição absoluta `pos` (1-based; negativa conta a partir do fim).
374    pub fn fetch_absolute(
375        &mut self,
376        conn: &mut Connection,
377        pos: i32,
378    ) -> Result<Option<Vec<Value>>> {
379        self.fetch_scroll(conn, scroll::ABSOLUTE, pos)
380    }
381
382    /// Linha `offset` posições à frente (positivo) ou atrás (negativo) da atual.
383    pub fn fetch_relative(
384        &mut self,
385        conn: &mut Connection,
386        offset: i32,
387    ) -> Result<Option<Vec<Value>>> {
388        self.fetch_scroll(conn, scroll::RELATIVE, offset)
389    }
390
391    /// Quantas linhas a última execução afetou, via `op_info_sql` com
392    /// `isc_info_sql_records`. Útil após um INSERT/UPDATE/DELETE — use
393    /// [`RowsAffected::total_modified`] para o total.
394    pub fn rows_affected(&self, conn: &mut Connection) -> Result<RowsAffected> {
395        let w = crate::connection::info_request(
396            op::INFO_SQL,
397            self.handle,
398            &[isql::RECORDS],
399            RECORDS_BUFFER_LEN,
400        );
401        conn.io().send(&w)?;
402        let resp = read_response(conn.io())?;
403        Ok(parse_records(&resp.data))
404    }
405
406    /// Fecha o cursor aberto (`op_free_statement` com `DSQL_close`) sem liberar a
407    /// instrução preparada, para que possa ser reexecutada.
408    pub fn close(&mut self, conn: &mut Connection) -> Result<()> {
409        if !self.cursor_open {
410            return Ok(());
411        }
412        self.free(conn, free::CLOSE)?;
413        self.cursor_open = false;
414        Ok(())
415    }
416
417    /// Libera a instrução no servidor (`op_free_statement` com `DSQL_drop`),
418    /// consumindo o handle.
419    pub fn drop_statement(mut self, conn: &mut Connection) -> Result<()> {
420        self.free(conn, free::DROP)?;
421        self.dropped = true;
422        Ok(())
423    }
424
425    fn free(&mut self, conn: &mut Connection, mode: i32) -> Result<()> {
426        let mut w = op_packet(op::FREE_STATEMENT);
427        w.put_i32(self.handle);
428        w.put_i32(mode);
429        conn.io().send(&w)?;
430        read_response(conn.io())?;
431        Ok(())
432    }
433
434    /// Marca o handle como transferido (não será liberado por este `Statement`),
435    /// suprimindo o aviso de [`Drop`]. Usado quando o handle passa a viver em
436    /// outro dono — p.ex. ao virar um [`crate::Batch`] em `create_batch`.
437    pub(crate) fn forget_handle(&mut self) {
438        self.dropped = true;
439    }
440}
441
442impl Drop for Statement {
443    fn drop(&mut self) {
444        if !self.dropped {
445            crate::warn_unclosed("Statement", self.handle);
446        }
447    }
448}
449
450/// Stream síncrono sobre as linhas de um cursor aberto, criado por
451/// [`Statement::rows`]. Entrega uma linha por vez via [`Self::try_next`], buscando
452/// lotes do servidor sob demanda — não materializa o resultado inteiro.
453///
454/// É um *lending iterator* (`try_next(&mut self)`), não um `Iterator` padrão: como
455/// o stream toma `&mut Connection` emprestado, a conexão fica presa ao cursor
456/// enquanto o stream existir. Para a maioria dos usos, o laço
457/// `while let Some(row) = rows.try_next()?` é suficiente; há também
458/// [`Self::try_collect`] e [`Self::try_for_each`].
459pub struct RowStream<'a> {
460    stmt: &'a mut Statement,
461    conn: &'a mut Connection,
462}
463
464impl RowStream<'_> {
465    /// A próxima linha, ou `None` no fim do cursor.
466    pub fn try_next(&mut self) -> Result<Option<Vec<Value>>> {
467        self.stmt.fetch(self.conn)
468    }
469
470    /// A próxima linha, ou `None` no fim do cursor.
471    ///
472    /// Prefera [`Self::try_next`] em código novo; este método fica como alias
473    /// compatível para exemplos/código existentes.
474    #[allow(clippy::should_implement_trait)]
475    pub fn next(&mut self) -> Result<Option<Vec<Value>>> {
476        self.try_next()
477    }
478
479    /// Coleta todas as linhas restantes num vetor (equivalente a
480    /// [`Statement::fetch_all`], mas consumindo o stream).
481    pub fn try_collect(mut self) -> Result<Vec<Vec<Value>>> {
482        let mut rows = Vec::new();
483        while let Some(row) = self.try_next()? {
484            rows.push(row);
485        }
486        Ok(rows)
487    }
488
489    /// Aplica `f` a cada linha restante, parando no primeiro erro.
490    pub fn try_for_each<F>(mut self, mut f: F) -> Result<()>
491    where
492        F: FnMut(Vec<Value>) -> Result<()>,
493    {
494        while let Some(row) = self.try_next()? {
495            f(row)?;
496        }
497        Ok(())
498    }
499}
500
501impl Connection {
502    /// Prepara uma instrução SQL dentro da transação informada.
503    pub fn prepare(&mut self, tx: &Transaction, sql: &str) -> Result<Statement> {
504        // 1. Aloca um handle de instrução.
505        let mut w = op_packet(op::ALLOCATE_STATEMENT);
506        w.put_i32(self.db_handle());
507        self.io().send(&w)?;
508        let handle = read_response(self.io())?.handle;
509
510        // 2. Prepara-a; a requisição de info de descrição (describe-info) segue
511        //    junto e seu resultado volta no campo data do op_response.
512        let mut w = op_packet(op::PREPARE_STATEMENT);
513        w.put_i32(tx.handle());
514        w.put_i32(handle);
515        w.put_i32(SQL_DIALECT);
516        w.put_str(sql);
517        w.put_bytes(prepare_info_items());
518        w.put_i32(INFO_BUFFER_LEN);
519        self.io().send(&w)?;
520        let resp = read_response(self.io())?;
521
522        let info = parse_prepare_response(&resp.data)?;
523        Ok(Statement {
524            handle,
525            stmt_type: info.stmt_type,
526            params: info.params,
527            columns: info.columns,
528            cursor_open: false,
529            buffered: std::collections::VecDeque::new(),
530            exhausted: false,
531            scrollable: false,
532            fetch_size: FETCH_BATCH,
533            dropped: false,
534        })
535    }
536}
537
538/// Info de descrição (describe-info) já parseada de uma resposta `op_prepare_statement`.
539struct PreparedInfo {
540    stmt_type: i32,
541    params: Vec<ColumnMeta>,
542    columns: Vec<ColumnMeta>,
543}
544
545/// Qual bloco de descrição (parâmetros de entrada vs colunas de saída) estamos lendo.
546#[derive(Clone, Copy, PartialEq)]
547enum Block {
548    None,
549    Bind,
550    Select,
551}
552
553/// Percorre o stream de info de descrição (describe-info). Cada item de dados é
554/// `tag(1) + len(2 LE) + value`; os marcadores de bloco
555/// (`isc_info_sql_select/bind/describe_end`) não carregam comprimento.
556fn parse_prepare_response(data: &[u8]) -> Result<PreparedInfo> {
557    let mut stmt_type = 0;
558    let mut params = Vec::new();
559    let mut columns = Vec::new();
560    let mut block = Block::None;
561    let mut cur: Option<ColumnMeta> = None;
562
563    let mut i = 0;
564    while i < data.len() {
565        let tag = data[i];
566        i += 1;
567        match tag {
568            INFO_END => break,
569            INFO_TRUNCATED => {
570                return Err(Error::protocol(
571                    "prepare describe-info truncated; buffer too small",
572                ));
573            }
574            isql::SELECT => block = Block::Select,
575            isql::BIND => block = Block::Bind,
576            isql::DESCRIBE_END => {
577                if let Some(c) = cur.take() {
578                    match block {
579                        Block::Bind => params.push(c),
580                        Block::Select => columns.push(c),
581                        Block::None => {}
582                    }
583                }
584            }
585            _ => {
586                // Item de valor prefixado por comprimento.
587                if i + 2 > data.len() {
588                    return Err(Error::protocol("prepare describe-info: short length"));
589                }
590                let len = u16::from_le_bytes([data[i], data[i + 1]]) as usize;
591                i += 2;
592                if i + len > data.len() {
593                    return Err(Error::protocol("prepare describe-info: short value"));
594                }
595                let val = &data[i..i + len];
596                i += len;
597                apply_info_item(tag, val, &mut stmt_type, &mut cur);
598            }
599        }
600    }
601
602    Ok(PreparedInfo {
603        stmt_type,
604        params,
605        columns,
606    })
607}
608
609fn apply_info_item(tag: u8, val: &[u8], stmt_type: &mut i32, cur: &mut Option<ColumnMeta>) {
610    match tag {
611        isql::STMT_TYPE => *stmt_type = read_le_int(val) as i32,
612        isql::SQLDA_SEQ => {
613            // Inicia uma nova variável; sqlda_seq começa em 1.
614            let seq = read_le_int(val) as usize;
615            *cur = Some(ColumnMeta {
616                index: seq.saturating_sub(1),
617                ..Default::default()
618            });
619        }
620        isql::TYPE => {
621            if let Some(c) = cur.as_mut() {
622                let t = read_le_int(val) as i32;
623                c.sql_type = t;
624                c.nullable = sql_type::is_nullable(t);
625            }
626        }
627        isql::SUB_TYPE => {
628            if let Some(c) = cur.as_mut() {
629                c.sub_type = read_le_int_signed(val) as i32;
630            }
631        }
632        isql::SCALE => {
633            if let Some(c) = cur.as_mut() {
634                c.scale = read_le_int_signed(val) as i32;
635            }
636        }
637        isql::LENGTH => {
638            if let Some(c) = cur.as_mut() {
639                c.length = read_le_int(val) as i32;
640            }
641        }
642        isql::FIELD => set_name(cur, val, |c, s| c.field = s),
643        isql::RELATION => set_name(cur, val, |c, s| c.relation = s),
644        isql::ALIAS => set_name(cur, val, |c, s| c.alias = s),
645        isql::OWNER => set_name(cur, val, |c, s| c.owner = s),
646        // isc_info_sql_describe_vars (count) e flags são informativos; os itens
647        // por variável carregam tudo que precisamos.
648        _ => {}
649    }
650}
651
652fn set_name(cur: &mut Option<ColumnMeta>, val: &[u8], assign: impl Fn(&mut ColumnMeta, String)) {
653    if let Some(c) = cur.as_mut() {
654        assign(c, String::from_utf8_lossy(val).into_owned());
655    }
656}
657
658/// Percorre a resposta de `op_info_sql`, extraindo o bloco aninhado
659/// `isc_info_sql_records`. Cada item de nível superior é `tag(1) + len(2 LE) +
660/// value`; o valor de `RECORDS` contém os contadores `isc_info_req_*`.
661fn parse_records(data: &[u8]) -> RowsAffected {
662    let mut out = RowsAffected::default();
663    for (tag, val) in InfoItems::new(data) {
664        if tag == isql::RECORDS {
665            for (sub, v) in InfoItems::new(val) {
666                let n = read_le_int(v) as u64;
667                match sub {
668                    info_req::SELECT_COUNT => out.selected = n,
669                    info_req::INSERT_COUNT => out.inserted = n,
670                    info_req::UPDATE_COUNT => out.updated = n,
671                    info_req::DELETE_COUNT => out.deleted = n,
672                    _ => {}
673                }
674            }
675        }
676    }
677    out
678}
679
680/// Iterador sobre itens de um fluxo de info no formato `tag(1) + len(2 LE) +
681/// value(len)`, parando em `isc_info_end` ou no fim/dado truncado.
682struct InfoItems<'a> {
683    data: &'a [u8],
684    pos: usize,
685}
686
687impl<'a> InfoItems<'a> {
688    fn new(data: &'a [u8]) -> Self {
689        InfoItems { data, pos: 0 }
690    }
691}
692
693impl<'a> Iterator for InfoItems<'a> {
694    type Item = (u8, &'a [u8]);
695
696    fn next(&mut self) -> Option<Self::Item> {
697        let tag = *self.data.get(self.pos)?;
698        if tag == INFO_END {
699            return None;
700        }
701        self.pos += 1;
702        let lo = *self.data.get(self.pos)? as usize;
703        let hi = *self.data.get(self.pos + 1)? as usize;
704        let len = lo | (hi << 8);
705        self.pos += 2;
706        let val = self.data.get(self.pos..self.pos + len)?;
707        self.pos += len;
708        Some((tag, val))
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    /// Constrói um item de info `tag + len(2 LE) + value`.
717    fn item(tag: u8, val: &[u8]) -> Vec<u8> {
718        let mut v = vec![tag];
719        v.extend_from_slice(&(val.len() as u16).to_le_bytes());
720        v.extend_from_slice(val);
721        v
722    }
723
724    #[test]
725    fn parses_select_describe_for_two_columns() {
726        // Espelha a captura em PROTOCOL-NOTES: stmt_type=select, sem parâmetros,
727        // duas colunas de saída (emp_no SMALLINT, first_name VARCHAR(15)).
728        let mut data = Vec::new();
729        data.extend(item(isql::STMT_TYPE, &stmt_type::SELECT.to_le_bytes()));
730        // bloco de entrada: zero parâmetros.
731        data.push(isql::BIND);
732        data.extend(item(isql::DESCRIBE_VARS, &0i32.to_le_bytes()));
733        // bloco de saída: duas colunas.
734        data.push(isql::SELECT);
735        data.extend(item(isql::DESCRIBE_VARS, &2i32.to_le_bytes()));
736
737        data.extend(item(isql::SQLDA_SEQ, &1i32.to_le_bytes()));
738        data.extend(item(isql::TYPE, &(sql_type::SHORT | 1).to_le_bytes())); // anulável
739        data.extend(item(isql::SUB_TYPE, &0i32.to_le_bytes()));
740        data.extend(item(isql::SCALE, &0i32.to_le_bytes()));
741        data.extend(item(isql::LENGTH, &2i32.to_le_bytes()));
742        data.extend(item(isql::FIELD, b"EMP_NO"));
743        data.extend(item(isql::ALIAS, b"EMP_NO"));
744        data.push(isql::DESCRIBE_END);
745
746        data.extend(item(isql::SQLDA_SEQ, &2i32.to_le_bytes()));
747        data.extend(item(isql::TYPE, &sql_type::VARYING.to_le_bytes()));
748        data.extend(item(isql::SUB_TYPE, &0i32.to_le_bytes()));
749        data.extend(item(isql::SCALE, &0i32.to_le_bytes()));
750        data.extend(item(isql::LENGTH, &15i32.to_le_bytes()));
751        data.extend(item(isql::FIELD, b"FIRST_NAME"));
752        data.extend(item(isql::ALIAS, b"FIRST_NAME"));
753        data.push(isql::DESCRIBE_END);
754
755        data.push(INFO_END);
756
757        let info = parse_prepare_response(&data).unwrap();
758        assert_eq!(info.stmt_type, stmt_type::SELECT);
759        assert!(info.params.is_empty());
760        assert_eq!(info.columns.len(), 2);
761
762        let emp_no = &info.columns[0];
763        assert_eq!(emp_no.index, 0);
764        assert_eq!(sql_type::base(emp_no.sql_type), sql_type::SHORT);
765        assert!(emp_no.nullable);
766        assert_eq!(emp_no.name(), "EMP_NO");
767
768        let first_name = &info.columns[1];
769        assert_eq!(sql_type::base(first_name.sql_type), sql_type::VARYING);
770        assert_eq!(first_name.length, 15);
771        assert!(!first_name.nullable);
772        assert_eq!(first_name.name(), "FIRST_NAME");
773    }
774
775    #[test]
776    fn truncated_info_is_an_error() {
777        let data = [INFO_TRUNCATED];
778        assert!(parse_prepare_response(&data).is_err());
779    }
780
781    #[test]
782    fn parses_record_counts() {
783        // Espelha a resposta real de op_info_sql para um UPDATE de 5 linhas:
784        // bloco RECORDS aninhado com os quatro contadores isc_info_req_*.
785        fn sub(tag: u8, n: i32) -> Vec<u8> {
786            let mut v = vec![tag, 4, 0]; // len = 4 (LE)
787            v.extend_from_slice(&n.to_le_bytes());
788            v
789        }
790        let mut nested = Vec::new();
791        nested.extend(sub(info_req::SELECT_COUNT, 5));
792        nested.extend(sub(info_req::INSERT_COUNT, 0));
793        nested.extend(sub(info_req::UPDATE_COUNT, 5));
794        nested.extend(sub(info_req::DELETE_COUNT, 0));
795
796        let mut data = vec![isql::RECORDS];
797        data.extend_from_slice(&(nested.len() as u16).to_le_bytes());
798        data.extend_from_slice(&nested);
799        data.push(INFO_END);
800
801        let r = parse_records(&data);
802        assert_eq!(r.selected, 5);
803        assert_eq!(r.updated, 5);
804        assert_eq!(r.inserted, 0);
805        assert_eq!(r.deleted, 0);
806        assert_eq!(r.total_modified(), 5);
807    }
808
809    #[test]
810    fn negative_scale_is_sign_extended() {
811        let mut data = Vec::new();
812        data.push(isql::SELECT);
813        data.extend(item(isql::SQLDA_SEQ, &1i32.to_le_bytes()));
814        data.extend(item(isql::TYPE, &sql_type::INT64.to_le_bytes()));
815        data.extend(item(isql::SCALE, &(-2i32).to_le_bytes()));
816        data.extend(item(isql::LENGTH, &8i32.to_le_bytes()));
817        data.push(isql::DESCRIBE_END);
818        data.push(INFO_END);
819
820        let info = parse_prepare_response(&data).unwrap();
821        assert_eq!(info.columns[0].scale, -2);
822    }
823}