LCOV - code coverage report
Current view: top level - dashql-app/src/platform/hyperdb - hyperdb_arrow_insert.ts Coverage Total Hit
Test: DashQL 915828b Lines: 93.7 % 142 133
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : 
       3              : import { binaryVector, boolVector, floatVector, intVector, utf8Vector } from '../../utils/arrow_vectors.js';
       4              : 
       5              : /// An Arrow table that Hyper can read with external(format => 'arrowstream').
       6              : export interface HyperArrowInsert {
       7              :     /// The table with the columns converted to types that Hyper reads
       8              :     table: arrow.Table;
       9              :     /// The column descriptor of the external scan, Hyper cannot infer it from the Arrow schema
      10              :     columns: string;
      11              : }
      12              : 
      13            1 : function quoteIdentifier(value: string): string {
      14            1 :     return `"${value.replace(/"/g, '""')}"`;
      15            1 : }
      16              : 
      17              : /// The Hyper type that the metadata of a field names, e.g. of a json or varchar(n) column exported by Hyper.
      18              : /// Hyper insists on this type when it reads the field back.
      19            1 : function annotatedHyperType(field: arrow.Field): string | null {
      20            1 :     const length = field.metadata.get('hyper:max_string_length');
      21            1 :     switch (field.metadata.get('hyper:type')) {
      22            1 :         case 'JSON':
      23            1 :             return 'json';
      24            1 :         case 'Oid':
      25            1 :             return 'oid';
      26            1 :         case 'Geography':
      27            1 :             return 'geography';
      28            1 :         case 'Char':
      29            1 :             return length == null ? null : `char(${length})`;
      30            1 :     }
      31            1 :     return length == null ? null : `varchar(${length})`;
      32            1 : }
      33              : 
      34              : /// The Hyper type of an Arrow field that Hyper reads, null when the column needs converting first.
      35              : /// Hyper insists on the type that corresponds to the Arrow type, a wider type is rejected.
      36            1 : function readableHyperType(field: arrow.Field): string | null {
      37            1 :     const type = field.type as arrow.DataType;
      38            1 :     const annotated = annotatedHyperType(field);
      39            1 :     if (annotated != null) return annotated;
      40            1 :     switch (type.typeId) {
      41            1 :         case arrow.Type.Int: {
      42            1 :             const int = type as arrow.Int;
      43            1 :             switch (int.bitWidth) {
      44            1 :                 case 8:
      45            1 :                 case 16:
      46            1 :                     return int.isSigned || int.bitWidth === 8 ? 'smallint' : 'integer';
      47            1 :                 case 32:
      48            1 :                     return int.isSigned ? 'integer' : 'bigint';
      49            1 :                 case 64:
      50            1 :                     return int.isSigned ? 'bigint' : 'numeric(20,0)';
      51            1 :             }
      52            0 :             return null;
      53            0 :         }
      54            1 :         case arrow.Type.Float:
      55            1 :             switch ((type as arrow.Float).precision) {
      56            1 :                 case arrow.Precision.SINGLE:
      57            1 :                     return 'real';
      58            1 :                 case arrow.Precision.DOUBLE:
      59            1 :                     return 'double precision';
      60            1 :             }
      61            1 :             return null;
      62            1 :         case arrow.Type.Utf8:
      63            1 :             return 'text';
      64            1 :         case arrow.Type.Binary:
      65            1 :             return 'bytea';
      66            1 :         case arrow.Type.Bool:
      67            1 :             return 'boolean';
      68            1 :         case arrow.Type.Decimal: {
      69            1 :             const decimal = type as arrow.Decimal;
      70            1 :             return decimal.bitWidth === 128 ? `numeric(${decimal.precision},${decimal.scale})` : null;
      71            1 :         }
      72            1 :         case arrow.Type.Date:
      73            1 :             return 'date';
      74            1 :         case arrow.Type.Time:
      75            1 :             return 'time';
      76            1 :         case arrow.Type.Timestamp: {
      77            1 :             const timestamp = type as arrow.Timestamp;
      78            1 :             if (timestamp.unit === arrow.TimeUnit.NANOSECOND) return null;
      79            1 :             return timestamp.timezone == null ? 'timestamp' : 'timestamptz';
      80            1 :         }
      81            1 :         case arrow.Type.Interval:
      82            1 :             return 'interval';
      83            1 :         case arrow.Type.List: {
      84              :             // The element nullability is part of the array type, e.g. array_agg yields non-null elements.
      85            1 :             const child = (type as arrow.List).children[0];
      86            1 :             const element = readableHyperType(child);
      87            1 :             if (element == null) return null;
      88            1 :             return child.nullable ? `array(${element})` : `array(${element} not null)`;
      89            1 :         }
      90            1 :         default:
      91            1 :             return null;
      92            1 :     }
      93            1 : }
      94              : 
      95              : /// A vector of values of an Arrow type in a type that Hyper reads, one chunk.
      96              : /// The vector is assembled from its buffers, the Arrow builders evaluate code (see arrow_vectors.ts).
      97            1 : function readableVector(type: arrow.DataType, values: unknown[]): arrow.Vector {
      98            1 :     switch (type.typeId) {
      99            1 :         case arrow.Type.Utf8:
     100            1 :         case arrow.Type.LargeUtf8:
     101            1 :             return utf8Vector(values as Array<string | null>);
     102            0 :         case arrow.Type.Binary:
     103            1 :         case arrow.Type.LargeBinary:
     104            1 :         case arrow.Type.FixedSizeBinary:
     105            1 :             return binaryVector(values as Array<Uint8Array | null>);
     106            1 :         case arrow.Type.Float:
     107            1 :             return (type as arrow.Float).precision === arrow.Precision.DOUBLE
     108            0 :                 ? floatVector(new arrow.Float64(), values as Array<number | null>)
     109            1 :                 : floatVector(new arrow.Float32(), values as Array<number | null>);
     110            1 :         case arrow.Type.Int:
     111            1 :             return intVector(type as arrow.Int, values as Array<number | bigint | null>);
     112            0 :         case arrow.Type.Bool:
     113            0 :             return boolVector(values as Array<boolean | null>);
     114            1 :         case arrow.Type.Null:
     115            1 :             return utf8Vector(values.map(() => null));
     116            0 :         default:
     117            0 :             throw new Error(`Hyper cannot import Arrow columns of type ${String(type)}`);
     118            1 :     }
     119            1 : }
     120              : 
     121              : /// Convert a column that Hyper cannot read to a type that it reads.
     122            1 : function convertColumn(vector: arrow.Vector): arrow.Vector {
     123            1 :     const type = vector.type as arrow.DataType;
     124            1 :     switch (type.typeId) {
     125            1 :         case arrow.Type.Dictionary:
     126            1 :             return readableVector((type as arrow.Dictionary).dictionary, [...vector]);
     127            1 :         case arrow.Type.LargeUtf8:
     128            1 :         case arrow.Type.LargeBinary:
     129            1 :         case arrow.Type.FixedSizeBinary:
     130            1 :         case arrow.Type.Float:
     131            1 :         case arrow.Type.Null:
     132            1 :             return readableVector(type, [...vector]);
     133            1 :         case arrow.Type.Timestamp: {
     134              :             // Hyper keeps microseconds, nanoseconds are truncated.
     135            1 :             const timezone = (type as arrow.Timestamp).timezone;
     136            1 :             return new arrow.Vector(vector.data.map(data => arrow.makeData({
     137            1 :                 type: new arrow.TimestampMicrosecond(timezone),
     138            1 :                 length: data.length,
     139            1 :                 nullCount: data.nullCount,
     140            1 :                 nullBitmap: data.nullBitmap,
     141            1 :                 data: (data.values as BigInt64Array).map(value => value / 1000n),
     142            1 :             })));
     143            1 :         }
     144            1 :         default:
     145            1 :             throw new Error(`Hyper cannot import Arrow columns of type ${String(type)}`);
     146            1 :     }
     147            1 : }
     148              : 
     149              : /// Prepare an Arrow table for an external Arrow stream scan in Hyper.
     150            1 : export function prepareHyperArrowInsert(table: arrow.Table): HyperArrowInsert {
     151            1 :     const columns: Record<string, arrow.Vector> = {};
     152            1 :     let converted = false;
     153            1 :     table.schema.fields.forEach((field, index) => {
     154            1 :         let column = table.getChildAt(index)!;
     155            1 :         if (readableHyperType(field) == null) {
     156            1 :             column = convertColumn(column);
     157            1 :             converted = true;
     158            1 :         }
     159            1 :         columns[field.name] = column;
     160            1 :     });
     161              :     // Converted columns are single chunks, the table redistributes the other columns into matching batches.
     162              :     // The rebuilt table drops the field metadata, so the descriptor follows the fields of the scanned table.
     163            1 :     const scanned = converted ? new arrow.Table(columns) : table;
     164            1 :     const descriptor = scanned.schema.fields.map(field => {
     165            1 :         const type = readableHyperType(field);
     166            1 :         if (type == null) {
     167            0 :             throw new Error(`Hyper cannot import Arrow columns of type ${String(field.type)}`);
     168              :         }
     169            1 :         return `${quoteIdentifier(field.name)} ${type}`;
     170            1 :     });
     171            1 :     return { table: scanned, columns: descriptor.join(', ') };
     172            1 : }
        

Generated by: LCOV version 2.0-1