Skip to content

Commit

Permalink
#92: Customize identifier quoting (#94)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Bär <sebastian.baer@exasol.com>
  • Loading branch information
kaklakariada and redcatbear authored Sep 10, 2024
1 parent 0422a6d commit 5e4d09e
Show file tree
Hide file tree
Showing 19 changed files with 216 additions and 99 deletions.
2 changes: 1 addition & 1 deletion doc/changes/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changes

* [4.1.0](changes_4.1.0.md)
* [5.0.0](changes_5.0.0.md)
* [4.0.1](changes_4.0.1.md)
* [4.0.0](changes_4.0.0.md)
* [3.1.0](changes_3.1.0.md)
Expand Down
16 changes: 0 additions & 16 deletions doc/changes/changes_4.1.0.md

This file was deleted.

31 changes: 31 additions & 0 deletions doc/changes/changes_5.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# virtual-schema-common-lua 5.0.0, released 2024-09-10

Code name: Support specifying source for `IMPORT` statement

## Summary

This release updates `ImportQueryBuilder` and `ImportAppender` to allow a custom source for the generated `IMPORT FROM` statement, e.g. `JDBC`, `EXA` and `ORA`, see the [documentation for details](https://docs.exasol.com/db/latest/sql/import.htm).

The release also updates `SelectAppender:_append_table()` to support catalogs in addition to schemas.

The release also allows specifying a custom quote character for identifiers instead of the default `"`. This is a breaking change, see below for details.

The release also formats all sources and adds type annotations using LuaLS.

## Breaking Change

Class `AbstractQueryAppender` now requires an `AppenderConfig` as second argument to the constructor method `:new()`. If the configuration is missing, the constructor will fail with error message `AbstractQueryAppender requires an appender configuration`. The following constructors are affected:
* `QueryRenderer:new()`
* `AggregateFunctionAppender:new()`
* `ExpressionAppender:new()`
* `ImportAppender:new()`
* `ScalarFunctionAppender:new()`
* `SelectAppender:new()`

The configuration allows customizing the identifier quote character. If the default value `"` is OK, you can use the predefined configuration `AbstractQueryAppender.DEFAULT_APPENDER_CONFIG`.

## Features

* #89: Added support for specifying source for `IMPORT` statement
* #91: Added support for catalogs
* #92: Added support for customizing identifier quote character
7 changes: 5 additions & 2 deletions spec/assertions/appender_assertions.lua
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
local say = require("say")
local assert = require("luassert")
local Query = require("exasol.vscl.Query")
local AbstractQueryAppender = require("exasol.vscl.queryrenderer.AbstractQueryAppender")

local function append_yields(_, arguments)
local appender_class = arguments[1]
local expected = arguments[2]
local original_query = arguments[3]
local appender_config = arguments[4] or AbstractQueryAppender.DEFAULT_APPENDER_CONFIG
local out_query = Query:new()
local appender = appender_class:new(out_query)
local appender = appender_class:new(out_query, appender_config)
local ok, result = pcall(appender.append, appender, original_query)
local actual = out_query:to_string()
arguments[1] = original_query
Expand All @@ -25,8 +27,9 @@ local function append_error(_, arguments)
local appender_class = arguments[1]
local expected = arguments[2]
local original_query = arguments[3]
local appender_config = arguments[4] or AbstractQueryAppender.DEFAULT_APPENDER_CONFIG
local out_query = Query:new()
local appender = appender_class:new(out_query)
local appender = appender_class:new(out_query, appender_config)
local ok, result = pcall(appender.append, appender, original_query)
arguments[1] = original_query
arguments[2] = expected
Expand Down
11 changes: 8 additions & 3 deletions spec/exasol/vscl/QueryRenderer_spec.lua
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
require("busted.runner")()
local QueryRenderer = require("exasol.vscl.QueryRenderer")
local AbstractQueryAppender = require("exasol.vscl.queryrenderer.AbstractQueryAppender")

local function testee(original_query)
return QueryRenderer:new(original_query, AbstractQueryAppender.DEFAULT_APPENDER_CONFIG)
end

describe("QueryRenderer", function()
it("renders SELECT *", function()
local original_query = {type = "select", from = {type = "table", name = "T1"}}
local renderer = QueryRenderer:new(original_query)
local renderer = testee(original_query)
assert.are.equals('SELECT * FROM "T1"', renderer:render())
end)

it("renders the aggregate function", function()
local renderer = QueryRenderer:new({
local renderer = testee({
type = "select",
selectList = {
{
Expand All @@ -24,7 +29,7 @@ describe("QueryRenderer", function()
end)

it("renders a query wrapped into an IMPORT", function()
local renderer = QueryRenderer:new({
local renderer = testee({
type = "import",
connection = "CON_A",
statement = {type = "select", selectList = {{type = "literal_string", value = "hello"}}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ local literal = require("exasol.vscl.queryrenderer.literal_constructors")
local reference = require("exasol.vscl.queryrenderer.reference_constructors")
local Query = require("exasol.vscl.Query")
local AggregateFunctionAppender = require("exasol.vscl.queryrenderer.AggregateFunctionAppender")
local AbstractQueryAppender = require("exasol.vscl.queryrenderer.AbstractQueryAppender")

local function it_asserts(expected, actual, explanation)
it(explanation or expected, function()
assert.are.equals(expected, actual)
end)
end

---@param out_query Query?
---@return AggregateFunctionAppender
local function testee(out_query)
return AggregateFunctionAppender:new(out_query or Query:new(), AbstractQueryAppender.DEFAULT_APPENDER_CONFIG)
end

local function run_complex_function(name, extra_attributes, ...)
local out_query = Query:new()
local renderer = AggregateFunctionAppender:new(out_query)
local renderer = testee(out_query)
local scalar_function = renderer["_" .. string.lower(name)]
assert(scalar_function ~= nil, "Aggregate function " .. name .. " must be present in renderer")
local wrapped_arguments = literal.wrap_literals(...)
Expand Down Expand Up @@ -85,7 +92,7 @@ describe("AggregateFunctionRenderer", function()
end

it("asserts functions that are not allowed to have a DISTINCT modifier", function()
local renderer = AggregateFunctionAppender:new(Query:new())
local renderer = testee()
assert.has_error(function()
renderer:append({name = "MEDIAN", distinct = "true"})
end, "Aggregate function 'MEDIAN' must not have a DISTINCT modifier.")
Expand Down
30 changes: 24 additions & 6 deletions spec/exasol/vscl/queryrenderer/ExpressionAppender_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ local Query = require("exasol.vscl.Query")
local literal = require("exasol.vscl.queryrenderer.literal_constructors")
local reference = require("exasol.vscl.queryrenderer.reference_constructors")
local ExpressionAppender = require("exasol.vscl.queryrenderer.ExpressionAppender")
local AbstractQueryAppender = require("exasol.vscl.queryrenderer.AbstractQueryAppender")

local function assert_expression_yields(expression, expected)
assert.append_yields(ExpressionAppender, expected, expression)
---@param expression any
---@param expected string
---@param appender_config AppenderConfig?
local function assert_expression_yields(expression, expected, appender_config)
assert.append_yields(ExpressionAppender, expected, expression, appender_config)
end

local function testee()
return ExpressionAppender:new(Query:new(), AbstractQueryAppender.DEFAULT_APPENDER_CONFIG)
end

describe("ExpressionRenderer", function()
Expand All @@ -15,6 +23,11 @@ describe("ExpressionRenderer", function()
"the_column"), '"the_table"."the_column"')
end)

it("renders column reference with custom quote", function()
assert_expression_yields(reference.column("the_table", "the_column"), '`the_table`.`the_column`',
{identifier_quote = "`"})
end)

describe("renders literal:", function()
it("null", function()
assert_expression_yields(literal.null(), "null")
Expand Down Expand Up @@ -258,26 +271,31 @@ describe("ExpressionRenderer", function()
it("raises an error if the output query is missing", function()
assert.has_error(function()
ExpressionAppender:new()
end, "Expression renderer requires a query object that it can append to.")
end, "AbstractQueryAppender requires a query object that it can append to.")
end)

it("raises an error if the appender configuration is missing", function()
assert.has_error(function()
ExpressionAppender:new(Query:new())
end, "AbstractQueryAppender requires an appender configuration.")
end)

it("raises an error if an unknown predicate type is used", function()
local appender = ExpressionAppender:new(Query:new())
local appender = testee()
assert.error_matches(function()
appender:_append_unary_predicate({type = "illegal predicate type"})
end, "Cannot determine operator for unknown predicate type 'illegal predicate type'.", 1, true)
end)

it("raises an error if the expression type is unknown", function()
local appender = ExpressionAppender:new(Query:new())
local appender = testee()
assert.error_matches(function()
appender:append_expression({type = "illegal expression type"})
end, "Unable to render unknown SQL expression type 'illegal expression type'.", 1, true)
end)

it("raises an error if the data type is unknown", function()
local appender = ExpressionAppender:new(Query:new())
local appender = testee()
assert.error_matches(function()
appender:_append_data_type({type = "illegal datatype"})
end, "Unable to render unknown data type 'illegal datatype'.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ local literal = require("exasol.vscl.queryrenderer.literal_constructors")
local geo = require("exasol.vscl.queryrenderer.geo_constructors")
local Query = require("exasol.vscl.Query")
local ScalarFunctionAppender = require("exasol.vscl.queryrenderer.ScalarFunctionAppender")
local AbstractQueryAppender = require("exasol.vscl.queryrenderer.AbstractQueryAppender")

local function it_asserts(expected, actual, explanation)
it(explanation or expected, function()
Expand All @@ -16,7 +17,7 @@ end
---@return string rendered_function function rendered as string
local function run_complex_function(name, extra_attributes, ...)
local out_query = Query:new()
local renderer = ScalarFunctionAppender:new(out_query)
local renderer = ScalarFunctionAppender:new(out_query, AbstractQueryAppender.DEFAULT_APPENDER_CONFIG)
local scalar_function = renderer["_" .. string.lower(name)]
assert(scalar_function ~= nil, "Scalar function " .. name .. " must be present in renderer")
local wrapped_arguments = literal.wrap_literals(...)
Expand Down Expand Up @@ -485,7 +486,7 @@ describe("ScalarFunctionRenderer", function()
end)

it("raises an error if you try to append a non-existent scalar function", function()
local renderer = ScalarFunctionAppender:new(Query:new())
local renderer = ScalarFunctionAppender:new(Query:new(), AbstractQueryAppender.DEFAULT_APPENDER_CONFIG)
local non_existent_scalar_function = {name = "NON_EXISTENT"}
assert.error_matches(function()
renderer:append(non_existent_scalar_function)
Expand Down
12 changes: 10 additions & 2 deletions spec/exasol/vscl/queryrenderer/SelectAppender_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ require("busted.runner")()
require("assertions.appender_assertions")
local SelectAppender = require("exasol.vscl.queryrenderer.SelectAppender")

local function assert_yields(expected, original_query)
assert.append_yields(SelectAppender, expected, original_query)
---@param expected string
---@param original_query any
---@param appender_config AppenderConfig?
local function assert_yields(expected, original_query, appender_config)
assert.append_yields(SelectAppender, expected, original_query, appender_config)
end

local function assert_select_error(expected, original_query)
Expand All @@ -26,6 +29,11 @@ describe("SelectAppender", function()
assert_yields('SELECT * FROM "C1"."S1"."T1"', original_query)
end)

it("renders SELECT * with custom identifier quote", function()
local original_query = {type = "select", from = {type = "table", catalog = "C1", schema = "S1", name = "T1"}}
assert_yields('SELECT * FROM `C1`.`S1`.`T1`', original_query, {identifier_quote = "`"})
end)

it("renders SELECT * ignoring catalog when schema is missing", function()
local original_query = {type = "select", from = {type = "table", catalog = "C1", name = "T1"}}
assert_yields('SELECT * FROM "T1"', original_query)
Expand Down
4 changes: 2 additions & 2 deletions spec/exasol/vscl/queryrenderer/literal_constructors.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ function literal_constructors.interval_ym(value, precision)
end

---@param value string
---@param precision integer
---@param fraction integer
---@param precision integer?
---@param fraction integer?
---@return LiteralInterval
function literal_constructors.interval_ds(value, precision, fraction)
return {
Expand Down
33 changes: 24 additions & 9 deletions src/exasol/vscl/QueryRenderer.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
--- Renderer for SQL queries.
---@class QueryRenderer
---@field original_query SelectSqlStatement
---@field _original_query QueryStatement
---@field _appender_config AppenderConfig
local QueryRenderer = {}
QueryRenderer.__index = QueryRenderer

Expand All @@ -9,25 +10,39 @@ local SelectAppender = require("exasol.vscl.queryrenderer.SelectAppender")
local ImportAppender = require("exasol.vscl.queryrenderer.ImportAppender")

--- Create a new query renderer.
---@param original_query Query query structure as provided through the Virtual Schema API
---@param original_query QueryStatement query structure as provided through the Virtual Schema API
---@param appender_config AppenderConfig configuration for the query renderer containing identifier quoting
---@return QueryRenderer query_renderer instance
function QueryRenderer:new(original_query)
function QueryRenderer:new(original_query, appender_config)
local instance = setmetatable({}, self)
instance:_init(original_query)
instance:_init(original_query, appender_config)
return instance
end

function QueryRenderer:_init(original_query)
self.original_query = original_query
---@param original_query QueryStatement query structure as provided through the Virtual Schema API
---@param appender_config AppenderConfig configuration for the query renderer containing identifier quoting
function QueryRenderer:_init(original_query, appender_config)
self._original_query = original_query
self._appender_config = appender_config
end

---@param query QueryStatement
---@return ImportAppender|SelectAppender
local function get_appender_class(query)
if query.type == "import" then
return ImportAppender
else
return SelectAppender
end
end

--- Render the query to a string.
---@return string rendered_query query as string
function QueryRenderer:render()
local out_query = Query:new()
local appender = (self.original_query.type == "import") and ImportAppender:new(out_query)
or SelectAppender:new(out_query)
appender:append(self.original_query)
local appender_class = get_appender_class(self._original_query)
local appender = appender_class:new(out_query, self._appender_config)
appender:append(self._original_query)
return out_query:to_string()
end

Expand Down
30 changes: 27 additions & 3 deletions src/exasol/vscl/queryrenderer/AbstractQueryAppender.lua
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
--- This class is the abstract base class of all query renderers.
--- It takes care of handling the temporary storage of the query to be constructed.
---@class AbstractQueryAppender
---@field _out_query Query
---@field _out_query Query query object that the appender appends to
---@field _appender_config AppenderConfig configuration for the query renderer (e.g. containing identifier quoting)
local AbstractQueryAppender = {}

local DEFAULT_IDENTIFIER_QUOTE<const> = '"'

---@type AppenderConfig Default configuration with double quotes for identifiers.
AbstractQueryAppender.DEFAULT_APPENDER_CONFIG = {identifier_quote = DEFAULT_IDENTIFIER_QUOTE}

local ExaError = require("ExaError")

---@param out_query Query
function AbstractQueryAppender:_init(out_query)
---Initializes the query appender and verifies that all parameters are set.
---Raises an error if any of the parameters is missing.
---@param out_query Query query object that the appender appends to
---@param appender_config AppenderConfig configuration for the query renderer (e.g. containing identifier quoting)
function AbstractQueryAppender:_init(out_query, appender_config)
assert(out_query ~= nil, "AbstractQueryAppender requires a query object that it can append to.")
assert(appender_config ~= nil, "AbstractQueryAppender requires an appender configuration.")
self._out_query = out_query
self._appender_config = appender_config
end

--- Append a token to the query.
Expand Down Expand Up @@ -142,4 +154,16 @@ function AbstractQueryAppender:_append_string_literal(literal)
self:_append("'")
end

---Append a quoted identifier, e.g. a schema, table or column name.
---@param identifier string identifier
function AbstractQueryAppender:_append_identifier(identifier)
local quote_char = self._appender_config.identifier_quote or DEFAULT_IDENTIFIER_QUOTE
self:_append(quote_char)
self:_append(identifier)
self:_append(quote_char)
end

return AbstractQueryAppender

---@class AppenderConfig
---@field identifier_quote string? quote character for identifiers, defaults to `"`
Loading

0 comments on commit 5e4d09e

Please sign in to comment.