cassandra_proto/frame/
frame_result.rs

1use std::io::Cursor;
2
3use crate::frame::{FromBytes, FromCursor, IntoBytes};
4use crate::error;
5use crate::types::*;
6use crate::types::rows::Row;
7use crate::frame::events::SchemaChange;
8
9/// `ResultKind` is enum which represents types of result.
10#[derive(Debug)]
11pub enum ResultKind {
12    /// Void result.
13    Void,
14    /// Rows result.
15    Rows,
16    /// Set keyspace result.
17    SetKeyspace,
18    /// Prepeared result.
19    Prepared,
20    /// Schema change result.
21    SchemaChange,
22}
23
24impl IntoBytes for ResultKind {
25    fn into_cbytes(&self) -> Vec<u8> {
26        match *self {
27            ResultKind::Void => to_int(0x0001),
28            ResultKind::Rows => to_int(0x0002),
29            ResultKind::SetKeyspace => to_int(0x0003),
30            ResultKind::Prepared => to_int(0x0004),
31            ResultKind::SchemaChange => to_int(0x0005),
32        }
33    }
34}
35
36impl FromBytes for ResultKind {
37    fn from_bytes(bytes: &[u8]) -> error::Result<ResultKind> {
38        try_from_bytes(bytes).map_err(Into::into)
39                             .and_then(|r| match r {
40                                           0x0001 => Ok(ResultKind::Void),
41                                           0x0002 => Ok(ResultKind::Rows),
42                                           0x0003 => Ok(ResultKind::SetKeyspace),
43                                           0x0004 => Ok(ResultKind::Prepared),
44                                           0x0005 => Ok(ResultKind::SchemaChange),
45                                           _ => Err("Unexpected result kind".into()),
46                                       })
47    }
48}
49
50impl FromCursor for ResultKind {
51    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<ResultKind> {
52        cursor_next_value(&mut cursor, INT_LEN as u64)
53            .and_then(|bytes| ResultKind::from_bytes(bytes.as_slice()))
54    }
55}
56
57/// `ResponseBody` is a generalized enum that represents all types of responses. Each of enum
58/// option wraps related body type.
59#[derive(Debug)]
60pub enum ResResultBody {
61    /// Void response body. It's an empty stuct.
62    Void(BodyResResultVoid),
63    /// Rows response body. It represents a body of response which contains rows.
64    Rows(BodyResResultRows),
65    /// Set keyspace body. It represents a body of set_keyspace query and usually contains
66    /// a name of just set namespace.
67    SetKeyspace(BodyResResultSetKeyspace),
68    /// Prepared response body.
69    Prepared(BodyResResultPrepared),
70    /// Schema change body
71    SchemaChange(SchemaChange),
72}
73
74impl ResResultBody {
75    /// It retrieves`ResResultBody` from `io::Cursor`
76    /// having knowledge about expected kind of result.
77    fn parse_body_from_cursor(mut cursor: &mut Cursor<&[u8]>,
78                              result_kind: ResultKind)
79                              -> error::Result<ResResultBody> {
80        Ok(match result_kind {
81            ResultKind::Void => ResResultBody::Void(BodyResResultVoid::from_cursor(&mut cursor)?),
82            ResultKind::Rows => ResResultBody::Rows(BodyResResultRows::from_cursor(&mut cursor)?),
83            ResultKind::SetKeyspace => {
84                ResResultBody::SetKeyspace(BodyResResultSetKeyspace::from_cursor(&mut cursor)?)
85            }
86            ResultKind::Prepared => {
87                ResResultBody::Prepared(BodyResResultPrepared::from_cursor(&mut cursor)?)
88            }
89            ResultKind::SchemaChange => {
90                ResResultBody::SchemaChange(SchemaChange::from_cursor(&mut cursor)?)
91            }
92        })
93    }
94
95    /// It converts body into `Vec<Row>` if body's type is `Row` and returns `None` otherwise.
96    pub fn into_rows(self) -> Option<Vec<Row>> {
97        match self {
98            ResResultBody::Rows(rows_body) => Some(Row::from_frame_body(rows_body)),
99            _ => None,
100        }
101    }
102
103    /// It returns `Some` rows metadata if frame result is of type rows and `None` othewise
104    pub fn as_rows_metadata(&self) -> Option<RowsMetadata> {
105        match *self {
106            ResResultBody::Rows(ref rows_body) => Some(rows_body.metadata.clone()),
107            _ => None,
108        }
109    }
110
111    /// It unwraps body and returns BodyResResultPrepared which contains an exact result of
112    /// PREPARE query.
113    pub fn into_prepared(self) -> Option<BodyResResultPrepared> {
114        match self {
115            ResResultBody::Prepared(p) => Some(p),
116            _ => None,
117        }
118    }
119
120    /// It unwraps body and returns BodyResResultSetKeyspace which contains an exact result of
121    /// use keyspace query.
122    pub fn into_set_keyspace(self) -> Option<BodyResResultSetKeyspace> {
123        match self {
124            ResResultBody::SetKeyspace(p) => Some(p),
125            _ => None,
126        }
127    }
128}
129
130impl FromCursor for ResResultBody {
131    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<ResResultBody> {
132        let result_kind = ResultKind::from_cursor(&mut cursor)?;
133
134        ResResultBody::parse_body_from_cursor(&mut cursor, result_kind)
135    }
136}
137
138/// Body of a response of type Void
139#[derive(Debug, Default)]
140pub struct BodyResResultVoid {}
141
142impl FromBytes for BodyResResultVoid {
143    fn from_bytes(_bytes: &[u8]) -> error::Result<BodyResResultVoid> {
144        // as it's empty by definition just create BodyResVoid
145        let body: BodyResResultVoid = Default::default();
146        Ok(body)
147    }
148}
149
150impl FromCursor for BodyResResultVoid {
151    fn from_cursor(mut _cursor: &mut Cursor<&[u8]>) -> error::Result<BodyResResultVoid> {
152        let body: BodyResResultVoid = Default::default();
153        Ok(body)
154    }
155}
156
157/// It represents set keyspace result body. Body contains keyspace name.
158#[derive(Debug)]
159pub struct BodyResResultSetKeyspace {
160    /// It contains name of keyspace that was set.
161    pub body: CString,
162}
163
164impl BodyResResultSetKeyspace {
165    /// Factory function that takes body value and
166    /// returns new instance of `BodyResResultSetKeyspace`.
167    pub fn new(body: CString) -> BodyResResultSetKeyspace {
168        BodyResResultSetKeyspace { body: body }
169    }
170}
171
172impl FromCursor for BodyResResultSetKeyspace {
173    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<BodyResResultSetKeyspace> {
174        CString::from_cursor(&mut cursor).map(BodyResResultSetKeyspace::new)
175    }
176}
177
178/// Structure that represents result of type
179/// [rows](https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec#L533).
180#[derive(Debug)]
181pub struct BodyResResultRows {
182    /// Rows metadata
183    pub metadata: RowsMetadata,
184    /// Number of rows.
185    pub rows_count: CInt,
186    /// From spec: it is composed of `rows_count` of rows.
187    pub rows_content: Vec<Vec<CBytes>>,
188}
189
190impl BodyResResultRows {
191    /// It retrieves rows content having knowledge about number of rows and columns.
192    fn get_rows_content(mut cursor: &mut Cursor<&[u8]>,
193                        rows_count: i32,
194                        columns_count: i32)
195                        -> Vec<Vec<CBytes>> {
196        (0..rows_count).map(|_| {
197                           (0..columns_count)
198                     // XXX unwrap()
199                         .map(|_| CBytes::from_cursor(&mut cursor).unwrap() as CBytes)
200                         .collect()
201                       })
202                       .collect()
203    }
204}
205
206impl FromCursor for BodyResResultRows {
207    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<BodyResResultRows> {
208        let metadata = RowsMetadata::from_cursor(&mut cursor)?;
209        let rows_count = CInt::from_cursor(&mut cursor)?;
210        let rows_content: Vec<Vec<CBytes>> =
211            BodyResResultRows::get_rows_content(&mut cursor, rows_count, metadata.columns_count);
212
213        Ok(BodyResResultRows { metadata: metadata,
214                               rows_count: rows_count,
215                               rows_content: rows_content, })
216    }
217}
218
219/// Rows metadata.
220#[derive(Debug, Clone)]
221pub struct RowsMetadata {
222    /// Flags.
223    /// [Read more...]
224    /// (https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec#L541)
225    pub flags: i32,
226    /// Number of columns.
227    pub columns_count: i32,
228    /// Paging state.
229    pub paging_state: Option<CBytes>,
230    // In fact by specification Vec should have only two elements representing the
231    // (unique) keyspace name and table name the columns belong to
232    /// `Option` that may contain global table space.
233    pub global_table_space: Option<Vec<CString>>,
234    /// List of column specifications.
235    pub col_specs: Vec<ColSpec>,
236}
237
238impl FromCursor for RowsMetadata {
239    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<RowsMetadata> {
240        let flags = CInt::from_cursor(&mut cursor)?;
241        let columns_count = CInt::from_cursor(&mut cursor)?;
242
243        let mut paging_state: Option<CBytes> = None;
244        if RowsMetadataFlag::has_has_more_pages(flags) {
245            paging_state = Some(CBytes::from_cursor(&mut cursor)?)
246        }
247
248        let mut global_table_space: Option<Vec<CString>> = None;
249        let has_global_table_space = RowsMetadataFlag::has_global_table_space(flags);
250        if has_global_table_space {
251            let keyspace = CString::from_cursor(&mut cursor)?;
252            let tablename = CString::from_cursor(&mut cursor)?;
253            global_table_space = Some(vec![keyspace, tablename])
254        }
255
256        let col_specs = ColSpec::parse_colspecs(&mut cursor, columns_count, has_global_table_space);
257
258        Ok(RowsMetadata { flags: flags,
259                          columns_count: columns_count,
260                          paging_state: paging_state,
261                          global_table_space: global_table_space,
262                          col_specs: col_specs, })
263    }
264}
265
266const GLOBAL_TABLE_SPACE: i32 = 0x0001;
267const HAS_MORE_PAGES: i32 = 0x0002;
268const NO_METADATA: i32 = 0x0004;
269
270/// Enum that represent a set of possible row metadata flags that could be set.
271pub enum RowsMetadataFlag {
272    GlobalTableSpace,
273    HasMorePages,
274    NoMetadata,
275}
276
277impl RowsMetadataFlag {
278    /// Shows if provided flag contains GlobalTableSpace rows metadata flag
279    pub fn has_global_table_space(flag: i32) -> bool {
280        (flag & GLOBAL_TABLE_SPACE) != 0
281    }
282
283    /// Sets GlobalTableSpace rows metadata flag
284    pub fn set_global_table_space(flag: i32) -> i32 {
285        flag | GLOBAL_TABLE_SPACE
286    }
287
288    /// Shows if provided flag contains HasMorePages rows metadata flag
289    pub fn has_has_more_pages(flag: i32) -> bool {
290        (flag & HAS_MORE_PAGES) != 0
291    }
292
293    /// Sets HasMorePages rows metadata flag
294    pub fn set_has_more_pages(flag: i32) -> i32 {
295        flag | HAS_MORE_PAGES
296    }
297
298    /// Shows if provided flag contains NoMetadata rows metadata flag
299    pub fn has_no_metadata(flag: i32) -> bool {
300        (flag & NO_METADATA) != 0
301    }
302
303    /// Sets NoMetadata rows metadata flag
304    pub fn set_no_metadata(flag: i32) -> i32 {
305        flag | NO_METADATA
306    }
307}
308
309impl IntoBytes for RowsMetadataFlag {
310    fn into_cbytes(&self) -> Vec<u8> {
311        match *self {
312            RowsMetadataFlag::GlobalTableSpace => to_int(GLOBAL_TABLE_SPACE),
313            RowsMetadataFlag::HasMorePages => to_int(HAS_MORE_PAGES),
314            RowsMetadataFlag::NoMetadata => to_int(NO_METADATA),
315        }
316    }
317}
318
319impl FromBytes for RowsMetadataFlag {
320    fn from_bytes(bytes: &[u8]) -> error::Result<RowsMetadataFlag> {
321        try_from_bytes(bytes).map_err(Into::into)
322                             .and_then(|f| match f as i32 {
323                                           GLOBAL_TABLE_SPACE => {
324                                               Ok(RowsMetadataFlag::GlobalTableSpace)
325                                           }
326                                           HAS_MORE_PAGES => Ok(RowsMetadataFlag::HasMorePages),
327                                           NO_METADATA => Ok(RowsMetadataFlag::NoMetadata),
328                                           _ => Err("Unexpected rows metadata flag".into()),
329                                       })
330    }
331}
332
333/// Single column specification.
334#[derive(Debug, Clone)]
335pub struct ColSpec {
336    /// The initial <ksname> is a [string] and is only present
337    /// if the Global_tables_spec flag is NOT set
338    pub ksname: Option<CString>,
339    /// The initial <tablename> is a [string] and is present
340    /// if the Global_tables_spec flag is NOT set
341    pub tablename: Option<CString>,
342    /// Column name
343    pub name: CString,
344    /// Column type defined in spec in 4.2.5.2
345    pub col_type: ColTypeOption,
346}
347
348impl ColSpec {
349    /// parse_colspecs tables mutable cursor,
350    /// number of columns (column_count) and flags that indicates
351    /// if Global_tables_spec is specified. It returns column_count of ColSpecs.
352    pub fn parse_colspecs(mut cursor: &mut Cursor<&[u8]>,
353                          column_count: i32,
354                          with_globale_table_spec: bool)
355                          -> Vec<ColSpec> {
356        (0..column_count).map(|_| {
357                                  let ksname: Option<CString> = if !with_globale_table_spec {
358                                      Some(CString::from_cursor(&mut cursor).unwrap())
359                                  } else {
360                                      None
361                                  };
362
363                                  let tablename = if !with_globale_table_spec {
364                                      Some(CString::from_cursor(&mut cursor).unwrap())
365                                  } else {
366                                      None
367                                  };
368
369                                  // XXX unwrap
370                                  let name = CString::from_cursor(&mut cursor).unwrap();
371                                  let col_type = ColTypeOption::from_cursor(&mut cursor).unwrap();
372
373                                  ColSpec { ksname: ksname,
374                                            tablename: tablename,
375                                            name: name,
376                                            col_type: col_type, }
377                              })
378                         .collect()
379    }
380}
381
382/// Cassandra data types which clould be returned by a server.
383#[derive(Debug, Clone)]
384pub enum ColType {
385    Custom,
386    Ascii,
387    Bigint,
388    Blob,
389    Boolean,
390    Counter,
391    Decimal,
392    Double,
393    Float,
394    Int,
395    Timestamp,
396    Uuid,
397    Varchar,
398    Varint,
399    Timeuuid,
400    Inet,
401    Date,
402    Time,
403    Smallint,
404    Tinyint,
405    List,
406    Map,
407    Set,
408    Udt,
409    Tuple,
410    Null,
411}
412
413impl FromBytes for ColType {
414    fn from_bytes(bytes: &[u8]) -> error::Result<ColType> {
415        try_from_bytes(bytes).map_err(Into::into)
416                             .and_then(|b| match b {
417                                           0x0000 => Ok(ColType::Custom),
418                                           0x0001 => Ok(ColType::Ascii),
419                                           0x0002 => Ok(ColType::Bigint),
420                                           0x0003 => Ok(ColType::Blob),
421                                           0x0004 => Ok(ColType::Boolean),
422                                           0x0005 => Ok(ColType::Counter),
423                                           0x0006 => Ok(ColType::Decimal),
424                                           0x0007 => Ok(ColType::Double),
425                                           0x0008 => Ok(ColType::Float),
426                                           0x0009 => Ok(ColType::Int),
427                                           0x000B => Ok(ColType::Timestamp),
428                                           0x000C => Ok(ColType::Uuid),
429                                           0x000D => Ok(ColType::Varchar),
430                                           0x000E => Ok(ColType::Varint),
431                                           0x000F => Ok(ColType::Timeuuid),
432                                           0x0010 => Ok(ColType::Inet),
433                                           0x0011 => Ok(ColType::Date),
434                                           0x0012 => Ok(ColType::Time),
435                                           0x0013 => Ok(ColType::Smallint),
436                                           0x0014 => Ok(ColType::Tinyint),
437                                           0x0020 => Ok(ColType::List),
438                                           0x0021 => Ok(ColType::Map),
439                                           0x0022 => Ok(ColType::Set),
440                                           0x0030 => Ok(ColType::Udt),
441                                           0x0031 => Ok(ColType::Tuple),
442                                           _ => Err("Unexpected column type".into()),
443                                       })
444    }
445}
446
447impl FromCursor for ColType {
448    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<ColType> {
449        cursor_next_value(&mut cursor, SHORT_LEN as u64)
450            .and_then(|bytes| ColType::from_bytes(bytes.as_slice()))
451            .map_err(Into::into)
452    }
453}
454
455/// Cassandra option that represent column type.
456#[derive(Debug, Clone)]
457pub struct ColTypeOption {
458    /// Id refers to `ColType`.
459    pub id: ColType,
460    /// Values depending on column type.
461    /// [Read more...]
462    /// (https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec#L569)
463    pub value: Option<ColTypeOptionValue>,
464}
465
466impl FromCursor for ColTypeOption {
467    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<ColTypeOption> {
468        let id = ColType::from_cursor(&mut cursor)?;
469        let value = match id {
470            ColType::Custom => {
471                Some(ColTypeOptionValue::CString(CString::from_cursor(&mut cursor)?))
472            }
473            ColType::Set => {
474                let col_type = ColTypeOption::from_cursor(&mut cursor)?;
475                Some(ColTypeOptionValue::CSet(Box::new(col_type)))
476            }
477            ColType::List => {
478                let col_type = ColTypeOption::from_cursor(&mut cursor)?;
479                Some(ColTypeOptionValue::CList(Box::new(col_type)))
480            }
481            ColType::Udt => Some(ColTypeOptionValue::UdtType(CUdt::from_cursor(&mut cursor)?)),
482            ColType::Tuple => {
483                Some(ColTypeOptionValue::TupleType(CTuple::from_cursor(&mut cursor)?))
484            }
485            ColType::Map => {
486                let name_type = ColTypeOption::from_cursor(&mut cursor)?;
487                let value_type = ColTypeOption::from_cursor(&mut cursor)?;
488                Some(ColTypeOptionValue::CMap((Box::new(name_type), Box::new(value_type))))
489            }
490            _ => None,
491        };
492
493        Ok(ColTypeOption { id: id,
494                           value: value, })
495    }
496}
497
498/// Enum that represents all possible types of `value` of `ColTypeOption`.
499#[derive(Debug, Clone)]
500pub enum ColTypeOptionValue {
501    CString(CString),
502    ColType(ColType),
503    CSet(Box<ColTypeOption>),
504    CList(Box<ColTypeOption>),
505    UdtType(CUdt),
506    TupleType(CTuple),
507    CMap((Box<ColTypeOption>, Box<ColTypeOption>)),
508}
509
510/// User defined type.
511/// [Read more...](https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec#L608)
512#[derive(Debug, Clone)]
513pub struct CUdt {
514    /// Keyspace name.
515    pub ks: CString,
516    /// UDT name
517    pub udt_name: CString,
518    /// List of pairs `(name, type)` where name is field name and type is type of field.
519    pub descriptions: Vec<(CString, ColTypeOption)>,
520}
521
522impl FromCursor for CUdt {
523    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<CUdt> {
524        let ks = CString::from_cursor(&mut cursor)?;
525        let udt_name = CString::from_cursor(&mut cursor)?;
526        let n = try_from_bytes(cursor_next_value(&mut cursor, SHORT_LEN as u64)?.as_slice())?;
527        let mut descriptions = Vec::with_capacity(n as usize);
528        for _ in 0..n {
529            let name = CString::from_cursor(&mut cursor)?;
530            let col_type = ColTypeOption::from_cursor(&mut cursor)?;
531            descriptions.push((name, col_type));
532        }
533
534        Ok(CUdt { ks: ks,
535                  udt_name: udt_name,
536                  descriptions: descriptions, })
537    }
538}
539
540/// User defined type.
541/// [Read more...](https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec#L608)
542#[derive(Debug, Clone)]
543pub struct CTuple {
544    /// List of types.
545    pub types: Vec<ColTypeOption>,
546}
547
548impl FromCursor for CTuple {
549    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<CTuple> {
550        let n = try_from_bytes(cursor_next_value(&mut cursor, SHORT_LEN as u64)?.as_slice())?;
551        let mut types = Vec::with_capacity(n as usize);
552        for _ in 0..n {
553            let col_type = ColTypeOption::from_cursor(&mut cursor)?;
554            types.push(col_type);
555        }
556
557        Ok(CTuple { types: types })
558    }
559}
560
561/// The structure represents a body of a response frame of type `prepared`
562#[derive(Debug)]
563pub struct BodyResResultPrepared {
564    /// id of prepared request
565    pub id: CBytesShort,
566    /// metadata
567    pub metadata: PreparedMetadata,
568    /// It is defined exactly the same as <metadata> in the Rows
569    /// documentation.
570    pub result_metadata: RowsMetadata,
571}
572
573impl FromCursor for BodyResResultPrepared {
574    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<BodyResResultPrepared> {
575        let id = CBytesShort::from_cursor(&mut cursor)?;
576        let metadata = PreparedMetadata::from_cursor(&mut cursor)?;
577        let result_metadata = RowsMetadata::from_cursor(&mut cursor)?;
578
579        Ok(BodyResResultPrepared { id: id,
580                                   metadata: metadata,
581                                   result_metadata: result_metadata, })
582    }
583}
584
585/// The structure that represents metadata of prepared response.
586#[derive(Debug)]
587pub struct PreparedMetadata {
588    pub flags: i32,
589    pub columns_count: i32,
590    pub pk_count: i32,
591    pub pk_indexes: Vec<i16>,
592    pub global_table_spec: Option<(CString, CString)>,
593    pub col_specs: Vec<ColSpec>,
594}
595
596impl FromCursor for PreparedMetadata {
597    fn from_cursor(mut cursor: &mut Cursor<&[u8]>) -> error::Result<PreparedMetadata> {
598        let flags = CInt::from_cursor(&mut cursor)?;
599        let columns_count = CInt::from_cursor(&mut cursor)?;
600        let pk_count = if cfg!(feature = "v3") {
601            0
602        } else {
603            // v4 or v5
604            CInt::from_cursor(&mut cursor)?
605        };
606        let pk_index_results: Vec<Option<i16>> = (0..pk_count).map(|_| {
607            cursor_next_value(&mut cursor, SHORT_LEN as u64)
608                    .ok()
609                    .and_then(|b| try_i16_from_bytes(b.as_slice()).ok())
610        })
611                                                              .collect();
612
613        let pk_indexes: Vec<i16> = if pk_index_results.iter().any(Option::is_none) {
614            return Err("pk indexes error".into());
615        } else {
616            pk_index_results.iter()
617                            .cloned()
618                            .map(|r| r.unwrap())
619                            .collect()
620        };
621        let mut global_table_space: Option<(CString, CString)> = None;
622        let has_global_table_space = RowsMetadataFlag::has_global_table_space(flags);
623        if has_global_table_space {
624            let keyspace = CString::from_cursor(&mut cursor)?;
625            let tablename = CString::from_cursor(&mut cursor)?;
626            global_table_space = Some((keyspace, tablename))
627        }
628        let col_specs = ColSpec::parse_colspecs(&mut cursor, columns_count, has_global_table_space);
629
630        Ok(PreparedMetadata { flags: flags,
631                              columns_count: columns_count,
632                              pk_count: pk_count,
633                              pk_indexes: pk_indexes,
634                              global_table_spec: global_table_space,
635                              col_specs: col_specs, })
636    }
637}