ExasolWebsocket.lua
--- This internal class represents a websocket connection to an Exasol database that -- provides convenient functions for sending commands and evaluating the result. -- @classmod luasql.exasol.ExasolWebsocket -- @field private websocket Websocket the raw websocket local ExasolWebsocket = {} local cjson = require("cjson") local ExaError = require("ExaError") -- [impl->dsn~logging-with-remotelog~1] local log = require("remotelog") local raw_websocket = require("luasql.exasol.Websocket") local constants = require("luasql.exasol.constants") --- Creates a new Exasol websocket. -- @tparam luasql.exasol.Websocket websocket the websocket to wrap -- @treturn luasql.exasol.ExasolWebsocket the new websocket function ExasolWebsocket._create(websocket) local object = {websocket = websocket, closed = false} object.closed = false ExasolWebsocket.__index = ExasolWebsocket setmetatable(object, ExasolWebsocket) return object end --- Connects to an Exasol database. -- @tparam string url the websocket URL, e.g. `wss://exasoldb.example.com:8563` -- @tparam luasql.exasol.ConnectionProperties connection_properties the connection properties -- @treturn luasql.exasol.ExasolWebsocket the new websocket function ExasolWebsocket.connect(url, connection_properties) local websocket<const> = raw_websocket.connect(url, connection_properties) return ExasolWebsocket._create(websocket) end --- Sends the login command. -- See Exasol API documentation for the -- [`login` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/loginV3.md). -- This returns a public RSA key used for encrypting the password before sending it with the -- luasql.exasol.ExasolWebsocket:send_login_credentials method. -- @treturn table|nil the public RSA key or `nil` if an error occurred -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_login_command() log.debug("Sending login command") return self:_send_json({command = "login", protocolVersion = 3}) end --- Sends the login credentials. -- See Exasol API documentation for the -- [`login` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/loginV3.md). -- @tparam string username the username -- @tparam string encrypted_password the password encrypted with the public key returned by -- luasql.exasol.ExasolWebsocket:send_login_command -- @treturn table|nil response data from the database or `nil` if an error occurred -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_login_credentials(username, encrypted_password) log.debug("Sending login credentials") return self:_send_json({username = username, password = encrypted_password, useCompression = false}) end --- Sends the disconnect command. -- See Exasol API documentation for the -- [`disconnect` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/disconnectV1.md). -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_disconnect() log.debug("Sending disconnect command") local _, err = self:_send_json({command = "disconnect"}, true) return err end --- Sends the execute command. -- See Exasol API documentation for the -- [`execute` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/executeV1.md). -- @tparam string statement the SQL statement to execute -- @treturn table|nil the result set response data from the database or `nil` if an error occurred -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_execute(statement) local payload = {command = "execute", sqlText = statement, attributes = {}} return self:_send_json(payload) end --- Sends the setAttribute command with a given attribute name and value. -- To set the `null` value for an attribute, use `constants.NULL` or `nil`. -- Both will be translated to `null` in the JSON command. -- See Exasol API documentation for the -- [`setAttributes` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/setAttributesV1.md). -- @tparam string attribute_name the name of the attribute to set, e.g. `"autocommit"` -- @tparam string|number|boolean|nil attribute_value the value of the attribute to set, e.g. `false` -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_set_attribute(attribute_name, attribute_value) if attribute_value == nil or attribute_value == constants.NULL then attribute_value = cjson.null end local _, err = self:_send_json({command = "setAttributes", attributes = {[attribute_name] = attribute_value}}) return err end --- Sends the fetch command. -- See Exasol API documentation for the -- [`fetch` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/fetchV1.md). -- @tparam number result_set_handle result set handle -- @tparam number start_position row offset (0-based) from which to begin data retrieval -- @tparam number num_bytes number of bytes to retrieve (max: 64MiB) -- @treturn table|nil result set from the database or `nil` if an error occurred -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_fetch(result_set_handle, start_position, num_bytes) local payload = { command = "fetch", resultSetHandle = result_set_handle, startPosition = start_position, numBytes = num_bytes, attributes = {} } return self:_send_json(payload) end --- Sends the closeResultSet command. -- See Exasol API documentation for the -- [`closeResultSet` command](https://github.com/exasol/websocket-api/blob/master/docs/commands/closeResultSetV1.md). -- @tparam number result_set_handle result set handle to close -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:send_close_result_set(result_set_handle) local payload = {command = "closeResultSet", resultSetHandles = {result_set_handle}, attributes = {}} local _, err = self:_send_json(payload) return err end --- Extract the error from the given database response. -- @tparam table response the response from the database -- @treturn nil|table `nil` the error that occurred or `nil` if the response was successful local function get_response_error(response) if response.status == "ok" then return nil end if response.exception then local sqlCode = response.exception.sqlCode or "nil" local text = response.exception.text or "nil" return ExaError:new("E-EDL-10", "Received DB status {{status}} with code {{sqlCode|uq}}: {{text}}", {status = response.status, sqlCode = sqlCode, text = text}) else return ExaError:new("E-EDL-17", "Received DB status {{status}} without exception details", {status = response.status}) end end --- Send the given payload serialized to JSON to the database and optionally wait for the response -- and deserialize it from JSON. -- @tparam table payload the payload to send -- @tparam boolean|nil ignore_response `false` if we expect a response, else `true`. -- Default is `false`. -- @treturn table|nil the received response or nil if ignore_response was `true` or an error occurred -- @treturn table|nil `nil` if the operation was successful, otherwise the error that occurred function ExasolWebsocket:_send_json(payload, ignore_response) local raw_payload = cjson.encode(payload) if self.closed then ExaError:new("E-EDL-22", "Websocket already closed when trying to send payload {{payload}}", {payload = raw_payload}):raise() end log.trace("Sending payload '%s', ignore response=%s", raw_payload, ignore_response) local raw_response, err = self.websocket:send_raw(raw_payload, ignore_response) if ignore_response then return nil, nil end if err then return nil, err end if raw_response == nil then err = ExaError:new("E-EDL-2", "Did not receive response for request payload {{payload}}.", {payload = raw_payload}) log.error(tostring(err)) err:raise() end log.trace("Received response of %d bytes", #raw_response) local response = cjson.decode(raw_response) err = get_response_error(response) if err then return nil, err else return response.responseData, nil end end --- Closes the websocket. -- @treturn boolean `true` if the operation was successful function ExasolWebsocket:close() if self.closed then log.warn(tostring(ExaError:new("W-EDL-37", "Trying to close a Websocket that is already closed"))) return false end self.closed = true self.websocket:close() self.websocket = nil return true end return ExasolWebsocket