Skip to main content

firebird_wire/
array.rs

1//! Colunas ARRAY do SQL (`op_get_slice` / `op_put_slice` / `op_slice`).
2//!
3//! Um ARRAY do Firebird é armazenado como um blob especial: numa linha a coluna
4//! chega como um id de 8 bytes ([`Value::Array`]), igual a um blob. Para ler ou
5//! escrever os elementos é preciso descrever a fatia com a **SDL** (Slice
6//! Description Language) — um pequeno bytecode que diz o tipo do elemento, a
7//! relação/campo e os limites de cada dimensão.
8//!
9//! ## Fluxo
10//!
11//! 1. [`Connection::array_desc`] consulta `RDB$RELATION_FIELDS`/`RDB$FIELDS`/
12//!    `RDB$FIELD_DIMENSIONS` e monta um [`ArrayDesc`] (tipo BLR do elemento,
13//!    tamanho, escala, dimensões).
14//! 2. [`Connection::read_array`] envia `op_get_slice` (id + SDL) e decodifica a
15//!    resposta `op_slice` num `Vec<Value>` (um por elemento, em ordem).
16//! 3. [`Connection::write_array`] envia `op_put_slice` (SDL + dados) e devolve o
17//!    id do novo array, que então vai como [`Value::Array`] num INSERT/UPDATE.
18//!
19//! ## Codificação na transmissão (wire)
20//!
21//! A `op_slice` traz `p_slr_length` (tamanho da fatia na representação do
22//! cliente = nº de elementos × *stride*) seguido do comprimento XDR e dos dados.
23//! Cada elemento é serializado por `xdr_datum`: tipos de largura fixa vão como
24//! inteiros XDR (4 ou 8 bytes); `VARYING` vai como `comprimento(4 B) + bytes +
25//! padding até 4`; `TEXT` como `bytes + padding até 4`. O nº de elementos é
26//! `p_slr_length / stride` — derivamos o stride do [`ArrayDesc`], pois o
27//! comprimento XDR reportado é o tamanho lógico, não a contagem de bytes na rede.
28
29use crate::charset::Charset;
30use crate::connection::Connection;
31use crate::error::{Error, Result};
32use crate::transaction::Transaction;
33use crate::value::Value;
34use crate::wire::consts::{blr, op, sdl};
35use crate::wire::response::{read_op, read_response, read_response_body};
36use crate::wire::stream::{FbStream, op_name, op_packet};
37
38/// Os limites (inferior, superior) de uma dimensão de array, ambos inclusivos.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct Dimension {
41    /// Primeiro índice válido desta dimensão.
42    pub lower: i32,
43    /// Último índice válido desta dimensão.
44    pub upper: i32,
45}
46
47impl Dimension {
48    /// Quantos elementos esta dimensão contém.
49    pub fn len(&self) -> usize {
50        (self.upper - self.lower + 1).max(0) as usize
51    }
52
53    /// Verdadeiro se a dimensão é vazia (superior < inferior).
54    pub fn is_empty(&self) -> bool {
55        self.upper < self.lower
56    }
57}
58
59/// Descreve uma coluna ARRAY: o tipo do elemento e as dimensões. Obtido por
60/// [`Connection::array_desc`]; consumido por [`Connection::read_array`] /
61/// [`Connection::write_array`] para montar a SDL.
62#[derive(Debug, Clone)]
63pub struct ArrayDesc {
64    /// Nome da relação (tabela), como armazenado (normalmente em maiúsculas).
65    pub relation: String,
66    /// Nome do campo (coluna), como armazenado.
67    pub field: String,
68    /// Tipo BLR do elemento (= `RDB$FIELD_TYPE`; ex.: 37 = VARYING, 8 = LONG).
69    pub blr_type: u8,
70    /// Sub-tipo (charset para texto: 1 = OCTETS; sub-tipo de blob, etc.).
71    pub sub_type: i32,
72    /// Escala (para NUMERIC/DECIMAL e inteiros escalados).
73    pub scale: i32,
74    /// Comprimento do elemento em bytes (largura declarada do tipo).
75    pub length: u16,
76    /// Limites de cada dimensão, da mais externa para a mais interna.
77    pub dimensions: Vec<Dimension>,
78}
79
80impl ArrayDesc {
81    /// Número total de elementos (produto dos tamanhos de todas as dimensões).
82    pub fn element_count(&self) -> usize {
83        self.dimensions.iter().map(Dimension::len).product()
84    }
85
86    /// O *stride* de um elemento na representação do cliente (o `dsc_length` que o
87    /// servidor usa para dividir `p_slr_length` e obter a contagem de elementos).
88    fn element_stride(&self) -> usize {
89        match self.blr_type {
90            blr::VARYING => self.length as usize + 2,
91            blr::TEXT => self.length as usize,
92            blr::SHORT => 2,
93            blr::LONG | blr::FLOAT | blr::SQL_DATE | blr::SQL_TIME => 4,
94            blr::INT64 | blr::DOUBLE | blr::D_FLOAT | blr::TIMESTAMP | blr::QUAD | blr::DEC64 => 8,
95            blr::INT128 | blr::DEC128 => 16,
96            blr::BOOL => 1,
97            // Tipos não previstos: usa o comprimento declarado.
98            _ => self.length.max(1) as usize,
99        }
100    }
101
102    /// Tamanho total da fatia na representação do cliente (`p_slc_length`).
103    fn slice_len(&self) -> usize {
104        self.element_count() * self.element_stride()
105    }
106
107    /// Gera a SDL que descreve a fatia inteira deste array. Reproduz o algoritmo
108    /// `gen_sdl` do fbclient (verificado byte a byte contra uma captura de
109    /// `isc_array_get_slice` em `JOB.LANGUAGE_REQ`).
110    pub fn to_sdl(&self) -> Vec<u8> {
111        let mut s = Vec::with_capacity(32);
112        s.push(sdl::VERSION1);
113        // Descritor do elemento dentro de um `struct` de 1 campo (códigos BLR).
114        s.push(sdl::STRUCT);
115        s.push(1);
116        s.push(self.blr_type);
117        match self.blr_type {
118            // Texto: comprimento como palavra de 2 bytes (LE).
119            blr::TEXT | blr::VARYING => s.extend_from_slice(&self.length.to_le_bytes()),
120            // Inteiros escalados: a escala como um byte com sinal.
121            blr::SHORT | blr::LONG | blr::INT64 | blr::INT128 | blr::QUAD => {
122                s.push(self.scale as i8 as u8)
123            }
124            // Os demais tipos não têm operando no descritor.
125            _ => {}
126        }
127        // Relação e campo.
128        s.push(sdl::RELATION);
129        s.push(self.relation.len() as u8);
130        s.extend_from_slice(self.relation.as_bytes());
131        s.push(sdl::FIELD);
132        s.push(self.field.len() as u8);
133        s.extend_from_slice(self.field.as_bytes());
134        // Um laço por dimensão: do1 quando o limite inferior é 1 (caso comum, só o
135        // superior viaja); senão do2 com os dois limites.
136        for (i, dim) in self.dimensions.iter().enumerate() {
137            if dim.lower == 1 {
138                s.push(sdl::DO1);
139                s.push(i as u8);
140                put_sdl_literal(&mut s, dim.upper);
141            } else {
142                s.push(sdl::DO2);
143                s.push(i as u8);
144                put_sdl_literal(&mut s, dim.lower);
145                put_sdl_literal(&mut s, dim.upper);
146            }
147        }
148        // Atribui o (único) elemento do struct indexado pelas variáveis de laço.
149        s.push(sdl::ELEMENT);
150        s.push(1);
151        s.push(sdl::SCALAR);
152        s.push(0); // índice do elemento no struct
153        s.push(self.dimensions.len() as u8); // nº de subscritos = nº de dimensões
154        for i in 0..self.dimensions.len() {
155            s.push(sdl::VARIABLE);
156            s.push(i as u8);
157        }
158        s.push(sdl::EOC);
159        s
160    }
161}
162
163/// Emite um literal inteiro de SDL com a menor largura que o comporta.
164fn put_sdl_literal(s: &mut Vec<u8>, v: i32) {
165    if (i8::MIN as i32..=i8::MAX as i32).contains(&v) {
166        s.push(sdl::TINY_INTEGER);
167        s.push(v as i8 as u8);
168    } else if (i16::MIN as i32..=i16::MAX as i32).contains(&v) {
169        s.push(sdl::SHORT_INTEGER);
170        s.extend_from_slice(&(v as i16).to_le_bytes());
171    } else {
172        s.push(sdl::LONG_INTEGER);
173        s.extend_from_slice(&v.to_le_bytes());
174    }
175}
176
177impl Connection {
178    /// Monta o [`ArrayDesc`] de uma coluna ARRAY consultando o catálogo do
179    /// sistema (`RDB$*`), exatamente como o fbclient faz antes de uma fatia.
180    /// `relation`/`field` são os nomes como armazenados (normalmente maiúsculas;
181    /// um `ColumnMeta` de saída já os traz assim).
182    pub fn array_desc(
183        &mut self,
184        tx: &Transaction,
185        relation: &str,
186        field: &str,
187    ) -> Result<ArrayDesc> {
188        // 1. Tipo/escala/comprimento/dimensões + a "fonte" do campo (o domínio,
189        //    p.ex. "RDB$4"), que é a chave de RDB$FIELD_DIMENSIONS.
190        let mut stmt = self.prepare(
191            tx,
192            "SELECT f.RDB$FIELD_TYPE, f.RDB$FIELD_SUB_TYPE, f.RDB$FIELD_SCALE, \
193                 f.RDB$FIELD_LENGTH, f.RDB$DIMENSIONS, f.RDB$FIELD_NAME \
194                 FROM RDB$RELATION_FIELDS rf \
195                 JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE \
196                 WHERE rf.RDB$RELATION_NAME = ? AND rf.RDB$FIELD_NAME = ?",
197        )?;
198        stmt.execute(
199            self,
200            tx,
201            &[Value::Text(relation.into()), Value::Text(field.into())],
202        )?;
203        let rows = stmt.fetch_all(self)?;
204        stmt.drop_statement(self)?;
205
206        let row = rows.into_iter().next().ok_or_else(|| {
207            Error::protocol(format!(
208                "coluna '{relation}.{field}' não encontrada no catálogo"
209            ))
210        })?;
211        let blr_type = val_i64(&row[0]).unwrap_or(0) as u8;
212        let sub_type = val_i64(&row[1]).unwrap_or(0) as i32;
213        let scale = val_i64(&row[2]).unwrap_or(0) as i32;
214        let length = val_i64(&row[3]).unwrap_or(0) as u16;
215        let dims = val_i64(&row[4]).unwrap_or(0);
216        let source = row[5].as_str().unwrap_or("").trim_end().to_string();
217        if dims <= 0 {
218            return Err(Error::protocol(format!(
219                "'{relation}.{field}' não é uma coluna ARRAY"
220            )));
221        }
222
223        // 2. Limites de cada dimensão, em ordem.
224        let mut stmt = self.prepare(
225            tx,
226            "SELECT fd.RDB$LOWER_BOUND, fd.RDB$UPPER_BOUND \
227                 FROM RDB$FIELD_DIMENSIONS fd WHERE fd.RDB$FIELD_NAME = ? \
228                 ORDER BY fd.RDB$DIMENSION",
229        )?;
230        stmt.execute(self, tx, &[Value::Text(source)])?;
231        let dim_rows = stmt.fetch_all(self)?;
232        stmt.drop_statement(self)?;
233
234        let dimensions: Vec<Dimension> = dim_rows
235            .iter()
236            .map(|r| Dimension {
237                lower: val_i64(&r[0]).unwrap_or(1) as i32,
238                upper: val_i64(&r[1]).unwrap_or(0) as i32,
239            })
240            .collect();
241
242        Ok(ArrayDesc {
243            relation: relation.into(),
244            field: field.into(),
245            blr_type,
246            sub_type,
247            scale,
248            length,
249            dimensions,
250        })
251    }
252
253    /// Lê todos os elementos de um array (`op_get_slice`). `array_id` é o id que
254    /// veio na coluna ([`Value::Array`]); `desc` descreve o tipo e as dimensões.
255    /// Devolve um valor por elemento, na ordem em que o servidor os transmite.
256    pub fn read_array(
257        &mut self,
258        tx: &Transaction,
259        array_id: u64,
260        desc: &ArrayDesc,
261    ) -> Result<Vec<Value>> {
262        let sdl = desc.to_sdl();
263        let count = desc.element_count();
264
265        let mut w = op_packet(op::GET_SLICE);
266        w.put_i32(tx.handle());
267        w.put_i64(array_id as i64); // id do array (quad)
268        w.put_i32(desc.slice_len() as i32); // p_slc_length: tamanho da fatia pedida
269        w.put_bytes(&sdl);
270        w.put_bytes(&[]); // parâmetros da fatia: nenhum
271        w.put_i32(0); // fatia de saída vazia (no get não enviamos dados)
272        self.io().send(&w)?;
273
274        // A resposta de sucesso é um op_slice (sem vetor de status); um erro vem
275        // como op_response.
276        let code = read_op(self.io())?;
277        if code == op::RESPONSE {
278            read_response_body(self.io())?.into_result()?;
279            return Err(Error::protocol("op_get_slice falhou sem status de erro"));
280        }
281        if code != op::SLICE {
282            return Err(Error::protocol(format!(
283                "esperava op_slice, recebi {} ({code})",
284                op_name(code)
285            )));
286        }
287        let _slr_length = self.io().read_i32()?; // tamanho na repr. do cliente
288        let _xdr_length = self.io().read_i32()?; // comprimento lógico XDR
289
290        let charset = self.charset();
291        let mut out = Vec::with_capacity(count);
292        for _ in 0..count {
293            out.push(decode_element(self.io(), desc, charset)?);
294        }
295        Ok(out)
296    }
297
298    /// Cria um novo array com `values` (`op_put_slice`) e devolve seu id, para
299    /// usar como [`Value::Array`] num INSERT/UPDATE. O número de valores deve
300    /// bater com `desc.element_count()`.
301    pub fn write_array(
302        &mut self,
303        tx: &Transaction,
304        desc: &ArrayDesc,
305        values: &[Value],
306    ) -> Result<u64> {
307        let count = desc.element_count();
308        if values.len() != count {
309            return Err(Error::protocol(format!(
310                "o array espera {count} elementos, recebeu {}",
311                values.len()
312            )));
313        }
314        let sdl = desc.to_sdl();
315        let charset = self.charset();
316
317        // Serializa os elementos no formato xdr_datum (mesmo do op_slice de leitura).
318        let mut data = Vec::new();
319        for v in values {
320            encode_element(&mut data, desc, v, charset)?;
321        }
322
323        let mut w = op_packet(op::PUT_SLICE);
324        w.put_i32(tx.handle());
325        w.put_i64(0); // id 0 ⇒ o servidor aloca um array novo
326        w.put_i32(desc.slice_len() as i32); // p_slc_length na repr. do cliente
327        w.put_bytes(&sdl);
328        w.put_bytes(&[]); // parâmetros: nenhum
329        w.put_i32(desc.slice_len() as i32); // comprimento lógico da fatia
330        w.put_raw(&data);
331        w.align();
332        self.io().send(&w)?;
333
334        // O id do novo array volta no campo blob_id do op_response.
335        Ok(read_response(self.io())?.blob_id)
336    }
337}
338
339/// Visão `i64` de um valor numérico do catálogo (SMALLINT/INTEGER), ou `None`.
340fn val_i64(v: &Value) -> Option<i64> {
341    v.as_i64()
342}
343
344/// Decodifica um elemento da fatia conforme seu tipo BLR (formato xdr_datum).
345fn decode_element(stream: &mut FbStream, desc: &ArrayDesc, charset: Charset) -> Result<Value> {
346    Ok(match desc.blr_type {
347        blr::SHORT => Value::Short(stream.read_i32()? as i16),
348        blr::LONG => Value::Int(stream.read_i32()?),
349        blr::INT64 => Value::BigInt(stream.read_i64()?),
350        blr::INT128 => {
351            let b = stream.read_raw(16)?;
352            Value::Int128(i128::from_be_bytes(b.try_into().unwrap()))
353        }
354        blr::FLOAT => Value::Float(f32::from_bits(stream.read_i32()? as u32)),
355        blr::DOUBLE | blr::D_FLOAT => Value::Double(stream.read_f64()?),
356        blr::SQL_DATE => Value::Date(stream.read_i32()?),
357        blr::SQL_TIME => Value::Time(stream.read_i32()? as u32),
358        blr::TIMESTAMP => {
359            let date = stream.read_i32()?;
360            let time = stream.read_i32()? as u32;
361            Value::Timestamp(date, time)
362        }
363        blr::BOOL => {
364            let b = stream.read_raw(1)?;
365            stream.read_pad(1)?;
366            Value::Bool(b[0] != 0)
367        }
368        blr::DEC64 => {
369            let b = stream.read_raw(8)?;
370            Value::DecFloat(crate::decfloat::DecFloat::from_decimal64(
371                b.try_into().unwrap(),
372            ))
373        }
374        blr::DEC128 => {
375            let b = stream.read_raw(16)?;
376            Value::DecFloat(crate::decfloat::DecFloat::from_decimal128(
377                b.try_into().unwrap(),
378            ))
379        }
380        blr::TEXT => {
381            let n = desc.length as usize;
382            let raw = stream.read_raw(n)?;
383            stream.read_pad(n)?;
384            text_or_bytes(desc, raw, charset, true)
385        }
386        blr::VARYING => {
387            let raw = stream.read_bytes()?; // comprimento(4) + bytes + padding
388            text_or_bytes(desc, raw, charset, false)
389        }
390        other => {
391            // Tipo não tratado: consome o stride como bytes opacos.
392            let _ = other;
393            Value::Bytes(stream.read_raw(desc.element_stride())?)
394        }
395    })
396}
397
398/// Serializa um elemento no formato xdr_datum (espelho de [`decode_element`]).
399fn encode_element(
400    out: &mut Vec<u8>,
401    desc: &ArrayDesc,
402    val: &Value,
403    charset: Charset,
404) -> Result<()> {
405    let mismatch = || {
406        Error::protocol(format!(
407            "valor não cabe no tipo de elemento BLR {}",
408            desc.blr_type
409        ))
410    };
411    match desc.blr_type {
412        blr::SHORT => put_i32_be(out, i32::from(val.as_i64().ok_or_else(mismatch)? as i16)),
413        blr::LONG => put_i32_be(out, val.as_i64().ok_or_else(mismatch)? as i32),
414        blr::INT64 => out.extend_from_slice(&val.as_i64().ok_or_else(mismatch)?.to_be_bytes()),
415        blr::INT128 => match val {
416            Value::Int128(v) => out.extend_from_slice(&v.to_be_bytes()),
417            _ => {
418                out.extend_from_slice(&i128::from(val.as_i64().ok_or_else(mismatch)?).to_be_bytes())
419            }
420        },
421        blr::FLOAT => match val {
422            Value::Float(f) => out.extend_from_slice(&f.to_bits().to_be_bytes()),
423            Value::Double(f) => out.extend_from_slice(&(*f as f32).to_bits().to_be_bytes()),
424            _ => return Err(mismatch()),
425        },
426        blr::DOUBLE | blr::D_FLOAT => match val {
427            Value::Double(f) => out.extend_from_slice(&f.to_bits().to_be_bytes()),
428            Value::Float(f) => out.extend_from_slice(&f64::from(*f).to_bits().to_be_bytes()),
429            _ => return Err(mismatch()),
430        },
431        blr::SQL_DATE => match val {
432            Value::Date(d) | Value::Timestamp(d, _) => put_i32_be(out, *d),
433            _ => return Err(mismatch()),
434        },
435        blr::SQL_TIME => match val {
436            Value::Time(t) | Value::Timestamp(_, t) => put_i32_be(out, *t as i32),
437            _ => return Err(mismatch()),
438        },
439        blr::TIMESTAMP => match val {
440            Value::Timestamp(d, t) => {
441                put_i32_be(out, *d);
442                put_i32_be(out, *t as i32);
443            }
444            _ => return Err(mismatch()),
445        },
446        blr::BOOL => {
447            out.push(matches!(val, Value::Bool(true)) as u8);
448            put_pad(out, 1);
449        }
450        blr::DEC64 => match val {
451            Value::DecFloat(d) => out.extend_from_slice(&d.to_decimal64().ok_or_else(mismatch)?),
452            _ => return Err(mismatch()),
453        },
454        blr::DEC128 => match val {
455            Value::DecFloat(d) => out.extend_from_slice(&d.to_decimal128().ok_or_else(mismatch)?),
456            _ => return Err(mismatch()),
457        },
458        blr::VARYING => {
459            let bytes = elem_text_bytes(val, charset)?;
460            put_i32_be(out, bytes.len() as i32);
461            out.extend_from_slice(&bytes);
462            put_pad(out, bytes.len());
463        }
464        blr::TEXT => {
465            let bytes = elem_text_bytes(val, charset)?;
466            let n = desc.length as usize;
467            out.extend_from_slice(&bytes);
468            for _ in bytes.len()..n {
469                out.push(b' '); // CHAR(n) é preenchido à direita com espaços
470            }
471            put_pad(out, n.max(bytes.len()));
472        }
473        _ => {
474            return Err(Error::protocol(format!(
475                "tipo de elemento BLR {} não suportado para escrita",
476                desc.blr_type
477            )));
478        }
479    }
480    Ok(())
481}
482
483fn put_i32_be(out: &mut Vec<u8>, v: i32) {
484    out.extend_from_slice(&v.to_be_bytes());
485}
486
487fn put_pad(out: &mut Vec<u8>, data_len: usize) {
488    for _ in 0..(crate::value::align4(data_len) - data_len) {
489        out.push(0);
490    }
491}
492
493fn elem_text_bytes(val: &Value, charset: Charset) -> Result<std::borrow::Cow<'_, [u8]>> {
494    use std::borrow::Cow;
495    match val {
496        Value::Text(s) => Ok(Cow::Owned(charset.encode(s))),
497        Value::Bytes(b) => Ok(Cow::Borrowed(b)),
498        _ => Err(Error::protocol(
499            "esperava um valor de texto/bytes para elemento de array",
500        )),
501    }
502}
503
504/// Texto OCTETS (sub-tipo 1) fica como bytes; o resto é decodificado pelo charset
505/// da conexão. CHAR tem o padding de espaços removido (`is_char`).
506fn text_or_bytes(desc: &ArrayDesc, raw: Vec<u8>, charset: Charset, is_char: bool) -> Value {
507    const CS_OCTETS: i32 = 1;
508    if desc.sub_type == CS_OCTETS {
509        Value::Bytes(raw)
510    } else {
511        let s = charset.decode(&raw);
512        if is_char {
513            Value::Text(s.trim_end_matches(' ').to_string())
514        } else {
515            Value::Text(s)
516        }
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    fn varchar15_1to5() -> ArrayDesc {
525        ArrayDesc {
526            relation: "JOB".into(),
527            field: "LANGUAGE_REQ".into(),
528            blr_type: blr::VARYING,
529            sub_type: 0,
530            scale: 0,
531            length: 15,
532            dimensions: vec![Dimension { lower: 1, upper: 5 }],
533        }
534    }
535
536    #[test]
537    fn sdl_matches_captured_bytes() {
538        // SDL exata capturada de isc_array_get_slice em JOB.LANGUAGE_REQ
539        // (VARCHAR(15)[1:5]) contra o employee.fdb.
540        let expected: &[u8] = &[
541            0x01, 0x06, 0x01, 0x25, 0x0f, 0x00, 0x02, 0x03, b'J', b'O', b'B', 0x04, 0x0c, b'L',
542            b'A', b'N', b'G', b'U', b'A', b'G', b'E', b'_', b'R', b'E', b'Q', 0x23, 0x00, 0x09,
543            0x05, 0x24, 0x01, 0x08, 0x00, 0x01, 0x07, 0x00, 0xff,
544        ];
545        assert_eq!(varchar15_1to5().to_sdl(), expected);
546    }
547
548    #[test]
549    fn element_count_and_stride() {
550        let d = varchar15_1to5();
551        assert_eq!(d.element_count(), 5);
552        assert_eq!(d.element_stride(), 17); // VARCHAR(15) ⇒ 15 + 2
553        assert_eq!(d.slice_len(), 85);
554    }
555
556    #[test]
557    fn sdl_uses_do2_when_lower_bound_not_one() {
558        let mut d = varchar15_1to5();
559        d.dimensions = vec![Dimension {
560            lower: -2,
561            upper: 3,
562        }];
563        let s = d.to_sdl();
564        // Procura o verbo de laço: deve ser DO2 (34) com os dois limites.
565        let pos = s.iter().position(|&b| b == sdl::DO2).expect("esperava DO2");
566        assert_eq!(s[pos + 1], 0); // variável 0
567        assert_eq!(s[pos + 2], sdl::TINY_INTEGER);
568        assert_eq!(s[pos + 3] as i8, -2); // limite inferior
569        assert_eq!(s[pos + 4], sdl::TINY_INTEGER);
570        assert_eq!(s[pos + 5] as i8, 3); // limite superior
571        assert_eq!(d.element_count(), 6);
572    }
573
574    #[test]
575    fn sdl_multidim_emits_two_loops_and_two_subscripts() {
576        let mut d = varchar15_1to5();
577        d.blr_type = blr::LONG;
578        d.length = 4;
579        d.dimensions = vec![
580            Dimension { lower: 1, upper: 2 },
581            Dimension { lower: 1, upper: 3 },
582        ];
583        let s = d.to_sdl();
584        assert_eq!(s.iter().filter(|&&b| b == sdl::DO1).count(), 2);
585        assert_eq!(s.iter().filter(|&&b| b == sdl::VARIABLE).count(), 2);
586        assert_eq!(d.element_count(), 6);
587        // scalar com ndims = 2.
588        let pos = s.iter().position(|&b| b == sdl::SCALAR).unwrap();
589        assert_eq!(s[pos + 2], 2);
590    }
591}