Source: types/result-set.js

"use strict";

const utils = require("../utils");
const errors = require("../errors");
const rust = require("../../index");
const resultsWrapper = require("./results-wrapper");
const Uuid = require("./uuid");

const asyncIteratorSymbol = Symbol.asyncIterator || "@@asyncIterator";

/** @module types */

/**
 * Represents the result of a query.
 */

class ResultSet {
    /**
     * Object representing Rust result wrapper.
     * This value is used for the fields that are retrieved lazily.
     * @type {rust.QueryResultWrapper}
     */
    #rustResult;

    /**
     * Cache for columns vale.
     * This field is filled, only when user requests its value.
     * @type {Array.<{String, type}>?}
     */
    #columns;

    /**
     * @param {rust.QueryResultWrapper} result
     * @param {rust.PagingStateResponseWrapper} [pagingState]
     */
    constructor(result, pagingState) {
        // Old constructor logic only for purpose of unit tests.
        if (!(result instanceof rust.QueryResultWrapper)) {
            console.warn(
                "Using legacy version of the ResultSet constructor.\n" +
                    "This is deprecated and may be removed at any moment.",
            );

            this.rows = result.rows;
            this.rowLength = this.rows ? this.rows.length : result.rowLength;
            this.#columns = null;
            this.pageState = null;
            this.nextPage = undefined;
            this.nextPageAsync = undefined;
            const meta = result.meta;

            if (meta) {
                this.#columns = meta.columns;

                if (meta.pageState) {
                    this.pageState = meta.pageState.toString("hex");
                    Object.defineProperty(this, "rawPageState", {
                        value: meta.pageState,
                        enumerable: false,
                    });
                }
            }
            return;
        }
        /**
         * Gets an array rows returned by the query.
         * When the result set represents a response from a write query, this property will be `undefined`.
         * When the read query result contains more rows than the fetch size (5000), this property will only contain the
         * first rows up to fetch size. To obtain all the rows, you can use the built-in async iterator that will retrieve the
         * following pages of results.
         * @type {Array<Row>|undefined}
         */
        this.rows = resultsWrapper.getRowsFromResultsWrapper(result);

        /**
         * Gets the row length of the result, regardless if the result has been buffered or not
         * @type {Number|undefined}
         */
        this.rowLength = this.rows ? this.rows.length : 0;

        this.#rustResult = result;
        this.#columns = null;

        /**
         * A string token representing the current page state of query. It can be used in the following executions to
         * continue paging and retrieve the remained of the result for the query.
         * @type {String|null}
         * @default null
         */
        this.pageState = null;

        if (pagingState && pagingState.hasNextPage()) {
            this.pageState = pagingState
                .nextPage()
                .getRawPageState()
                .toString("hex");
            Object.defineProperty(this, "rawPageState", {
                value: pagingState.nextPage().getRawPageState(),
                enumerable: false,
            });
        }

        let traceId = result.getTraceId();
        if (traceId !== null) traceId = Uuid.fromRust(traceId);

        /**
         * Information on the execution of a successful query:
         * @member {Object}
         * @property {Number} achievedConsistency The consistency level that has been actually achieved by the query.
         * @property {String} queriedHost The Cassandra host that coordinated this query.
         * @property {Object} triedHosts Gets the associative array of host that were queried before getting a valid response,
         * being the last host the one that replied correctly.
         * @property {Object} speculativeExecutions The number of speculative executions (not including the first) executed before
         * getting a valid response.
         * @property {Uuid} traceId Identifier of the trace session.
         * @property {Array.<string>} warnings Warning messages generated by the server when executing the query.
         * @property {Boolean} isSchemaInAgreement Whether the cluster had reached schema agreement after the execution of
         * this query.
         *
         * TODO: Check if this is the case in rust driver:
         *
         * After a successful schema-altering query (ex: creating a table), the driver will check if
         * the cluster's nodes agree on the new schema version. If not, it will keep retrying for a given
         * delay (see `protocolOptions.maxSchemaAgreementWaitSeconds`).
         *
         * Note that the schema agreement check is only performed for schema-altering queries For other
         * query types, this method will always return `true`. If this method returns `false`,
         * clients can call [Metadata.checkSchemaAgreement()]{@link module:metadata~Metadata#checkSchemaAgreement} later to
         * perform the check manually.
         */
        this.info = {
            queriedHost: undefined, // Not yet supported by rust driver: https://github.com/scylladb/scylla-rust-driver/issues/1030
            triedHosts: undefined, // FIXME: Fill this field
            speculativeExecutions: undefined, // FIXME: Fill this field: https://github.com/scylladb-zpp-2024-javascript-driver/scylladb-javascript-driver/pull/37#discussion_r1817998702
            achievedConsistency: undefined, // FIXME: Find out more about this field: https://github.com/scylladb-zpp-2024-javascript-driver/scylladb-javascript-driver/pull/37#discussion_r1818000903
            traceId: traceId,
            warnings: result.getWarnings(),
            customPayload: null, // Not exposed by the rust driver: https://github.com/scylladb-zpp-2024-javascript-driver/scylladb-javascript-driver/pull/37#discussion_r1817998912
            isSchemaInAgreement: false, // FIXME: Look into this field: https://github.com/scylladb-zpp-2024-javascript-driver/scylladb-javascript-driver/pull/37#discussion_r1818002641
        };

        /**
         * Method used to manually fetch the next page of results.
         * This method is only exposed when using the {@link Client#eachRow} method and there are more rows available in
         * following pages.
         * @type Function
         */
        this.nextPage = undefined;

        /**
         * Method used internally to fetch the next page of results using promises.
         * @internal
         * @ignore
         * @type {Function}
         */
        this.nextPageAsync = undefined;
    }

    /**
     * Gets the columns returned in this ResultSet.
     * @type {Array.<{String, type}>}
     * @readonly
     */
    get columns() {
        if (!this.#columns) {
            this.#columns = resultsWrapper.getColumnsMetadata(this.#rustResult);
        }
        return this.#columns;
    }

    set columns(_) {
        throw new SyntaxError("ResultSet is read-only");
    }

    /**
     * Returns the first row or null if the result rows are empty.
     */
    first() {
        if (this.rows && this.rows.length) {
            return this.rows[0];
        }
        return null;
    }

    /**
     * When this instance is the result of a conditional update query, it returns whether it was successful.
     * Otherwise, it returns `true`.
     *
     * For consistency, this method always returns `true` for non-conditional queries (although there is
     * no reason to call the method in that case). This is also the case for conditional DDL statements
     * (CREATE KEYSPACE... IF NOT EXISTS, CREATE TABLE... IF NOT EXISTS), for which the server doesn't return
     * information whether it was applied or not.
     */
    wasApplied() {
        if (!this.rows || this.rows.length === 0) {
            return true;
        }
        const firstRow = this.rows[0];
        const applied = firstRow["[applied]"];
        return typeof applied === "boolean" ? applied : true;
    }

    /**
     * Gets the iterator function.
     *
     * Retrieves the iterator of the underlying fetched rows, without causing the driver to fetch the following
     * result pages. For more information on result paging,
     * [visit the documentation]{@link http://docs.datastax.com/en/developer/nodejs-driver/latest/features/paging/}.
     * @alias module:types~ResultSet#@@iterator
     * @see {@link module:types~ResultSet#@@asyncIterator}
     * @example <caption>Using for...of statement</caption>
     * const query = 'SELECT user_id, post_id, content FROM timeline WHERE user_id = ?';
     * const result = await client.execute(query, [ id ], { prepare: true });
     * for (const row of result) {
     *   console.log(row['email']);
     * }
     * @returns {Iterator.<Row>}
     */
    [Symbol.iterator]() {
        if (!this.rows) {
            return utils.emptyArray[Symbol.iterator]();
        }
        return this.rows[Symbol.iterator]();
    }

    /**
     * Gets the async iterator function.
     *
     * Retrieves the async iterator representing the entire query result, the driver will fetch the following result
     * pages.
     *
     * Use the async iterator when the query result might contain more rows than the `fetchSize`.
     *
     * Note that using the async iterator will not affect the internal state of the `ResultSet` instance.
     * You should avoid using both `rows` property that contains the row instances of the first page of
     * results, and the async iterator, that will yield all the rows in the result regardless on the number of pages.
     *
     * Multiple concurrent async iterations are not supported.
     * @alias module:types~ResultSet#@@asyncIterator
     * @example <caption>Using for await...of statement</caption>
     * const query = 'SELECT user_id, post_id, content FROM timeline WHERE user_id = ?';
     * const result = await client.execute(query, [ id ], { prepare: true });
     * for await (const row of result) {
     *   console.log(row['email']);
     * }
     * @returns {AsyncIterator<Row>}
     */
    [asyncIteratorSymbol]() {
        let index = 0;
        let pageState = this.rawPageState;
        let rows = this.rows;

        if (!rows || rows.length === 0) {
            return { next: () => Promise.resolve({ done: true }) };
        }

        const self = this;

        // Async generators are not present in Node.js 8, implement it manually
        return {
            async next() {
                if (index >= rows.length && pageState) {
                    if (!self.nextPageAsync) {
                        throw new errors.DriverInternalError(
                            "Property nextPageAsync should be set when pageState is defined",
                        );
                    }

                    const rs = await self.nextPageAsync(pageState);
                    rows = rs.rows;
                    index = 0;
                    pageState = rs.rawPageState;
                }

                if (index < rows.length) {
                    return { done: false, value: rows[index++] };
                }

                return { done: true };
            },
        };
    }

    /**
     * Determines whether there are more pages of results.
     * If so, the driver will initially retrieve and contain only the first page of results.
     * To obtain all the rows, use the [AsyncIterator]{@linkcode module:types~ResultSet#@@asyncIterator}.
     * @returns {boolean}
     */
    isPaged() {
        return !!this.rawPageState;
    }
}

module.exports = ResultSet;