Cursor.lua

-- [impl->dsn~logging-with-remotelog~1]
local log = require("remotelog")
local ExaError = require("ExaError")
local CursorData = require("luasql.exasol.CursorData")

local FETCH_MODE_NUMERIC_INDICES = "n"
local FETCH_MODE_ALPHANUMERIC_INDICES = "a" -- luacheck: ignore 211 # unused variable

--- This class represents a cursor that allows retrieving rows from a result set.
-- @classmod luasql.exasol.Cursor
-- @field private col_name_provider function
-- @field private num_columns number
-- @field private num_rows number
-- @field private data CursorData
-- @field private websocket ExasolWebsocket
-- @field private result_set_handle string|nil
local Cursor = {}

--- This result table index provider returns the column index.
-- This is used for fetch mode "n" (numeric indices in the result table).
-- To avoid creating a new function for each row we create this only once and re-use it.
-- @tparam number col_index the column index
-- @treturn number the column index
local function col_index_provider(col_index)
    return col_index
end

--- This function creates a result table index provider that returns the column name.
-- This is used for fetch mode "a" (alphanumeric indices in the result table).
-- To avoid creating a new function for each row we create this only once in the constructor and re-use it.
-- @tparam table column_names a list of column names
-- @treturn function result table index provider that maps the column index to column names
local function create_col_name_provider(column_names)
    return function(col_index)
        return column_names[col_index]
    end
end

--- This function extracts the column names from a result set.
-- @tparam table result_set the result set
-- @treturn table a list of column names
-- @raise an error if the number of columns is not equal to the number reported by the result set
local function get_column_names(result_set)
    if #result_set.columns ~= result_set.numColumns then
        local args = {expected_col_count = result_set.numColumns, actual_col_count = #result_set.columns}
        ExaError:new("E-EDL-24", "Result set reports {{expected_col_count}} but only "
                             .. "{{actual_col_count}} columns are available", args):add_ticket_mitigation():raise()
    end
    local names = {}
    for _, column in ipairs(result_set.columns) do
        table.insert(names, column.name)
    end
    return names
end

--- Extracts the column types from a result set.
-- @tparam table result_set the result set
-- @treturn table a list of column types
local function get_column_types(result_set)
    local types = {}
    local E<const> = {}
    for _, column in ipairs(result_set.columns) do
        table.insert(types, (column.dataType or E).type)
    end
    return types
end

--- Create a new instance of the Cursor class.
-- @tparam luasql.exasol.ConnectionProperties connection_properties connection properties
-- @tparam luasql.exasol.ExasolWebsocket websocket the websocket connection to the database
-- @tparam string session_id the session ID of the current database connection
-- @tparam table result_set the result set returned by the database
-- @treturn luasql.exasol.Cursor a new Cursor instance
-- @raise an error in case the result set is invalid, e.g. the number of columns or rows is inconsistent
function Cursor:create(connection_properties, websocket, session_id, result_set)
    local column_names = get_column_names(result_set)
    local column_types = get_column_types(result_set)
    local object = {
        websocket = websocket,
        session_id = session_id,
        result_set_handle = result_set.resultSetHandle,
        num_columns = result_set.numColumns,
        num_rows = result_set.numRows,
        column_names = column_names,
        column_types = column_types,
        col_name_provider = create_col_name_provider(column_names),
        data = CursorData:create(connection_properties, websocket, result_set),
        closed = false
    }
    self.__index = self
    setmetatable(object, self)
    return object
end

--- Gets a result table index provider for the given fetch mode.
-- @tparam "a"|"n" modestring the fetch mode: `"a"` for alphanumeric indices, `"n"` for numeric indices
-- @treturn function a function that maps column indices to a table index
--   in the result table
function Cursor:_get_result_table_index_provider(modestring)
    if modestring ~= FETCH_MODE_NUMERIC_INDICES then
        return self.col_name_provider
    else
        return col_index_provider
    end
end

--- Fills the given table with the values of the current row.
-- @tparam table table the table to fill
-- @tparam "a"|"n" modestring determines which indices are used when filling the table:
--   `"a"` for alphanumeric indices, `"n"` for numeric indices
function Cursor:_fill_row(table, modestring)
    local col_name_provider = self:_get_result_table_index_provider(modestring)
    for col = 1, self.num_columns do
        local col_name = col_name_provider(col)
        if not col_name then
            local args = {index = col}
            ExaError:new("E-EDL-23", "No column name found for index {{index}}", args):add_ticket_mitigation():raise()
        end
        table[col_name] = self.data:get_column_value(col)
    end
end

--- Retrieves the next row of results.
-- If fetch is called without parameters, the results will be returned directly to the caller.
-- If fetch is called with a table, the results will be copied into the table and the changed table will be returned.
-- In this case, an optional modestring parameter can be used. It is a string indicating how the resulting table
-- should be constructed. The mode string can contain:
--
-- * `"n"`: the resulting table will have numerical indices (default)
-- * `"a"`: the resulting table will have alphanumerical indices
--
-- The numerical indices are the positions of the result columns in the `SELECT` statement;
-- the alphanumerical indices are the names of the fields.
--
-- The optional table parameter is a table that should be used to store the next row. This allows
-- the use of a single table for many fetches, which can improve the overall performance.
--
-- A call to fetch after the last row has already being returned, will close the corresponding cursor.
-- The result values are converted to Lua types, i.e. `nil`, number and string.
--
-- Null values from the database are converted `luasql.exasol.NULL`.
-- You can test for it with `value == luasql.exasol.NULL`.
--
-- @tparam table|nil table the table to which the result will be copied or `nil` to return a new table
-- @tparam nil|"a"|"n" modestring the mode as described above
-- @treturn table|nil row data as described above or `nil` if there are no more rows
function Cursor:fetch(table, modestring)
    -- [impl -> dsn~luasql-cursor-fetch~0]
    if self.closed then
        ExaError:new("E-EDL-13", "Cursor closed while trying to fetch datasets from cursor"):raise()
    end
    if not self.data:has_more_rows() then
        log.trace("End of result set reached, no more rows after %d", self.num_rows)
        if not self.closed then
            self:close()
        end
        return nil
    end
    table = table or {}
    modestring = modestring or FETCH_MODE_NUMERIC_INDICES
    self:_fill_row(table, modestring)
    self.data:next_row()
    return table
end

--- Gets the list of column names.
-- @treturn table the list of column names
function Cursor:getcolnames()
    -- [impl -> dsn~luasql-cursor-getcolnames~0]
    return self.column_names
end

--- Gets the list of column types.
-- @treturn table the list of column types
function Cursor:getcoltypes()
    -- [impl -> dsn~luasql-cursor-getcoltypes~0]
    return self.column_types
end

--- Closes this cursor.
-- @treturn boolean `true` in case of success and `false` when the cursor is already closed
function Cursor:close()
    -- [impl -> dsn~luasql-cursor-close~0]
    if self.closed then
        log.warn(tostring(ExaError:new("W-EDL-33", "Attempted to close an already closed cursor")))
        return false
    end
    if self.result_set_handle == nil then
        log.trace("Cursor without result set handle: no need to close")
        self.closed = true
        return true
    end

    local err = self.websocket:send_close_result_set(self.result_set_handle)
    if err then
        ExaError:new("E-EDL-28", "Failed to close result set {{result_set_handle}}: {{error}}",
                     {result_set_handle = self.result_set_handle, error = err}):raise()
        return false
    else
        log.trace("Successfully closed result set %d", self.result_set_handle)
        self.closed = true
        return true
    end
end

return Cursor
generated by LDoc 1.5.0 Last updated 2025-03-06 11:54:17