cube-js/cube · error · Error

${this.constructor} driver supports only rows upload

Error message

${this.constructor} driver supports only rows upload

What it means

uploadTableWithIndexes requires the source data to be row-form (tableData.rows). If tableData contains only columnar data (columns) — typically because the source driver returned results in column format — the QuestDB driver cannot convert it and throws. QuestDB ingestion in this driver is implemented only for row arrays.

Source

Thrown at packages/cubejs-questdb-driver/src/QuestDriver.ts:232

  // eslint-disable-next-line camelcase
  public async getTablesQuery(_schemaName: string): Promise<({ table_name?: string, TABLE_NAME?: string })[]> {
    return this.query('SHOW TABLES', []);
  }

  public async tableColumnTypes(table: string): Promise<TableStructure> {
    const response: any[] = await this.query(`SHOW COLUMNS FROM ${escapeStringLiteral(table)}`, []);

    return response.map((row) => ({ name: row.column, type: this.toGenericType(row.type) }));
  }

  public async uploadTableWithIndexes(
    table: string,
    columns: TableStructure,
    tableData: DownloadTableMemoryData,
    indexesSql: IndexesSQL
  ) {
    if (!tableData.rows) {
      throw new Error(`${this.constructor} driver supports only rows upload`);
    }

    await this.createTable(table, columns);

    try {
      for (let i = 0; i < tableData.rows.length; i++) {
        await this.query(
          `INSERT INTO ${escapeStringLiteral(table)}
        (${columns.map(c => this.quoteIdentifier(c.name)).join(', ')})
        VALUES (${columns.map((c, paramIndex) => this.param(paramIndex)).join(', ')})`,
          columns.map(c => this.toColumnValue(tableData.rows[i][c.name] as string, c.type))
        );
      }
      // Make sure to commit the data to make it visible for later queries.
      await this.query('COMMIT', []);

      for (let i = 0; i < indexesSql.length; i++) {

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Ensure the source download uses downloadQueryResults with row format so tableData.rows is populated
  2. Convert columnar data to rows before calling: rows = columnNames.map((_, i) => Object.fromEntries(names.map(n => [n, cols[n][i]])))
  3. Upload without the column-only data path, or use a driver version that converts columns to rows automatically

Example fix

// before
await questDriver.uploadTableWithIndexes(table, structure, { columns: colData }, indexes);
// after
const rows = colData.ids.map((_, i) => ({ id: colData.ids[i], ts: colData.ts[i] }));
await questDriver.uploadTableWithIndexes(table, structure, { rows }, indexes);
Defensive patterns

Strategy: validation

Validate before calling

if (!tableData.rows || tableData.rows.length === 0) throw new Error('QuestDB upload requires row-form tableData.rows');

Type guard

function hasRows(d) { return Array.isArray(d?.rows) && d.rows.length > 0; }

Try / catch

try { await driver.uploadTableWithIndexes(table, columns, tableData, indexes); } catch (e) { if (e.message.includes('supports only rows upload')) { return uploadConvertedToRows(table, columns, tableData, indexes); } throw e; }

Prevention

When it happens

Trigger: Calling uploadTableWithIndexes (via loadTable / export queries / pre-aggregation upload) with a DownloadTableMemoryData built from a driver that sets `columns` but not `rows`.

Common situations: Cross-database pre-aggregation exports where the source driver downloads results column-wise; combining a column-oriented source driver with QuestDB as target without a rows conversion step.

Related errors


AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02). Data as JSON: /api/errors/b8c3e7608460ca54. Report an issue: GitHub.